Linux网络数据包的揭秘以及常见的调优方式总结

可直接点击上方蓝字

(网易游戏运维平台)

关注我们,获一手游戏运维方案

lott

网易游戏业务 SRE, 专注于业务运维的质量和效率 , 喜欢研究 Linux 系统原理。目前负责《一梦江湖》、《猎魂觉醒》、《非人学园》等产品的运维工作。

总结是进步的阶梯、分享是快乐的源泉 , 技术人就是要不断总结、不断分享。

作为业务 SRE,我们所运维的业务,常常以 Linux+TCP/UDP daemon 的形式对外提供服务。SRE 需要对服务器数据包的接收和发送路径有全面的了解,以方便在服务异常时能快速定位问题。

以 tcp 协议为例,本文将对 Linux 内核网络数据包接收的路径进行整理和说明,希望对大家所有帮助。

Linux 数据包接收路径的整体说明

接收数据包是一个复杂的过程,涉及很多底层的技术细节 , 这里先做一下大概的说明 :

NIC (network interface card) 在系统启动过程中会向系统注册自己的各种信息,系统会分配专门的内存缓冲区,

NIC 接收到数据包之后,就会存放在内存缓冲区,通过硬件中断通知内核有新的数据包需要处理 .

内核从缓冲区取走 NIC 接收过来的数据,交给 TCP/IP 协议栈处理。

内核的 TCP/IP 协议栈代码进行处理后,更新协议的各种状态,然后交给应用程序的 socket buffer。

然后应用程序就可以通过 read() 系统调用,从对应的 socket 文件中,读取数据。

