作者 | 饶全成

来源 | 码农桃花源(ID:CoderPark)

最近遇到了一起依赖升级 + 异常数据引发的线上事故,教训惨痛,本文对此进行回故和总结。

背景

起因是我们使用的服务框架版本比较老,GC 次数的 metrics 打点一直为 0,咨询了相关同学后,决定升级框架。升级的过程中,出现了 useofinternalpackagexxxnotallowed 的报错,又咨询了一下相关同学后,尝试使用 go mod 解决。

从 go vendor 到 go mod 的升级的过程也不太顺利,这里按下不表,最终是升级成功了。一同升级的还有 Go 版本,从 1.11 升级到 1.13。

周四上完线后,一切都看似很不错:内存占用、GC 消耗的 CPU 有了优化,GC 次数的监控也有了。因为涉及到公司内部数据,图我就不放了。

周五、周六都平安度过,周日出问题了,小组的同学从下午 12 点左右一直忙到凌晨 12 点,才松了一口气。可怜我们来之不易的一个周日!

现象

周日 11 点 45 左右,端口的调用失败率报警,同时有业务方反馈调用接口报错。

同志们,关键时刻,完善的报警能给事故的处理和恢复赢得时间啊!

By case 排查,发现服务 shard3 集群的机器报 i/o timeout 错误。服务共有 4 个分片集群(根据 ID hash 到对应分片),其他 3 个集群完全正常。接着发现 shard3 集群的机器内存正常、端口还在,但 in/out 流量全部掉到几十 KB/s,看日志也没有发现任何异常。

重启 shard3 集群的服务,重启后的服务恢复正常,访问 debug 端口,也是正常的。然而,十几分钟后,恢复的服务再次出现异常:in/out 流量再次掉到几十 KB/s,访问 debug 端口也没有任何响应,开始慌了。

处理

上线出问题,第一时间回滚!

稳定性里面很重要的一条就是:有问题,先回滚。先止损,将事故影响降到最低,事后再来追查根因,总结复盘。

于是开始操作回滚, reset 到周四上线之前的一个 commit,重新打包,上线 shard3 集群。之后,对外接口完全恢复,操作回滚其他集群。

服务启动之前,需要先加载几十个 G 左右的数据,启动过程长达 10+ min。我申请了一台线上问题机器的 root 权限,执行了 strace-p 命令:

发现服务卡在 futex 系统调用上,这很明显是一个 timer,但是 timer 为何会卡住?正常情况下,会有各种像 write,read 的系统调用,至少打日志、上报 mertrics 打点数据都会有 write 系统调用吧,哈?再执行 perf top 命令:

相关的只有 codec 函数,再看服务进程:

看 perf 输出的结果,全部聚焦到 codec 这个第三方库上,主要的两个函数竟然是 codec.quoteStr 和 utf8.DecodeRuneInString。而我们用 codec 的地方是在程序启动时加载数据文件以及定时的 dump 文件到本地。现在程序已经启动了,只可能是 dump 文件出问题了。查看相关日志,果然有开始 dump 文件的日志记录,却一直没有 dump 成功的记录。

追查

事后追查阶段尝试在 test 集群上重现故障,因为只有单个分片出问题,说明此故障和特定数据有关,是 hash 到分片 3 的数据引起的问题。

又因为 test 集群并没有分片,所以强行(改代码 && 改环境变量)将其伪装成 shard3 集群,然则并没有复现,猜测可能是计划下线了。

周二的时候,终于在 test 集群上模拟分片 1 时重现了线上故障。

对比 codec 的版本问题,果然有问题:周四上线前, vendor.json 里的版本是 v1.1.7,上线后,升级到了 v1.1.8,看来找到问题了!修改 codec 的版本,重新编译、部署,问题依然存在!

这时,组里其他同学反馈 2018 年的时候也出过 codec 的问题,当时也是出现了异常数据导致重启时加载文件不成功。于是我直接将周四上线前 vendor 文件夹里 codec.quoteStr 函数的代码和 codec 的 v1.1.7 代码进行对比,并不相同!vendor.json 里的版本并没有正确反应 vendor 里实际的 codec 版本!!!

进一步查看提交记录,发现在 2017 年 11 月份的时候有一次提交,修改了 vendor 文件夹里的代码,但这时 vendor.json 并没有 codec 记录。而在 2019 年 11 月的一次提交,则只在 vendor.json 里增加了一条 codec 记录,vendor 文件夹里的代码并没有更改:

