我们按照数据传输过程依次介绍基本概念:
TCP发送缓冲区:
每个TCP套接字有一个发送缓冲区,我们可以用SO_SNDBUF套接字选项来更改该缓冲区的大小。
- 当某个应用进程调用send时,找到socket内核对象,把要发送的buffer地址,数据长度等参数打包到msghdr对象,然后由系统调用进入协议栈;
- 传输层,内核分配skb,skb放到socket的发送队列,拷贝要发送的数据,满足一定条件才会发送数据
- 满足窗口要求(拥塞、流控),将skb由传输层传到网络层,先复制skb用于网卡发送(复制是因为网卡发送就删除,用于重发等情况),然后修改skb的tcp头
- 网卡是多队列,包括发送和接收队列,寻找发送队列,然后把复制的skb放到发送队列中
- 网络层发送阶段包括,路由查找,ip头设置,skb切片(大于MTU的话,这里也会再分配skb并复制切片)等,如果能够在用户层控制数据包小于MTU的话(降低切片处理开销,一个切片丢失导致整包重传开销)
- 网卡发送出去后,cpu收到发送完成软中断通知NET_RX_SOFTIRQ,然后清理ring buffer,待收到ack,传输层删除socket发送队列的skb
如果该套接字的发送缓冲区容不下该应用进程的所有数据(或是应用进程的缓冲区大于套接字的发送缓冲区,或是套接字的发送缓冲区中已有其他数据),该应用进程将被投入睡眠。这里假设该套接字是阻塞的,它通常是默认设置。内核将不从write系统调用返回,直到应用进程缓冲区中的所有数据都复制到套接字发送缓冲区。因此,从写一个TCP套接字的write调用成功返回仅仅表示我们可以重新使用原来的应用进程缓冲区,并不表明对端的TCP或应用进程已接受到数据。
这一端的TCP提取套接字发送缓冲区中的数据并把它发送给对端的TCP,其过程基于TCP数据传送的所有规则。对端TCP必须确认收到的数据,伴随来自对端的ACK的不断到达,本段TCP至此才能从套接字发送缓冲区中丢弃已确认的数据。TCP必须为已发送的数据保留一个副本,直到它被对端确认为止。本端TCP以MSS大小或是更小的块把数据传递给IP,同时给每个数据块安上一个TCP首部以构成TCP分节,其中MSS或是由对端告知的值,或是536(若未发送一个MSS选项为576-TCP首部-IP首部)。IP给每个TCP分节安上一个IP首部以构成IP数据报,并按照其目的的IP地址查找路由表项以确定外出接口,然后把数据报传递给相应的数据链路。每个数据链路都有一个数据队列,如果该队列已满,那么新到的分组将被丢弃,并沿协议栈向上返回一个错误:从数据链路到IP,在从IP到TCP。TCP将注意到这个错误,并在以后某个时候重传相应的分节。应用程序不知道这种暂时的情况。
(2)UDP发送缓冲区
任何UDP套接字都有发送缓冲区大小(我们可以用SO_SNDBUF套接字选项更改它),不过它仅仅是可写道套接字UDP数据报大小上限。如果一个应用进程写一个大于套接字发送缓冲区大小的数据报,内核将返回该进程一个EMSGSIZE错误。既然UDP是不可靠的,它不必保存应用进程数据的一个副本,因此无需一个真正的发送缓冲区。(应用进程的数据在沿协议栈向下传递时,通常被复制到某种格式的一个内核缓冲区中,然而当该数据被发送之后,这个副本被数据链路层丢弃了。)
UDP简单地给来自用户的数据报安上8字节首部以构成UDP数据报,然后传递给IP。IPv4或IPv6给UDP数据报安上相应的IP首部以构成IP数据报,执行路由操作确定外出接口,然后或者直接把数据报加入数据链路层输出队列(如果适合于MTU),或者分片后在把每个片段加入数据集链路层的输出队列。如果某个UDP进程发送大数据报,那么它们相比TCP应用数据更有可能被分片,因为TCP会把应用数据划分成MSS大小的块,而UDP却没有对等的手段。
从写一个UDP套接字的write调用成功返回表示所写的数据报或其所有片段已被加入数据链路层的输出队列。如果该队列没有足够的空间存放该数据报或它的某个片段,内核通常会返回一个ENOBUFS错误给它的应用进程。有些UDP实现不返回这种错误,这样甚至数据报未经发送就被丢弃的情况进程也不知道。
(3)MSS(maximun segment size)
TCP有一个最大分段大小,用于对端TCP通告对端每个分段中能发送的最大TCP数据量。MSS的目的是告诉对端其重组缓冲区大小的实际值,从而避免分片。MSS经常设计成MTU减去IP和TCP首部的固定长度。以太网中使用IPv4MSS值为1460,使用IPv6的MSS值为1440(两者TCP首部都是20字节,但是IPv6首部是40字节,IPv4首部是20字节)。
TCP分段的原因是MSS,IP分片的原因是MTU,由于一直有MSS<=MTU,很明显,分段后的每一段TCP报文段再加上IP首部后的长度不可能超过MTU,因此也就不需要在网络层进行IP分片了。因此TCP报文段很少会发生IP分片的情况。
再来看UDP数据报,由于UDP数据报不会自己进行分段,因此当长度超过了MTU时,会在网络层进行IP分片。同样,ICMP(在网络层中)同样会出现IP分片情况。
(4)MTU
链路层的这个特性称为MTU,即最大传输单元。以太网和802.3对数据帧的长度都有一个限制,其最大值分别是1500字节和1492字节。有一些链路的MTU可以配置。
两台主机之间的PMTU不一定是个常数,它取决于当时所选择的路径,而且路由选择也不一定是对称的(从A到B的路由可能与从B到A的路由不同),因此,PMTU在两个方向上不一定是一致的。
RFC1191描述了PMTU的发现机制,即确定路径MTU的方法。ICMP的不可到达错误采用的就是这种方法, traceroute程序也是用这种方法来确定到达目的节点的PMT的。
数据链路不同,最大传输单元( Maximum transmission Unit,MTU)也不同,由于IP协议是数据链路的上一层,所以它必须不受数据链路的MTU大小的影响能够加以利用。当IP数据报太大时,就要采用分片技术,以保证数据帧不大于要过的网络的MTU。
IPv4要求的最小链路MTU为68字节。这允许最大的IPv4首部(包括20字节的固定长度部分和最多40字节的选项部分)拼接最小的片段(IPv4首部中片段偏移字段以8个字节为单位)IPv6要求的最小链路MTU为1280字节。
(5)分片(fragmentation)
当一个IP数据报从某个接口送出时,如果它的大小超过相应链路的MTU,IPv4和IPv6都将执行分片。这些片段在到达终点之前通常不会被重组(reassembling)。IPv4主机对其产生的数据报执行分片,IPv4路由器则对其转发的数据报进行分片。然后IPv6只有主机对其产生的数据报执行分片,IPv6路由器不对其转发的数据报执行分片。
IPv4首部的“不分片”(do not fragment)位(即DF位)若被设置,那么不管是发送这些数据报的主机还是转发他们的路由器,都不允许对它们分片。当路由器接收到一个超过其外出链路MTU大小且设置了DF位的IPv4数据报时,它将产生一个ICMPv4“destination unreachable,fragmentation needed but DF bit set”(目的不可到达,需分片但DF位已设置)的出错消息。
既然IPv6路由器不执行分片,每个IPv6数据报于是隐含一个DF位。当IPv6路由器接收到一个超过其外出链路MTU大小的IPv6数据报时,它将产生一个ICMPv6 “packet too big”的出错消息。IPv4的DF位和隐含DF位可用于路径MTU发现。
我们要避免出现IP分片,因为IP层是没有超时重传机制的,如果IP层对一个数据包进行了分片,只要有一个分片丢失了,只能依赖于传输层进行重传,结果是所有的分片都要重传一遍,这个代价有点大。
tcp稳定,所以分片没问题,但是udp不行,他重组可能出现顺序或丢包情况,更加危险,因此不建议udp超过mtu
高并发下一些参数调整:
第一个:
- echo "net.ipv4.tcp_rmem = 4096 4096 16777216">> /etc/sysctl.conf
- echo "net.ipv4.tcp_wmem = 4096 4096 16777216">> /etc/sysctl.conf
也可以通过setsockopt修改SO_RCVBUF、SO_SNDBUF;
第二个需要调整的tcp_mem,调整TCP的内存大小,其单位是页,1页等于4096字节。系统默认值:
- cat /proc/sys/net/ipv4/tcp_mem
- 932448 1243264 1864896
tcp_mem(3个INTEGER变量):low, pressure, high
- low:当TCP使用了低于该值的内存页面数时,TCP不会考虑释放内存。
- pressure:当TCP使用了超过该值的内存页面数量时,TCP试图稳定其内存使用,进入pressure模式,当内存消耗低于low值时则退出pressure状态。
- high:允许所有tcp sockets用于排队缓冲数据报的页面量,当内存占用超过此值,系统拒绝分配socket,后台日志输出“TCP: too many of orphaned sockets”。
一般情况下这些值是在系统启动时根据系统内存数量计算得到的。 根据当前tcp_mem最大内存页面数是1864896,当内存为(1864896*4)/1024K=7284.75M时,系统将无法为新的socket连接分配内存,即TCP连接将被拒绝。
实际测试环境中,据观察大概在99万个连接左右的时候(零头不算),进程被杀死,触发out of socket memory错误(dmesg命令查看获得)。每一个连接大致占用7.5K内存(下面给出计算方式),大致可算的此时内存占用情况(990000 * 7.5 / 1024K = 7251M)。
- echo "net.ipv4.tcp_mem = 786432 2097152 3145728">> /etc/sysctl.conf
第三个net.core.netdev_max_backlog:
该参数定义了当接口收到包的速率大于内核处理包的速率时,设备的输入队列中的最大报文数。
第四个net.ipv4.tcp_max_syn_backlog:
每一个连接请求(SYN报文)都需要排队,直至本地服务器接收,该变量就是控制每个端口的 TCP SYN队列长度的。如果连接请求多余该值,则请求会被丢弃。