关于”集群使用“常遇见的几个概念(备忘)

在使用集群时,经常会遇到节点、CPU、线程等概念。了解并区分这些概念有助于我们在使用过程中合理的设置利用资源,提高计算速度防止资源浪费。

集群和分区

集群是由多个计算节点(物理服务器或虚拟服务器)组成的计算机系统,可以用于大规模的数据处理、模拟、科学计算等任务。分区,通常也称之为队列,是将集群中的计算资源(如计算节点、CPU 核心、内存等)按不同的需求和使用场景进行逻辑上的划分,帮助将集群资源根据不同的需求或任务类型进行隔离,避免资源冲突等问题。

节点和CPU

在使用集群或者超算时,我们最常接触到的一个概念就是节点,可以将其理解为一个独立的计算单元(类似于一台电脑),而一个节点可以有一个到多个 CPU。多数的计算服务器都是 2 个 CPU。

CPU核心和线程

一个 CPU 可以有多个 CPU 核心;而线程是 CPU 调度和分配的最基本单位,是一个虚拟元件(即逻辑层面的,只有操作系统可见),又称为逻辑内核(逻辑 CPU)。一个 CPU 核心可以有一个线程,也可以有两个线程(即超线程技术,由英特尔(Intel)提出的技术,用于“欺骗”操作系统,使其认为有额外的内核)。若支持超线程,则说明一个 CPU 核心可以当成 2 个核心来发挥作用,也就是说每个线程都作为独立的 CPU 实例运行。现在,很多终端设备都会有多个 CPU 核心,并且还区分为两种内核:性能核(Performance Cores) + 效率核(Efficient Cores),简称为 P 核和 E 核,也就是通常所说的大(P)小(E)核。

集群、队列、节点、CPU、CPU核心和线程示意图
图 1 集群、队列、节点、CPU、CPU核心和线程示意图

CPU串行和并行示意图
图 2 CPU串行和并行示意图

进程

进程是程序运行的一个实体的运行过程,是一个动态概念,是操作系统进行资源(例如 CPU、内存和磁盘 IO 等)分配和调配的一个独立单位。一个进程可能有多个子任务,比如聊天工具要接受消息,发送消息,单个子任务要在某个线程上运行,而多个子任务就需要多个线程。如图 2 所示,如果只启用1个 CPU 核心那就是串行,启动多个 CPU 核心就是并行。资源分配给进程,线程共享进程资源。

概括起来是:一个集群有多个分区,而一个分区由一到多个节点组成;一个节点可以有多个 CPU,而一个 CPU 也可以有多个 CPU 核心,同时一个 CPU 核心可以有一个以上的线程。一个程序可以调用多个进程,一个进程可调用至少一个线程;一般而言,一个 CPU 核心在某个时钟分区只能执行一个进程,如果一个进程同时调用的线程数超过一个 CPU 核心的线程数,则需要调用其他 CPU 核心实现并行。

NUMA node & socket

NUMA node & socket 是关于信息快速存储、访问与传递的两项技术。

NUMA node

在 NUMA 之前,系统采用的是 SMP (对称多处理器) 架构,所有 CPU 对内存的访问都是通过 Bus 来实现,即所有的 CPU 和内存管理器都与 Bus 相链接通信。也就是说,物理内存对所有 CPU 来说没有区别,每个 CPU 访问内存的方式也一样,这种架构称之为 UMA (Uniform Memory Access)。

UMA 和 NUMA 示意图
图 3 UMA 和 NUMA 示意图

如图 3 所示,UMA 架构使得每个 CPU 核心共享相通的内存地址空间,保证了所有的内存访问是一致的,但有着一定的缺点:首先很容易想到的就是对于 Bus 带宽需求的增加,系统的 Bus 将会成为大型计算机的发展瓶颈;其次有可能出现多块 CPU 访问同一地址内存的冲突。

随着技术的突破,NUMA (Non-Uniform Memory Access),译为“非一致性内存访问”方案逐渐被应用起来。在物理层面上,可以将物理内存设置为分布式的,由多个 cell 组成,每个核对应于自己的一个或多个 cell。这样 CPU 访问自己对应的内存时就比较快,而访问其它 CPU 对应的内存时就比较慢。在软件层面上,对NUMA的概念进行抽象,无论硬件上是连续或者不连续的内存,Linux 都可以将其视为一个完整的物理内存,并划分为若干的 node。每个 node 可以拥有多个 CPU 及其对应的内存,并且每个 node 有自己的集成内存控制器 (IMC,Integrated Memory Controller)。

