【摸鱼吃瓜工作录】面试加大分之-高并发系统优化思路

200 阅读14分钟

我是「 kangarooking(袋鼠帝) 」 真心给大家分享经验和技术干货,面试次数100+。面试经验绝对丰富,也当过面试官,内容绝对用心可靠。点赞在看,养成习惯。关注me,每天进步亿点点 ❗ ❗ ❗

2022-8-17

前言

本篇改编自真实事件,记录了本king最近负责系统进行压测的一些心得以及经验。干货满满,学了面试去吹吹牛,也是绝对的加分,加大分!

包括的知识面:

  • 压力测试
  • 高并发Nginx调优
  • 高并发Linux服务器调优
  • CPU飙高问题排查

压测接口:

get请求获取数据,数据量不是很大,正常响应在10ms左右(返回一堆json数据)。属于大并发量请求获取小数据接口。

压测方式

参数

  • 压测时间长度:2分钟
  • 线程数:8、16、32、50、100、200
  • 线程启动时间:5~10秒

模拟用户发送https请求,走外网进行压测。请求逻辑(以16个线程为例)是在5~10秒之内压力机启动16个线程,开始不停请求接口,每个线程在接收到响应之后立马发起下一个请求,这样持续2分钟。压力机一台(8核16G

注意:走外网压测前,先将服务前面链路上的影响因素整理出来,以免后续压测影响问题的判断,如关闭ng对单个ip的限流,查看防火墙、或者安全策略是否有限制。

请求链路

1.png

容器云的微服务是供应商提供的,是go语言写的微服务。本king目前对go语言还不太熟悉,在学习阶段。

初现问题

并发大概1200每秒,使用jmeter跑了半分钟左右报错逐渐变多:

2.png

3.png

nginx错误日志如下:

4.png

第一阶段-解决问题,并优化

在并发增加,并且出现报错的整个过程中,容器云服务响应时间并没有增加,也没有报错。考虑是nginx服务器的问题。

根据ngerror.log日志初步判断是连接数太多处理不过来的问题。

使用linux命令查看tcp请求中各个状态数据:

netstat -n|grep  ^tcp | awk '{++S[$NF]} END {for(a in S) print a, S[a]}'

或者

netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

TCP状态:

  • CLOSED:无连接是活动的或正在进行
  • LISTEN:服务器在等待进入呼叫
  • SYN_RECV:一个连接请求已经到达,等待确认
  • SYN_SENT:应用已经开始,打开一个连接
  • ESTABLISHED:正常数据传输状态
  • FIN_WAIT1:应用说它已经完成
  • FIN_WAIT2:另一边已同意释放
  • ITMED_WAIT:等待所有分组死掉
  • CLOSING:两边同时尝试关闭
  • TIME_WAIT:表示处理完毕,等待超时结束的请求数
  • LAST_ACK:等待所有分组死掉

5.png

根据TCP连接状态分析:处理TIME_WAIT的连接数较多,所以设置nginx在向上游容器云服务转发的时候开启长连接(连接的复用)。因为nginx在转发的时候会把客户端发送过来的浏览器信息清除掉,所以需要在upstream的配置中加上长连接配置才能在向上游服务的请求中开启长连接。

注意:对于Nginx来说转发请求的容器云服务相当于它的upstream(上游服务)。 对于upstream里面添加keepalive参数不理解的朋友可以参考:blog.csdn.net/qq_34168988…

所以就在ng配置中增加了一个参数:keepalive 128;

upstream applet {
        server xx.xx.xx.xx:80 max_fails=3 fail_timeout=10s weight=2;
        keepalive 128;
    }

此参数设置在每个 worker(worker)的缓存中保留的 upstream 服务器的最大空闲 keepalive 连接数。超过此数量时,将关闭最近最少使用的连接。

注意:keepalive 指令不限制 nginx worker 进程可以打开的 upstream 服务器的连接总数。此 参数应设置为足够小的数字,以便 upstream 服务器也可以处理新的传入连接。

效果:之前是总请求数到7万左右 错误率就疯狂增加。增加这个参数之后,总请求数到15万之后错误率增加到15%,然后过了一会儿又开始往下降了。

继续优化

查看linux参数:

sysctl -n net.ipv4.tcp_fin_timeout

6.png

临时修改该参数:

sysctl -w net.ipv4.tcp_fin_timeout=30

net.ipv4.tcp_fin_timeout = 30 表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间。

效果:这次请求量到10万之后错误率开始增加,一直稳定在3%左右。最终30万次请求有3%错误。客户端错误是:socket closedng端错误跟之前一样。

一阶段优化总结

主要有两个参数调整后效果明显:

  • ng的配置Keepalive=32 表示在worker线程中,保持的空闲连接数达到32之后会开始清除最近最少使用的连接;
  • 设置linux内核参数net.ipv4.tcp_fin_timeout = 30减少了tcp连接保持在FIN-WAIT-2状态的时间。

上述两个参数调整对数据量小,并发大的接口来说,增加了服务器释放资源的速度,从而能够快速的处理后续的请求。增加系统吞吐量。

当然还有一些其他linux内核参数,比如linux句柄数,开放的端口数等:

内核参数作用高并发建议值
net.ipv4.ip_local_port_range用于向外连接的端口范围sysctl -w net.ipv4.ip_local_port_range="1024 60999"
net.ipv4.tcp_tw_recycle开启TCP连接中TIME-WAIT sockets的快速回收 Linux 4.12 版本后取消了这个参数默认是=0关闭,开启设置为1
net.ipv4.tcp_tw_reuse如果服务器会主动向上游服务器发起连接的话,就可以把 tcp_tw_reuse 参数设置为 1,它允许作为客户端的新连接,在安全条件下复用 TIME_WAIT 状态下的端口每个WorkProcessor(共同消费者)的SequenceBarrier实例是同一个
net.ipv4.tcp_fin_timeout限制TCP连接处于FIN_WAIT2 状态的时长,默认是60秒推荐设置为30秒
net.ipv4.tcp_window_scaling配置为1时设置TCP接收窗口超过64KB大小配置为1时,窗口的最大值可以达到 1GB(2的30次方)
net.ipv4.tcp_moderate_rcvbuf发送缓冲区的调节功能是自动开启的,而接收缓冲区则需要配置 tcp_moderate_rcvbuf 为 1 来开启调节功能推荐1
net.ipv4.tcp_max_syn_backlog设置TCP SYN半连接队列的大小推荐1024
net.ipv4.tcp_syncookies开启该配置,如果 SYN 半连接队列已满,并不会丢弃连接,可以在不使用 SYN 队列的情况下成功建立连接推荐开启,设置为1
net.ipv4.tcp_abort_on_overflow如果设置了net.ipv4.tcp_abort_on_overflow 参数,那么在检测到监听backlog 队列已满时,直接发 RST 包给客户端终止此连接,此时客户端程序会收到104 Connection reset by peer错误。这个参数很暴力,慎用
net.ipv4.tcp_max_tw_buckets当 TIME_WAIT 的连接数量超过该参数时,新关闭的连接就不再经历 TIME_WAIT 而直接关闭sysctl -w net.ipv4.tcp_max_tw_buckets = 5000
ulimit -n查看linux的限制句柄数,每个连接都会占用句柄。推荐值:655350

补充:

查看进程已使用的句柄数:lsof -n|awk '{print $2}'|sort|uniq -c|sort -nr|more

查看服务端SYN_RCV, 半连接队列已满而导致的失败次数:netstat -s | grep "SYNs to LISTEN"

注意:sysctl -n 是查看当前参数值,sysctl -w 是临时修改参数值。如果要保存修改的参数值,最好将修改的参数配置到/etc/sysctl.conf,此文件内容对应/proc/sys/这个目录的子目录及文件,修改后sysctl -p /etc/sysctl.conf 让配置生效。

说明:在调整linux内核参数时,不要只参考上面的配置,上面给出的推荐配置不一定适合你的系统或者说你的场景。但在你毫无优化头绪的时候推荐参考上述配置,调整linux的内核参数,边调整边验证,从验证结果来倒推出问题。

第二阶段优化-优化压力机(客户端)

调整完nginx配置以及linux内核参数之后,第二天压力机上面跑压测没有再报错了,但是性能上面感觉有问题。tps只能到1200左右。生产环境是两台ng

7.png

然后猜测是否是走外网的原因,然后我们在生产环境内网搞了一台压力机来进行压测,不走ng,直接请求容器云暴露的ingress域名。结果如下:

8.png

走内网压测tps可以达到3200左右,同样线程数和相同时长情况下比走外网压测多了2000tps。不科学鸭。。。难到走外网损失这么多性能吗???

这个1200tps在经过一系列服务器调优以及增加pod之后都没有变化,并且压测过程中ng以及容器云服务的cpu有大量空闲,内存也没怎么消耗,怎么看都不像是服务端有瓶颈。最后我将问题锁定在发送请求的压力机上。经过观察,果不其然是压力机的瓶颈。压测过程中压力机(8核16g)8个核心全部打满了。

压测过程中,这里每个core基本都快到100%了(这里使用的是阿里巴巴的arthas工具查看,当然也可以直接top命令查看,那应该就是到700%多)

9.png

通过cpu飙高那一套原始组合拳操作,查看占用cpu高的线程的堆栈情况--显示在等待资源(Wait on condition):

10.png

最终从压测同事给出的火焰图里面发现,是这个代码消耗了大量的cpu。一分析就知道了,问题所在:因为走外网压测是https请求,压测同事的代码有点问题,每个线程发送请求前都需要重新建立ssl通道,导致cpu占满。

11.png

知道了问题所在,我们将每次请求都建立ssl,改成第一次请求建立,后续的请求都使用第一次的。修改之后继续压测,果然压力机的cpu消耗变小了,cpu消耗在10~20%左右。再次压测,tps由原来的1200增加到2200左右:

12.png

其实更好的方式是:不修改原来每个请求建立ssl的逻辑,然后只增加压力机来进行压测。因为其实正常情况下每个用户访问也是需要建立ssl的,所以原请求逻辑不变,增加压力机来进行压测会更接近真实的访问情况。但是奈何资源有限0.0只有一台服务器做压力机,但是总结下来下次压测的话需要更严谨,有必要的话申请更多资源。

这里在补充一个点:为什么压测都是使用linux服务器来做压力机呢?因为linux可以更轻松的调整内核参数,来增加连接句柄数等等。如果使用windows就有很多限制,支持的连接数不高,可能会出现目标服务器还尚有余力,但是windows压力机已经达到了瓶颈。

第三阶段优化-优化系统缓存(服务端)

在我们将线程数提升到200的时候,会出现几十个报错,虽然这个报错在几十万请求量中显得贼少,但是毕竟是服务端500的错误,证明服务端有问题:

13.png

但是这个外购系统的日志目前没有落地,还没有对接公司的监控以及日志平台,所以这增加了排查问题的难度(因为大量的请求里面只有少量报错,rancher的实时日志里面很难找出报错的根源)。通过和供应商的沟通中定位了一个可能点--该get接口的逻辑是:先从redis获取,没有的话就从mysql获取最后放入redis。因为这个数据的key是根据用户的网络、系统版本、手机型号等参数生成的,所以生产环境如果并发大的话会生成很多key,内存消耗大。所以有设置key的过期时间,上rancher一看配置,竟然只配置了10秒的过期时间。所以预估问题就是压测时每隔10秒缓存过期,大量请求打到mysql,导致mysql压力过大而出现报错情况。

容器云调用链路:

14.png

知道了这个情况,立马把缓存的过期时间设置到3分钟(因为目前压测是跑两分钟),跑完之后惊了,吞吐量直接提升1倍,从原来的2500tps提升到5000多tps,并且报错也没有了,次多压测之后也没有出现之前的报错情况了,到这里初步判断就是mysql压力过大导致,这可以在后续对接了日志平台后我再复现然后再评论区给出结果:

15.png

惊了,以前只是知道redis支持高并发,能提升系统的吞吐量,但是没有一个概念,这次压测我才知道原来全走redis这么香,性能直接起飞!!!

然后就是判断过期时间设置多久的问题了:场景追求稳定,高性能。所以我决定将过期时间设置长一些。甚至可以设置到12小时过期,在预估了后续这个接口的访问高频时间段和极端情况下所占redis内存大小之后,我决定过期时间设置为半个小时。

结论:最终得出这个接口的系统性能瓶颈在MySql上面。另一个瓶颈可能在ng上,由于ssl也会消耗资源,主要是cpu资源。当大量ssl连接过来可能导致ng的cpu打满,这时就需要调优或者扩展来解决。

总结

我们做个总结

压力机瓶颈--当压测结果不符合预期时,服务端没有瓶颈的情况下很有可能就是压力机(客户端)的瓶颈,尽量使用多台压力机来发起请求,保证客户端给的压力大于服务端的承受能力。才能压测到服务端的瓶颈以及边界值。

有得必有失--想要安全就要牺牲性能,想要节约内存资源,吞吐量就会降低。所以我们需要找到一个适用需求场景的适当点。比如要求吞吐量高,系统稳定性强,我可以完全都走缓存,设置缓存永不过期(在redis内存够大的情况下,并且缓存短期不会显著增长)。

其实在系统达到瓶颈的时候调优无状态服务(自己写的业务服务)的效果是最明显的。其次才是去考虑调优NginxLinux服务器等。

要实现一个高性能的系统,要考虑到方方面面:

  • 架构设计
  • 编码阶段:考虑cpu缓存,内存池,索引,算法,零拷贝,锁优化,异步处理
  • 网络层:优化tcp
  • 协议优化:使用grpc等高性能的协议,或者对ssl进行优化
  • 系统监控,日志落地:更好的感知系统运行情况,根据实际情况来调节系统
  • and so on...

纸上得来终觉浅,绝知此事要躬行 ❗ ❗ ❗

继续深入学习可以参考

陶辉老师的-系统性能调优必知必会:time.geekbang.org/column/intr…

Nginx最大并发数怎么到5W:www.zhihu.com/question/40…

Linux内核网络参数优化小结:www.coder4.com/archives/72…

jiewei.png

微信公众号「 袋鼠先生的客栈 」,有问题评论区见。如果你觉得我的分享对你有帮助,或者觉得我有两把刷子,就支持一下我这个干货writer吧,三连,三连,三连就是我最大的动力~,接下来会持续分享干货~(应该是k8s相关知识)。点赞👍 关注❤️ 分享👥