网络模型
为了解决网络互联中异构设备的兼容性问题,并解耦复杂的网络包处理流程,国际标准化组织制定了开放式系统互联通信参考模型(Open System Interconnection Reference Model),简称 OSI 网络模型。OSI 模型把网络互联的框架分为应用层、表示层、会话层、传输层、网络层、数据链路层以及物理层等 七层网络模型,每个层负责不同的功能。
- 应用层 Application,负责为应用程序提供统一的接口。
- 表示层 Presentation,负责把数据转换成兼容接收系统的格式。
- 会话层 Session,负责维护计算机之间的通信连接。
- 传输层 Transport,负责为数据加上传输表头,形成数据包。
- 网络层 Network,负责数据的路由和转发。
- 数据链路层 Data Link,负责 MAC 寻址、错误侦测和改错。
- 物理层 Physical,负责在物理网络中传输数据帧。
更为实用的 四层网络模型,即 TCP/IP 网络模型。
- 应用层,负责向用户提供一组应用程序,比如 HTTP、FTP、DNS 等。
- 传输层,负责端到端的通信,比如 TCP、UDP 等。
- 网络层,负责网络包的封装、寻址和路由,比如 IP、ICMP 等。
- 网络接口层,负责网络包在物理网络中的传输,比如 MAC 寻址、错误侦测以及通过网卡传输网络帧等。
Linux 网络栈
TCP/IP 模型中的数据包进行网络传输时,会按照协议栈,对上一层发来的数据进行逐层处理;然后封装上该层的协议头,再发送给下一层。
- 处理:逻辑取决于各层采用的网络协议,如将 json 数据处理成 HTTP 协议;
- 封装:只是在原来的负载前后,增加固定格式的元数据,原始的负载数据并不会被修改。
物理链路中不能传输任意大小的数据包,网络接口配置的最大传输单元(MTU)规定了最大的 IP 包大小。在我们最常用的以太网中,MTU 默认值是 1500(这也是 Linux 的默认值)。一旦网络包超过 MTU 的大小,就会在网络层分片,以保证分片后的 IP 包不大于 MTU 值。显然,MTU 越大,需要的分包也就越少,自然,网络吞吐能力就越好。
在系统启动过程中,网卡通过内核中的网卡驱动程序注册到系统中。而在网络收发过程中,内核通过中断跟网卡进行交互。网卡硬中断只处理最核心的网卡数据读取或发送,而协议栈中的大部分逻辑,都会放到软中断中处理。
Linux 网络收发流程
网络性能指标
- 带宽,表示链路的最大传输速率,单位通常为 b/s (比特 / 秒)。
- 吞吐量,表示单位时间内成功传输的数据量,单位通常为 b/s(比特 / 秒)或者 B/s(字节 / 秒)。吞吐量受带宽限制,而吞吐量 / 带宽,也就是该网络的使用率。
- 延时,表示从网络请求发出后,一直到收到远端响应,所需要的时间延迟。在不同场景中,这一指标可能会有不同含义。比如,它可以表示,建立连接需要的时间(比如 TCP 握手延时),或一个数据包往返所需的时间(比如 RTT)。
- PPS,是 Packet Per Second(包 / 秒)的缩写,表示以网络包为单位的传输速率。PPS 通常用来评估网络的转发能力,比如硬件交换机,通常可以达到线性转发(即 PPS 可以达到或者接近理论最大值)。而基于 Linux 服务器的转发,则容易受网络包大小的影响。
- 网络的可用性(网络能否正常通信)、并发连接数(TCP 连接数量)、丢包率(丢包百分比)、重传率(重新传输的网络包比例)
网络接口的配置和状态
ifconfig 和 ip
分别属于软件包 net-tools 和 iproute2,但 iproute2 是 net-tools 的下一代,更推荐使用。
$ ifconfig eth0
eth0: flags=4163 mtu 1500
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
ether 78:0d:3a:07:cf:3a txqueuelen 1000 (Ethernet)
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: 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
- (1)网络接口的状态标志。ifconfig 输出中的 RUNNING ,或 ip 输出中的 LOWER_UP ,都表示物理网络是连通的,即网卡已经连接到了交换机或者路由器中。如果你看不到它们,通常表示网线被拔掉了。
- (2)MTU 的大小。MTU 默认大小是 1500,根据网络架构的不同(比如是否使用了 VXLAN 等叠加网络),你可能需要调大或者调小 MTU 的数值。
- (3)网络接口的 IP 地址、子网以及 MAC 地址。这些都是保障网络功能正常工作所必需的,你需要确保配置正确。
- (4)网络收发的字节数、包数、错误数以及丢包情况,特别是 TX 和 RX 部分的 errors、dropped、overruns、carrier 以及 collisions 等指标不为 0 时,通常表示出现了网络 I/O 问题。
- errors 表示发生错误的数据包数,比如校验错误、帧同步错误等;
- dropped 表示丢弃的数据包数,即数据包已经收到了 Ring Buffer,但因为内存不足等原因丢包;
- overruns 表示超限数据包数,即网络 I/O 速度过快,导致 Ring Buffer 中的数据包来不及处理(队列满)而导致的丢包;
- carrier 表示发生 carrirer 错误的数据包数,比如双工模式不匹配、物理电缆出现问题等;
- collisions 表示碰撞数据包数。
套接字信息
netstat / ss
可用来查看套套接字的状态、接收队列、发送队列、本地地址、远端地址、进程 PID 和进程名称等。
$ netstat -nlp | head -n 3
# -n 表示显示数字地址和端口 (而不是名字)
# -l 表示只显示监听套接字
# -p 表示显示进程信息
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
$ ss -nlpt | head -n 3
# -n 表示显示数字地址和端口 (而不是名字)
# -l 表示只显示监听套接字
# -p 表示显示进程信息
# -t 表示只显示 TCP 套接字
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。当你发现它们不是 0 时,说明有网络包的堆积发生。
- 当套接字处于连接状态(Established)时,Recv-Q 表示套接字缓冲还没有被应用程序取走的字节数(即接收队列长度)。而 Send-Q 表示还没有被远端主机确认的字节数(即发送队列长度)。
- 当套接字处于监听状态(Listening)时,Recv-Q 表示 syn backlog 的当前值。而 Send-Q 表示最大的 syn backlog 值。
- syn backlog 是 TCP 协议栈中的半连接队列长度,相应的也有一个全连接队列(accept queue),它们都是维护 TCP 状态的重要机制。
- 半连接,是还没有完成 TCP 三次握手的连接,连接只进行了一半,而服务器收到了客户端的 SYN 包后,就会把这个连接放到半连接队列中,然后再向客户端发送 SYN+ACK 包。
- 全连接,是指服务器收到了客户端的 ACK,完成了 TCP 三次握手,然后就会把这个连接挪到全连接队列中。这些全连接中的套接字,还需要再被 accept() 系统调用取走,这样,服务器就可以开始真正处理客户端的请求了。
协议栈统计信息
查看协议栈的统计信息:
$ 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
...
网络吞吐和 PPS
sar -n 参数
查看网络的统计信息:网络接口(DEV)、网络接口错误(EDEV)、TCP、UDP、ICMP 等。
$ sar -n DEV 1 # 数字 1 表示每隔 1 秒输出一组数据
Linux 4.15.0-1035-azure (ubuntu) 01/06/19 _x86_64_ (2 CPU)
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
- rxpck/s 和 txpck/s 分别是接收和发送的 PPS,单位为包 / 秒。
- rxkB/s 和 txkB/s 分别是接收和发送的吞吐量,单位是 KB/ 秒。
- rxcmp/s 和 txcmp/s 分别是接收和发送的压缩数据包数,单位是包 / 秒。
- %ifutil 是网络接口的使用率,即半双工模式下为 (rxkB/s+txkB/s)/Bandwidth,而全双工模式下为 max(rxkB/s, txkB/s)/Bandwidth。
- Bandwidth 通过
ethtool eth0 | grep Speed
查询,单位通常是 Gb/s 或者 Mb/s,是bit不是字节,还有千兆网卡、万兆网卡都是bit。
- Bandwidth 通过
连通性和延时
ping 基于 ICMP 协议,测试远程主机的连通性和延时。
$ ping -c3 114.114.114.114 # -c3 表示发送三次 ICMP 包后停止
PING 114.114.114.114 (114.114.114.114) 56(84) bytes of data.
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
--- 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
- TTL,生存时间,或者跳数
- RTT,平均往返延时
基础问题 C10K、C1000K、C10M
C 指的是 Client,
C10K 就是单机同时处理 1 万个请求(并发连接 1 万)的问题;
C1000K 就是单机支持处理 100 万个请求(并发连接 100 万)的问题;
C10M 就是单机支持处理 1 千万个请求(并发连接 1 千万)的问题。
(1)C10K
对 2GB 内存和千兆网卡这等资源有限的服务器来说,只要每个请求处理占用不到 200KB(2GB/10000)的内存和 100Kbit (1000Mbit/10000)的网络带宽就可以。所以,物理资源是足够的,接下来自然是软件的问题,特别是网络的 I/O 模型问题。
在 C10K 以前,Linux 中网络处理都用同步阻塞的方式,也就是每个请求都分配一个进程或者线程,但并发量增加到 10K 后进程或线程的调度、上下文切换等都会成为瓶颈。解决方法是:
(1)一个线程内响应多个网络 I/O:非阻塞 I/O 或者异步 I/O 来处理多个网络请求(I/O 多路复用)。
(2)用更少的线程来服务这些请求。
两种 I/O 事件通知的方式:
(1)水平触发:只要文件描述符可以非阻塞地执行 I/O ,就会触发通知。也就是说,应用程序可以随时检查文件描述符的状态,然后再根据状态,进行 I/O 操作。
(2)边缘触发:只有在文件描述符的状态发生改变(也就是 I/O 请求达到)时,才发送一次通知。这时候,应用程序需要尽可能多地执行 I/O,直到无法继续读写,才可以停止。如果 I/O 没执行完,或者因为某种原因没来得及处理,那么这次通知也就丢失了。
I/O 多路复用(I/O Multiplexing)
(1)使用非阻塞 I/O 和水平触发通知,如 select 或者 poll。水平触发:select 或者 poll 轮询文件描述符列表,找出可以执行的 I/O,然后进行真正的网络 I/O 读写;非阻塞:一个线程同时监控一批套接字的文件描述符。但请求数多时,比较耗时,并且 select 有最大描述符数量的限制,select和poll需要把文件描述符从用户空间传入内核空间,再传出用户空间。
(2)使用非阻塞 I/O 和边缘触发通知,如 epoll。使用红黑树,在内核中管理文件描述符的集合,就不需要应用程序在每次操作时都传入、传出这个集合。使用事件驱动的机制,只关注有 I/O 事件发生的文件描述符,不需要轮询扫描整个集合。由于边缘触发只在文件描述符可读或可写事件发生时才通知,那么应用程序就需要尽可能多地执行 I/O,并要处理更多的异常事件。
(3)使用异步 I/O(Asynchronous I/O,简称为 AIO)。允许应用程序同时发起很多 I/O 操作,而不用等待这些操作完成。而在 I/O 完成后,系统会用事件通知(比如信号或者回调函数)的方式,告诉应用程序。使用难度比较高。
工作模型优化
在 I/O 多路复用中,有两种不同的工作模型:
(1)主进程 + 多个 worker 子进程。主进程执行 bind() + listen() 后,创建多个子进程;每个子进程通过 accept() 或 epoll_wait() ,来处理相同的套接字。主进程主要用来初始化套接字,并管理子进程的生命周期;而 worker 进程,则负责实际的请求处理。
accept() 和 epoll_wait() 调用,还存在一个惊群的问题,nginx 通过竞争全局锁来唤醒子进程。
(2)监听到相同端口的多进程模型。所有的进程都监听相同的接口,并且开启 SO_REUSEPORT 选项,由内核负责将请求负载均衡到这些监听进程中去。
(2)C1000K
epoll 配合线程池,再加上 CPU、内存和网络接口的性能和容量提升,大部分情况下,C100K 可以达到。进一步还可以加上多队列网卡、中断负载均衡、CPU 绑定、RPS/RFS(软中断负载均衡到多个 CPU 核上),以及将网络包的处理卸载(Offload)到网络设备(如 TSO/GSO、LRO/GRO、VXLAN OFFLOAD)等各种硬件和软件的优化。
C1000K 的解决方法,本质上还是构建在 epoll 的非阻塞 I/O 模型上。只不过,除了 I/O 模型之外,还需要从应用程序到 Linux 内核、再到 CPU、内存和网络等各个层次的深度优化,特别是需要借助硬件,来卸载那些原来通过软件处理的大量功能。
(3)C10M
实际上,在 C1000K 问题中,各种软件、硬件的优化很可能都已经做到头了,想实现 1000 万请求的并发,都是极其困难的。究其根本是 Linux 内核协议栈做了太多太繁重的工作。解决方法:
(1)DPDK,跳过内核协议栈,直接由用户态进程通过轮询的方式,来处理网络接收。DPDK 还通过大页、CPU 绑定、内存对齐、流水线并发等多种机制,优化网络包的处理效率。
(2)XDP(eXpress Data Path),Linux 内核提供的一种高性能网络数据路径,允许网络包,在进入内核协议栈之前,就进行处理,也可以带来更高的性能。