在 Node 内部,类似于 SMP (对称多处理器) 架构,使用 IMC Bus 进行不同核心间的通信;不同的 Node 间通过 QPI (Quick Path Interconnect) 进行通信,如上图 3 所示。其中,QPI 的延迟要高于 IMC Bus,所以,CPU 访问内存有了远近 (remote/local) 之别,访问不同的内存速度差别非常明显。同时要注意,默认情况下,内核不会将内存页面从一个 NUMA node 迁移到另外一个 NUMA node;UMA 可以认为是只有一个 node 的NUMA。

socket (此部分摘录自微信公众号“网管叨bi叨”的文章:socket到底是什么?别再把它叫套接字了)

socket 译为“插座”。在网络编程领域,通过 socket,我们可以与某台机子建立”连接”,这个建立”连接”的过程,就像是将插头插入插排。

socket 建立机子与机子之间的“连接”
图 4 socket 建立机子与机子之间的“连接”

socket 的使用场景

我们想要将数据从 A 电脑的某个进程发到 B 电脑的某个进程。这时候需要选择将数据发过去的方式,如果需要确保数据要能发给对方,那就选可靠的 TCP 协议,如果数据丢了也没关系,看天意,那就选择不可靠的 UDP 协议。初学者毫无疑问,首选 TCP

TCP 是什么
图 5 TCP 是什么

那这时候就需要用 socket 进行编程,于是第一步就是创建个关于 TCP 的 socket,就像下面这样:

$ sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

这个方法会返回 socket_fd,它是 socket 文件的句柄,是个 int 类型的数字,相当于 socket 的身份证号。得到了 socket_fd 之后,对于服务端,就可以依次执行 bind(), listen(), accept() 方法,然后坐等客户端的连接请求;对于客户端,得到 socket_fd 之后,你就可以执行 connect() 方法向服务端发起建立连接的请求,此时就会发生 TCP 三次握手。

握手建立连接流程
图 6 握手建立连接流程

连接建立完成后,客户端可以执行 send() 方法发送消息,服务端可以执行 recv() 方法接收消息,反过来,服务器也可以执行 send(),客户端执行 recv() 方法。到这里为止,就是大部分程序员最熟悉的使用场景。

socket 的设计

对大部分程序员来说,socket 是个黑盒。既然是黑盒,索性假设忘了 socket,重新设计一个内核网络传输功能。网络传输,从操作上看,就是发数据和远端之间互相收发数据,也就是对应着写数据和读数据。

但显然,事情没那么简单。这里还有两个问题:

① 接收端和发送端可能不止一个,因此需要一些信息做下区分。这个大家肯定很熟悉,可以用 IP 和端口:IP 用来定位是哪台电脑,端口用来定位是这台电脑上的哪个进程。

② 发送端和接收端的传输方式有很多区别,可以是可靠的 TCP 协议,也可以是不可靠的 UDP 协议,甚至还有需要支持基于 icmp 协议的 ping 命令。

  • sock 是什么

写过代码的都知道,为了支持这些功能,我们需要定义一个数据结构去支持这些功能,这个数据结构,叫 sock。为了解决上面的第一个问题,我们可以在 sock 里加入IP和端口字段。而第二个问题,我们会发现这些协议虽然各不相同,但还是有一些功能相似的地方,比如收发数据时的一些逻辑完全可以复用。按面向对象编程的思想,可以将不同的协议当成是不同的对象类(或结构体),将公共的部分提取出来,通过”继承”的方式,复用功能。

sock加入IP和端口字段
图 7 sock加入IP和端口字段

  • 基于各种sock实现网络传输功能

将功能重新划分下,如图 8 所示,定义了一些数据结构。sock 是最基础的结构,维护一些任何协议都有可能会用到的收发数据缓冲区。inet_sock 特指用了网络传输功能的 sock,在 sock 的基础上还加入了 TTL,端口,IP地址这些跟网络传输相关的字段信息;而 Unix domain socket 用于本机进程之间的通信,直接读写文件,不需要经过网络协议栈。inet_connection_sock 指面向连接的 sock,在 inet_sock 的基础上加入面向连接的协议里相关字段,比如 accept 队列,数据包分片大小,握手失败重试次数等。虽然我们现在提到面向连接的协议就是指 TCP,但设计上 linux 需要支持扩展其他面向连接的新协议,tcp_sock 就是正儿八经的 TCP 协议专用的 sock 结构了,在 inet_connection_sock 基础上还加入了 TCP 特有的滑动窗口、拥塞避免等功能。同样 udp 协议也会有一个专用的数据结构,叫 udp_sock。有了这套数据结构之后,将它们跟硬件网卡对接一下,就实现了网络传输的功能。

继承sock的各类sock
图 8 继承sock的各类sock

  • 提供socket层

