高性能网络编程系列
本系列转自陶辉大牛的博客。
高性能网络编程(一)----accept建立连接
高性能网络编程2----TCP消息的发送
- SO_SNDTIMEO
发送超时时间,可以简单的认为是把用户态数据copy到TCP发送缓冲区的超时时间。
JVM中该参数就是0。
3. 高性能网络编程3----TCP消息的接收
3.1 四种队列
为了功能区分和减小并发加锁竞争,所以组织了多种队列
1.1 receive(数据是已去除TCP协议之后的可以直接被copy到用户态的数据)
1.2 out-of-order
1.3 prequeue
1.4 backlog(socket被加锁时放入)
3.2 prequeue与tcp_low_latency
当有socket正在睡眠以等待更多的数据时,新到的包根据tcp_low_latency的配置可能到prequeue队列,也可能到receive或者out-of-order队列。
tcp_low_latency默认为0,即关闭,此时新到的包进入tcp_low_latency
3.2.1 prequeue
-
tcp_low_latency打开时
在TCP中断时需要处理ACK响应等TCP协议,去除TCP头等信息(放入receive队列的需要去除),甚至还可能把数据直接copy到用户态buffer。
所以,这种模式下用户进程能快速的得到数据,但是软中断的时间长,造成TCP吞吐量下降。 -
tcp_low_latency关闭时
与普通机制的主要区别在于,在进程没有收取到足够的数据而睡眠等待时,prequeue机制会将skb放入prequeue队列中再唤醒进程,再由进程对skb进行TCP协议处理,再copy数据;而普通模式下skb会在软中断上下文处理,在放入sk->sk_receive_queue队列中后再唤醒进程,进程被唤醒后只是copy数据。对比普通模式,prequeue机制下使得skb的TCP协议处理延迟,延迟的时间为从skb被放入prequeue队列并唤醒进程开始,到进程被调度到时调用tcp_prequeue_process函数处理skb时截止。对于收数据的进程而言在一次数据接收过程中其实并没有延迟,因为普通模式下进程也会经历睡眠-唤醒的过程。但由于TCP协议处理被延迟,导致ACK的发送延迟,从而使数据发送端的数据发送延迟,最终会使得整个通信过程延迟增大。现在我们知道prequeue机制延迟大的原因了:skb的TCP协议处理不是在软中断中进行,而是推迟到应用进程调用收包系统调用时。
3.2.2 为什么有用户进程因等待数据睡眠时才有tcp_low_latency机制
因为有进程在等待,所以可以让新到的包的TCP协议完成由等待的进程完成。
3.3 一些参数
-
SO_RCVTIMEO
JAVA不不支持设置该参数,该参数JVM设的为0 -
SO_RCVLOWAT
《UNIX网络编程中》描述该参数用于select/epoll,和本位描述不符。 - TP_LOW_LATENCY
4. 网络编程4--TCP连接的关闭
4.1 监听句柄的关闭
半连接直接发RST
4.2 关闭ESTABLISH状态的连接
- 如果还有数据未读取
发RST - 如果还有待发送的数据
发送,在最后一个报文加上FIN - so_linger
so_linger是close(无论socket是否工作在阻塞模式,都是阻塞的)的超时时间,用来尽量保证对方收到了close时发出的消息,即,至少需要对方通过发送ACK且到达本机。
5. 高性能网络编程5--IO复用与并发编程
- select和epoll
select每次调用都需要把所有欲监控的socket传入内核态 - epoll提供的2种玩法ET和LT
LT是每次满足期待状态的连接,都得在epoll_wait中返回,所以它一视同仁,都在一条水平线上。ET则不然,它倾向更精确的返回连接。在上面的例子中,连接第一次变为可写后,若是程序未向连接上写入任何数据,那么下一次epoll_wait是不会返回这个连接的。ET叫做 边缘触发,就是指,只有连接从一个状态转到另一个状态时,才会触发epoll_wait返回它。可见,ET的编程要复杂不少,至少应用程序要小心的防止epoll_wait的返回的连接出现:可写时未写数据后却期待下一次“可写”、可读时未读尽数据却期待下一次“可读”。
6. 高性能网络编程6--reactor反应堆与定时器管理
7. 高性能网络编程7--tcp连接的内存使用
net.ipv4.tcp_rmem = 8192 87380 16777216
net.ipv4.tcp_wmem = 8192 65536 16777216
net.ipv4.tcp_mem = 8388608 12582912 16777216
net.core.rmem_default = 262144
net.core.wmem_default = 262144
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
7.1 net.ipv4.tcp_adv_win_scale = 2
读取缓冲包含两部分:
- 于应用程序的延时报文读取
- 接收窗口
tcp_adv_win_scale意味着,将要拿出1/(2^tcp_adv_win_scale)缓存出来做应用缓存。即,默认tcp_adv_win_scale配置为2时,就是拿出至少1/4的内存用于应用读缓存,那么,最大的接收滑动窗口的大小只能到达读缓存的3/4。
7.2 初始的拥塞窗口
以广为使用的linux2.6.18内核为例,在以太网里,MSS大小为1460,此时初始窗口大小为4倍的MSS。有些网络中,会在TCP的可选头部里,使用12字节作为时间戳使用,这样,有效数据就是MSS再减去12,初始窗口就是(1460-12)4=5792*,这与窗口想表达的含义是一致的,即:我能够处理的有效数据长度。
在linux3以后的版本中,初始窗口调整到了10个MSS大小,这主要来自于GOOGLE的建议。原因是这样的,接收窗口虽然常以指数方式来快速增加窗口大小(拥塞阀值以下是指数增长的,阀值以上进入拥塞避免阶段则为线性增长,而且,拥塞阀值自身在收到128以上数据报文时也有机会快速增加),若是传输视频这样的大数据,那么随着窗口增加到(接近)最大读缓存后,就会“开足马力”传输数据,但若是通常都是几十KB的网页,那么过小的初始窗口还没有增加到合适的窗口时,连接就结束了。这样相比较大的初始窗口,就使得用户需要更多的时间(RTT)才能传输完数据,体验不好。
7.3 接收窗口应该设置多大?
image.png所以:接收buffer大小=BDP*4/3
7.4 内存
7.4.1 TCP缓存上限自动调整策略关闭
- SO_SNDBUF和SO_RCVBUF
应用程序可以为某个连接设置的参数,分别代表写缓冲和读缓冲的最大值。
在内核中会把这个值翻一倍再作为写缓存上限使用。 - net.core.wmem_max和net.core.rmem_max
操作系统级别的定义的参数。当SO_SNDBUF和SO_RCVBUF大于系统级的参数时,以系统级的为准。
在内核中也会把这个值翻一倍再作为写缓存上限使用。 - net.core.rmem_default和net.core.wmem_default
定义了读写缓冲区大小的默认值
7.4.2 TCP缓存上限自动调整策略
在并发连接比较少时,把缓存限制放大一些,让每一个TCP连接开足马力工作;当并发连接很多时,此时系统内存资源不足,那么就把缓存限制缩小一些,使每一个TCP连接的缓存尽量的小一些,以容纳更多的连接。
net.ipv4.tcp_moderate_rcvbuf = 1
默认tcp_moderate_rcvbuf配置为1,表示打开了TCP内存自动调整功能。若配置为0,这个功能将不会生效(慎用)。