Kyle's Notebook

Linux 网络原理与分析

Word count: 8.6kReading time: 33 min
2021/04/05

Linux 网络原理与分析

本文侧重点在于讲解 Linux 内核协议栈网络相关的内容,而 TCP/IP 网络模型的内容不会过多提及。

了解更多网络相关的原理知识请参考《计算机网络》。

  • 网络性能指标
    • 应用层
      • QPS(每秒请求数)
      • 套接字缓冲区大小
      • DNS 响应延迟
      • 响应时间
      • 错误数
    • 传输层
      • TCP 连接数
        • 全连接
        • 半连接
        • TIMEWAIT
      • 连接跟踪数
      • 重传数
      • 丢包数
      • 延迟
    • 网络层
      • 丢包数
      • TTL
      • 拆包
    • 链路层
      • PPS(每秒网络帧数)
      • BPS(每秒字节数)
      • 丢包数
      • 错误数

网络模型

OSI 网络模型:即来自国际标准化组织制定的开放式系统互联通信参考模型(Open System Interconnection Reference Model),分为七层,其各自负责:

  • 应用层:为应用程序提供统一的接口。

  • 表示层:把数据转换成兼容接收系统的格式。

  • 会话层:维护计算机之间的通信连接。

  • 传输层:为数据加上传输表头,形成数据包。

  • 网络层:数据的路由和转发。

  • 数据链路层:MAC 寻址、错误侦测和改错。

  • 物理层:在物理网络中传输数据帧。

TCP/IP 网络模型:Linux 中具体实现的四层网络模型。

  • 应用层:向用户提供一组应用程序,比如 HTTP、FTP、DNS 等。对应 OSI 模型的应用层、表示层和会话层。

  • 传输层:端到端的通信,比如 TCP、UDP 等。

  • 网络层:网络包的封装、寻址和路由,比如 IP、ICMP 等。

  • 网络接口层:网络包在物理网络中的传输,比如 MAC 寻址、错误侦测以及通过网卡传输网络帧等。对应 OSI 模型的数据链路层和物理层。

网络传输时,数据包会按照协议栈对上层数据逐层进行处理,然后封装该层的协议头,再发送给下一层(在原有的负载前后增加固定格式的元数据)。

由于物理链路中不能传输任意大小的数据包,网络接口配置的最大传输单元(MTU,以太网中默认值为 1500)规定了最大的 IP 包大小。

Linux 内核中的网络协议栈也类似 TCP/IP 的四层结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+--------------------+
| 应用程序 |
+-------------------+
| 系统调用 |
+------------+------+
| VFS |
+------------+------+
| 套接字 |
+-----+-----+--------+
| TCP | UDP | +-----+
+----+----+------+ ICMP |
| IP +------+
+----------------- ARP |
| 链路层 ------+
+----+-----+----+
| igb | ... | bnx |
+----+-----+----+
| 网卡 | 网卡 | 网卡 |
+-----+------+-----+

其中网卡是发送和接收网络包的基本设备。在系统启动时,网卡通过内核中的网卡驱动程序注册到系统中。在网络收发过程中,内核通过中断与网卡进行交互。

网络收发流程

网络包接收

网卡接收到网络帧:

  • 通过 DMA 把包放到收包队列中。

  • 通过硬中断告知中断处理程序已收到网络包。

  • 网卡中断处理程序为网络帧分配内核数据结构(sk_buff),将其拷贝到 sk_buff 缓冲区中。

  • 通过软中断通知内核收到了新的网络帧。

  • 内核协议栈从缓冲区中取出网络帧,通过网络协议栈从下到上逐层处理。

DMA(Direct Memory Access):在内存和 I/O 设备进行数据传输时,经过独立的协处理器(Co-Processor)DMAC(DMA Controller)辅助处理,而无需经过 CPU 控制。

网络协议栈解析:

  • 链路层:检查报文合法性,找出上层协议类型(比如 IPv4),再去除帧头帧尾,交给网络层。

  • 网络层:取出 IP 头,判断网络包下一步走向:交给上层处理或转发等。当确认这个包要发送到本机后,即取出上层协议类型(比如 TCP),去除 IP 头再交给传输层。

  • 传输层:取出 TCP/UDP 头后,根据 <源 IP、源端口、目的 IP、目的端口> 四元组作为标识,并把数据拷贝到对应的 Socket 的接收缓存中。

随即应用程序可以通过 Socket 接口读取到新接收的数据。

网络包发送

与接收相反,应用程序调用 Socket API(比如 sendmsg)发起系统调用发送网络包。

进入内核态的套接字层,把数据包放到 Socket 发送到缓冲区中。

网络协议栈从 Socket 发送缓冲区中取出数据包,再按照 TCP/IP 栈从上到下逐层处理:比如增加 TCP 头、IP 头,执行路由查找下一跳 IP、按照 MTU(Maximum Transmission Unit)分片等。

