踩坑记#2:Go服务锁死

再挖个坟,讲讲去年踩的另一个坑。


== 前方低能 ==

那是去年7月的一天,被透过落地玻璃的宇宙中心五道口的夕阳照着的正在工位搬砖的我,突然听到一阵骚乱,转头一看,收到夺命连环call的D同学反馈,流量严重异常。

点开报警群,一串异常赫然在目:

[ 规则 ]:「流量波动过大(严重) 」

[ 报警上下文 ]:change:-70.38%

值班人:D(不是我)

报警方式:电话&Lark

报警URL:报警详情页

再点开报警详情页一看:


== 排爆 ==

解释一下:在字节跳动,我们有一个基于OpenTSDB的metrics平台(时序数据库),用于采集和查询各项监控指标。

图中是一个流量源的请求QPS,在短时间内从 7k 暴降至 2k,从而触发了报警规则。

除了流量源之外,在这个指标上我们还有机房、Server IP等其他tag;通过这些tag,我们发现:

  • 来自所有流量源的QPS都在暴降

  • 所有机房的QPS都在暴降

  • 按IP看,许多机器的QPS降到0,但部分机器仍在正常接客

对于异常的机器,从监控上可以看到,其CPU占用陡降至很低的值(约2.5%):

于是这条线索让我们把问题范围缩小到某台机器上。

进一步的排查情况情况是:

  • ssh可以正常登录,看起来OS没啥问题

  • 通过 lsof 可以看到,进程仍然在监听业务端口,只是不响应请求

  • top看到该进程在第一位,占了 99.8%的cpu,有点奇怪

    • 注:这是一台40核的物理机,top里 99.8% cpu表示进程占满了1个核心,和前面的CPU监控遥相呼应。

PID  USER     RES  %CPU  COMMAND
316  user  968497  99.8  ./service
  1  root    3796   0.0  systemd

虽然尚未定位到问题,但至少进一步把问题缩小到进程级别了。

既然是 go 程序的问题,那当然是要搬出神器 pprof 了,只可惜很快就装逼失败 —— 因为这个进程已经不响应任何请求了。


== 减压 ==

此时距故障发生已经过了 15 分钟,对于广告业务,不能正常处理请求,对应的就是真金白银的损失。

但这个case以前没出现过,一时半会又定位不了病根,大家都压力山大。

在这种情况下,经验丰富的老司机们一定不会忘记的,就是压箱底的大招 —— 重启大法。

由于线上是大面积机器异常,我们随便找了一台机器重启该进程,很快,这台机器就开始正常接客了。

于是我们留下几台异常机器(保留现场备查)、从服务注册中摘掉,并将其余异常机器全部重启,暂时恢复了业务。

但病根未除,问题随时可能复辟,还得继续。


== 深挖 ==

前面说到,在top中看到,D项目的进程CPU占用率是100%,跑满了一个CPU。

这是一个奇怪的现象,说明它并不是简单地陷入死锁,而是在反复执行一些任务,这意味着,如果我们知道它在跑什么,可能就找到病因了。

这时候老司机W同学祭出了 linux 的 perf 命令

$ sudo perf top

幸福来的太突然,连函数名都给出来了,送命题秒变送分题

捞了一下代码,这个 GetXXX 是一个二分查找的函数,根据输入的价格,查询对应价格区间的相关配置,大概长这样:

type Item struct {
    left int
    right int
    value int
}


var config = []*Item{
    {0, 10, 1},
    {10, 20, 2},
    {20,100, 8},
    {100, 1<<31, 10},
}


func GetXXX(price int) int {
    start, end := 0, len(config)
    mid := (start + end) / 2
    for mid >= 0 && mid < len(config) {
        if price < config[mid].left {
            end = mid - 1
        } else if price >= config[mid].right {
            start = mid + 1
        } else {
            return config[mid].value
        }
        mid = (start + end) / 2
    }
    return -1
}

按说这么简单的代码,线上跑了至少也几个月了,不应该有啥BUG,按照胡尔莫斯·柯南的教诲,排除一切不可能的,真相只有一个,那就是:输入的价格是个负数

负的价格?这不由得让我想起了中行的某款理财产品……

当然这还只是个推论,在日记里三省吾身的胡适告诉我们,要大胆假设,小心求证。

