一次导致服务僵死的问题排查

问题背景

QA同学在线下对直播服务做压测,要预先创建1500场活动,遇到的问题是:

  • 用30个并发,每个线程请求50次,创建到200多场时批量任务就开始卡住不动;
    一次导致服务僵死的问题排查_第1张图片

查看详情时,大量创建活动的请求报失败,表现为socket读取数据超时,如下:

一次导致服务僵死的问题排查_第2张图片

排查过程

1. 服务日志排查

出问题的时间点大概是在09/24 18:30~09/24 18:40之间,先在相应时间点随机找了一个未正常返回的请求,显示在创建group这一步走完日志就断了,如下:

从日志中又翻了几个其它失败的,日志也都在中断在这一步。

从代码上看,创建group这步失败是作了降级的,并不会中断预约会议的流程,如下:

一次导致服务僵死的问题排查_第3张图片

见到类似问题,最先想到的是程序崩出去了,虽然recover的日志没打出来,但还是本能的会去看下nohup日志。

2. nohup的日志排查

从nohup日志中搜不到panic信息,业务日志和nohup中都没有panic,基本可以判断程序并没有panic。一次导致服务僵死的问题排查_第4张图片

但是,nohup中显示创建约会的请求耗时都很长,极有可能程序block有了某个地方。

3. goroutine监控排查

如果goroutine block在某个地方,通过pprof提供的goroutine监控应该是能看到一些信息的,

万幸的是问题现场还在,想到了beego提供的程序监控后台,打开lookup goroutine:一次导致服务僵死的问题排查_第5张图片

可以看到,有30个goroutine都block在了unsstore.AddEvent这个方法上(前面提到压测用的也正好是30个并发)。

再详细统计,会发现:

  • 其中有20个goroutine block在了unsstore/eventInfos.go:888行的orm.Begin()这句方法调用上;
  • 其中有10个goroutine block在了unsstore/eventInfos.go:906行的GetEmailLang()这句方法调用上;一次导致服务僵死的问题排查_第6张图片

所以初步猜测,代码中可能有资源争抢导致的死锁,并且最有可能的是DB连接资源。

想到了DB连接,就先看下Mysql连接数是否充足,打开/uc/etc/uniformserver.conf:

一次导致服务僵死的问题排查_第7张图片

目前仅配了10个连接,在30个并发去争抢10个连接资源的场景下,如果某个业务代码比较贪,手握一个连接还去抢新的连接,可能就会出现不仅自己抢不到被阻塞,并且由于它手握资源不释放 其它goroutine也拿不到资源,最后整个进程都僵死的惨烈结果。

4. 代码排查

unsstore.AddEvent()方法的功能:将活动信息入库保存。上文提到的goroutine block的两句代码见下面红框标注:

一次导致服务僵死的问题排查_第8张图片

888行的o.Begin()会获取一个DB连接,如果方法没退出(连接未释放)之前906行的GetEmailLang()方法里又去获取新连接,就可能在并发场景下互相争抢连接资源彼此都不释放的死锁状况。

GetEmailLang方法的实现如下:

一次导致服务僵死的问题排查_第9张图片

一次导致服务僵死的问题排查_第10张图片

一次导致服务僵死的问题排查_第11张图片

一次导致服务僵死的问题排查_第12张图片

如代码所示,经过几个方法调用,最终在getInfos方法的96行创建了新的orm对象(beego中的每个orm都需要一个独立的DB连接资源)。

最终的代码分析也和pprof中的Goroutine Bock情况一致,该问题属于并发场景下方法内部嵌套获取DB连接引发的资源争抢死锁问题。

结论及建议:

  • 一次业务请求内不要同时持有多个ormer对象;
  • ormer对象是非常昂贵的DB连接资源,要尽量缩短ormer对象的生命周期,并减少耗时和复杂事务对连接资源的占用和消耗;
  • 特别需要注意的是:手动控制事务的方法及其子方法中,一定不能再去创建新的ormer对象;

你可能感兴趣的:(故障剖析,后端,golang,beego)