1、内核如何接收网络包
1.1 RingBuffer到底是什么,RingBuffer为什么会丢包?
问:RingBuffer到底存在那一块,是如何被使用到的,真的就只是一个环形队列吗?RingBuffer内存是预先分配好的,还是随着网络包的收发动态分配?为什么RingBuffer会丢包,如果丢包了应该怎么去解决?
答:
- RingBuffer是内存中的一块特殊区域,RingBuffer包含内核使用的igb_rx_buffer数组和网卡使用的e1000_adv_rx_desc数组,这两个数组存放指向skb的指针,数组的内存是预先分配的,skb是根据收包数据动态申请的。
- RingBuffer有大小和长度限制,长度可以通过ethtool工具查看。如执行"ethtool -g eth0";Pre-set maximums指的是RingBuffer的最大值,Current hardware setting指的是当前的设置。如果内核处理的不及时导致RingBuffer满了,那后面新来的数据包就会被丢弃,通过ethtool和ifconfig工具可以查看是否有RingBuffer溢出发生,可使用"ethtool -S eth0",如果rx_fifo_errors不为0(在ifconfig中体现为overruns指标增长)就表示有包因为RingBuffer装不下而被丢弃了。
- 可以通过"ethtool -G eth1 rx 4096 tx 4096"增大RingBuffer,但是这种方法只能解决偶发的瞬时的丢包,而且会在包排队过多时导致延时增大,正确的做法应该是让内核处理网络包的速度更快一些,
1.2 网络相关的硬中断、软中断都是什么?
问:硬中断和软中断的区别是什么,二者是怎么协作的?为什么操作硬中断号和CPU之间的绑定关系,但最终的效果是软中断跟着一起绑定调整了,软中断开销也被绑定到不同的CPU?
在网卡将数据放到RingBuffer中后,接着就发起硬中断,通知CPU进行处理。不过硬中断的上下文里做的工作很少,将传过来的poll_list添加到CPU变量softnet_data的poll_list里(softnet_data中的poll_list是一个双向列表,其中的设备都带有输入帧等着被处理),接着触发软中断NET_RX_SOFTIRQ。
在软中断中对softnet_data的设备列表poll_list进行遍历,执行网卡驱动提供的poll来收取网络包。处理完后会送到协议栈的ip_rcv、udp_rcv、tcp_rcv_v4等函数中处理。
1.3 Linux里的ksoftirqd内核线程是干什么的?
这几个内核线程是做什么用的?机器上会有几个?为什么有这么多?它们和软中断又是什么关系?
设备有几个核就会有几个ksoftirqd线程,内核线程koftirqd包含了所有软中断的类型,执行不同的处理函数。对于软中断NET_RX_SOFTIRQ。软中断的信息可以从/proc/softirqs读取。
1.4 为什么网卡开启多队列能提升网络性能?
多队列提升网络性能的优化方案的基本原理是什么?什么时候该动用这个方法,用的话开到几个队列合适?
通过ethtool可以查看当前网卡的多队列情况("ethtool -l eth0")。使用"ls /sys/class/net/eth0/queues"命令也可以看到真正生效的队列数,如果要加大队列数"ethtool -L eth0 combined 32"调整。通过/proc/interrupts可以看到该队列对应的硬件中断号。通过该中断号对应的smp_affinity可以查看到亲和的CPU和是哪一个"cat /proc/irq/中断号/smp_affinity"。这个亲和性通过比特位来标记。8代表的是第4个CPU核心-CPU3。在硬中断的处理中,发起软中断的时候是基于当前CPU核的smp_processor_id的,这意味着哪个核响应的硬中断,那么该硬中断发起的软中断任务就必然由这个核来处理。
如果网络包的接收频率高而导致个别核si偏高,那么通过加大网卡队列数,并设置每个队列中断号的smp_affinty,将各个队列打散到不同的CPU就行了。
1.5 tcpdump是如何工作的?
tcpdump工作在设备层,是通过虚拟协议的方式工作的。它通过调用packet_create将抓包函数以协议的形式挂到ptype_all上。
1.6 iptable/netfilter是在哪一层实现的?
netfilter主要是在IP、ARP等层实现的。可以通过搜索NF_HOOK函数的引用来深入了解其实现。如果配置过于复杂的规则,会消耗过多的CPU,加大网络延迟。
1.7 tcpdump能否抓到被iptable封禁的包?
tcpdump可以抓到iptable封禁的收包,但是抓不到iptable封禁的发包。
1.8 网络接收过程中的CPU开销如何查看?
在网络接收过程中,CPU是如何被消耗的?CPU中的si、sy开销究竟是什么含义?
在网络包的接收处理过程中,主要工作集中在硬中断核软中断上,二者的消耗可以通过top命令来查看。
其中hi是CPU处理硬中断的开销,si是处理软中断的开销,都是以百分比的形式来展示。
如果发现某个核的si过高,那么可能是当前数据包的接收已经非常频繁了,需要通过上面的多队列配置让其它核参与进来。
2、内核是如何与用户进程协作的
2.1 阻塞到底是怎么一回事?
阻塞就是进程因为等待某个事件而主动让出CPU挂起的操作。评估是否阻塞,关键要看进程是否让出CPU。
2.2 同步阻塞IO都需要哪些开销?
从CPU开销角度来看,一次同步阻塞网络IO将导致两次进程上下文切换开销。每一次大约花费3~5微秒。
2.3 多路复用epoll为什么就能提高网络性能?
其它epoll高性能最根本的原因是极大程度地减少无用的进程上下文切换,让进程更专注地处理网络请求。至于红黑树只是提高了epoll查找、添加、删除的效率。
2.4 epoll也是阻塞的?
阻塞不会导致低性能,过多过频繁的阻塞才会。epoll的阻塞和它的高性能并不冲突。
3、内核是如何发送网络包的
3.1在查看内核发送数据消耗的CPU时,应该看sy还是si?
在网络包的发送过程中,用户进程(在内核态)完成了绝大部分的工作,甚至连调用驱动的工作都干了。只有当内核态进程被切走前才会发起软中断。发送过程中,绝大部分(90%)以上的开销都是在用户进程内核态消耗掉了。只有一少部分情况才会触发软中断(NET_TX类型),由软中断ksoftirqd内核线程来发送。所以在监控网络IO对服务器造成的CPU的开销时候,不能仅看si,而是应该把si、sy都考虑进来。
3.2 在服务器上查看/proc/softirqs,为什么NET_RX要比NET_TX大得多?
- 当数据发送完之后,通过硬中断的方式通知驱动发送完毕。但是硬中断无论是有数据接收还是发送完毕,触发的软中断都是NET_RX_SOFTIRQ。
- 对于读来说都要经过NET_RX软中断,都走ksoftirqd线程。对于发送来说,绝大部分工作都在用户进程内核态处理了,只有系统态配额用尽了才会发出NET_TX。
3.3 发送网络数据的时候都涉及哪些内存拷贝操作?
- 第一次拷贝操作是在内核申请完skb之后,这时候会将用户传递进来的buffer里面的数据拷贝到skb,如果要发送的数据量比较大,这个拷贝操作的开销还是不小的(CPU拷贝)。
- 第二拷贝操作是从传输层进入网络层的时候,每一个skb都会被克隆出来一个新的副本。目的是保存原始的skb,当没有收到对方返回ACK时,还可以重新发送,以实现TCP要求的可靠传输。不过这次是浅拷贝,仅拷贝skb描述符本身,所指向的数据还是复用的。
- 第三次拷贝不是必需的,只有当IP层发现skb大于MTU时才需要进行。此时会再申请额外的skb,并将原来的skb拷贝为多个小的skb。
3.4 零拷贝到底是怎么回事?
零拷贝是指数据不用拷贝到用户内存,可以节省两次CPU内存拷贝。
3.5为什么Kafka的网络性能很突出?
使用了零拷贝技术。
4、深度理解本机网络IO
4.1 127.0.0.1本机网络IO需要经过网卡吗?
不需要经过网卡。
4.2 数据包在内核中是什么走向,和外网发送相比流程上有什么差别?
本机网络IO和跨机网络IO比较起来,确实是节约了驱动上的一些开销。发送数据不需要近Ringbuffer的驱动队列,直接把skb传给接收协议栈(经过软中断)。
4.3 访问本机服务时,使用127.0.0.1能比使用本机IP(例如192.168.x.x)更快吗?
没有区别,都会走环回设备lo。
5、深度理解TCP连接建立过程
5.1 为什么服务端程序都需要先listen一下?
内核在响应listen调用的时候是创建了半连接、全连接两个队列,这两个队列是三次握手中很重要的数据结构,有了它们才能正常响应客户端的三次握手。
5.2 半连接队列和全连接队列长度如何确定?
服务端在执行listen的时候就确定好了半连接和全连接队列的长度。
对于半连接队列来说,其最大长度是min(backlog,somaxconn,tcp_max_syn_backlog)+1再向上取整到2dN次幂,但最小不能小于16。如果考虑要加大半连接队列,那么需要一并考虑backlog,somaxconn,tcp_max_syn_backlog。
对于全连接队列来说,其最大长度是listen时传入的backlog和net.core.somaxconn之间较小的那个值。如果需要加大全连接队列,那么调整backlog和somaxconn。
5.3 "Cannot assign requested address"这个报错你知道是怎么回事吗?该如何解决?
一条TCP连接由一个四元组构成:Server IP、Server PORT、Client IP、Client Port。在连接建立前,前面的三个元素是确定了的,只有Client Port是需要动态选择出来的。
客户端在connect发起的时候自动选择端口号。具体的选择过程就是随机地从ip_local_port_rang选择一个位置开始循环判断,跳过ip_local_reserver_ports里设置的端口,然后挨个判断是否可用。如果循环完也没有找到可用的端口,会报错"Cannot assign requested address"。
解决方法:扩大可用端口范围、减小最大TIME_WAIT状态连接数量等方法都是可行的。
5.4 一个客户端端口可以同时用在两条连接上吗?
可以。
5.5 服务端半/全连接队列满了会怎么样?
服务端响应的第一次握手的时候,会进行半连接队列和全连接队列满的判断。如果半连接队列满了,且未开启tcp_syncookies,那么该握手包将直接被丢弃,所以建议不要关闭这个内核参数。如果全连接队列满了,且有young_ack(表示刚刚有ACK到达),那么同样也是直接丢弃。
5.6 新连接的socket内核对象是什么时候建立的?
在第三次握手完成时就创建好了。
5.7 建立一条TCP连接需要消耗多长时间?
正常情况是1个RTT时间。如果丢包就至少1秒了。
5.8 把服务器部署在北京,给纽约的用户访问可行吗?
可以,但是服务延迟太高,最好在当地建立服务器。
5.9服务器负载很正常,但是CPU被打到底是怎么回事?
在端口极其不充足的情况下,connect系统调用的内部循环需要全部执行完毕才能判断出来没有端口可用。如果发出的连接特别频繁就会消耗掉大量的CPU。