1 前言
在基于Linux上开发的网络程序,在TCP收发包过程中受哪些因素的影响呢?而且我们在工作中又经常会遇到一些和此相关的问题,我们又会需要从这些因素入手去排查问题,所以在定位网络连接这类的问题之前,我们一定要了解这些因素都有哪些。
2 数据包发送过程
数据包的发送从上往下经过了三层:用户态空间的应用、系统内核空间、最后到网卡驱动。应用先将数据写入 TCP sendbuffer ,TCP 层将 sendbuffer 中的数据构建成数据包转交给 IP 层。经过IP层处理后,Link层会将待发送的数据包放入队列 QDisc 。数据包成功放入 QDisc 后,指向数据包的描述符 sk_buff 被放入Ring Buffer输出队列,随后网卡驱动调用 DMA engine 将数据发送到网络链路上。
tcp_wmem中这三个数字的含义分别为min、default、max。TCP发送缓冲区的大小会在min和max之间动态调整,初始的大小是default,这个动态调整的过程是由内核自动来做的,应用程序无法干预。自动调整的目的,是为了在尽可能少的浪费内存的情况下来满足发包的需要。
应用程序有的时候会很明确地知道自己发送多大的数据,需要多大的TCP发送缓冲区,这个时候就可以通过setsockopt()里的SO_SNDBUF来设置固定的缓冲区大小。一旦进行了这种设置后,tcp_wmem就会失效,而且这个缓冲区大小设置的是固定值,内核也不会对它进行动态调整。
tcp_wmem以及wmem_max的大小设置都是针对单个TCP连接的,这两个值的单位都是Byte(字节)。系统中可能会存在非常多的TCP连接,如果TCP连接太多,就可能导致内存耗尽。因此,所有TCP连接消耗的总内存也有限制:
与前两个选项不同的是,该选项中这些值的单位是Page(页数),也就是4K。它也有3个值:min、pressure、max。当所有TCP连接消耗的内存总和达到max后,也会因达到限制而无法再往外发包。
2.5 txqueuelen
为了能够对TCP/IP数据流进行流控,Linux内核在IP层实现了qdisc(排队规则)。我们平时用到的TC就是基于qdisc的流控工具。qdisc的队列长度是我们用ifconfig来看到的txqueuelen,我们在生产环境中也遇到过因为txqueuelen太小导致数据包被丢弃的情况,这类问题可以通过下面这个命令来观察:
dropped这一项不为0,那就有可能是txqueuelen太小导致的,你需要增加这个值的大小
2.6 default_qdisc
Linux系统默认的qdisc为pfifo_fast(先进先出),通常情况下我们无需调整它。如果想使用TCP BBR来改善TCP拥塞控制的话,那就需要将它调整为fq(公平队列)
3 数据包接收过程
从下往上经过了三层:网卡驱动、系统内核空间,最后到用户态空间的应用。Linux 内核使用 sk_buff 数据结构描述一个数据包。当一个新的数据包到达,NIC 调用 DMA engine ,通过Ring Buffer将数据包放置到内核内存区。Ring Buffer的大小固定,它不包含实际的数据包,而是包含了指向 sk_buff 的描述符。当Ring Buffer满的时候,新来的数据包将给丢弃。一旦数据包被成功接收, 发起中断,由内核的中断处理程序将数据包传递给 Link 层。经过 Link层和IP层的处理,数据包被放入队列等待 TCP 层处理。每个数据包经过 TCP 层一系列复杂的步骤,更新 TCP 状态机,最终到达 recvBuffer ,等待被应用接收处理。有一点需要注意,数据包到达 recvBuffer ,TCP 就会回 ACK 确认,即 TCP 的 ACK 表示数据包已经被操作系统内核收到,但并不确保应用层一定收到数据(例如这个时候系统 crash),因此一般建议应用协议层也要设计自己的确认机制。
TCP数据包的接收流程在整体上与发送流程类似,只是方向是相反的。数据包到达网卡后,就会触发中断(IRQ)来告诉CPU读取这个数据包。但是在高性能网络场景下,数据包的数量会非常大,如果每来一个数据包都要产生一个中断,那CPU的处理效率就会大打折扣,所以就产生了NAPI(New API)这种机制让CPU一次性地去轮询(poll)多个数据包,以批量处理的方式来提升效率,降低网卡中断带来的性能开销。
那在poll的过程中,一次可以poll多少个呢?这个poll的个数可以通过sysctl选项来控制:
该控制选项的默认值是300,在网络吞吐量较大的场景中,我们可以适当地增大该值,比如增大到600。增大该值可以一次性地处理更多的数据包。但是这种调整也是有缺陷的,因为这会导致CPU在这里poll的时间增加,如果系统中运行的任务很多的话,其他任务的调度延迟就会增加。
与 TCP发送缓冲区类似,TCP接收缓冲区的大小也是受控制的。通常情况下,默认都是使用tcp_rmem来控制缓冲区的大小。
它也有3个字段:min、default、max。TCP接收缓冲区大小也是在min和max之间动态调整 。
不过跟发送缓冲区不同的是,这个动态调整是可以通过控制选项来关闭的,这个选项是tcp_moderate_rcvbuf 。通常我们都是打开它,这也是它的默认值:
之所以接收缓冲区有选项可以控制自动调节,而发送缓冲区没有,那是因为TCP接收缓冲区会直接影响TCP拥塞控制,进而影响到对端的发包,所以使用该控制选项可以更加灵活地控制对端的发包行为。
除了tcp_moderate_rcvbuf 可以控制TCP接收缓冲区的动态调节外,也可以通过setsockopt()中的配置选项SO_RCVBUF来控制,这与TCP发送缓冲区是类似的。如果应用程序设置了SO_RCVBUF这个标记,那么TCP接收缓冲区的动态调整就是关闭,即使tcp_moderate_rcvbuf为1,接收缓冲区的大小始终就为设置的SO_RCVBUF这个值。
也就是说,只有在tcp_moderate_rcvbuf为1,并且应用程序没有通过SO_RCVBUF来配置缓冲区大小的情况下,TCP接收缓冲区才会动态调节。
领取专属 10元无门槛券
私享最新 技术干货