{"checksumSHA1": "wfboMqCTVImg0gW31jvyvCymJPE=","path": "github.com/ugorji/go/codec","revision": "e118e2d506a6b252f6b85f2e2f2ac1bfed82f1b8","revisionTime": "2019-07-23T09:17:30Z","tree": true
}

仔细比对代码,主要差异在这:

从现象及源码看,大概率是在 codec.quoteStr 里死循环了!由于 Go 1.14 前都无法抢占正在执行无限循环且没有任何函数调用的 goroutine,因此一旦出现死循环,将要进行 GC 的时候,其他所有 goroutine 都会停止,并且都在等着无限循环的 goroutine 停下来,遗憾的是,由于 for{} 循环里没有进行函数调用,无法插入抢占标记并进行抢占。于是,就出现了这样一幕:

只有 dump 数据文件这一个 goroutine 在干活,而且做的又是无限循环,服务整体对外表现就像是“死机”了一样。并且这个 goroutine 由一个 timer 触发工作,所以一开始我们看到的卡在一个 futex 调用上就可以解释得通。因为 runtime 都停止工作了,timer 自然就没法“到期”了。

接着,使用 Go 1.14 去编译有问题的代码版本,上到 test 集群,果然问题“消失”。服务状态完全恢复正常,唯一不正常的是数据文件无法 dump 下来了,因为即使是 Go 1.14,也依然在执行无限循环,不干“正事”。

接下来的问题就是找到异常的数据了。使用上线前的版本(使用 go vendor),将 codec 替换为最新的 v1.1.8 版本,并且在 quoteStr 函数里打上了几行日志:

部署到 test 集群,问题复现:

异常数据就是:“孙���雷”:

为什么会引发死循环,在调用 utf8.DecodeRuneInString 函数后:

c == utf8.RuneError
size == 3

再看 RuneError 的定义:

const RuneError = '\uFFFD'

看一下两个版本的代码不同之处:

老版本的代码,不会进入 if 分支,而新版本的代码,由于 c==utf8.RuneError,所以先进入 if 分支,之后, size==3,不满足里层分支,直接 continue 了,因此 i 值并没有发生变化,死循环就这么发生了。

最后就是找到异常数据到底属于哪个计划。我尝试去每个集群的机器上,从数据文件里寻找“孙���雷”。但文件太大了,几十个 G, grep 搞不定,没关系,使用 dd 工具:

dd if=model_20200423155728 bs=1024 skip=3600000 count=1200 | grep '孙���雷'

使用二分法找到了“孙���雷”!关于 dd+grep 的用法,总结了几点:

  1. 每次从文件开头先跳过 skip*bs 大小的内容,复制 count*bs 大小的内容过来用 grep 查询。

  2. 如果不设置 count,就会查找整个文件,如果查到,则会有输出;否则无。

  3. 对于特别大的文件,可以先把 count 设为跳过一半文件大小的值,采用二分法查找。如果找到,则限定在了前半范围,否则在后半部分。使用类似的方法继续查找……

  4. 如果找到,最后会输出 count*bs 大小的内容。

反思

  1. 服务重大版本更新,至少在线下跑一周。

  2. 有问题,第一时间回滚。

  3. 对于工具的使用要规范。如不要随意更改 vendor 文件夹的内容而不同步更新 vendor.json 文件,反之亦然。

  4. 因为 go mod 的版本选择以及不遵守开源规范的第三方库作者会让使用者不知不觉、被动地引入一些难以发现的问题。可以使用 go mod vendor 代替,如果要锁死版本的话,使用 replace。

相关阅读:

【stw 如何停止 goroutine】https://medium.com/a-journey-with-go/go-how-does-go-stop-the-world-1ffab8bc8846

【煎鱼 Go Modules 终极入门】https://eddycjy.com/posts/go/go-moduels/2020-02-28-go-modules/

今日福利

遇见大咖

由 CSDN 全新专为技术人打造的高端对话栏目《大咖来了》来啦!

CSDN 创始人&董事长、极客帮创投创始合伙人蒋涛携手京东集团技术副总裁、IEEE Fellow、京东人工智能研究院常务副院长、深度学习及语音和语言实验室负责人何晓冬,来也科技 CTO 胡一川,共话中国 AI 应用元年来了,开发者及企业的路径及发展方向!

戳链接或点击阅读原文,直达报名:https://t.csdnimg.cn/uZfQ