实锤起来倒也很简单,到上游的数据库去查了一下,确实出现了负的价格。

再后面就是上游系统的BUG了,通过日志记录发现在17:44确实有价格被改成负数,排查代码确认,Web UI及对应的后端代码是有合法性校验的,但是提供的 API 漏了,最终导致了这次事故。

既然找到了病根,修复起来就简单了:

  • 修复API代码,加上合法性校验

  • 修复数据库里的无效数据

  • GetXXX里加上无效价格检测(防御性编程)


== 填坑 ==

业务的坑是填完了,但是技术的坑还没:为什么一个死循环会导致进程卡死呢?

按照调度的常识来推理,一个线程(或goroutine)不应该阻塞其他线程的执行。

比如运行下面这段代码,可以看到,进程并没有卡死,第一个 for 循环确实会不断输出 i 的值。

var i int64 = 0


func main() {
    go func() {
        for {
            fmt.Println(atomic.LoadInt64(&i))
            time.Sleep(time.Millisecond * 500)
        }
    }()
    for {
        atomic.AddInt64(&i, 1)
    }
}

这说明“卡死”的原因还没有这么简单。

这样简单的代码无法复现前面的问题,还是要把死循环放到复杂场景下才能复现,比如加入前述 D 项目的代码里,简单粗暴直接有效。

通过加上 GODEBUG 环境变量:

$ GODEBUG="schedtrace=2000,scheddetail=1" ./service

可以看到有一个线索:gcwaiting=1

SCHED 2006ms: gomaxprocs=64 idleprocs=0 threads=8 spinningthreads=0 idlethreads=5 runqueue=0 gcwaiting=1 nmidlelocked=0 stopwait=1 sysmonwait=0

这就形成了闭环:gc需要STW,但是这个goroutine在死循环,无法被中断,而其他goroutine已经被中断、等待gc完成,最终导致了这个局面。

可以在前述代码里手动触发 gc 实锤一下:现象和线上完全一致。

var i int64 = 0


func main() {
    go func() {
        for {
            fmt.Println(atomic.LoadInt64(&i))
            time.Sleep(time.Millisecond * 500)
            runtime.GC() //手动触发GC
        }
    }()
    for {
        atomic.AddInt64(&i, 1)
    }
}

但还没完 —— 以上现象似乎并不符合 go 宣称的“抢占式调度”啊!

实际上 Go 实现的是一个 Cooperating Scheduler(协作式调度器)。一般而言,协作式调度器需要线程(准确地说是协程)主动放弃CPU来实现调度(runtime.Gosched(),对应 python/java 的 yield),但 Go 的 runtime 会在函数调用判断是否要扩展栈空间的同时,检测 G 的抢占flag,从而实现了一个看起来像是抢占式调度的scheduler。

这还有个小问题 —— 上面的代码里不是调用了 atomic.AddInt64 么?这个倒是简单,通过  go tool compile -S main.go 可以看到,AddInt64 已经被 inline 了;但只要在这里再加上个 fmt.Println 就可以破功了(试试看?)。

(被inline了的AddInt64)


== 收尾 ==

最后,如果你发现前面的代码怎么都不能复现 —— 那你一定是在用 go 1.14+ 了,这个版本实现了一个基于 SIGURG 信号的抢占式调度,再也不怕死循环/密集计算搞死 gc 了(不过代价是,出现死循环导致的性能下降问题更难排查了),对此感兴趣的同学推荐学习《Go语言原本:6.7 协作与抢占》,详见文末“阅读原文”。

简单总结一下:

  1. 二分查找可能会死循环;

  2. 在 go 1.13 及以下版本,死循环/密集计算会导致调度问题;

  3. 特别是遇到 gc 的情况,可能会锁死进程;

  4. 在Linux下可以用perf top来做 profiling;

学了这么多,感觉又无用武之地?快来穿山甲,百万级的QPS,各种酸爽的问题等着你。

~ 投递链接 ~

投放研发工程师(上海)

https://job.toutiao.com/s/J8DRDyG

高级广告研发工程师(北京)

https://job.toutiao.com/s/J8DNwJY


参考链接

[1] 如何定位 golang 进程 hang 死的 bug

https://xargin.com/how-to-locate-for-block-in-golang/