可以想象得到,这里面的代码肯定非常复杂,同时还操作了网卡硬件,需要比较高的操作系统权限,再考虑到性能和安全,于是决定将它放在操作系统内核里。既然网络传输功能做在内核里,那用户空间的应用程序想要用这部分功能的话,该怎么办呢?

这个好办,本着不重复造轮子的原则,将这部分功能抽象成一个个简单的接口。以后只需要调用这些接口,就可以驱动写好的这一大堆复杂的数据结构去发送数据。那么问题又来了,怎么样将这部分功能暴露出去呢?让程序员更方便的使用呢?

既然跟远端服务端进程收发数据可以抽象为“读和写”,操作文件也可以抽象为”读和写”,正好有句话叫,”linux里一切皆是文件”,那索性将内核的 sock 封装成文件就好了。创建 sock 的同时也创建一个文件,文件有个句柄 fd,说白了就是个文件系统里的身份证号码,通过它可以唯一确定是哪个 sock

这个文件句柄 fd 其实就是 sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) 里的 sock_fd

将句柄暴露给用户之后,用户就可以像操作文件句柄那样去操作这个 sock 句柄。在用户空间里操作这个句柄,文件系统就会将操作指向内核 sock 结构,整个操作流程如图 9 所示。

通过文件找到sock
图 9 通过文件找到sock

有了 sock_fd 句柄之后,就需要提供一些接口方法,让用户更方便的实现特定的网络编程功能。需要的接口有 send()recv()bind(), listen()connect() 。至此,内核网络传输功能就算设计完成了。

现在是不是眼熟了,上面这些接口方法其实就是 socket 提供出来的接口。也就是说,socket 其实就是个代码库 or 接口层,它介于内核和应用程序之间,提供了一些高度封装过的接口,让用户去使用内核网络传输功能。

基于sock实现网络传输功能
图 10 基于sock实现网络传输功能

到这里,我们应该明白了。我们平时写的应用程序里代码里虽然用了 socket 实现了收发数据包的功能,但其实真正执行网络通信功能的,不是应用程序,而是 linux 内核。相当于应用程序通过 socket 提供的接口,将网络传输的这部分工作外包给了 linux 内核。

综上所述,在操作系统内核空间里,实现网络传输功能的结构是 sock,基于不同的协议和应用场景,会被泛化为各种类型的 xx_sock,它们结合硬件,共同实现了网络传输功能。为了将这部分功能暴露给用户空间的应用程序使用,于是引入了 socket 层,同时将 sock 嵌入到文件系统的框架里,sock 就变成了一个特殊的文件,用户就可以在用户空间使用文件句柄,也就是 socket_fd 来操作内核 sock 的网络传输能力。

socket 如何实现网络通信

上面关于怎么实现网络通信功能这一块一笔带过了。但这套 sock 的结构其实非常复杂。以最常用的 TCP 协议为例,简单了解下它是怎么实现网络传输功能的。其主要分为两阶段,分别是建立连接数据传输

  • 建立连接

对于 TCP,要传数据,就得先在客户端和服务端中间建立连接。在客户端,代码执行 socket 提供的 connect(sockfd, "ip:port") 方法时,会通过 sockfd 句柄找到对应的文件,再根据文件里的信息指向内核的 sock 结构,通过这个 sock 结构主动发起三次握手;而在服务端握手次数还没达到”三次”的连接,叫半连接,完成好三次握手的连接,叫全连接。它们分别会用半连接队列和全连接队列来存放,这两个队列会在你执行 listen() 方法的时候创建好。当服务端执行 accept() 方法时,就会从全连接队列里拿出一条全连接。至此,连接就算准备好了,可以开始传输数据。

TCP三次握手
图 11 TCP三次握手

半连接队列和全连接队列
图 12 半连接队列和全连接队列

虽然都叫队列,但半连接队列其实是个hash表,而全连接队列其实是个链表。

  • 数据传输

为了实现发送和接收数据的功能,sock 结构体里带了一个发送缓冲区和一个接收缓冲区,说是缓冲区,但其实就是个链表,上面挂着一个个准备要发送或接收的数据。当应用执行 send() 方法发送数据时,同样也会通过 sock_fd 句柄找到对应的文件,根据文件指向的 sock 结构,找到这个 sock 结构里带的发送缓冲区,将数据会放到发送缓冲区,然后结束流程,内核看“心情”决定什么时候将这份数据发送出去。接收数据流程也类似,当数据送到 linux 内核后,数据不是立马给到应用程序的,而是先放在接收缓冲区,数据静静躺着,卑微的等待应用程序什么时候执行 recv() 方法来提取一下。