分片后的网络包再送到网络接口层进行物理地址寻址,以找到下一跳的 MAC 地址;再添加帧头和帧尾放到发包队列中。

随后发起软中断通知驱动程序,由驱动程序通过 DMA 从发包队列中读出网络帧,通过物理网卡把它发送出去。

套接字

套接字可屏蔽 Linux 内核中不同协议的差异,为应用程序提供统一访问接口。

每个套接字都有读写缓冲区:

  • 读缓冲区缓存远端发来的数据,读缓冲区满就不能再接收新的数据。

  • 写缓冲区缓存要发出去的数据,写缓冲区满,应用程序写操作就会被阻塞。

img

性能指标

分析网络问题常参考以下指标:

  • 带宽:链路的最大传输速率,单位通常为 b/s。常用带宽有 1000M、10G、40G、100G 等。

  • 吞吐量:表示单位时间内成功传输的数据量,单位通常为 b/s(比特 / 秒)或者 B/s(字节 / 秒)。其受带宽限制,网络的使用率 == 吞吐量 / 带宽。

  • 延时:表示从网络请求发出后一直到收到远端响应所需的时间。可以表示为建立连接需要的时间(比如 TCP 握手延时),或一个数据包往返所需的时间(比如 RTT)等。

  • 传输速率:一般以 PPS(Packet Per Second,包 / 秒)衡量,即以网络包为单位的传输速率,常用于评估网络转发能力,比如硬件交换机通常可以达到线性转发(即 PPS 可以达到或者接近理论最大值),基于 Linux 服务器的转发则容易受网络包大小的影响;而对 TCP 或 Web 服务,更多会用并发连接数和每秒请求数(QPS,Query per Second)等指标反映实际应用程序的性能。

其中带宽跟物理网卡配置是直接关联的,只是实际带宽会受限于整个网络链路中最小的模块。而“网络带宽测试”实际上是测试吞吐量。

除此之外还关心网络可用性(网络能否正常通信)、并发连接数(TCP 连接数量)、丢包率(丢包百分比)、重传率(重新传输的网络包比例)等。

网络配置

分析网络问题需要查看网络接口的配置状态,使用 ifconfigip 命令(net-tools/iproute2):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ifconfig eth0
# 物理连通 MTU,因网络架构而作调整(比如 VXLAN 等)
# eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
# IP 地址、子网掩码、MAC 地址
# inet 10.240.0.30 netmask 255.240.0.0 broadcast 10.255.255.255
# inet6 fe80::20d:3aff:fe07:cf2a prefixlen 64 scopeid 0x20<link>
# ether 78:0d:3a:07:cf:3a txqueuelen 1000 (Ethernet)
# 收发字节数、包数、错误数和丢包情况:指标不为 0 时通常表示出现网络 I/O 问题。
# RX packets 40809142 bytes 9542369803 (9.5 GB)
# RX errors 0 dropped 0 overruns 0 frame 0
# TX packets 32637401 bytes 4815573306 (4.8 GB)
# TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

ip -s addr show dev eth0
# 网络连通
# 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
# link/ether 78:0d:3a:07:cf:3a brd ff:ff:ff:ff:ff:ff
# inet 10.240.0.30/12 brd 10.255.255.255 scope global eth0
# valid_lft forever preferred_lft forever
# inet6 fe80::20d:3aff:fe07:cf2a/64 scope link
# valid_lft forever preferred_lft forever
# RX: bytes packets errors dropped overrun mcast
# 9542432350 40809397 0 0 0 193
# TX: bytes packets errors dropped carrier collsns
# 4815625265 32637658 0 0 0 0

网络 I/O 异常状态:

  • errors:错误数据包数,校验错误、帧同步错误等。

  • dropped:丢弃数据包数。数据包已经收到了 Ring Buffer,但因为内存不足等原因丢包。

  • overruns:超限数据包数。网络 I/O 速度过快,导致 Ring Buffer 中的数据包来不及处理(队列满)而导致的丢包;

  • carrier:发生 carrirer 错误的数据包数。比如双工模式不匹配、物理电缆出现问题等。

  • collisions:碰撞数据包数。

套接字信息

使用 netstatss 查看套接字信息:

1
2
3
4
5
6
7
8
9
10
11
12

# -l 只显示监听套接字,-n 显示数字地址和端口,-p 显示进程信息
netstat -nlp | head -n 3
# Active Internet connections (only servers)
# Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
# tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN 840/systemd-resolve

# -l 只显示监听套接字,-t 只显示 TCP 套接字,-n 显示数字地址和端口,-p 表示显示进程信息
ss -ltnp | head -n 3
# State Recv-Q Send-Q Local Address:Port Peer Address:Port
# LISTEN 0 128 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=840,fd=13))
# LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1459,fd=3))