对内核数据包接收的路径做一下分层,总体可分为三层 :

  1. 网卡层面

  • 1.1 网卡接收到数据包

  • 1.2 将数据包从网卡硬件转移到主机内存中 .

  • 内核层面

    • 2.1 TCP/IP 协议逐层处理

  • 应用程序层面

    • 3.1 应用程序通过 read() 系统调用 , 从 socket buffer 读取数据

    如下图 :

    接下来解释一下什么是 NAPI

    什么是 NAPI

    系统启动时会为网卡分配  Ring Buffer (环形缓冲区 ), Ring Buffer 放的是一个个 Packet Descriptor(数据包描述符),是实际数据包的指针。实际的数据包是存放在另一块内存区域中(由网卡 Driver 预先申请好),称为 sk_buffers, sk_buffers 是可以由 DMA(https://en.wikipedia.org/wiki/DMA) 直接访问的 .

    Ring Buffer 里的 Packet Descriptor ,有两种状态:ready 和 used 。初始时 Descriptor 是空的,指向一个空的 sk_buffer,处在 ready 状态。当有数据时,DMA 负责从 NIC 取数据,并在 Ring Buffer 上按顺序找到下一个 ready 的 Descriptor,将数据存入该 Descriptor 指向的 sk_buffer 中,并标记 Descriptor 为 used。因为是按顺序找 ready 的 Descriptor, 所以 Ring Buffer 是个 FIFO 的队列。

    内核采用 struct sk_buffer(https://elixir.bootlin.com/linux/v4.4/source/include/linux/skbuff.h#L545) 来描述一个收到的数据包, sk_buffer 内有个 data 指针会指向实际的物理内存。

    当通过 DMA 机制存放完数据之后,NIC 会触发一个 IRQ(硬件中断) 让 CPU 去处理收到的数据。因为每次触发 IRQ 后 CPU 都要花费时间去处理 Interrupt Handler,如果 NIC 每收到一个 Packet 都触发一个 IRQ 会导致 CPU 花费大量的时间执行 Interrupt Handler,而每次执行只能从 Ring Buffer 中拿出一个 Packet,虽然 Interrupt Handler 执行时间很短,但这么做非常低效,并会给 CPU 带来很多负担。所以目前都是采用一个叫做 New API(NAPI)(https://wiki.linuxfoundation.org/networking/napi) 的机制,去对 IRQ 做合并以减少 IRQ 次数,目前大部分网卡 Driver 都支持 NAPI 机制。NAPI 机制是如何合并和减少 IRQ 次数的 , 可以简单理解为: 中断 + 轮询 。在数据量大时,一次中断后通过轮询接收一定数量数据包再返回,避免产生多次中断 , 具体细节大家可以参考这篇文章 (https://ylgrgyq.github.io/2017/07/23/linux-receive-packet-1/).

    概括一下网卡层面整个数据包的接收过程:

    1. 驱动程序事先在内存中分配一片缓冲区来接收数据包 , 叫做 sk_buffers.

    2. 将上述缓冲区的地址和大小(即数据包描述符),加入到 rx ring buffer。描述符中的缓冲区地址是 DMA 使用的物理地址 ;

    3. 驱动程序通知网卡有新的描述符 (或者说有空闲可用的描述符 )

    4. 网卡从 rx ring buffer 中取出描述符 , 从而获取缓冲区的地址和大小 .

    5. 当一个新的数据包到达,网卡 (NIC) 调用 DMA engine,把数据包放入 sk_buffer.

    如果整个过程正常 , 网卡会发起中断,通知内核的中断程序将数据包传递给 IP 层,进入 TCP/IP 协议栈处理。

    每个数据包经过 TCP 层一系列复杂的步骤,更新 TCP 状态机,最终到达 socket 的 recv Buffer,等待被应用程序接收处理。

    然后 , 内核应该会把刚占用掉的描述符重新放入 ring buffer,这样网卡就可以继续使用描述符了。

    我们可以使用 ethtool 命令,进行 Ring Buffer 的查看和设置 .

    1 查看网卡当前的设置(包括Ring  Buffer): ethtool -g eth1
    2 改变Ring Buffer大小: ethtool -G eth1 rx 4096 tx 4096
    

    四 中断处理程序如何把数据包传递给网络协议层

    我们通过一张图来说明下 ,

    上图中涉及到非常多的技术细节,限于篇幅我们只做总体的说明 :

    1. NIC 发起的硬件中断(也称为中断处理的上半部),被内核执行之后,开启了软中断(中断处理的下半部),并马上退出硬件中断处理程序 , 以便其他硬件可以继续发起硬件中断 .

    2. 软中断处理程序中,通过 poll 循环把数据从 Ring Buffer 取走,传给网络协议层处理,然后重新开启之前已经禁用的网卡硬件中断 .

    3. 当有新的数据包到达网卡时 , 回到第 1 步 .

    这里有几点需要额外说明 :

    什么是中断处理的上半部和下半部

    我们知道中断随时可能发生,因此中断处理程序也就随时可能执行。所以必须保证中断处理程序能够快速执行,这样才能尽快恢复被中断的代码。因此尽管对硬件而言,操作系统能迅速对其中断进行服务非常重要,而对于系统其他部分而言,让中断处理程序尽可能在短时间内完成运行也同样重要。所以我们一般把中断处理切为 2 个部分,上半部在接收到一个中断时立刻开始执行,但他只做必要的工作,例如对接收的中断进行应答或复位硬件,这些工作都是在所有中断被禁止的情况下完成的。而那些允许被稍后执行的工作,都会推到下半部去,下半部并不会马上执行,而是会在稍后适当的时机执行。

    网卡的软中断处理

    现在的网卡基本都支持 RSS(Receive Side Scaling)(https://en.wikipedia.org/wiki/Network_interface_controller#RSS),也就是多对列技术。一张网卡有多个队列,每个队列都有各自的 IRQ 号和 Ring Buffer,但是默认情况下网卡的软中断都是在 CPU0 上处理,在流量大的时候,会造成 CPU0 负载打满,引起丢包. 我们可以通过绑定中断和 CPU 的亲和性,把中断处理均衡到多核心上 (https://www.vpsee.com/2010/07/load-balancing-with-irq-smp-affinity/),提升系统整体性能 .

    什么是 RPS

    RPS 全称是 Receive Packet Steering, 采用软件模拟的方式,实现了多队列网卡所提供的功能,分散了在多 CPU 系统上数据接收时的软中断负载, 把软中断分到各个 CPU 处理,而不需要硬件支持,在多核 CPU 和单队列网卡的情况下,开启 RPS 可以大大提升网络性能 .

    如果系统开了 RPS, 数据包会被缓冲在 TCP 层之前的队列中 , 我们可以通过 net.core.netdev_max_backlog 适当加大这个队列的长度,以保证上层的处理时间 .

    TCP/IP 协议栈层面

    此时数据包已经接入内核处理区域,由内核的 TCP/IP 协议栈处理

    (一) 连接建立

    大家知道,两个基于 tcp 协议的 socket 要通信,首先要进行连接建立的过程,然后才是数据传输的过程。

    我们先简单看下连接的建立过程,客户端向 server 发送 SYN 包,server 回复 SYN+ACK,同时将这个处于 SYN_RECV 状态的连接保存到半连接队列。客户端返回 ACK 包完成三次握手,server 将 ESTABLISHED 状态的连接移入 accept 队列,等待应用调用 accept()。

    可以看到建立连接涉及两个队列:

    • 半连接队列 (SYN Queue): 保存 SYN_RECV 状态的连接。队列长度由 net.ipv4.tcp_max_syn_backlog 设置

    • 完整连接队列 (ACCEPT Queue): 保存 ESTABLISHED 状态的连接。队列长度为 min(net.core.somaxconn, backlog)。其中 backlog 是我们创建 ServerSocket(int port,int backlog) 时指定的参数,最终会传递给 listen 方法:

    #include
    int listen(int sockfd, int backlog);
    

    如果我们设置的 backlog 大于 net.core.somaxconn,完整连接队列的长度将被设置为 net.core.somaxconn。

    注意:不同的编程语言都有相应的 socket 申请方法 , 比如 Python 是 socket 模块.在服务端监听一个端口,底层都要经过 3 个步骤:

    申请 socket、bind 相应的 IP 和 port、调用 listen 方法进行监听。这个 listen 方法 python 会进行封装,别的编程语言也会进行封装,但最终都是调用系统的 listen() 调用

    我们对这两个队列做一下总结 :

    (二) 数据传输

    连接建立后 , 就到了 socket 数据传输的层面。此时 kernel 能够为应用程序做的,就是通过 socket Recv Buffer 缓存数据 , 尽量保证上层处理时间 .

    1  Recv Buffer 自动调节机制

    kernel 可以根据实际情况,自动调节 Recv Buffer 的大小 , 以期找到性能和资源的平衡点 .

    当 net.ipv4.tcp_moderate_rcvbuf 设置为 1 时,自动调节机制生效,每个 TCP 连接的 recv Buffer 由下面的 3 元数组指定 (min, default, max):

    net.ipv4.tcp_rmem = 4096    87380   16777216
    

    最初 Recv Buffer 被设置为 87380,同时这个缺省值会覆盖 net.core.rmem_default 的设置 , 随后 recv buffer 根据实际情况在最大值和最小值之间动态调节。

    当 net.ipv4.tcp_moderate_rcvbuf 被设置为 0,或者设置了 socket 选项 SO_RCVBUF,缓冲的动态调节机制被关闭。

    如果缓冲的动态调节机制被关闭 , 同时 socket 自己也没有设置 SO_RCVBUF 选项,那么一个 socket 的默认 Buffer 大小将由 net.core.rmem_default 决定,但是应用程序仍然可以通过 setsockopt() 系统调用,加大自己的 Recv Buffer, 最大不能超过 net.core.rmem_max 的设定 .

    因此,我们可以得出如下总结 :

    • 没有特殊情况 , 建议打开 net.ipv4.tcp_moderate_rcvbuf=1, 这样 kernel 会自动调整每个 socket 的 Recv Buffer

    • 我们应该把 net.ipv4.tcp_rmem 中 max 值和 net.core.rmem_max 值设置成一致,这样假设应用程序没有关注到这个点,仍然可以由 kernel 把它自动调节成系统最大的 Recv Buffer.

    • Recv Buffer 的默认值可以适当进行提高 , 包括 net.core.rmem_default 和 net.ipv4.tcp_rmem 中的 default 设置 , 以更加激进的方式传输数据 .

    关于 Linux 接收数据包链路优化的整体总结

    参考文章

    1. linux 网络之数据包的接受过程

      https://www.jianshu.com/p/e6162bc984c8

    2. Linux 网络协议栈收消息过程 -Ring Buffer

      https://ylgrgyq.github.io/2017/07/23/linux-receive-packet-1/

    3. Linux 网络协议栈收消息过程 -Per CPU Backlog

      https://ylgrgyq.github.io/2017/07/24/linux-receive-packet-2/

    4. 网卡收发包总结

      https://www.zybuluo.com/myecho/note/1068383

    5. /proc/sys/net 文档说明

      https://www.kernel.org/doc/Documentation/sysctl/net.txt

    6. NAPI

      https://wiki.linuxfoundation.org/networking/napi

    7. Linux 技巧 : 多核下绑定网卡中断到不同 CPU(core)总结

      https://blog.csdn.net/benpaobagzb/article/details/51044420

    8. Network interface controller

      https://en.wikipedia.org/wiki/Network_interface_controller#RSS