[2] 关于 Go1.14,你一定想知道的性能提升与新特性

https://gocn.vip/topics/9611

[3] Go语言原本:6.7 协作与抢占

https://changkun.de/golang/zh-cn/part2runtime/ch06sched/preemption/


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于微信小程序的家政服务预约系统采用PHP语言和微信小程序技术,数据库采用Mysql,运行软件为微信开发者工具。本系统实现了管理员和客户、员工三个角色的功能。管理员的功能为客户管理、员工管理、家政服务管理、服务预约管理、员工风采管理、客户需求管理、接单管理等。客户的功能为查看家政服务进行预约和发布自己的需求以及管理预约信息和接单信息等。员工可以查看预约信息和进行接单。本系统实现了网上预约家政服务的流程化管理,可以帮助工作人员的管理工作和帮助客户查询家政服务的相关信息,改变了客户找家政服务的方式,提高了预约家政服务的效率。 本系统是针对网上预约家政服务开发的工作管理系统,包括到所有的工作内容。可以使网上预约家政服务的工作合理化和流程化。本系统包括手机端设计和电脑端设计,有界面和数据库。本系统的使用角色分为管理员和客户、员工三个身份。管理员可以管理系统里的所有信息。员工可以发布服务信息和查询客户的需求进行接单。客户可以发布需求和预约家政服务以及管理预约信息、接单信息。 本功能可以实现家政服务信息的查询和删除,管理员添加家政服务信息功能填写正确的信息就可以实现家政服务信息的添加,点击家政服务信息管理功能可以看到基于微信小程序的家政服务预约系统里所有家政服务的信息,在添加家政服务信息的界面里需要填写标题信息,当信息填写不正确就会造成家政服务信息添加失败。员工风采信息可以使客户更好的了解员工。员工风采信息管理的流程为,管理员点击员工风采信息管理功能,查看员工风采信息,点击员工风采信息添加功能,输入员工风采信息然后点击提交按钮就可以完成员工风采信息的添加。客户需求信息关系着客户的家政服务预约,管理员可以查询和修改客户需求信息,还可以查看客户需求的添加时间。接单信息属于本系统里的核心数据,管理员可以对接单的信息进行查询。本功能设计的目的可以使家政服务进行及时的安排。管理员可以查询员工信息,可以进行修改删除。 客户可以查看自己的预约和修改自己的资料并发布需求以及管理接单信息等。 在首页里可以看到管理员添加和管理的信息,客户可以在首页里进行家政服务的预约和公司介绍信息的了解。 员工可以查询客户需求进行接单以及管理家政服务信息和留言信息、收藏信息等。
数字社区解决方案是一套综合性的系统,旨在通过新基建实现社区的数字化转型,打通智慧城市建设的"最后一公里"。该方案以国家政策为背景,响应了国务院、公安部和中央政法会议的号召,强调了社会治安防控体系的建设以及社区治理创新的重要性。 该方案的建设标准由中央综治办牵头,采用"9+X"模式,通过信息采集、案(事)件流转等手段,实现五级信息中心的互联互通,提升综治工作的可预见性、精确性和高效性。然而,当前社区面临信息化管理手段不足、安全隐患、人员动向难以掌握和数据资源融合难等问题。 为了解决这些问题,数字社区建设目标提出了"通-治-服"的治理理念,通过街道社区、区政府、公安部门和居民的共同努力,实现社区的平安、幸福和便捷。建设思路围绕"3+N"模式,即人工智能、物联网和数据资源,结合态势感知、业务分析和指挥调度,构建起一个全面的数据支持系统。 数字社区的治理体系通过"一张图"实现社区内各维度的综合态势可视化,"一套表"进行业务分析,"一张网"完成指挥调度。这些工具共同提升了社区治理的智能化和效率。同时,数字社区还提供了包括智慧通行、智慧环保、居家养老和便民服务等在内的多样化数字服务,旨在提升居民的生活质量。 在硬件方面,数字社区拥有IOT物联网边缘网关盒子和AI边缘分析盒子,这些设备能够快速集成老旧小区的物联设备,实现传统摄像设备的智能化改造。平台优势体现在数字化能力中台和多样化的应用,支持云、边、端的协同工作,实现模块化集成。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值