如果接收队列(Recv-Q)和发送队列(Send-Q)的值不为 0,说明有网络包的堆积发生。当 Socket 的状态为:

  • Established:Recv-Q 表示套接字缓冲未被应用程序取走的字节数(即接收队列长度),Send-Q 表示还没有被远端主机确认的字节数(即发送队列长度)。

  • Listening:Recv-Q 表示全连接队列的长度,Send-Q 表示全连接队列的最大长度。

全连接:服务器收到了客户端的 ACK、完成了 TCP 三次握手,然后把这个连接放到全连接队列中。全连接中的套接字还需要被 accept() 系统调用取走,服务器才可以开始真正处理客户端请求。

半连接:未完成 TCP 三次握手的连接,服务器收到客户端 SYN 包后会把它放到半连接队列中,再向客户端发送 SYN+ACK 包。

协议栈信息

使用 netstatss 查看协议栈信息:包括 TCP 协议的主动连接、被动连接、失败重试、发送和接收的分段数量等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
netstat -s
# ...
#
# Tcp:
# 3244906 active connection openings
# 23143 passive connection openings
# 115732 failed connection attempts
# 2964 connection resets received
# 1 connections established
# 13025010 segments received
# 17606946 segments sent out
# 44438 segments retransmitted
# 42 bad segments received
# 5315 resets sent
# InCsumErrors: 42
# ...

ss -s
# Total: 186 (kernel 1446)
# TCP: 4 (estab 1, closed 0, orphaned 0, synrecv 0, timewait 0/0), ports 0
#
# Transport Total IP IPv6
# * 1446 - -
# RAW 2 1 1
# UDP 2 2 0
# TCP 4 3 1
# ...

网络质量统计信息

使用 sar 可查看网络统计信息,比如网络接口(DEV)、网络接口错误(EDEV)、TCP、UDP、ICMP 等:

1
2
3
4
5
6
7
sar -n DEV 1
# Linux 4.15.0-1035-azure (ubuntu) 01/06/19 _x86_64_ (2 CPU)
# 接收和发送的 PPS 接收和发送的吞吐量 接收和发送的压缩数据包数 网络接口使用率
# 13:21:40 IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s %ifutil
# 13:21:41 eth0 18.00 20.00 5.79 4.25 0.00 0.00 0.00 0.00
# 13:21:41 docker0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
# 13:21:41 lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00

其中网络接口的使用率:半双工模式下为 (rxkB/s+txkB/s)/Bandwidth,全双工模式下为 max(rxkB/s, txkB/s)/Bandwidth。

使用 ethtool 可查询带宽(单位 Gb/s 或 Mb/s,注意是比特):

1
2
ethtool eth0 | grep Speed
# Speed: 1000Mb/s

使用基于 ICMP 协议的 ping 可测试远程主机的连通性和延时:

1
2
3
4
5
6
7
8
9
10
11
12
ping -c3 114.114.114.114
# PING 114.114.114.114 (114.114.114.114) 56(84) bytes of data.

# 每个 ICMP 请求的信息,包括 ICMP 序列号(icmp_seq)、TTL(生存时间,或者跳数)和往返延时。
# 64 bytes from 114.114.114.114: icmp_seq=1 ttl=54 time=244 ms
# 64 bytes from 114.114.114.114: icmp_seq=2 ttl=47 time=244 ms
# 64 bytes from 114.114.114.114: icmp_seq=3 ttl=67 time=244 ms

# 三次 ICMP 请求汇总。
# --- 114.114.114.114 ping statistics ---
# 3 packets transmitted, 3 received, 0% packet loss, time 2001ms
# rtt min/avg/max/mdev = 244.023/244.070/244.105/0.034 ms

优化方法

从 C10K、C100K、C1000K、C10M 问题入手可以更好理解 Linux 网络工作原理。

从 C1000K 开始,依靠单机支撑就需要仔细考虑:

  • 从物理资源的角度:处理大量请求需要配备大内存、万兆网卡,以及基于多网卡绑定以承载大吞吐量。

  • 从软件资源的角度:大量的连接也会占用大量的软件资源,如文件描述符、连接状态跟踪(CONNTRACK)、网络协议栈缓存(比如套接字读写缓存、TCP 读写缓存)等。

  • 从优化的角度:中断处理处理成本也很高,需要多队列网卡、中断负载均衡、CPU 绑定、RPS/RFS(软中断负载均衡到多个 CPU 核上),以及将网络包的处理卸载(Offload)到网络设备(如 TSO/GSO、LRO/GRO、VXLAN OFFLOAD)等各种硬件和软件的优化。

I/O 模型优化

当物理资源足够,首先考虑的是 I/O 模型:如何在一个线程内处理多个请求、如何节省资源地处理用户请求。

