引言
在上文的TCP优化背景和架构方案当中,我们提到过aeproxy这个应用以及他的职责,他是一个域名代理的应用,负责将对应域名的url经过分析、代理转发给相应的业务系统去处理;
在这篇文章里,与大家分享一些在构建和实测aeproxy这种反向代理应用时,遇到的问题和对应的解决方案;
反向代理系统分析
关键消耗定位
在实测开始之前,先针对这个应用的消耗做一下简单的分析;分析的思路与上文一样,依然是从RT入手;上文详细分析过代理转发场景下RT的具体组成:
我们这里再针对RT进行一下分析,可以简单分为三部分:
- 前端代理转发请求时间消耗
- 主请求处理请求和响应时间
- 主子请求处理连接时间(建立、关闭)
- 后端服务处理请求时间消耗
- 网络传输时间消耗
- 用户端与反向代理应用之间的外部网络消耗
- 反向代理应用与后端服务直接的内部网络消耗
这三部分消耗其中第二部分完全取决于后端服务集群的处理能力,而第三部分取决于传输过程当中的时延以及网络、路由器、交换机的处理能力等等外部因素,并不在反向代理系统的控制范围之内,因此不做具体分析;
第一部分前端代理转发请求时间消耗当中,主请求处理请求和响应时间具体在aeproxy这个应用上,只有TMD和sysguard,我们暂时忽略他们的影响;因此实际反向代理应用的消耗关键在于主子请求处理连接时间;
消耗资源分析
下面我们就从各个计算机资源来看,主子请求处理连接对他们的消耗是怎么样的:
- CPU
由于Nginx本身使用IO多路复用以及事件驱动的并发模型,进程切换和调度成本都是比较小的;但是连接的建立socket(),监听listen(),发起connect(),接收accpet(),发送数据send(),接收数据recv(),关闭close()等等网络套接字操作所造成的消耗却是不可忽略的;前面分析过,反向代理应用的RT消耗关键在于主子请求处理连接时间,因此网络连接操作越频繁,CPU消耗就越大;
- Memory
内存消耗主要来源是在于网络连接层面的消耗,Nginx自身的进程数消耗内存较少,也并不运行缓存等消耗内存的模块,但是每个网络连接自身的栈空间和传输的buffer空间的消耗还是有的,虽然每个连接消耗较小,但是反向代理应用连接数较高,因此这部分内存消耗会随着连接数的不断攀升,而增长的;因此内存消耗是与连接数有必然的联系;
- 网卡
网卡是一个系统网络的入口和出口,针对网页内容的反向代理应用,网页大小一般在几十KB左右,因此一个千兆网卡能支撑的QPS输出大概几w左右,网卡是物理层面的瓶颈,是系统优化往往是不能解决的问题;
网络连接是关键,keepalive是调优的重点
前面提到将消耗资源简单分析之后,我们看到在网络连接是反向代理系统的关键;
能否降低网络连接数,能否减少网络连接操作的次数,用较少的连接数和网络连接操作,输出较高的QPS,是我们最关心的问题;
而就目前而言,keepalive是解决这些问题当中比较有效的手段;
- keepalive可以复用网络连接,避免反复创建、关闭等系统操作带来的消耗; (这里注意keepalive实际上并不会降低网络连接数,这个是常见的误区,后面会讲到)
- keepalive从TCP层面降低了三次握手和四次挥手的网络时延;
- keepalive也可以降低TCP慢启动等无谓的拥塞控制;
针对反向代理应用,主请求是客户端与代理应用之间的连接,而子请求是代理应用与后端业务系统之间的连接;所以keepalive涉及到两部分,主请求目前一般都是开启的,而子请求的keepalive(即upstream keepalive)往往都是忽略的;
压测与调优实战
我们首先以1200并发压测单台aeproxy,对比是否开启子请求keepalive的效果;
压测数据对比
未开启keepalive压测数据:
开启keepalive压测数据:
开启keepalive之后,整体的QPS有所下降,并且cpu使用率和load都飙升:
按照之前的分析开启keepalive是应该提升性能和容量的,但是貌似适得其反。。。
端口不足怎么办?
继续排查发现,在Nginx错误日志当中有这样的错误:
2015/02/26 01:38:28 [crit] 27076#0: *150847 connect() to 172.20.21.92:80 failed (99: Cannot assign requested address) while connecting to upstream, client: 172.20.196.37, server: www.aliexpress.com, request: "GET /api/getFreightCountrySearchList.vhtml HTTP/1.1", upstream: "http://172.20.21.92:80/api/getFreightCountrySearchList.vhtml", host: "www.aliexpress.com"
“Cannot assign requested address”,这个是端口不足的错误信息;针对反向代理应用,主请求是被动接收连接,在代理应用上的连接使用的端口都是复用80端口,而子请求是主动发起连接,使用的是代理应用用于TCP传输的端口;
在linux系统当中,端口是16进制的一个数值,因此他的最大不能超过65536;而1024以下端口都是为系统服务预留的端口(如:HTTP、SSH),子请求只能只用临时端口(临时端口一般是提供给客户服务器通讯的端口。这种端口是临时的,并且仅在应用程序使用协议建立通讯联系的周期中有效);
查看系统配置“net.ipv4.ip_local_port_range”,这个配置是配可以使用的临时端口的范围;系统默认配置的是:
net.ipv4.ip_local_port_range = 32768 61000
这意味着子请求只可以使用32768到61000之间的端口(大概2.8w个端口),因此建议变更配置为:
net.ipv4.ip_local_port_range = 1024 65535
改动之后,再进行压测,端口不足的错误不见了,cpu使用率和load也恢复正常;
TIME_WAIT连接过多怎么办?
端口不足的问题解决了,但是TCP连接数还是比不开keepalive增长了将近6倍,看一下TCP相关监控数据:
TCP连接数超过18.3w,其中TIME-WAIT连接就占18w;
什么是TIME_WAIT
TCP在关闭连接是需要经过四次挥手,如下图:
因为TCP连接是双向的,所以在关闭连接的时候,两个方向各自都需要关闭。先发FIN包的一方执行的是主动关闭;后发FIN包的一方执行的是被动关闭。主动关闭的一方会进入TIME_WAIT状态,并且在此状态停留两倍的MSL时长。
MSL指的是报文段的最大生存时间,如果报文段在网络活动了MSL时间,还没有被接收,那么会被丢弃;协议当中建议MSL设置为2分钟;不过实际上不同的操作系统可能有不同的设置,以Linux为例,通常是30秒,两倍的MSL就是1分钟,并且这个数值是硬编码在内核中的,除非编译内核,否则无法修改;
缩短TIME_WAIT等待时间
TIME_WAIT等待时间长达60秒,霸占了系统有限的端口和连接,尝试缩短这个时间,看压测效果;
集团重新编译过内核,增加了net.ipv4.tcp_tw_timeout选项,通过改动这个选项的值,可以调整TIME_WAIT的等待时间;
默认值是(单位是秒):
net.ipv4.tcp_tw_timeout = 60
我们将这个值调小:
net.ipv4.tcp_tw_timeout = 3
调整之后进行压测,发现TCP连接数下降至8k+,大幅度降低连接和端口的使用;
为什么需要TIME_WAIT
缩短了等待时间,效果很明显,但是这个可以这么做吗?我们要追本溯源,看为什么需要TIME_WAIT,TIME_WAIT等待时间设置长短所产生的影响;
- TIME_WAIT存在的原因是什么?
简单的讲,有以下两点原因:
1)可靠的终止连接
当主动关闭的一方收到被动关闭的一方发出的FIN包后,回应ACK包,同时进入TIME_WAIT状态,但是因为网络传输时延,主动关闭的一方发送的这个ACK包有可能延迟,从而触发被动连接一方重传FIN包。这之间一去一回,最长会达到两倍的MSL时长。
如果主动关闭的一方跳过TIME_WAIT直接进入CLOSED,或者在TIME_WAIT停留的时长不足两倍的MSL,那么当被动关闭一方早先发出的延迟包到达时,TCP连接可能已经关闭了,只能发送RST(复位报文段),而被动关闭一方将视其为错误;
2)保证让迟来的TCP报文段有足够的时间被识别并丢弃
在Linux系统当中,一个TCP端口是不能被同时打开两次及其以上的,当一个TCP连接处于TIME_WAIT状态时,我们将无法立即使用该连接占用着的端口来建立一个新连接;反过来说,如果不存在TIME_WAIT状态,则应用程序有可能立即建立一个和刚关闭的连接相同端口的新连接,这个新连接就有可能收到属于原来的连接的、携带应用程序数据的TCP报文段(即迟到的报文段),这是明显会造成问题的;
在针对aeproxy的场景,变更TIME_WAIT等待时间会影响主子请求,分别看一下各自的情况:
针对子请求的连接,如果出现上面的状况,有可能产生程序处理错误或数据混乱的,但是由于子请求是在机房内部之间进行网络传输,网络时延都比较可控而且相对较低,因此针对子请求,TIME_WAIT等待时间是可以适当缩短的;
同样针对主请求的连接,也会产生程序处理错误或数据混乱的,但是主请求是客户端与服务器之间的连接,涉及外部网络的传输,相对子请求的网络时延更加严重,而且AE作为跨境网站,这种传输时延十分不可控,因此造成处理错误或数据混乱的概率会加大很多;
因此,可以看出能否缩短TIME_WAIT等待时间,很大程度上依赖网络传输的时延;而针对类似AE跨境网站,网络延迟较高,缩短TIME_WAIT等待时间有一定风险;
开启upstream keepalive为什么TIME_WAIT连接数更多?
缩短TIME_WAIT等待时间,是可以减少TIME_WAIT次数的,但是为什么开启子请求的keepalive会导致TIME_WAIT连接增多呢?
打开子请求upstream的keepalive,子请求出现TIME_WAIT的可能就是代理应用主动关闭了连接,导致TIME_WAIT状态出现。那么子请求关闭连接这块Nginx是怎么处理呢?
Nginx如何管理upstream keepalive连接
Nginx是使用free队列和cache队列来进行upstream长连接的管理的。
free队列相当于upstream keepalive的配额池,配置初始化是会按照upstream keepalive到配置数量,创建相应长度的free队列;free队列当中是存储连接地址信息的item元素;应用初始化时,free队列当中存储的都是空的item元素;而cache队列存储的是已经缓存了的连接地址信息,cache队列的长度和free队列相同。
当一个请求到来是,先从cache队列查找是否有已经缓存的未使用的连接地址信息可以使用,如果有,则直接使用这个连接,不再创建新的连接;
如果没有,则新建一个连接(ngx_socket),用这个新建连接处理当前请求;当请求处理完毕时,从free队列当中获取一个空的item元素,将连接信息设置到此元素当中,将这个item元素放到cache队列当中;流程如下图:
但是当free队列当中没有可以使用的配额时,Nginx处理方式会发生改变,流程如下图:
当free队列为空,证明没有可以使用的配额(即upstream keepalive连接达到了配置的上限),这时Nginx选择从cache队列当中找到最近最少使用的连接并关闭他,再将新连接放置到cache队列当中;Nginx源码如下:
if (ngx_queue_empty(&kp->conf->free)) { // free队列判空
q = ngx_queue_last(&kp->conf->cache); // 从cache队列取最近最少使用的item元素
ngx_queue_remove(q);
item = ngx_queue_data(q, ngx_http_upstream_keepalive_cache_t, queue);
ngx_http_upstream_keepalive_close(item->connection); // 关闭最近最少使用的连接
}
item->connection = c;
ngx_queue_insert_head(&kp->conf->cache, q); // 将item元素放置到cache队列
结合aeproxy具体情况分析upstream keepalive情况
当Nginx配置的upstream连接数不够用时,Nginx会出现主动关闭连接的情况;我们再结合tcpdump信息排查一下主动关闭连接时的发包情况:
这个case是在压测tcpdump信息当中筛选的主动关闭连接的case,可以看到在编号为24783的记录,是172.20.200.57(aeproxy)主动发出FIN包(结束报文段),主动关闭连接;这个包距离上一个编号为23696的包,有400ms的时间差,有1000+包的间隔,与前面分析关闭最近最少使用的连接场景很相近;
查看aeproxy upstream配置:
upstream wholesale_proxy {
server us-wholesale.en.vip.alidc.net;
keepalive 8;
}
upstream aedetail_proxy {
server us-aedetail.en.vip.alidc.net;
keepalive 8;
}
upstream aedetailsubsite_proxy {
server us-aedetailsubsite.en.vip.alidc.net;
keepalive 8;
}
upstream store_proxy {
server store.vip.scl.en.alidc.net;
keepalive 8;
}
upstream usi18n_proxy {
server us-wsi18n.en.vip.alidc.net;
keepalive 8;
}
keepalive设置为8,将配置调大,压测试一下
合理配置upstream keepalive压测效果对比
为了将问题简化,单独针对一个upstream进行压测,upstream keepalive配置为512和8,压测数据对比:
TCP连接数下降2/3,确定TCP连接数上涨就是这个问题;
关于upstream keepalive如何配置,配置多少合适?如果需要比较精确度量,可以从较低一个数值,逐步往大调整,并观察TCP连接数量;当keepalive配置扩大,而连接数维持基本稳定时,此时的配置最为合适;
(推荐大家使用ss查看网络连接,ss比netstat快很多,大并发下netstat就挂了)
keepalive开启前后数据对比
经过前面问题的排查和优化,最后再来看一下开启upstream keepalive和不开启upstream keepalive的数据对比;
(三种数据横向对比,第一个是upstream keepalive 512,第二个是upstream keepalive 8,第三个是不开启upstream keepalive)
CPU使用率:
load:
TCP连接数:
可以清晰看到在CPU、load、TCP连接数三个方面,合理开启upstream keepalive都有一定幅度的提升,而且这个数据会随着压测并发加大,效果会更加明显;
(考虑到对后端集群服务的影响,并未将压力压到极限)
这里再强调一个问题,就是前面提到keepalive可以减少连接数的问题;这个地方容易形成误区;keepalive实际上只是将TCP连接进行复用,并不能减少连接数;在HTTP协议下,一个TCP连接同一时间只能服务于一个请求,因此在请求量相同的场景下,实际使用的连接数并不会减少;其实只是TCP协议当中,TIME_WAIT有一段等待时间,造成大量连接处于等待关闭状态,因此才造成可以减少连接数的假象;
实际压测据我观察,ESTABLISHED连接,反倒是由于连接充分复用,比不开启keepalive增长了一些;
小结
前面分析很多东西,我们汇总一下几个重点结论:
1. 针对反向代理应用,大量子请求需要申请临时端口,因此将临时端口扩大是很有必要的;相关配置如下:
net.ipv4.ip_local_port_range = 1024 65535
2. 针对TIME_WAIT连接过多的情况,类似AE这种跨境网站,网络时延较大的场景,降低TIME_WAIT等待时间,有可能导致连接错误终止,甚至是迟到TCP报文不能被识别和抛弃,因此缩短TIME_WAIT等待时间要谨慎,有可能导致上面的相关问题出现;
3. 针对upstream keepalive的配置,需要结合测试,进行合理配置,否则会因为配额较小,导致频繁主动关闭连接,连接数猛增,keepalive效果变差;
4. keepalive开启之后,复用连接,降低相关网络操作,因此在cpu和连接数方面都有一定幅度的提升;
5. keepalive并不能减少连接数,只是复用连接而已;
关于后续
前面消耗资源分析和整个调优的过程当中发现,网卡等物理硬件因素除外,反向代理应用的最关键的问题是端口和连接;
端口不足,限制连接扩展
前面提到过Linux操作系统端口最多只有65535个,而反向代理应用子请求的连接都需要使用代理机器的临时端口,而当网卡升级之后,更多的请求发送过来时,随着连接数的不断增长,6w多个肯定是不够用的;
一个连接是通过四个元素进行确认的,源IP地址、源端口、目的IP地址、目的端口,前面提到过反向代理应用源端口是受限的,但是我们可以通过扩展源IP地址方式解决这个问题;Linux当中可以提供一个网卡绑定多个IP的,这样源IP地址也是可以扩展的,这样连接的数量也就可以扩展了,从而规避了端口受限的问题;
协议层面无法充分做到连接的复用
TCP/IP协议是基于连接的、可靠的、全双工网络协议,而目前基于TCP/IP协议之上的HTTP1.1协议,针对一个连接,同一时间只能完成一次请求、应答交互,没有发挥出全双工通信的优势,无法复用连接,从而导致大量连接创建和维护;
目前新的HTTP2.0协议已经正式定稿,新的HTTP2.0协议接纳了SPDY作为原型,支持在一个网络连接同时进行多个请求和应答,在连接层面做到了充分的复用,降低连接创建和维护的成本;
TCP优化分享相关文章: