专栏首页程序员升级之路线上故障处理实践

线上故障处理实践

一、背景

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

服务端架构如下:

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

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

底层的存储是Mysql和Hbase,Mysql存一些元数据,真正的业务数据存放在Hbase中;

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

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

二、问题分析

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

Nginx错误日志如下:

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

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

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

 local headers = ngx.req.get_headers()
 local token = headers["xxx"]
 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。

为什么不返回200呢,因为这样会丢失数据,以联系人为例,同步过程如下:

客户端将最后一次修改/增加的联系人增量上传给服务端,如果这时候服务端返回200,客户端以为服务端保存成功,下次就不会上传上次的数据了。

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

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

local headers = ngx.req.get_headers()
local token = headers["xxx"]
local tokenHash

if (token == nil) or (token == "") then
  return
end
  
tokenHash = ngx.crc32_long(token)
if ((tokenHash % 64) == 1 then
else
  ngx.sleep(120)
  ngx.exit(505)
end

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

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

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

调整了nginx几个超时时间:

 proxy_connect_timeout 60s;
 proxy_send_timeout 60s;
 proxy_read_timeout 60s;

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

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

Nginx日志如下:

可以看到服务层的日志是在15:43:10左右处理的,而进入nginx的时间是15:24:44,前后差了19分钟,但我们的超时时间是60秒,这是为什么呢?

再分析下代码,原因是 因为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)
    end
end

--启动
run()

经过上面处理后,问题最终得到解决。

三、写在最后

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

此次排查主要的手段有:

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

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

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

本文分享自微信公众号 - 程序员升级之路(gh_1fab42db66cb),作者:刘江华

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-06-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Codis Proxy是如何处理一个请求的

    前面我们分析了Codis各组成部件,其中Proxy是用来处理客户端请求的,今天我们具体分析下一次请求在Codis内部是如何处理的。

    心平气和
  • 360 Atlas生产环境使用心得

    以下是其github代码库:https://github.com/Qihoo360/Atlas

    心平气和
  • 加密服务设计

    为什么要做加密服务,最近GDPR对个人数据查的很严,如果违反规定,罚款是很大的,大部分开发的同学是没太多安全意识的,说不定哪天因为系统漏洞导致数据被泄露...

    心平气和
  • 农业为何会成为第一个实现自动化的行业 | 附报告

    大数据文摘
  • 正则表达式

    强烈建议:正则一律加上r字符(不加可能有问题,加上r肯定没有问题(分组里面不加r会出现问题))

    小闫同学啊
  • [剑指offer] 数组中的逆序对

    在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的...

    尾尾部落
  • 两段小视频轻松理解CPU & GPU的工作原理

    CPU的工作原理 ? GPU的工作原理 ? 视频来源:啃芝士

    刘盼
  • redis 4 增量同步的日志详解

    6855:M 02 Sep 15:43:16.871 # Setting secondary replication ID to 2ba403b0a69dcac...

    二狗不要跑
  • 数据采集技术指南 第一篇 技术栈总览-附总图和演讲ppt

    从事爬虫虽然时间不长,但是经历的项目都具有特例性,从亿级数据采集到各种伪造隐藏技术,从极验验证码破解到淘宝百度等反爬虫破解,从分布式架构部署到多种ip跟换技术,...

    十四君
  • angularJS配合bootstrap动态加载弹出提示内容

    1.bootstrp的弹出提示   bootstrap已经帮我们封装了非常好用的弹出提示Popover。   http://v3.bootcss.com/jav...

    kklldog

扫码关注云+社区

领取腾讯云代金券