I/O 多路复用是著名的并发解决方案,其提供两种 I/O 事件通知方式:

  • 水平触发:只要文件描述符可以非阻塞地执行 I/O 就会触发通知。即应用程序可随时检查文件描述符的状态,然后再根据状态进行 I/O 操作。

  • 边缘触发:只有在文件描述符的状态发生改变(I/O 请求达到)时才发送一次通知。此时应用程序需要尽可能多地执行 I/O,直到无法继续读写才可以停止。如果 I/O 没执行完,或因为某种原因未来得及处理,这次通知就会丢失。

I/O 多路复用也有多种实现方法:

  • 非阻塞 I/O + 水平触发通知:select 和 poll 从文件描述符列表中找出可以执行 I/O ,再进行真正的读写。由于 I/O 是非阻塞的,一个线程中就可以同时监控一批套接字文件描述符,达到单线程处理多请求的目的。但也存在以下问题:

    • 需要对文件描述符列表进行轮询,请求数多时会比较耗时。

    • select 使用固定长度的位相量表示文件描述符集合,有最大描述符数量的限制(32 位系统 1024);轮询检查套接字状态,处理耗时 O(N)。

    • poll 使用不定长数组存放文件描述符,没有最大描述符数量的限制(只受系统文件描述符限制),但也需要轮询文件描述符列表。

    • 调用 select 和 poll 时需要把文件描述符集合从用户空间传入内核空间,由内核修改后再传出到用户空间中,存在切换成本。

  • 非阻塞 I/O + 边缘触发通知:Linux 2.6 新增的 epoll 使用 红黑树 在内核中管理 文件描述符,不需要应用程序在每次操作时都传入传出;使用事件驱动机制,只关注有 I/O 事件发生的文件描述符,而不是轮询扫描整个集合。

  • 异步 I/O(Asynchronous I/O):Linux 2.6 新增的异步 I/O 允许应用程序同时发起多个 I/O 操作,在 I/O 完成后系统以事件通知(比如信号或者回调函数)告知应用程序前来查询结果。然而在较长时间内都不完善(比如 glibc 提供的异步 I/O 库),且使用难度较高。

工作模型优化

I/O 多路复用还可以结合不同工作模型使用:

主进程 + 多个 worker 子进程

Nginx 反向代理的原理就是基于此:主进程用于初始化套接字,执行 bind() + listen() 后创建多个子进程、管理其生命周期,每个子进程都通过 accept()epoll_wait() 来处理相同套接字。

惊群效应:当网络 I/O 事件发生时,多个进程被同时唤醒,但实际上只有一个进程来响应这个事件,其他被唤醒的进程都会重新休眠。accept()(Linux 2.6 中解决)和 epoll_wait()(Linux 4.5 中通过 EPOLLEXCLUSIVE 解决)。

Nginx 在每个 worker 进程中增加全局锁(accept_mutex),worker 进程需要先竞争到锁才会加入到 epoll 中,确保只有一个 worker 子进程被唤醒。

可以用线程代替进程:主线程负责套接字初始化和子线程状态的管理,而子线程则负责实际的请求处理。线程的调度和切换成本比较低,再进一步把 epoll_wait() 都放到主线程中,保证每次事件都只唤醒主线程,子线程只需负责后续的请求处理。

监听到相同端口的多进程

所有的进程监听相同端口,并开启 SO_REUSEPORT 选项(Linux 3.9+),由内核将请求负载均衡到监听进程中。

内核确保只有一个进程被唤醒,不会出现惊群问题。

DPDK 和 XDP

在 C10M 问题中,单靠常规软硬件优化已经难以满足需求。

其根本原因是 Linux 内核协议栈工作太繁重:

网卡硬中断 -> 中断处理程序 -> 网卡软中断 -> 内核协议栈 -> 网络协议栈 -> 应用程序的路径太长,导致网络包处理难以优化。

DPDK(Data Plane Development Kit,数据平面开发套件)是用户态网络标准,其跳过内核协议栈、由用户态进程轮询处理网络接收:

  • PPS 非常高的场景中,查询时间比实际工作时间少很多,绝大部分时间都能处理网络包。

  • 跳过内核协议栈后省去了繁杂的处理路径,应用程序可针对具体场景优化网络包处理逻辑,不需要关注所有细节。

  • 通过大页、CPU 绑定、内存对齐、流水线并发等多种机制,优化网络包的处理效率。

pr-3303

XDP(eXpress Data Path):是 Linux 内核提供的高性能网络数据路径,它允许网络包在进入内核协议栈之前就进行处理。XDP 底层和 bcc-tools 一样,都是基于 Linux 内核的 eBPF 机制实现。其对内核的要求比较高(Linux 4.8+),并且不提供缓存队列。基于 XDP 的应用程序通常是专用的网络应用,如 IDS(入侵检测系统)、DDoS 防御、 cilium 容器网络插件等。

XDP Packet Processing

性能测试