程序员惊魂 12 小时:“���”引发线上事故相关推荐

  1. 程序员用12小时复刻《羊了个羊》,代码已开源!

    [CSDN 编者按]过去一周,不少人被<羊了个羊>这款游戏虐的不轻,有多少个"再玩一把"的念头,就有多少次被打入深渊的凄凉,甚至还有人评价道:"什么事都可以过 ...

  2. “���”引发的线上事故

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 最近遇到了一起依赖升级 + 异常数据引发的线上事故,教训惨痛,本文 ...

  3. 1024程序员节:最能讨好程序员的12件礼物

    每年的今天,是程序员节.程序员是通过键盘改变世界的一个群体,他们的大脑里充满了各种神奇的代码.对于这类人群,很难用一个简单的小礼物就打动他们.W3Cschool精选最能讨好程序员的12件礼物,也许可以 ...

  4. 程序员毕业1-2年如何正确编写自己简历

    程序员毕业1-2年如何正确编写自己简历 个人简历模板 个人概况 教育背景 职业技能 职业技能 项目经验 自我评价,所有证书 简历错误分析 第一份简历分析 第二份简历分析 总结 想要获取简历模板word ...

  5. 《程序员》12月刊约稿:技术走向管理要实现的转变

    CTO俱乐部与CSDN<程序员>杂志联合打造系列专栏,面向技术团队管理者约稿,邀请您参与分享. 话题讨论:技术走向管理的过程中要实现的转变(<程序员>12月刊,11月15日截稿 ...

  6. 《程序员》12月精彩内容:双11技术决战

    双十一酣战方休,双十二蓄势以待,街谈巷议的都是一个话题"今天,你买了吗?"席卷购物车的狂欢之下,是对系统架构的巨大挑战,如何在兼顾用户体验的同时保证系统的灵活敏捷都对电商架构提出了 ...

  7. 程序员如何跳出35岁魔咒,史上最全思维图收集解救你

    时常有人在知乎.百度等平台抛出问题:程序员过了 35 岁或 40 岁是不是就失去了竞争力,要转管理岗了吗? 100offer 在2017年对其平台上的5844 位技术岗位求职者做了一个抽样调查,得出了 ...

  8. 黑马程序员_java高级篇网络编程TCP实战Day8(上)

    ---------------------ASP.Net+Android+IOS开发.Net培训.期待与您交流! ----------- 黑马程序员_java高级篇网络编程TCP实战Day8(上) ( ...

  9. 学 AI 容易翻车?寸步难行的程序员,90%都输在了这点上!

    为什么程序员都觉得AI很难? Google 人工智能开发者专家彭靖田老师说--超90%的程序员在初学AI时,都会遇到下面3个问题: "应用方向太多了!不知从何学起,也不知道学完做什么&quo ...

最新文章

  1. 当深度学习遇上异构并行计算
  2. CentOS7下搭建LAMP+FreeRadius+Daloradius Web管理
  3. 学习Raft算法的笔记
  4. hadoop rpc客户端初始化和调用过程详解
  5. Linux下安装vim
  6. HTML5 Canvas 和 SVG
  7. pyqt5 显示更新进度条_python3.x+pyqt5实现主窗口状态栏里(嵌入)显示进度条功能...
  8. 计算机办公自动化取证,办公自动化课程总结范文
  9. clodop控件使用
  10. MIUI 10 已连接 但无法访问互联网 的解决方案
  11. windows 11 去掉桌面图标的小箭头方法
  12. 游戏音乐制作/游戏音效制作/游戏配音首选
  13. 一款轻量级的权限框架,轻松搞定项目权限
  14. 美国国家机器人计划2.0部分项目简介
  15. ssm搭建整合,这一篇你直接拷走就是一个搭好的框架,前后端可分离,可不分离
  16. 靶机Who Wants To Be King 1渗透记录
  17. 计算机CPU像人的大脑,人脑与电脑的相似性与差异性及全球脑
  18. POI 导出Excel 带图片导出 使用XSSFWorkbook
  19. Tu te prends pour qui?
  20. python 3 or 5的值_python表达式3or5的值为_表达式 3 or 5 的值为

热门文章

  1. Way to MongoDB
  2. SQL基础(3)-索引/触发器/视图操作
  3. UVa 11889 (GCD) Benefit
  4. rails常用验证方法 (转)
  5. 拒绝平庸——浅谈WEB登录页面设计
  6. CF119D(字符串-哈希求解(KMP求了半天,结果哈希更简单!))
  7. java程序设计专业介绍_简介Java编程中的Object类
  8. 服务器虚拟化nas存储服务器搭建,采用NAS架构优化VMware服务器虚拟化环境的存储管理.pdf...
  9. docker修改镜像的存储位置_win10家庭版Docker环境搭建步骤
  10. mysqlli php7.0_php7配置mysqli并使用mysqli连接mysql