高性能网络编程系列

本系列转自陶辉大牛的博客。

高性能网络编程(一)----accept建立连接

高性能网络编程2----TCP消息的发送

  1. 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

  1. tcp_low_latency打开时
    在TCP中断时需要处理ACK响应等TCP协议,去除TCP头等信息(放入receive队列的需要去除),甚至还可能把数据直接copy到用户态buffer。
    所以,这种模式下用户进程能快速的得到数据,但是软中断的时间长,造成TCP吞吐量下降。
  2. 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 一些参数

  1. SO_RCVTIMEO
    JAVA不不支持设置该参数,该参数JVM设的为0
  2. SO_RCVLOWAT
    《UNIX网络编程中》描述该参数用于select/epoll,和本位描述不符。
  3. TP_LOW_LATENCY

4. 网络编程4--TCP连接的关闭

4.1 监听句柄的关闭

半连接直接发RST

4.2 关闭ESTABLISH状态的连接

  1. 如果还有数据未读取
    发RST
  2. 如果还有待发送的数据
    发送,在最后一个报文加上FIN
  3. so_linger
    so_linger是close(无论socket是否工作在阻塞模式,都是阻塞的)的超时时间,用来尽量保证对方收到了close时发出的消息,即,至少需要对方通过发送ACK且到达本机。

5. 高性能网络编程5--IO复用与并发编程

  1. select和epoll
    select每次调用都需要把所有欲监控的socket传入内核态
  2. 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

读取缓冲包含两部分:

  1. 于应用程序的延时报文读取
  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缓存上限自动调整策略关闭

  1. SO_SNDBUF和SO_RCVBUF
    应用程序可以为某个连接设置的参数,分别代表写缓冲和读缓冲的最大值。
    在内核中会把这个值翻一倍再作为写缓存上限使用。
  2. net.core.wmem_max和net.core.rmem_max
    操作系统级别的定义的参数。当SO_SNDBUF和SO_RCVBUF大于系统级的参数时,以系统级的为准。
    在内核中也会把这个值翻一倍再作为写缓存上限使用。
  3. 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,这个功能将不会生效(慎用)。

推荐阅读更多精彩内容

  • 最近在部门内做了个高性能网络编程的培训,近日整理了下PPT,欲写成一系列文章从应用角度谈谈它。 编写服务器时,许多...
    泥孩儿0107阅读 70评论 0 1
  • pdf下载地址:Java面试宝典 第一章内容介绍 20 第二章JavaSE基础 21 一、Java面向对象 21 ...
    王震阳阅读 89,441评论 26 538
  • 最近在看《UNIX网络编程 卷1》和《FREEBSD操作系统设计与实现》这两本书,我重点关注了TCP协议相关的内容...
    腩啵兔子阅读 391评论 0 7
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 84,849评论 14 122
  • 相逢不语,一朵芙蓉著秋雨。 小晕红潮,斜溜鬟心只凤翘。 待将低唤,直为凝情恐人见。 欲诉幽怀,转...
    孤寂的猫生阅读 38评论 0 1