基准测试

测试前应确定待评估的应用程序网络性能属于协议栈哪一层。

转发性能

包括网络接口层和网络层,通常关注每秒可处理的网络包数 PPS,特别是 64B 小包的处理能力。

使用 hping3 工具可测试网络包处理能力的性能,但更常用的是内核自带的 pktgen,其作为一个内核线程运行,需要加载 pktgen 内核模块后再通过 /proc 文件系统来交互:

1
2
3
4
5
6
7
8
9
# 需要配置 pktgen 内核模块(即 CONFIG_NET_PKTGEN=m)后重新编译内核才可以使用。
modprobe pktgen

ps -ef | grep pktgen | grep -v grep
# root 26384 2 0 06:17 ? 00:00:00 [kpktgend_0]
# root 26385 2 0 06:17 ? 00:00:00 [kpktgend_1]

ls /proc/net/pktgen/
# kpktgend_0 kpktgend_1 pgctrl

pktgen 在每个 CPU 上启动一个内核线程,可通过 /proc/net/pktgen 下的同名文件与线程交互;pgctrl 用于开启和停止测试。

假设一台机器通过 eth0 网卡向另一台机器发包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 定义一个工具函数,方便后面配置各种测试选项
function pgset() {
local result
echo $1 > $PGDEV

result=`cat $PGDEV | fgrep "Result: OK:"`
if [ "$result" = "" ]; then
cat $PGDEV | fgrep Result:
fi
}

# 为 0 号线程绑定 eth0 网卡
PGDEV=/proc/net/pktgen/kpktgend_0
pgset "rem_device_all" # 清空网卡绑定
pgset "add_device eth0" # 添加eth0网卡

# 配置 eth0 网卡的测试选项
PGDEV=/proc/net/pktgen/eth0
pgset "count 1000000" # 总发包数量
pgset "delay 5000" # 不同包之间的发送延迟(单位纳秒)
pgset "clone_skb 0" # SKB包复制
pgset "pkt_size 64" # 网络包大小
pgset "dst 192.168.0.30" # 目的IP
pgset "dst_mac 11:11:11:11:11:11" # 目的MAC

# 启动测试
PGDEV=/proc/net/pktgen/pgctrl
pgset "start"

一段时间测试完成后,可以从 /proc 获取结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

$ cat /proc/net/pktgen/eth0
# 测试选项
# Params: count 1000000 min_pkt_size: 64 max_pkt_size: 64
# frags: 0 delay: 0 clone_skb: 0 ifname: eth0
# flows: 0 flowlen: 0
# ...
# 测试进度,packts so far(pkts-sofar)表示已经发送了 100 万个包。
# Current:
# pkts-sofar: 1000000 errors: 0
# started: 1534853256071us stopped: 1534861576098us idle: 70673us
# ...
# 测试结果,包含测试所用时间、网络包数量和分片、PPS、吞吐量以及错误数。
# Result: OK: 8320027(c8249354+d70673) usec, 1000000 (64byte,0frags)
# 120191pps 61Mb/sec (61537792bps) errors: 0

# 测试结果:PPS 为 120k,吞吐量为 61Mb/s,无发生错误。

测试结果可以参照千兆交换机的 PPS:线速(满负载、无差错转发)PPS 即 1000Mbit 除以以太网帧的大小,1000Mbps/((64+20)*8bit) = 1.5 Mpps(20B 为以太网帧前导和帧间距的大小)。

TCP/UDP 性能

一些应用直接基于 TCP/UDP 构建服务,适合使用 iperfnetperf 测试吞吐量。安装 iperf3:

1
apt -y install iperf3

目标机器上启动 iperf3 服务端:

1
2
# -s 启动服务端,-i 汇报间隔,-p 监听端口
iperf3 -s -i 1 -p 10000

在另一台机器上运行 iperf3 客户端:

1
2
# -c 启动客户端(指定目标服务器 ip),-b 目标带宽(单位是bits/s),-t 表示测试时间,-P 并发数,-p 目标服务器监听端口
iperf3 -c 192.168.0.30 -b 1G -t 15 -P 2 -p 10000

在服务端上查看输出:

1
2
3
4
5
6
# [ ID] Interval           Transfer     Bandwidth
# ...
# [SUM] 0.00-15.04 sec 0.00 Bytes 0.00 bits/sec sender
# [SUM] 0.00-15.04 sec 1.51 GBytes 860 Mbits/sec receiver

# 测试结果:TCP 接收的带宽(吞吐量)为 860 Mb/s

HTTP 性能

webbenchab(Apache 自带)等都是常用的 HTTP 压力测试工具,主要测试 HTTP 服务的每秒请求数、请求延迟、吞吐量以及请求延迟的分布情况等。安装 ab:

1
2
apt install -y apache2-utils
# CentOS: yum install -y httpd-tools

