写点什么

线上故障处理实践

用户头像
心平气和
关注
发布于: 2020 年 06 月 06 日
线上故障处理实践

一、背景

最近公司一个系统发生线上故障,系统架构为 C/S 的,客户端是 APP;系统的功能有:联系人、短信、通话记录等,每个业务都有备份、恢复的功能,即用户可以在 APP 内备份自己的联系人、短信、通话记录至服务端,然后可以后续某个时间段恢复数据。



第 1 层 Nginx,主要做一些流量清洗、流控等处理;

第 2 层是应用层,分应用接入层和服务层,应用接入层做一些参数检查和登录检查等,服务层处理业务逻辑,这 2 层之间通过 RPC 通信;


该系统经过几次接手,没有人能对系统逻辑理解很清楚;

该系统从去年下半年开始一直偶尔有 500 的报错,但每次重启就好了,本次发生故障后,重启仍然是大量 500;


二、问题分析

先查看接入层日志,发现大量的 500 错误:


发现是连接应用接入层超时,应该是应用接入层压力大,赶紧将接入层扩容,增加了 1 倍的服务器;


应用层扩容后,发现连接 Hbase 报错超时了(这里就不列日志了,日志很重要~)。


因为 Hbase 扩容后需要 Rebalance,这个过程需要一段时间,为了尽量减少对线上影响,开始在 nginx 上限流,具体是通过 accessbylua_file 指令进行限流,代码如下:


 local tokenHash
if (token == nil) or (token == "") then return end tokenHash = ngx.crc32_long(token) if ((tokenHash % 64) == 1 then else ngx.exit(505) end
复制代码


代码比较简单,先从 http 头中取出标识,然后 hash 一下,然后让 1/64 的流量返回给后端,其余的直接返回 505。


经过上述处理后,运维同学反应机房带宽打爆了,通过分析发现流量爆增 10 倍以上,和客户端的同学确认,如果服务端返回的不是 200 客户端会马上重试。


怎么办呢,将上面的代码改下,加个 sleep:

local headers = ngx.req.get_headers() local token = headers["xxx"]
local headers = ngx.req.get_headers()local token = headers["xxx"]local tokenHash
if (token == nil) or (token == "") then returnend tokenHash = ngx.crc32_long(token)if ((tokenHash % 64) == 1 thenelse ngx.sleep(120) ngx.exit(505)end
复制代码


我们查了资料, ngx.sleep 不会阻塞 nginx 进程,所以才敢放心的用。


这样处理之后,带宽还是满了,问题没有解决,因为所有在线客户端基本上都在重试了;


这时候 Hbase 扩容完了,我们将接入层、服务层的应用都重启了,现象是有一段时间是 200,过会又是 500 了,通过日志分析发现前后端的超时时间不一致,导致 nginx 返回给应用是 500,实际上后端还在处理;


调整了 nginx 几个超时时间:

proxyconnecttimeout 60s;

proxysendtimeout 60s;

proxyreadtimeout 60s;


RPC 的超时间也改为 60 秒,这样应用有些缓和,但还是有不少 500 报错;

再通过分析日志发现后端请求处理的请求是几分钟前的日志:


Nginx 日志如下:


再分析下代码,原因是 因为 RPC 框架设计的不合理,此框架线程池参考的是 Dubbo 设计的,有 threads 和 queues 的配置,只不过框架中 queues 参数不能改,默认是 threads*100,即如果线程数设置为 500,则等待队列是 50000,并且一直要处理等待队列才能处理新请求,所以造成新请求一直在 nginx 层报超时,但后端服务层还在处理很早以前的请求,即做一些无用功。


此时修改框架已经来不及,为了解决问题,我们再次进行限流,不过限流策略有些调整,即让每个客户端都有时间处理:


--入口函数function run() local headers = ngx.req.get_headers() local token = headers["XXX"] local tokenHash local curTimeSec local minute local baseNum
if (token == nil) or (token == "") then return end
--current time curTimeSec = os.time() minute = math.floor(curTimeSec / 60) baseNum = 10
tokenHash = ngx.crc32_long(token) if ((tokenHash % baseNum) == (minute % baseNum)) then else ngx.sleep(20) ngx.exit(505) endend
--启动run()
复制代码


三、写在最后

整体来说,系统问题就是 RPC 框架设计不合理, queues 参数写死并且还不能调整,做为一个中间件,面对的场景较多,需要一些参数来让使用者平衡具体场景的差异;


此次排查主要的手段有:

1、限流,主要是通过 lua 进行实现;

2、仔细分析日志发现应用是否正常,特别是系统异常日志的打印很重要;

3、从接入层到底层存储调和的超时时间对应上,从上往下可以逐渐小些,但不能下层比上层的大;


Redis稳定性实践

扩展Redis:增加Redis命令

如何做好稳定性

从一次线上故障来看redis删除机制

一次线上Mysql死锁分析


发布于: 2020 年 06 月 06 日阅读数: 1554
用户头像

心平气和

关注

欢迎关注公众号:程序员升级之路 2018.03.06 加入

还未添加个人简介

评论 (4 条评论)

发布
用户头像
建议文章加个头图,这样显示的效果好些。
2020 年 06 月 07 日 13:38
回复
上传图片失败
2020 年 06 月 07 日 14:13
回复
我能看到图啦,好奇怪的bug。
2020 年 06 月 09 日 12:35
回复
用户头像
很好的实践分享,InfoQ首页推荐。
2020 年 06 月 07 日 09:00
回复
没有更多了
线上故障处理实践