深入理解Linux网络
内核是如何接收网络包的
当用户执行完recvfrom调用后,用户进程就通过系统调用进行到内核态工作了。如果接收队列没有数据,进程就进入睡眠状态被操作系统挂起。
- 数据帧从外部网络到达网卡
- 网卡把帧DMA到内存
- 硬中断通知CPU
- CPU响应硬中断,简单处理后发出软中断
- ksoftirqd线程处理软中断,调用网卡驱动注册的poll函数开始收包
- 帧被从RingBuffer上摘下来保存为一个SKB
- 协议层开始处理网络帧,处理完后的数据data被放到Socket的接收队列中
- 内核唤醒用户进程
- RingBuffer到底是什么, RingBuffer为什么会丢包?
网卡在收到数据的时候以DMA的方式将包写到RingBuffer中。软中断收包的时候来这里把SKB取走,并申请新的SKB重新挂上去。
RingBuffer的大小是可以设置的,长度可以通过ethtool工具查看。
$ ethtool -g ens3
Ring parameters for ens3:
Pre-set maximums:
RX: 256
RX Mini: n/a
RX Jumbo: n/a
TX: 256
Current hardware settings:
RX: 256
RX Mini: n/a
RX Jumbo: n/a
TX: 256
如果内核处理不及时导致RingBuffer满了,那后面新来的数据包就会被丢弃,通过ethtool或ifconfig工具可以查看是否有RingBuffer溢出发生。
$ ethtool -S ens3
NIC statistics:
rx_queue_0_packets: 857
rx_queue_0_bytes: 306826
rx_queue_0_drops: 0
rx_queue_0_xdp_packets: 0
rx_queue_0_xdp_tx: 0
rx_queue_0_xdp_redirects: 0
rx_queue_0_xdp_drops: 0
rx_queue_0_kicks: 1
tx_queue_0_packets: 668
tx_queue_0_bytes: 105070
tx_queue_0_xdp_tx: 0
tx_queue_0_xdp_tx_drops: 0
tx_queue_0_kicks: 634
修改RingBuffer大小, 不过改大之后会增加处理网络包的延时。另外一种解决思路更好,那就是让内核处理网络包的速度更快一些。
$ ethtool -G ens3 rx 4096 tx 4096
- ksoftirqd内核线程是干什么的?
机器上有几个核,内核就会创建几个ksoftirqd线程出来。
$ ps -ef | grep ksoftirqd
root 13 2 0 20:50 ? 00:00:00 [ksoftirqd/0]
ksoftirqd内核线程包含了所有的软中断处理逻辑。
$ cat /proc/softirqs
CPU0
HI: 0
TIMER: 21167
NET_TX: 1
NET_RX: 1908
BLOCK: 11021
IRQ_POLL: 0
TASKLET: 74
SCHED: 0
HRTIMER: 0
RCU: 22435
- 为什么网卡开启多队列能提升网络性能
现在主流网卡基本上都是支持多队列的,通过ethtool可以查看:
$ ethtool -l ens3
Channel parameters for ens3:
Pre-set maximums:
RX: n/a
TX: n/a
Other: n/a
Combined: 4
Current hardware settings:
RX: n/a
TX: n/a
Other: n/a
Combined: 1
通过sysfs文件系统可以看到真正生效的队列数。
$ ls /sys/class/net/ens3/queues/
rx-0 tx-0
如果想加大队列数,ethtool也可以搞定
$ ethtool -L ens3 combined 2
通过/proc/interrupts可以看到该队列对应的硬件中断号。再通过该中断号对应的smp_affinity可以查看到亲和的CPU核是哪一个?
$ cat /proc/interrupts
...
$ cat /proc/irq/<中断号>/smp_affinity
8
这个亲和性是通过二进制中的比特位来标记的。例如8是二进制的1000, 第4位是1,代表是第4个CPU核心-CPU3
每个队列都有独立的,不同的中断号。所以不同的队列可以向不同的CPU发起硬中断通知。且相应的软中断也是由这个核处理的。
所以工作中,如果网络包的接收频率高而导致个别核si偏高,那么通过加大网卡队列数,并设置每个队列中断号上的smp_affinity, 将各个队列的硬中断打散到不同的CPU就行了。
- tcpdump是如何工作的
tcpdump工作在设备层,是通过虚拟协议栈的形式工作的。将抓包函数以协议的形式挂到ptype_all上。
当收包的时候,驱动在将包送到协议栈(ip_rcv, arp_rcv等)之前,将包先送到ptype_all抓包点。
- iptable/netfilter是在哪一层实现的?
netfilter主要是在IP,ARP层实现的。如果配置过于复杂的规则,则会消耗过多CPU,加大网络延迟。
- tcpdump能否抓到被iptable封禁的包?
tcpdump工作在设备层,而netfilter工作在IP/ARP层, 收包时,iptable封禁规则不影响tcpdump的抓包。 发包过程则相反,netfilter过滤后,tcpdump看不到被封禁的包。
- 网络接收过程中的CPU开销如何查看
在网络包的接收处理过程中,主要工作集中在硬中断和软中断上,二者的消耗都可以通过top命令查看。
其中hi是cpu处理硬中断的开销,si是CPU处理软中断的开销,都是以百分比的形式来展示的。
内核是如何与用户进程协作的?
- 阻塞到底是怎么一回事?
阻塞其实说的是进程因为等待某个事件而主动让出CPU挂起的操作。
在网络IO中,当进程等待socket上的数据时,如果数据还没有到来,那就把当前进程状态从TASK_RUNNING 修改为 TASK_INTERRUPTIPLE, 然后主动让出CPU。 由调度器来调度下一个就绪的进程来执行。
- 同步阻塞IO都需要哪些开销?
- 从CPU开销角度看,一次同步阻塞IO将导致两次进程上下文切换开销。每一次切换大约花费3~5微妙。
- 一个进程同时只能等待一条连接,如果有许多并发,则需要很多进程,每个进程都将占用大约几MB的内存。
- 多路复用epoll为什么就能提高网络性能?
epoll高性能的最根本原因是极大程度地减少了无用的进程上下文切换,让进程更专注地处理网络请求。
在内核的硬软中断上下文中,包从网卡接收过来进行处理,然后放到Socket的接收队列。再找到socket关联的epitem, 并把它添加到epoll对象的就绪链表中。
在用户进程中,通过调用epoll_wait来查看就绪链表是否有事件到达,如果有,直接取走进行处理。处理完毕再次调用epoll_wait。在高并发的实践中,只要活儿足够多,epoll_wait根本不会让进程阻塞。
直到epoll_wait里实在没活儿可干的时候才会让出CPU。这就是epoll高效的核心所在。
- 为什么Redis的网络性能好?
Redis在网络IO上表现非常突出,单进程的服务器在极限情况下可以达到10万的QPS。
Redis的主要业务逻辑就是在本机内存上的数据结构的读写,单个请求处理起来很快。所以它把主服务端程序干脆做成了单线程的,这样省去了多线程之前协作的负担,也更大程度减少了线程切换。
内核是如何发送网络包的
- 用户进程send系统调用发送
- 进入内核态,申请SKB,内存拷贝
- 协议处理,传输层tcp头设置,滑动窗口管理 =》 网络层查找路由项,netfilter过滤,IP分片 =》 邻居子系统发送arp请求,获取目标MAC =》网络设备子系统,选择发送队列,skb入队
- 进入驱动RingBuffer
- 网卡实际发送
- 网卡硬中断通知CPU发送完成
- 触发软中断NET_RX_SOFTIRQ, 清理RingBuffer
- 我们在监控内核发送数据消耗的CPU时,应该看sy还是si ?
在网络包的发送过程中,用户进程(在内核态)完成了绝大部分工作,甚至连调用驱动的工作都干了。只当内核态进程被切走前才会发起软中断。 在发送过程中,绝大部分(90%)以上的开销都是在用户进程内核态消耗掉的。 只有一少部分情况才会触发软中断(NET_TX类型),由软中断ksoftirqd内核线程来发送。 所以,在监控网络IO对服务器造成的CPU开销的时候,不能仅看si, 而是应该把si, sy都考虑进来。
- 在服务器上查看/proc/softirqs, 为什么NET_RX要比NET_TX大得多的多?
- 原因1: 当数据发送完之后,触发的软中断是NET_RX_SOFTIRQ, 并不是NET_TX_SOFTIRQ.
- 原因2: 收包时,都是要经过NET_RX软中断的,都走ksoftirqd内核线程。而发包时,绝大部分工作都是在用户进程内核态处理了,只有系统态配额用完才会发出NET_TX, 让软中断上。
- 发送网络数据的时候,都涉及哪些内存拷贝操作?
- 将用户进程传递进来的buffer里的数据都拷贝到skb
- 从传输层进入网络层的时候,进行浅拷贝,只拷贝skb描述符,所指向的数据复用。目的是网络对方没有回复ACK的时候,还可以重新发送,以实现TCP要求中的可靠传输。
- 当网络层发现skb大于MTU时,进行分片,拷贝为多个小skb
- 零拷贝到底是怎么回事?为什么kafka的网络性能很突出?
采用了sendfile系统调用来发送网络数据包,减少了内核态和用户态之间的频繁数据拷贝。
而read + send系统调用发送文件过程如下:
- 从硬盘DMA到内核态的page cache
- CPU拷贝page cache到用户内存
- cpu拷贝用户内存到socket发送缓冲区
- 拷贝到RingBuffer
- DMA拷贝到网卡
深度理解本机网络IO
- 127.0.0.1本机网络IO需要经过网卡吗?
不需要经过网卡,即使把网卡拔了,本机网络还是可以正常使用。
- 数据包在内核中是什么走向,和外网发送相比流程上有什么差别?
节约了驱动上的一些开销,发送数据不需要进RingBuffer的驱动队列,直接把skb传给接收协议栈(经过软中断)。但是 系统调用,协议栈(传输层,网络层等),设备子系统整个走了一遍。连“驱动”都走了(虽然回环设备是纯软件虚拟的)
如果想在本机网络IO绕开协议栈的开销,可以使用eBPF, 使用eBPF的sockmap和sk redirect可以达到真正不走协议栈的目的。
- 访问本机服务时,使用127.0.0.1能比使用其他IP更快吗?
没有差别,都是走虚拟的环回设备lo, 这是因为内核在设置IP的时候,把所有的本机IP都初始化到local路由表里了。
深度理解TCP连接建立过程
- 服务端listen
- 申请并初始化接收队列,包括半连接队列和全连接队列
- 全连接队列是1个链表,其最大长度min(listen时传入的backlog, net.core.somaxconn)
- 半连接队列由于需要快速地查找,使用的是一个哈希表, 其最大长度是min(backlog, somaxconn, tcp_max_syn_backlog) + 1再向上取整到2的N次幂,但最小不能小于16
- 客户端connect
- 随机地从ip_local_port_range选择一个位置开始循环判断,选择可用的本地端口, 如果端口快用光了,内核大概率要循环多轮才能找到可用端口,这会导致connect系统调用的CPU开销上涨。如果端口查找失败,会报错“Cannot assign requested address”
- 发出SYN握手请求
- 启动重传定时器
- 服务端收到SYN握手请求
- 发出SYC ACK
- 进入半连接队列
- 启动定时器
- 客户端收到SYN ACK
- 消除重传定时器
- 设置为已连接
- 发送ACK确认
- 服务端收到ACK
- 创建新sock
- 从半连接队列删除
- 加入全连接队列
- 服务端accept
- 从全连接队列取走socket
握手异常总结
- 如果端口不充足,处理方法有那么几个
- 通过调整ip_local_port_range来尽量加大端口范围。
- 尽量复用连接,使用长连接来削减频繁的握手处理
- 有用但不太推荐的方法是开启tcp_tw_reuse和tcp_tw_recycle
- 半连接队列满,全连接队列满等导致丢包,应如何应对
- 打开tcp_syncookies来防止过多请求打满半连接队列,包括SYN Flood攻击,来解决服务端因为半连接队列满而发生的丢包
- 加大连接队列长度,可通过
ss -nlt
命令中输出的Send-Q来最终生效长度 - 尽快调用accept, 应用程序应该尽快在握手成功后通过accept把新连接取走。
- 尽早拒绝,例如将Redis,MySQL等服务器的内核参数tcp_abort_on_overflow设置为1,这是客户端会收到错误“connection reset by peer”
- 用长连接代替短连接,减少过于频繁的三次握手
一条TCP连接消耗多大内存?
- 内核是如何管理内存的
- 把所有内存条和CPU进行分组,组成node
- 把每一个node划分成多个zone
- 每个zone下都用伙伴系统来管理空闲页面
- 提供slab分配器来管理各种内核对象, 每个slab缓存都是用来存储固定大小,甚至特定的一种内核对象。这样当一个对象释放后,另一个同类对象可以直接使用这块内存,几乎没有任何碎片,极大地提高了分配效率。
- 如何查看内核使用的内存信息
-
sudo cat /proc/slabinfo
可以看到所有的kmem cache -
sudo slabtop
从大往小按照占用内存进行排列
-
- 服务器上一条ESTABLISH状态的空连接需要消耗多少内存?总共3.3KB左右
- struct socket_alloc, 大小约为0.62KB, slab缓存名是sock_inode_cache
- struct tcp_sock, 大小约为1.94KB, slab缓存名是tcp
- struct dentry, 大小约为0.19KB, slab缓存名是dentry
- struct file, 大小约为0.25KB, slab缓存名是flip
- 服务器上出现大量的TIME_WAIT, 内存开销会不会很大?
- 一条TIME_WAIT状态的连接金占用0.4KB左右内存
- 端口占用问题,可以考虑使用
tcp_max_tw_buckets
来限制TIME_WAIT连接总数,或者打开tcp_tw_recycle, tcp_tw_reuse来快速回收端口。 - 使用长连接代替频繁的短连接