使用 ab 测试 Nginx 性能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# -c 并发请求数,-n 总请求数
ab -c 1000 -n 10000 http://192.168.0.30/
# ...
# Server Software: nginx/1.15.8
# Server Hostname: 192.168.0.30
# Server Port: 80

# ...
# 请求汇总
# 每秒处理请求数。
# Requests per second: 1078.54 [#/sec] (mean)
# 平均延迟,包括线程运行的调度时间和网络请求响应时间。
# Time per request: 927.183 [ms] (mean)
# 实际请求的响应时间。
# Time per request: 0.927 [ms] (mean, across all concurrent requests)
# 吞吐量。
# Transfer rate: 890.00 [Kbytes/sec] received

# 连接时间汇总
# Connection Times (ms)
# min mean[+/-sd] median max
# Connect: 0 27 152.1 1 1038 # 连接
# Processing: 9 207 843.0 22 9242 # 处理
# Waiting: 8 207 843.0 22 9242 # 等待
# Total: 15 233 857.7 23 9268 # 总

# 请求延迟汇总(百分位数)
# Percentage of the requests served within a certain time (ms)
# 50% 23 50% 的请求可在 23ms 内完成
# 66% 24
# 75% 24
# 80% 26
# 90% 274
# 95% 1195
# 98% 2335
# 99% 4663
# 100% 9268 (longest request)

应用负载性能

工具本身的性能对测试至关重要。通过以上工具测试不能直接表示应用程序的实际性能,其与用户的实际请求很可能不一致。比如用户请求往往附带各种负载(payload),会影响 Web 应用程序内部的处理逻辑从而影响最终性能。

因此测试还要求性能工具本身可以模拟用户的请求负载,常用的工具有 wrk、TCPCopy、Jmeter、LoadRunner 等(后两者甚至提供脚本录制、回放、GUI 等功能)。

wrk 内置 LuaJIT,可以用来实现复杂场景的性能测试。在调用 Lua 脚本时可将 HTTP 请求分为三个阶段,即 setup、running、done:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
setup:
+--------------------------------+
| setup(trread) | 每个线程执行一次
+--------------------------------+

running:
+--------------------------------+
| init(args) | 每个线程执行一次
+--------------------------------+
| delay() | {
+--------------------------------+
| request() | 每次请求调用
+--------------------------------+
| response(status,header,body) | }
+--------------------------------+

done:
+--------------------------------+
| done(summary,latency,requests) | 整个过程执行一次
+--------------------------------+

比如可以在 setup 阶段,为请求设置认证参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- auth.lua
-- example script that demonstrates response handling and
-- retrieving an authentication token to set on all future
-- requests
token = nil
path = "/authenticate"

request = function()
return wrk.format("GET", path)
end

response = function(status, headers, body)
if not token and status == 200 then
token = headers["X-Token"]
path = "/resource"
wrk.headers["X-Token"] = token
end
end

再启动测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 源码安装
cd wrk
apt -y install build-essential
make
cp wrk /usr/local/bin/

# -c 并发连接数,-t 线程数
wrk -c 1000 -t 2 -s auth.lua http://192.168.0.30/

# Running 10s test @ http://192.168.0.30/
# 2 threads and 1000 connections
# Thread Stats Avg Stdev Max +/- Stdev
# Latency 65.83ms 174.06ms 1.99s 95.85%
# Req/Sec 4.87k 628.73 6.78k 69.00%
# 96954 requests in 10.06s, 78.59MB read
# Socket errors: connect 0, read 0, write 0, timeout 179
# Requests/sec: 9641.31
# Transfer/sec: 7.82MB

优化总结

性能优化首先是要确定目标,不同应用中每个指标的优化标准、优先级都不同:

  • 对于 NAT 网关而言,其直接影响整个数据中心的网络出入性能,所以通常需要达到或接近线性转发,PPS 是最主要的性能目标。

  • 对于数据库、缓存等系统而言,目的是快速完成网络收发,即延迟是主要的性能目标。

  • 对于 Web 服务而言,需要同时兼顾吞吐量和延迟。

应用层

即优化 I/O 模型、工作模型以及应用层网络协议。

选用网络模型

  • 使用 I/O 多路复用 epoll 取代 select 和 poll。

  • 使用异步 I/O(比较复杂)。

选用工作模型

  • 主进程(管理网络连接)+ 多个 worker 子进程(实际的业务处理)。

  • 监听到相同端口的多进程模型。所有进程监听相同接口,开启 SO_REUSEPORT 选项,由内核负责进程的负载均衡。

协议优化

  • 使用长连接取代短连接:可显著降低 TCP 连接建立成本,在每秒请求次数较多时效果非常明显。

  • 使用内存来缓存不常变化的数据:可降低网络 I/O 次数,同时加快应用程序的响应速度。

  • 使用 Protocol Buffer 等序列化的方式,压缩网络 I/O 的数据量,可提高应用程序的吞吐。

  • 使用 DNS 缓存、预取、HTTPDNS 等方式减少 DNS 解析的延迟,并提升网络 I/O 整体速度。

