高性能网络编程(2)
高性能网络编程5—IO复用与并发编程
对于服务器的并发处理能力,我们需要的是:每一毫秒服务器都能及时处理这一毫秒内收到的数百个不同TCP连接上的报文,与此同时,可能服务器上还有数以十万计的最近几秒没有收发任何报文的相对不活跃连接。同时处理多个并行发生事件的连接,简称为并发;同时处理万计、十万计的连接,则是高并发。服务器的并发编程所追求的就是处理的并发连接数目无限大,同时维持着高效率使用CPU等资源,直至物理资源首先耗尽。
并发编程有很多种实现模型,最简单的就是与“线程”捆绑,1个线程处理1个连接的全部生命周期。优点:这个模型足够简单,它可以实现复杂的业务场景,同时,线程个数是可以远大于CPU个数的。然而,线程个数又不是可以无限增大的,为什么呢?因为线程什么时候执行是由操作系统内核调度算法决定的,调度算法并不会考虑某个线程可能只是为了一个连接服务的,它会做大一统的玩法:时间片到了就执行一下,哪怕这个线程一执行就会不得不继续睡眠。这样来回的唤醒、睡眠线程在次数不多的情况下,是廉价的,但如果操作系统的线程总数很多时,它就是昂贵的(被放大了),因为这种技术性的调度损耗会影响到线程上执行的业务代码的时间。举个例子,这时大部分拥有不活跃连接的线程就像我们的国企,它们执行效率太低了,它总是唤醒就睡眠在做无用功,而它唤醒争到CPU资源的同时,就意味着处理活跃连接的民企线程减少获得了CPU的机会,CPU是核心竞争力,它的无效率进而影响了GDP总吞吐量。我们所追求的是并发处理数十万连接,当几千个线程出现时,系统的执行效率就已经无法满足高并发了。
对高并发编程,目前只有一种模型,也是本质上唯一有效的玩法。
从这个系列的前4篇文章可知,连接上的消息处理,可以分为两个阶段:等待消息准备好、消息处理。当使用默认的阻塞套接字时(例如上面提到的1个线程捆绑处理1个连接),往往是把这两个阶段合而为一,这样操作套接字的代码所在的线程就得睡眠来等待消息准备好,这导致了高并发下线程会频繁的睡眠、唤醒,从而影响了CPU的使用效率。
高并发编程方法当然就是把两个阶段分开处理。即,等待消息准备好的代码段,与处理消息的代码段是分离的。当然,这也要求套接字必须是非阻塞的,否则,处理消息的代码段很容易导致条件不满足时,所在线程又进入了睡眠等待阶段。那么问题来了,等待消息准备好这个阶段怎么实现?它毕竟还是等待,这意味着线程还是要睡眠的!解决办法就是,线程主动查询,或者让1个线程为所有连接而等待!
这就是IO多路复用了。多路复用就是处理等待消息准备好这件事的,但它可以同时处理多个连接!它也可能“等待”,所以它也会导致线程睡眠,然而这不要紧,因为它一对多、它可以监控所有连接。这样,当我们的线程被唤醒执行时,就一定是有一些连接准备好被我们的代码执行了,这是有效率的!没有那么多个线程都在争抢处理“等待消息准备好”阶段,整个世界终于清净了!
多路复用有很多种实现,在linux上,2.4内核前主要是select和poll,现在主流是epoll,它们的使用方法似乎很不同,但本质是一样的。
效率却也不同,这也是epoll完全替代了select的原因。
简单的谈下epoll为何会替代select。
前面提到过,高并发的核心解决方案是1个线程处理所有连接的“等待消息准备好”,这一点上epoll和select是无争议的。但select预估错误了一件事,就像我们开篇所说,当数十万并发连接存在时,可能每一毫秒只有数百个活跃的连接,同时其余数十万连接在这一毫秒是非活跃的。select的使用方法是这样的:
返回的活跃连接 ==select(全部待监控的连接)
什么时候会调用select方法呢?在你认为需要找出有报文到达的活跃连接时,就应该调用。所以,调用select在高并发时是会被频繁调用的。这样,这个频繁调用的方法就很有必要看看它是否有效率,因为,它的轻微效率损失都会被“频繁”二字所放大。它有效率损失吗?显而易见,全部待监控连接是数以十万计的,返回的只是数百个活跃连接,这本身就是无效率的表现。被放大后就会发现,处理并发上万个连接时,select就完全力不从心了。
看几个图。当并发连接为一千以下,select的执行次数不算频繁,与epoll似乎并无多少差距:
然而,并发数一旦上去,select的缺点被“执行频繁”无限放大了,且并发数越多越明显:
再来说说epoll是如何解决的。它很聪明的用了3个方法来实现select方法要做的事:
新建的epoll描述符==epoll_create()
epoll_ctrl(epoll描述符,添加或者删除所有待监控的连接)
返回的活跃连接 ==epoll_wait( epoll描述符 )
这么做的好处主要是:分清了频繁调用和不频繁调用的操作。例如,epoll_ctrl是不太频繁调用的,而epoll_wait是非常频繁调用的。这时,epoll_wait却几乎没有入参,这比select的效率高出一大截,而且,它也不会随着并发连接的增加使得入参越发多起来,导致内核执行效率下降。
epoll是怎么实现的呢?其实很简单,从这3个方法就可以看出,它比select聪明的避免了每次频繁调用“哪些连接已经处在消息准备好阶段”的 epoll_wait时,是不需要把所有待监控连接传入的。这意味着,它在内核态维护了一个数据结构保存着所有待监控的连接。这个数据结构就是一棵红黑树,它的结点的增加、减少是通过epoll_ctrl来完成的。
图中左下方的红黑树由所有待监控的连接构成。左上方的链表,同是目前所有活跃的连接。于是,epoll_wait执行时只是检查左上方的链表,并返回左上方链表中的连接给用户。这样,epoll_wait的执行效率能不高吗?
最后,再看看epoll提供的2种玩法ET和LT,即翻译过来的边缘触发和水平触发。其实这两个中文名字倒也有些贴切。这2种使用方式针对的仍然是效率问题,只不过变成了epoll_wait返回的连接如何能够更准确些。
例如,我们需要监控一个连接的写缓冲区是否空闲,满足“可写”时我们就可以从用户态将响应调用write发送给客户端 。但是,或者连接可写时,我们的“响应”内容还在磁盘上呢,此时若是磁盘读取还未完成呢?肯定不能使线程阻塞的,那么就不发送响应了。但是,下一次epoll_wait时可能又把这个连接返回给你了,你还得检查下是否要处理。可能,我们的程序有另一个模块专门处理磁盘IO,它会在磁盘IO完成时再发送响应。那么,每次epoll_wait都返回这个“可写”的、却无法立刻处理的连接,是否符合用户预期呢?
于是,ET和LT模式就应运而生了。LT是每次满足期待状态的连接,都得在epoll_wait中返回,所以它一视同仁,都在一条水平线上。ET则不然,它倾向更精确的返回连接。在上面的例子中,连接第一次变为可写后,若是程序未向连接上写入任何数据,那么下一次epoll_wait是不会返回这个连接的。ET叫做 边缘触发,就是指,只有连接从一个状态转到另一个状态时,才会触发epoll_wait返回它。可见,ET的编程要复杂不少,至少应用程序要小心的防止epoll_wait的返回的连接出现:可写时未写数据后却期待下一次“可写”、可读时未读尽数据却期待下一次“可读”。
当然,从一般应用场景上它们性能是不会有什么大的差距的,ET可能的优点是,epoll_wait的调用次数会减少一些,某些场景下连接在不必要唤醒时不会被唤醒(此唤醒指epoll_wait返回)。但如果像我上面举例所说的,有时它不单纯是一个网络问题,跟应用场景相关。当然,大部分开源框架都是基于ET写的,框架嘛,它追求的是纯技术问题,当然力求尽善尽美。
高性能网络编程6—reactor反应堆与定时器管理
反应堆开发模型被绝大多数高性能服务器所选择,上一篇所介绍的IO多路复用是它的实现基础。定时触发功能通常是服务器必备组件,反应堆模型往往还不得不将定时器的管理囊括在内。本篇将介绍反应堆模型的特点和用法。
反应堆模式可以在软件工程层面,将事件驱动框架分离出具体业务,将不同类型请求之间用OO的思想分离。通常,反应堆不仅使用IO复用处理网络事件驱动,还会实现定时器来处理时间事件的驱动(请求的超时处理或者定时任务的处理),就像下面的示意图:
这幅图有5点意思:
(1)处理应用时基于OO思想,不同的类型的请求处理间是分离的。例如,A类型请求是用户注册请求,B类型请求是查询用户头像,那么当我们把用户头像新增多种分辨率图片时,更改B类型请求的代码处理逻辑时,完全不涉及A类型请求代码的修改。
(2)应用处理请求的逻辑,与事件分发框架完全分离。什么意思呢?即写应用处理时,不用去管何时调用IO复用,不用去管什么调用epoll_wait,去处理它返回的多个socket连接。应用代码中,只关心如何读取、发送socket上的数据,如何处理业务逻辑。事件分发框架有一个抽象的事件接口,所有的应用必须实现抽象的事件接口,通过这种抽象才把应用与框架进行分离。
(3)反应堆上提供注册、移除事件方法,供应用代码使用,而分发事件方法,通常是循环的调用而已,是否提供给应用代码调用,还是由框架简单粗暴的直接循环使用,这是框架的自由。
(4)IO多路复用也是一个抽象,它可以是具体的select,也可以是epoll,它们只必须提供采集到某一瞬间所有待监控连接中活跃的连接。
(5)定时器也是由反应堆对象使用,它必须至少提供4个方法,包括添加、删除定时器事件,这该由应用代码调用。最近超时时间是需要的,这会被反应堆对象使用,用于确认select或者epoll_wait执行时的阻塞超时时间,防止IO的等待影响了定时事件的处理。遍历也是由反应堆框架使用,用于处理定时事件。
下面用极简流程来形象说明下反应堆是如何处理一个请求的,下图中桔色部分皆为反应堆的分发事件流程:
可以看到,分发IO、定时器事件都由反应堆框架来完成,应用代码只会关注于如何处理可读、可写事件。
当然,上图是极度简化的流程,实际上要处理的异常情况都没有列入。
这里可以看到,为什么定时器集合需要提供最近超时事件距离现在的时间?因为,调用epoll_wait或者select时,并不能够始终传入-1作为timeout参数。因为,我们的服务器主营业务往往是网络请求处理,如果网络请求很少时,那么CPU的所有时间都会被频繁却又不必要的epoll_wait调用所占用。在服务器闲时使进程的CPU利用率降低是很有意义的,它可以使服务器上其他进程得到更多的执行机会,也可以延长服务器的寿命,还可以省电。这样,就需要传入准确的timeout最大阻塞时间给epoll_wait了。
什么样的timeout时间才是准确的呢?这等价于,我们需要准确的分析,什么样的时段进程可以真正休息,进入sleep状态?
一个没有意义的答案是:不需要进程执行任务的时间段内是可以休息的。
这就要求我们仔细想想,进程做了哪几类任务,例如:
1、所有网络包的处理,例如TCP连接的建立、读写、关闭,基本上所有的正常请求都由网络包来驱动的。对这类任务而言,没有新的网络分组到达本机时,就是可以使进程休息的时段。
2、定时器的管理,它与网络、IO复用无关,虽然它们在业务上可能有相关性。定时器里的事件需要及时的触发执行,不能因为其他原因,例如阻塞在epoll_wait上时耽误了定时事件的处理。当一段时间内,可以预判没有定时事件达到触发条件时(这也是提供接口查询最近一个定时事件距当下的时间的意义所在),对定时任务的管理而言,进程就可以休息了。
3、其他类型的任务,例如磁盘IO执行完成,或者收到其他进程的signal信号,等等,这些任务明显不需要执行的时间段内,进程可以休息。
于是,使用反应堆模型的进程代码中,通常除了epoll_wait这样的IO复用外,其他调用都会基于无阻塞的方式使用。所以,epoll_wait的timeout超时时间,就是除网络外,其他任务所能允许的进程睡眠时间。而只考虑常见的定时器任务时,就像上图中那样,只需要定时器集合能够提供最近超时事件到现在的时间即可。
从这里也可以推导出,定时器集合通常会采用有序容器这样的数据结构,好处是:
1、容易取到最近超时事件的时间。
2、可以从最近超时事件开始,向后依次遍历已经超时的事件,直到第一个没有超时的事件为止即可停止遍历,不用全部遍历到。
因此,粗暴的采用无序的数据结构,例如普通的链表,通常是不足取的。但事无绝对,redis就是用了个毫无顺序的链表,原因何在?因为redis的客户端连接没有超时概念,所以对于并发的成千上万个连上,都不会因为超时被断开。redis的定时器唯一的用途在于定时的将内存数据刷到磁盘上,这样的定时事件通常只有个位数,其性能无关紧要。
如果定时事件非常多,综合插入、遍历、删除的使用频率,使用树的机会最多,例如小根堆(libevent)、二叉平衡树(nginx红黑树)。当然,场景特殊时,尽可以用有序数组、跳跃表等等实现。
高性能网络编程7—tcp连接的内存使用
当服务器的并发TCP连接数以十万计时,我们就会对一个TCP连接在操作系统内核上消耗的内存多少感兴趣。socket编程方法提供了SO_SNDBUF、SO_RCVBUF这样的接口来设置连接的读写缓存,linux上还提供了以下系统级的配置来整体设置服务器上的TCP内存使用,但这些配置看名字却有些互相冲突、概念模糊的感觉,如下(sysctl -a命令可以查看这些配置):
1 | net.ipv4.tcp_rmem = 8192 87380 16777216 |
这篇文章主要描述linux内核为了TCP连接上传输的数据是怎样管理读写缓存的。
一、缓存上限是什么?
(1)先从应用程序编程时可以设置的SO_SNDBUF、SO_RCVBUF说起。
无论何种语言,都对TCP连接提供基于setsockopt方法实现的SO_SNDBUF、SO_RCVBUF,怎么理解这两个属性的意义呢?
SO_SNDBUF、SO_RCVBUF都是个体化的设置,即,只会影响到设置过的连接,而不会对其他连接生效。SO_SNDBUF表示这个连接上的内核写缓存上限。实际上,进程设置的SO_SNDBUF也并不是真的上限,在内核中会把这个值翻一倍再作为写缓存上限使用,我们不需要纠结这种细节,只需要知道,当设置了SO_SNDBUF时,就相当于划定了所操作的TCP连接上的写缓存能够使用的最大内存。然而,这个值也不是可以由着进程随意设置的,它会受制于系统级的上下限,当它大于上面的系统配置wmem_max(net.core.wmem_max)时,将会被wmem_max替代(同样翻一倍);而当它特别小时,例如在2.6.18内核中设计的写缓存最小值为2K字节,此时也会被直接替代为2K。
SO_RCVBUF表示连接上的读缓存上限,与SO_SNDBUF类似,它也受制于rmem_max配置项,实际在内核中也是2倍大小作为读缓存的使用上限。SO_RCVBUF设置时也有下限,同样在2.6.18内核中若这个值小于256字节就会被256所替代。
(2)那么,可以设置的SO_SNDBUF、SO_RCVBUF缓存使用上限与实际内存到底有怎样的关系呢?
TCP连接所用内存主要由读写缓存决定,而读写缓存的大小只与实际使用场景有关,在实际使用未达到上限时,SO_SNDBUF、SO_RCVBUF是不起任何作用的。对读缓存来说,接收到一个来自连接对端的TCP报文时,会导致读缓存增加,当然,如果加上报文大小后读缓存已经超过了读缓存上限,那么这个报文会被丢弃从而读缓存大小维持不变。什么时候读缓存使用的内存会减少呢?当进程调用read、recv这样的方法读取TCP流时,读缓存就会减少。因此,读缓存是一个动态变化的、实际用到多少才分配多少的缓冲内存,当这个连接非常空闲时,且用户进程已经把连接上接收到的数据都消费了,那么读缓存使用内存就是0。
写缓存也是同样道理。当用户进程调用send或者write这样的方法发送TCP流时,就会造成写缓存增大。当然,如果写缓存已经到达上限,那么写缓存维持不变,向用户进程返回失败。而每当接收到TCP连接对端发来的ACK确认了报文的成功发送时,写缓存就会减少,这是因为TCP的可靠性决定的,发出去报文后由于担心报文丢失而不会销毁它,可能会由重发定时器来重发报文。因此,写缓存也是动态变化的,空闲的正常连接上,写缓存所用内存通常也为0。
因此,只有当接收网络报文的速度大于应用程序读取报文的速度时,可能使读缓存达到了上限,这时这个缓存使用上限才会起作用。所起作用为:丢弃掉新收到的报文,防止这个TCP连接消耗太多的服务器资源。同样,当应用程序发送报文的速度大于接收对方确认ACK报文的速度时,写缓存可能达到上限,从而使send这样的方法失败,内核不为其分配内存。
二、缓存的大小与TCP的滑动窗口到底有什么关系?
(1)滑动窗口的大小与缓存大小肯定是有关的,但却不是一一对应的关系,更不会与缓存上限具有一一对应的关系。因此,网上很多资料介绍rmem_max等配置设置了滑动窗口的最大值,与我们tcpdump抓包时看到的win窗口值完全不一致,是讲得通的。下面我们来细探其分别在哪里。
读缓存的作用有2个:1、将无序的、落在接收滑动窗口内的TCP报文缓存起来;2、当有序的、可以供应用程序读取的报文出现时,由于应用程序的读取是延时的,所以会把待应用程序读取的报文也保存在读缓存中。所以,读缓存一分为二,一部分缓存无序报文,一部分缓存待延时读取的有序报文。这两部分缓存大小之和由于受制于同一个上限值,所以它们是会互相影响的,当应用程序读取速率过慢时,这块过大的应用缓存将会影响到套接字缓存,使接收滑动窗口缩小,从而通知连接的对端降低发送速度,避免无谓的网络传输。当应用程序长时间不读取数据,造成应用缓存将套接字缓存挤压到没空间,那么连接对端会收到接收窗口为0的通知,告诉对方:我现在消化不了更多的报文了。
反之,接收滑动窗口也是一直在变化的,我们用tcpdump抓三次握手的报文:
1 | 14:49:52.421674 IP houyi-vm02.dev.sd.aliyun.com.6400 > r14a02001.dg.tbsite.net.54073: S 2736789705:2736789705(0) ack 1609024383 win 5792 <mss 1460,sackOK,timestamp 2925954240 2940689794,nop,wscale 9> |
可以看到初始的接收窗口是5792,当然也远小于最大接收缓存(稍后介绍的tcp_rmem[1])。
这当然是有原因的,TCP协议需要考虑复杂的网络环境,所以使用了慢启动、拥塞窗口(参见 高性能网络编程2——TCP消息的发送 ),建立连接时的初始窗口并不会按照接收缓存的最大值来初始化。这是因为,过大的初始窗口从宏观角度,对整个网络可能造成过载引发恶性循环,也就是考虑到链路上各环节的诸多路由器、交换机可能扛不住压力不断的丢包(特别是广域网),而微观的TCP连接的双方却只按照自己的读缓存上限作为接收窗口,这样双方的发送窗口(对方的接收窗口)越大就对网络产生越坏的影响。慢启动就是使初始窗口尽量的小,随着接收到对方的有效报文,确认了网络的有效传输能力后,才开始增大接收窗口。
不同的linux内核有着不同的初始窗口,我们以广为使用的linux2.6.18内核为例,在以太网里,MSS大小为1460,此时初始窗口大小为4倍的MSS,简单列下代码(*rcv_wnd即初始接收窗口)
大家可能要问,为何上面的抓包上显示窗口其实是5792,并不是14604为5840呢?这是因为1460想表达的意义是:将1500字节的MTU去除了20字节的IP头、20字节的TCP头以后,一个最大报文能够承载的有效数据长度。但有些网络中,会在TCP的可选头部里,使用12字节作为时间戳使用,这样,有效数据就是MSS再减去12,初始窗口就是(1460-12)4=5792,这与窗口想表达的含义是一致的,即:我能够处理的有效数据长度。
(2)最大读缓存到底应该设置到多少为合适呢?
当应用缓存所占的份额通过tcp_adv_win_scale配置确定后,读缓存的上限应当由最大的TCP接收窗口决定。初始窗口可能只有4个或者10个MSS,但在无丢包情形下随着报文的交互窗口就会增大,当窗口过大时,“过大”是什么意思呢?即,对于通讯的两台机器的内存而言不算大,但是对于整个网络负载来说过大了,就会对网络设备引发恶性循环,不断的因为繁忙的网络设备造成丢包。而窗口过小时,就无法充分的利用网络资源。所以,一般会以BDP来设置最大接收窗口(可计算出最大读缓存)。BDP叫做带宽时延积,也就是带宽与网络时延的乘积,例如若我们的带宽为2Gbps,时延为10ms,那么带宽时延积BDP则为2G/80.01=2.5MB,所以这样的网络中可以设最大接收窗口为2.5MB,这样最大读缓存可以设为4/32.5MB=3.3MB。
经常提及的所谓长肥网络,“长”就是是时延长,“肥”就是带宽大,这两者任何一个大时,BDP就大,都应导致最大窗口增大,进而导致读缓存上限增大。所以在长肥网络中的服务器,缓存上限都是比较大的。(当然,TCP原始的16位长度的数字表示窗口虽然有上限,但在RFC1323中定义的弹性滑动窗口使得滑动窗口可以扩展到足够大。)
发送窗口实际上就是TCP连接对方的接收窗口,所以大家可以按接收窗口来推断,这里不再啰嗦。
三、linux的TCP缓存上限自动调整策略
那么,设置好最大缓存限制后就高枕无忧了吗?对于一个TCP连接来说,可能已经充分利用网络资源,使用大窗口、大缓存来保持高速传输了。比如在长肥网络中,缓存上限可能会被设置为几十兆字节,但系统的总内存却是有限的,当每一个连接都全速飞奔使用到最大窗口时,1万个连接就会占用内存到几百G了,这就限制了高并发场景的使用,公平性也得不到保证。我们希望的场景是,在并发连接比较少时,把缓存限制放大一些,让每一个TCP连接开足马力工作;当并发连接很多时,此时系统内存资源不足,那么就把缓存限制缩小一些,使每一个TCP连接的缓存尽量的小一些,以容纳更多的连接。
linux为了实现这种场景,引入了自动调整内存分配的功能,由tcp_moderate_rcvbuf配置决定,如下:
net.ipv4.tcp_moderate_rcvbuf = 1
默认tcp_moderate_rcvbuf配置为1,表示打开了TCP内存自动调整功能。若配置为0,这个功能将不会生效(慎用)。
另外请注意:当我们在编程中对连接设置了SO_SNDBUF、SO_RCVBUF,将会使linux内核不再对这样的连接执行自动调整功能!
那么,这个功能到底是怎样起作用的呢?看以下配置:
1 | net.ipv4.tcp_rmem = 8192 87380 16777216 |
tcp_rmem[3]数组表示任何一个TCP连接上的读缓存上限,其中 tcp_rmem[0]表示最小上限, tcp_rmem[1]表示初始上限(注意,它会覆盖适用于所有协议的rmem_default配置), tcp_rmem[2]表示最大上限。
tcp_wmem[3]数组表示写缓存,与tcp_rmem[3]类似,不再赘述。
tcp_mem[3]数组就用来设定TCP内存的整体使用状况,所以它的值很大(它的单位也不是字节,而是页—4K或者8K等这样的单位!)。这3个值定义了TCP整体内存的无压力值、压力模式开启阀值、最大使用值。以这3个值为标记点则内存共有4种情况:
1、当TCP整体内存小于tcp_mem[0]时,表示系统内存总体无压力。若之前内存曾经超过了tcp_mem[1]使系统进入内存压力模式,那么此时也会把压力模式关闭。
这种情况下,只要TCP连接使用的缓存没有达到上限(注意,虽然初始上限是tcp_rmem[1],但这个值是可变的,下文会详述),那么新内存的分配一定是成功的。
2、当TCP内存在tcp_mem[0]与tcp_mem[1]之间时,系统可能处于内存压力模式,例如总内存刚从tcp_mem[1]之上下来;也可能是在非压力模式下,例如总内存刚从tcp_mem[0]以下上来。
此时,无论是否在压力模式下,只要TCP连接所用缓存未超过tcp_rmem[0]或者tcp_wmem[0],那么都一定都能成功分配新内存。否则,基本上就会面临分配失败的状况。(注意:还有一些例外场景允许分配内存成功,由于对于我们理解这几个配置项意义不大,故略过。)
3、当TCP内存在tcp_mem[1]与tcp_mem[2]之间时,系统一定处于系统压力模式下。其他行为与上同。
4、当TCP内存在tcp_mem[2]之上时,毫无疑问,系统一定在压力模式下,而且此时所有的新TCP缓存分配都会失败。
下图为需要新缓存时内核的简化逻辑:
当系统在非压力模式下,上面我所说的每个连接的读写缓存上限,才有可能增加,当然最大也不会超过tcp_rmem[2]或者tcp_wmem[2]。相反,在压力模式下,读写缓存上限则有可能减少,虽然上限可能会小于tcp_rmem[0]或者tcp_wmem[0]。
所以,粗略的总结下,对这3个数组可以这么看:
1、只要系统TCP的总体内存超了 tcp_mem[2] ,新内存分配都会失败。
2、tcp_rmem[0]或者tcp_wmem[0]优先级也很高,只要条件1不超限,那么只要连接内存小于这两个值,就保证新内存分配一定成功。
3、只要总体内存不超过tcp_mem[0],那么新内存在不超过连接缓存的上限时也能保证分配成功。
4、tcp_mem[1]与tcp_mem[0]构成了开启、关闭内存压力模式的开关。在压力模式下,连接缓存上限可能会减少。在非压力模式下,连接缓存上限可能会增加,最多增加到tcp_rmem[2]或者tcp_wmem[2]。