1.2 阿里中间件TCP四次挥手性能调优实战
重用数据库连接最主要的原因是可以减少应用程序与数据库之间创建或销毁TCP连接的开销,数据库连接池的概念应运而生。如果不使用连接池,TCP四次挥手过程中TIME_WAIT的性能调优是相对比较复杂的,请看下面这个小例子。
1.2.1 亿级消息网关Rowan架构
Rowan是一个亿级企业消息网关中间件服务,如图1-1所示。它在业务上为B2B、Aliexpress、集团安全、共享事业部、淘宝等大部门提供邮件、短信、旺旺、站内信、钉钉等消息的持续发送能力,支持的业务包括会员注册、评价、仲裁、用户触达EDM、资金中心对账、交易、营销、物流追踪、卖家认证、CRM、风控合规、反欺诈、风险评测、处罚等。
图1-1 Rowan介绍
技术上,Rowan由多个服务和中间件构成,具有模版管理、用户触达、消息管理、EDM无线引流、打点追踪等功能,每天产生的消息过千万,仅仅2016年“双十一大促”当天产生的消息,光邮件类就超过6亿。
以邮件模块为例,Rowan技术架构有过两个时期。第1个时期,业务方请求通过HSF(一种阿里内部类似DUBBO的RPC)请求调用Rowan, Rowan再透传调用阿里云邮SMTP服务。当业务洪峰抵达的时候,海外集群会经常出现超时的情况。这是因为,当时阿里云国内集群的系统设计能力比较强,基本上可以支撑国内的邮件发送;相对于国内有5个数据库集群,国外阿里云集群则显得有些薄弱,仅有一个数据库集群。国内集群的系统设计能力是1亿/天,日常2000多万/天;海外日常1000多万/天,峰值1500多万/天。阿里云使用写磁盘的方式,若投递信息失败,重试5次,大致时间点是2分钟、5分钟、30分钟、1小时、2小时,若2小时仍发送失败则丢弃信息。但是,只要是被阿里云SMTP挡住的,如返回给Rowan的connection reset、451、526等异常,不进入阿里云队列,这就会造成只有Rowan这里会打印出异常信息,而阿里云服务端则打印不出具体异常信息,排查问题相当棘手。
由于国内外知名的邮件服务器,如亚马逊AWS、搜狐SENDCLOUD等都是支持抗堆积的,在无法推动阿里云从SMTP支持到HTTP的背景下,我进行了第2个时期的技术改造设计,如图1-2所示。
图1-2 改造后Rowan技术架构图
在这个架构中,所有外部服务通过Rowan Service调用对应的服务,如“邮件”等。首先会根据Cache缓存中的模版信息组装成消息体,并存储在HBASE中进行发送状态的记载;其次请求的消息会直接丢到对应的中美MQ(阿里内部叫Metaq,对外开源叫RocketMQ)消息队列集群中,在线流式实时计算Jstorm会分别处理MQ中的邮件、短信、站内信、HTTP、旺旺、钉钉等消息,来进行消息的投递。
由于杭州的阿里云邮系统能力远远高于美国集群,所以美国集群若命中了国内邮箱,比如126、163、QQ等,会直接路由到国内阿里云网关进行发送。在图1-2中,Rowan服务是MQ的生产者,Jstorm是MQ的消费者,故流控和暂停功能放在了消费者这里。流控功能是基于阿里配置中心Diamond定制化开发的,Jstorm启动时直接从配置项读取MQ最大线程、最小线程数。在Diamond中还可以配置海外集群的压力疏导功能,支持按照百分比将流量转移到国内。利用Diamond配置中心实时更新的特性,当Diamond修改时可以实时推送给对应机房以指定的流控线程数重启MQ消费者客户端。
阿里云可以配置发件账号的优先级,并暂停低优先级的账号。Rowan这套新架构在此基础上额外支持两个类似的扩展功能:自带无效地址,可以关掉对指定收件人的发送;模版禁用也可以直接关掉此类模版的发送。
这套架构采用疏导的策略,旨在将海外集群的压力转移到国内分担,从而大幅度提高系统的整体QPS。这套架构需要业务进行配合,通过统计国内外邮件账号的QPS指标、进行国内外发件账号的优先级排序、准备发送故障预案、精简合并系统通知邮件、均匀化编排大促营销邮件发送等一系列措施,来保障整体系统“双十一大促”的稳定性。这也是我在阿里巴巴公司工作期间学会的,技术驱动业务,团队合作,“贴着业务走,以结果为导向”是中间件团队的职责。
1.2.2 人脸识别服务:异曲同工的架构
提到疏导的架构和QPS的提升,顺便提一个我在2019年工作中设计的大幅度提升QPS的异曲同工的技术方案。
如图1-3所示,FaceServer是基于Java的人脸识别服务,对业务服务提供人脸识别验证的Dubbo接口,同时调用基于Python的算法人脸识别服务。算法服务包括多个原子服务,如人脸、质量、属性、活体、特征等。据统计,FaceServer服务调用算法人脸识别服务,平均每天有40~50次超时异常,最多的时候有100次。技术团队因此对算法服务器单机进行了压测,单机10个线程,平均响应时间在2.878秒左右(FaceServer认为3秒即超时),最长响应时间为13.814秒,QPS为1。
图1-3 历史架构
FaceServer在物联网时代,就类似于淘宝网等网站的会员登录,是互联网的门面,QPS为1显然不能满足商业化需求。很多做研发的资深人士可能会想,可以堆机器来提高QPS呀!这其实是不可行的,因为算法服务器非常特别,不同于普通的服务器,它是昂贵的GPU密集型服务器:一台阿里云GPU服务器单机每月要6000元,一年7.2万元,按照图1-3采购两台,一年总计14.4万元。也就是说,目前单机只有1QPS,两台机器2QPS,要支持200QPS,如果堆机器,一年就需要花1400多万元。为了QPS达到200,一年1400多万元的开销,绝对不是中小型创业公司能够承担的,在降成本的大型BAT,诸如阿里巴巴这样的公司,想必也是不能接受的。
目前的算法服务器是单点,另一台算法GPU服务器由于历史原因一直没有启用,所以第1步改造我决定先高效利用好两台已有的算法GPU服务器。在进行了将单机部署提升为集群部署、算法服务内部的串行改并行、耗时原子服务多开进程等工作后,FaceServer的维护者表示再也没有出现过线上的超时异常的情况,此时的架构如图1-4所示。经团队压测后,单机的QPS已经从1提升到了7,由于线上正在使用人脸识别服务,整体集群QPS没有进行压测,估计应该是1+1>2的效果(集群14QPS)。
图1-4 第1版优化架构
基于百度人脸识别算法,对于企业认证的用户可以直接免费提供每月15QPS的服务推测,百度应该也是有一些性能优化的技巧来高效利用GPU服务器的,因此目前这样的架构还是可以继续提高QPS的。于是,我又和算法团队进行了进一步的沟通,有收获的是,据算法团队反馈,算法服务器内部单独处理一张人脸图片和批量处理其实是一样的处理。算法团队表示,在单模型情况下,当前人脸特征模型单机进程如果使用批处理至少可以将QPS提高到35,而这恰恰就是可以大幅度提升QPS的切入点。根据算法团队批处理的需求,我于是想到了一个请求合并的架构。
如图1-5所示就是第2版优化架构。这是一种请求合并的处理架构,也是中间件性能提升中较为常见的方案。对于业务服务来说,依然是通过Dubbo同步调用FaceServer服务的,然而FaceServer则是将每次请求丢到一个队列中去,这个队列当请求收集到一定数量(比如算法服务的35QPS)时则进行提交或者每秒进行提交,这种同步转异步的方式可以充分利用算法GPU服务器的资源,从而大幅度提升QPS。在图1-5的架构中,我采用了Guava而没有采用Redis,原因是Redis进行人脸图片的读写会有网络开销,采用Guava会在更大程度上提升响应时间。另外,关于图1-5重试的部分我也使用了GuavaRetry,充分利用了Guava的缓存和重试工具特性。
图1-5 第2版优化架构
这种Dubbo同步转异步、请求合并的方案,其实和Rowan的抗堆积、流控与暂停有着异曲同工之妙,都是在有限的资源(人力、物力)的基础上充分合理利用已有资源,从而达到更为极致的QPS。
1.2.3 “双十一大促”全链路压测发现TCP问题
聊完架构,我们继续回到Rowan和TCP的话题。
Rowan邮件功能使用的是SMTP协议,SMTP在七层协议中属于应用层协议,属TCP/IP协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。Rowan对接阿里云云邮网关,阿里云云邮再对接Hotmail、Gmail、Yahoo等邮件服务商。在我日常运维的过程中,会偶发性地出现“Could not connect to SMTP host”异常提示,尤其是在业务方进行EDM(Email Direct Marketing)邮件大规模营销时会频繁发生。
备战“双十一”过程中,我对Rowan进行了全链路技术改造和压测,在进行线上压测邮件服务过程中发现美国集群(US代表美国,HZ代表杭州)在达到1万QPS压力时,Rowan服务端大量产生60多万次的SMTP连接不上阿里云网关的异常,如图1-6所示。
图1-6 全链路压测发现TCP问题
阿里的中间件存在百花齐放、百家争鸣的历史,也可以说是野蛮生长、重复造轮子、适者生存的历史。当时还有一款同样的功能的中间件叫EVE,这是一款从阿里巴巴公司B2B时代就存在的历史悠久的中间件,而Rowan则是一款年轻的中间件。和EVE不同的是,Rowan并不是通过堆机器和增加系统复杂度来换高吞吐量的,只使用了中美各4台共计8台Server服务器,Rowan引入Apache的Jstorm技术,SMTP的邮件发送逻辑放在了Jstorm中,引入Jstorm就是想用最少的机器做极致的事。在Jstorm的日志中,我们又拿到了更加详细的异常信息。
Caused by: javax.mail.MessagingException: Could not connect to SMTP host: smtp- usa.ocm.aliyun.com, port: 25; nested exception is: java.net.NoRouteToHostException: Cannot assign requested address at com.sun.mail.smtp.AliyunSMTPTransport. openServer(AliyunSMTPTransport.java:1764) at com.sun.mail.smtp.AliyunSMTPTransport. protocolConnect(AliyunSMTPTransport.java:587) Caused by: java.net. NoRouteToHostException: Cannot assign requested address at java.net.PlainSocketImpl. socketConnect(Native Method) at java.net.AbstractPlainSocketImpl.doConnect(AbstractP lainSocketImpl.java:339)
然而登录阿里云的服务器查看系统指标,以及阿里云云邮系统的神农监控、盘古等指标都没有问题。查看Jstorm服务器套接字socket使用概况,可以根据上述异常SMTP 25端口用netstat -tn |grep :25 |wc -l命令,或者使用ss -s(Socket Statistics缩写)命令。如图1-7和图1-8所示是连续两次截图记录,可以看出timewait非常多。
图1-7 第1次查看结果
图1-8 第2次查看结果
种种迹象表明,问题发生在Client端而不是Server端,这是一起典型的TCP调优案例。
1.2.4 Linux内核网络参数调优
首先我们调整一下Linux内核参数来提高服务器并发处理能力。一般来说,这种方式可以不用升级服务器硬件就能最大程度地提高服务器性能,是一种节省成本的做法。内核参数修改了以下4个配置:
❑ulimit -n。该文件表示系统里打开文件描述符的最大值,查看之后,发现是1024,这个值偏小,我们调大一些。
❑somaxconnSocket。cat /proc/sys/net/core/somaxconn,该文件表示等待队列的长度,默认是128,我们调大到1000。
❑netdev_max_backlog。cat /proc/sys/net/core/netdev_max_backlog,该文件表示在每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目,我们将该值调整到1000。
❑tcp_max_syn_backlog。cat /proc/sys/net/ipv4/tcp_max_syn_backlog,该文件表示SYN队列的长度,默认为1024,加大队列长度为8192,以容纳更多等待连接的网络连接。
修改完毕以后,重新进行线上压测,然而问题没有解决,QPS也没有得到提升。
小窍门
本节主要是对/proc/sys/net/进行优化,该目录下的配置文件主要用来控制内核和网络层之间的交互行为,一些技巧补充如下:
1)/proc/sys/net/core/message_burst该文件表示写新的警告消息所需的时间(以1/10秒为单位),在这个时间内系统接收到的其他警告消息会被丢弃。这用于防止某些企图用消息“淹没”系统的人所使用的拒绝服务(Denial of Service)攻击。缺省:50(5秒)。
2)/proc/sys/net/core/message_cost该文件表示写每个警告消息相关的成本值。该值越大,越有可能忽略警告消息。缺省:5。
3)/proc/sys/net/core/netdev_max_backlog该文件表示当每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目。缺省:300。
4)/proc/sys/net/core/optmem_max该文件表示每个套接字所允许的最大缓冲区的大小。缺省:10240。
5)/proc/sys/net/core/rmem_default该文件指定了接收套接字缓冲区大小的缺省值(以字节为单位)。缺省:110592。
6)/proc/sys/net/core/rmem_max该文件指定了接收套接字缓冲区大小的最大值(以字节为单位)。缺省:131071。
7)/proc/sys/net/core/wmem_default该文件指定了发送套接字缓冲区大小的缺省值(以字节为单位)。缺省:110592。
8)/proc/sys/net/core/wmem_max该文件指定了发送套接字缓冲区大小的最大值(以字节为单位)。缺省:131071。
1.2.5 Linux TCP参数调优
TIME_WAIT是TCP中一个很重要的状态,在大并发的短连接下,会产生很多TIME_WAIT,这会消耗很多系统资源;端口的数量只有65535,占用一个就会少一个,进而严重影响新连接。所以需要调优TCP参数,从而让系统更快地释放TIME_WAIT的连接。TCP的传输连接有连接建立、数据传送和连接释放3个阶段,多年前在与阿里巴巴公司叶军博士一次闲聊中得知,作为阿里巴巴公司面试官他经常会考察应聘者TCP的3次握手和4次挥手这个知识点。
对于TIME_WAIT,我们主要进行如下的修改:
net.ipv4.tcp_tw_recycle = 1 net.ipv4.tcp_tw_reuse = 1
这两个参数默认是关闭的,但是不建议开启该设置,NAT模式下可能引起连接RST。这两个参数的作用是主动断连接,它由于违反了TCP协议(RFC 1122),在其官方文档中也强调“It should not be changed without advice/request of technical”。
下面4个是TCP主要内核参数的说明:
❑net.ipv4.tcp_syncookies = 1表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击。默认为0,表示关闭。
❑net.ipv4.tcp_tw_reuse = 1表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接。默认为0,表示关闭。
❑net.ipv4.tcp_tw_recycle = 1表示开启TCP连接中TIME-WAIT sockets的快速回收。默认为0,表示关闭。
❑net.ipv4.tcp_fin_timeout修改系统默认的TIMEOUT时间。
我们可以再继续进行并发连接backlog调优,并对可用端口调优范围、TCP Socket读写Buffer、TIME-WAIT Socket最大数量、FIN-WAIT-2 Socket超时设置等进行优化。特别说明一下,tcp_max_tw_buckets用于控制并发时,TIME_WAIT的数量默认值是180000,如果超过,系统会清除多余的,并警告“time wait bucket table overflow”。这些参数的调优不但能够提升服务器的负载,还可以在一定程度上防御DDoS等攻击。
在参数调优之后,又上了一道双保险,重新修改了com.sun.mail.smtp源码的SO_REUSEADDR,通过serverSocket.setReuseAddress(true)让其在服务器绑定端口前生效,目的是让端口释放后立即就可以被再次使用。
但是发布到线上进行压测以后,却依然没有效果。
1.2.6 一行代码大幅提升QPS
经过了反复的调优以后,真正解决问题的其实只有一行代码“TCP option SO_LINGER”。在上节中socket.setReuseAddress(true)代码之后我增加了一行代码“socket. setSoLinger(true,0)”,重新进行线上压测,压测过程中启用setSoLinger前后数据结果如图1-9、图1-10、图1-11所示。
图1-9 启用setSoLinger前
图1-10 启用setSoLinger后验证一
图1-11 启用setSoLinger后验证二
从图1-9可以发现,启用l_linger前连接很难超过700, timewait状态的很多;启用socket.setSoLinger(true,0)后连接数到1045, timewait状态没有了(见图1-11)。正是因为放弃了TCP中的4次挥手,所以客户端(Rowan)会给服务端发出RST(阿里云)很多RST。记得当时和阿里资深技术专家叶军博士交流过,他告诉我说,关闭4次挥手这个案例他以前调优时也做过类似处理,有需要的场景可以将4次挥手改成3次挥手,不过要经过专业大规模测试,一旦TCP层面的Buffer数据丢掉,还是存在一些隐患的。SO_LINGER虽然可以让服务器性能提升不少,但是在《UNIX网络编程》卷1中提到TIME_WAIT的作用是在主动关闭端口后,保证数据让对端收到,Richard.Steven的原话是:“TIME_WAIT是我们的朋友。”如果服务存在大量或者多个通讯,而且之间还有一些时序关系,那么我们就不能使用这种让用户丢弃一部分数据的方式,否则可能因为最后阶段丢失一些服务器的返回命令而造成程序的错误。
那什么是SO_LINGER呢?它是一个可以通过API设置的socket选项,仅仅适用于TCP和SCTP。它主要由on和linger两个属性决定,其数据结构如下所示:
struct linger { int l_onoff; /* 0 = off, nozero = on */开关,零或者非零 int l_linger; /* linger time */优雅关闭最长时间 };
如表1-1所示,socket.setSoLinger(true,0)对应的是立即返回,“强制关闭失效”的情况。
表1-1 linger理解图
如图1-12所示,这种情况并不是4次挥手,TCP不会进入TIME_WAIT状态。在send buffer中的数据都发送完之前,close就返回,client向SERVER发送一个RST信息。
图1-12 l_linger设置值太小
为了避免使用socket.setSoLinger(true,0)可能导致的应用数据包丢失问题,在如图1-13所示的javamail官方文档中,SMTP启用quitwait参数,可以保证应用层消息完整地发完并收到服务端的发送响应。
图1-13 javamail API
所以,关闭Socket连接部分的代码调整为,如果quiteWait为true,则需要接收响应。修改完之后,当天13:00以后进行了多次压测,Rowan服务端中“Could not connect to SMTP host”这个异常再也没有出现,如图1-14所示。
图1-14 13:00以后异常消失
同一时间,阿里云神农监控显示收到邮件的QPS比之前的压测要略大,曲线上升波动更快,完全符合预期,如图1-15所示。
1-15 阿里云13:00开始收到大量的请求
至此,整体TCP调优结束。
思考
SMTP也支持一个连接多封邮件,发完后rset命令,再重新认证发送就可以了,TCP方面并不是与阿里云服务器建立的连接,而是与阿里云的LVS建立的TCP连接。
阿里云使用的还是SMTP的协议,其实我们可以看一下业界的邮件服务,如AWS、SENDCLOUD等,不仅仅支持SMTP,也同样支持HTTP等。其实真正解决问题的方法是将连接方式改进为长连接,或者实现client的连接复用(连接池的理念)。所以推动阿里云从SMTP改为HTTP也是一个方向。