套接字层

为了提高网络的吞吐量,通常需要调整缓冲区的大小。

  • 增大每个套接字的缓冲区大小 net.core.optmem_max。

  • 增大套接字接收缓冲区大小 net.core.rmem_max 和发送缓冲区大小 net.core.wmem_max。

  • 增大 TCP 接收缓冲区大小 net.ipv4.tcp_rmem 和发送缓冲区大小 net.ipv4.tcp_wmem(三个数值分别是 min,default,max,系统会根据设置自动调整 TCP 接收 / 发送缓冲区的大小,UDP 同理)。

修改网络连接行为的配置项:

  • 为 TCP 连接设置 TCP_NODELAY 可禁用 Nagle 算法。

  • 为 TCP 连接开启 TCP_CORK 可让小包聚合成大包后再发送(会阻塞小包发送)。

  • 使用 SO_SNDBUF 和 SO_RCVBUF 可分别调整套接字发送缓冲区和接收缓冲区大小。

传输层

主要时优化 TCP 和 UDP 协议。首先要了解 TCP 基本原理(比如流量控制、慢启动、拥塞避免、延迟确认)以及状态流转:

img

TCP

TIME_WAIT 状态优化:对于请求量较大的场景,可能会看到大量处于 TIME_WAIT 状态的连接占用内存和端口资源。

  • 增大处于 TIME_WAIT 状态的连接数量 net.ipv4.tcp_max_tw_buckets ,并增大连接跟踪表的大小 net.netfilter.nf_conntrack_max。

  • 减小 net.ipv4.tcp_fin_timeout 和 net.netfilter.nf_conntrack_tcp_timeout_time_wait ,让系统尽快释放其所占用的资源。

  • 开启端口复用 net.ipv4.tcp_tw_reuse。被 TIME_WAIT 状态占用的端口还能用到新连接中。增大本地端口的范围 net.ipv4.ip_local_port_range 以支持更多连接、提高整体并发能力。

  • 增加最大文件描述符数量。使用 fs.nr_open 和 fs.file-max 分别增大进程和系统的最大文件描述符数;或在应用程序的 systemd 配置文件中设置应用程序的最大文件描述符数 LimitNOFILE。

缓解 SYN FLOOD

  • 增大 TCP 半连接最大数量 net.ipv4.tcp_max_syn_backlog ,或开启 TCP SYN Cookies net.ipv4.tcp_syncookies 绕开半连接数量限制(不可同时使用)。

  • 减少 SYN_RECV 状态连接重传 SYN+ACK 包次数 net.ipv4.tcp_synack_retries。

Keepalive 优化:长连接场景通常使用 Keepalive 检测 TCP 连接状态,以便对端连接断开后自动回收。系统默认的 Keepalive 探测间隔和重试次数一般无法满足应用程序的性能要求。

  • 缩短最后一次数据包到 Keepalive 探测包的间隔时间 net.ipv4.tcp_keepalive_time。

  • 缩短发送 Keepalive 探测包的间隔时间 net.ipv4.tcp_keepalive_intvl。

  • 减少 Keepalive 探测失败后,一直到通知应用程序前的重试次数 net.ipv4.tcp_keepalive_probes。

UDP

  • 增大套接字缓冲区大小以及 UDP 缓冲区范围。

  • 增大本地端口号的范围。

  • 根据 MTU 大小调整 UDP 数据包大小,减少或避免分片。

需要注意优化手段之间的冲突,比如服务器端开启 Nagle 算法、客户端开启延迟确认,很容易导致网络延迟增大;在 NAT 服务器上开启 net.ipv4.tcp_tw_recycle 容易导致各种连接失败。

网络层

主要是对路由、 IP 分片以及 ICMP 等进行调优。

从路由和转发的角度出发:

  • 在需要转发的服务器中(比如 NAT 网关或使用 Docker 容器时),开启 IP 转发,即设置 net.ipv4.ip_forward = 1。

  • 调整数据包的生存周期 TTL,比如设置 net.ipv4.ip_default_ttl = 64。而增大该值会降低系统性能。

  • 开启数据包的反向地址校验,比如设置 net.ipv4.conf.eth0.rp_filter = 1。可以防止 IP 欺骗,并减少伪造 IP 带来的 DDoS 问题。

从分片的角度出发:最主要是调整 MTU 的大小。

  • 通常根据以太网标准设置,网络帧最大为 1518B,去掉以太网头部 18B 后剩余 1500 即为 MTU。在使用 VXLAN、GRE 等叠加网络技术会使原来的网络包变大,MTU 也要相应增大。

  • 很多网络设备支持巨帧,还可以把 MTU 调大为 9000,以提高网络吞吐量。