sock的发送和接收缓冲区
图 13 sock的发送和接收缓冲区

IP和端口其实不在sock下,而在inet_sock下,上图这么画只是为了简化。

那么问题来了,发送数据是应用程序主动发起。那接收数据呢?数据从远端发过来了,怎么通知并给到应用程序呢?这就需要用到等待队列。当应用进程执行 recv() 方法尝试获取(阻塞场景下)接收缓冲区的数据时。

① 如果有数据,那正好,取走就好了。

② 但如果没数据,就会将自己的进程信息注册到这个 sock 用的等待队列里,然后进程休眠。如果这时候有数据从远端发过来了,数据进入到接收缓冲区时,内核就会取出 sock 的等待队列里的进程,唤醒进程来取数据。

有时候,会看到多个进程通过 fork 的方式,listen 了同一个 socket_fd。在内核,它们都是同一个 sock,多个进程执行 listen() 之后,都嗷嗷等待连接进来,所以都会将自身的进程信息注册到这个 socket_fd 对应的内核 sock 的等待队列中。如果这时真来了一个连接,是该唤醒等待队列里的哪个进程来接收连接呢?

sock内的等待队列
图 14 sock内的等待队列

recv时无数据进程进入等待队列
图 15 recv时无数据进程进入等待队列

在 linux 2.6 以前,会唤醒等待队列里的所有进程。但最后其实只有一个进程会处理这个连接请求,其他进程又重新进入休眠,这些被唤醒了又无事可做最后只能重新回去休眠的进程会消耗一定的资源,在计算机领域中,就叫惊群效应

惊群效应
图 16 惊群效应

在 linux 2.6 之后,只会唤醒等待队列里的其中一个进程。socket监听的惊群效应问题被修复了。

到这里,问题又来了。服务端 listen 的时候,那么多数据到一个 socket 怎么区分多个客户端的?

以 TCP 为例,服务端执行 listen 方法后,会等待客户端发送数据来。客户端发来的数据包上会有源 IP 地址和端口,以及目的 IP 地址和端口,这四个元素构成一个四元组,可以用于唯一标记一个客户端。

其实说四元组并不严谨,因为过程中还有很多其他信息,也可以说是五元组。。。大概理解就好

四元组
图 17 四元组

服务端会创建一个新的内核 sock,并用四元组生成一个 hash key,将它放入到一个 hash 表中。下次再有消息进来的时候,通过消息自带的四元组生成 hash key 再到这个 hash 表里重新取出对应的 sock 就好了。所以说服务端是通过四元组来区分多个客户端的。

四元组映射成hash键
图 18 四元组映射成hash键

socket 怎么实现“继承”

最后一个问题:大家都知道 linux 内核是 C 语言实现的,而 C 语言没有类也没有继承的特性,是怎么做到”继承”的效果的呢?在C语言里,结构体里的内存是连续的,将要继承的”父类”,放到结构体的第一位,就像下面这样

struct tcp_sock {
    /* inet_connection_sock has to be the first member of tcp_sock */
    struct inet_connection_sock inet_conn;
        // 其他字段
}

struct inet_connection_sock {
    /* inet_sock has to be the first member! */
    struct inet_sock   icsk_inet;
        // 其他字段
}

然后就可以通过结构体名的长度来强行截取内存,这样就能转换结构体,从而实现类似”继承”的效果。

// sock 转为 tcp_sock
static inline struct tcp_sock *tcp_sk(const struct sock *sk)
{
    return (struct tcp_sock *)sk;
}

内存布局
图 19 内存布局

综上所述,可以知道:

① socket 中文套接字,我理解为一套用于连接的数字。并不一定准确,欢迎评论。

② sock 在内核,socket_fd 在用户空间,socket 层介于内核和用户空间之间。

③ 在操作系统内核空间里,实现网络传输功能的结构是 sock,基于不同的协议和应用场景,会被泛化为各种类型的 xx_sock,它们结合硬件,共同实现了网络传输功能。为了将这部分功能暴露给用户空间的应用程序使用,于是引入了 socket 层,同时将 sock 嵌入到文件系统的框架里,sock 就变成了一个特殊的文件,用户就可以在用户空间使用文件句柄,也就是 socket_fd 来操作内核 sock 的网络传输能力。

④ 服务端可以通过四元组来区分多个客户端。

⑤ 内核通过 C 语言”结构体里的内存是连续的”这一特点实现了类似继承的效果。

参考

1. socket到底是什么?别再把它叫套接字了

版权声明:除特殊说明,本博客文章均采用CC BY-SA 4.0许可证授权,转载请附上出处链接及本声明。 | 博客订阅:RSS | 广告招租请留言 |
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