比如使用 VXLAN,在原来报文的基础上,增加了 14B 的以太网头部、 8B 的 VXLAN 头部、8B 的 UDP 头部以及 20B 的 IP 头部,因此交换机、路由器等的 MTU 要增大到 1550,或封包前的 MTU(比如虚拟化环境中的虚拟网卡)缩小到 1450。

从 ICMP 的角度出发:为避免 ICMP 主机探测、ICMP Flood 等网络问题,可通过内核选项限制 ICMP 行为。

  • 禁止 ICMP 协议,即设置 net.ipv4.icmp_echo_ignore_all = 1,外部主机无法通过 ICMP 探测主机。

  • 禁止广播 ICMP,即设置 net.ipv4.icmp_echo_ignore_broadcasts = 1。

链路层

主要是优化网络包的收发、网络功能卸载以及网卡选项。

网卡收包后调用中断处理程序(特别是软中断)需要消耗大量的 CPU,而将中断处理程序调度到不同的 CPU 上执行可以显著提高网络吞吐量。

  • 为网卡硬中断配置 CPU 亲和性(smp_affinity),或者开启 irqbalance 服务。

  • 开启 RPS(Receive Packet Steering)和 RFS(Receive Flow Steering),将应用程序和软中断的处理调度到相同 CPU 上,可以增加 CPU 缓存命中率,减少网络延迟。

利用网卡的功能:在内核中通过软件处理的功能转移到网卡中。

  • TSO(TCP Segmentation Offload)和 UFO(UDP Fragmentation Offload):在 TCP/UDP 协议中直接发送大包;而 TCP 包的分段(按照 MSS 分段)和 UDP 的分片(按照 MTU 分片)功能由网卡来完成 。

  • GSO(Generic Segmentation Offload):在网卡不支持 TSO/UFO 时,将 TCP/UDP 包的分段延迟到进入网卡前再执行。可以减少 CPU 的消耗,以及在发生丢包时只重传分段后的包。

  • LRO(Large Receive Offload):在接收 TCP 分段包时,由网卡将其组装合并后再交给上层网络处理。在需要 IP 转发时下不能开启 LRO,如果多个包的头部信息不一致,LRO 合并会导致网络包的校验错误。

  • GRO(Generic Receive Offload):修复了 LRO 的缺陷并且更为通用,同时支持 TCP 和 UDP。

  • RSS(Receive Side Scaling):也称为多队列接收,基于硬件的多个接收队列来分配网络接收进程,使得让多个 CPU 来处理接收到的网络包。

  • VXLAN 卸载:让网卡完成 VXLAN 的组包功能。

优化网络接口:可提升网络吞吐量。

  • 开启网络接口的多队列功能。每个队列可以用不同的中断号调度到不同 CPU 上执行,从而提升网络的吞吐量。

  • 增大网络接口的缓冲区大小以及队列长度等,提升网络传输的吞吐量(可能导致延迟增大)。

  • 使用 Traffic Control 工具为不同网络流量配置 QoS。

绕过内核协议栈:

  • DPDK:跳过内核协议栈,直接由用户态进程轮询处理网络请求。再结合大页、CPU 绑定、内存对齐、流水线并发等多种机制,优化网络包的处理效率。

  • XDP:内核自带,在网络包进入内核协议栈前就对其进行处理。

关于工具和参数的选用,可以参考下表:

img img img img

参考

分析案例可参考:

CATALOG
  1. 1. Linux 网络原理与分析
    1. 1.1. 网络模型
    2. 1.2. 网络收发流程
      1. 1.2.1. 网络包接收
      2. 1.2.2. 网络包发送
      3. 1.2.3. 套接字
    3. 1.3. 性能指标
      1. 1.3.1. 网络配置
      2. 1.3.2. 套接字信息
      3. 1.3.3. 协议栈信息
      4. 1.3.4. 网络质量统计信息
    4. 1.4. 优化方法
      1. 1.4.1. I/O 模型优化
      2. 1.4.2. 工作模型优化
        1. 1.4.2.1. 主进程 + 多个 worker 子进程
        2. 1.4.2.2. 监听到相同端口的多进程
      3. 1.4.3. DPDK 和 XDP
    5. 1.5. 性能测试
      1. 1.5.1. 基准测试
      2. 1.5.2. 转发性能
      3. 1.5.3. TCP/UDP 性能
      4. 1.5.4. HTTP 性能
      5. 1.5.5. 应用负载性能
    6. 1.6. 优化总结
      1. 1.6.1. 应用层
      2. 1.6.2. 套接字层
      3. 1.6.3. 传输层
        1. 1.6.3.1. TCP
        2. 1.6.3.2. UDP
      4. 1.6.4. 网络层
      5. 1.6.5. 链路层
    7. 1.7. 参考