最近接手了一个“公共”服务,负责维护它的稳定性。代码库有很多人参与“维护”,其实就是各种业务方使劲往上堆逻辑。虽然入库前我会进行 CR,但多了之后,也看不过来,还有一些人自己偷摸就把代码合到 master 上去了。总之,代码质量无法得到很好的保证。

当然了,如果把合代码的权限收敛到我一个人,理论上是可行的。但是,一方面,业务迭代的速度很可能就 block 在我这了;另一方面,业务方的迭代逻辑涉及很多具体的业务,我也不太熟。所以,CR 的时候也只能看一些诸如 go 出去的 func 有没有加 recover、有没有异常使用空指针等等,对于业务相关的代码提不出什么有用的意见。

其实有一些业务方的逻辑和其他业务方完全独立(使用的接口和其他业务方独立),后续会将当前的服务完全“复制”一份出来,交给业务方自行维护。

但眼下有一个问题需要解决:报警群里时不时来一个 recovered panic 的报警,我看到报警后就要登上机器看日志,执行 “grep -C 10 panic xxx.log” 这样的命令看 panic 发生在哪里。再执行 git blame 看看究竟是谁写的,再去群里 @ 他进行处理。但很多情况下是这些 panic 是由脏数据导致的,发生的也不频繁,并且 panic 被 recover 住了,所以也不太着急。

问题是业务方写完了代码之后,基本也不太关心服务运行地怎么样,但作为服务负责人得管。像前面提到的 panic 报警发生的多了,我“查日志,定位到代码提交人再通知他处理”的事情多了之后,就想能不能写一个 panic blame 机器人来做这件事。这样就能省不少事,而且还显得那么优雅。

想好了要做这件事,其实也并不困难。

最朴素的思路就是在 recover 函数里把 panic 发生时的一些信息,例如 pod-name、机器 ip、服务名、stack 等通过 HTTP 请求发送到某个服务,这个服务收到 stack 后分析出 panic 的那行代码,再请求 git 服务的某个接口,拿到提交人及提交时间。整体如下:

整体框架

我们再看看具体代码是怎么写的。例如,Recover 函数是这样的:

func RecoverFromPanic(funcName string) {if e := recover(); e != nil {buf := make([]byte, 64<<10)buf = buf[:runtime.Stack(buf, false)]logs.Errorf("[%s] func_name: %v, stack: %s", funcName, e, string(buf))panicError := fmt.Errorf("%v", e)panic_reporter_client.ReportPanic(panicError.Error(), funcName, string(buf))}return
}

向机器人服务端发送 panic 信息的 panic_reporter_client 代码:

const url = "http://localhost:8888/report-panic"// 为了避免造成 panic report 服务被打挂,降低发送 http 请求频率,进程生命周期内只发一次
var panicReportOnce sync.Oncetype PanicReq struct {Service   string `json:"service"`ErrorInfo string `json:"error_info"`Stack     string `json:"stack"`LogId     string `json:"log_id"`FuncName  string `json:"func_name"`Host      string `json:"host"`PodName   string `json:"pod_name"`
}func ReportPanic(errInfo, funcName, stack string) (err error) {panicReportOnce.Do(func() {defer func() {recover()}()go func() {panicReq := &PanicReq {Service:   env.Service(),ErrorInfo: errInfo,Stack:     stack,FuncName:  funcName,Host:      env.HostIP(),PodName:   env.PodName(),}var jsonBytes []bytejsonBytes, err = json.Marshal(panicReq)if err != nil {return}var req *http.Requestreq, err = http.NewRequest("GET", url, bytes.NewBuffer(jsonBytes))if err != nil {return}req.Header.Set("Content-Type", "application/json")client := &http.Client{Timeout: 5 * time.Second}var resp *http.Responseresp, err = client.Do(req)if err != nil {return}defer resp.Body.Close()return}()})return
}

解析出 panic 消息的代码也不难,我们需要看一下如何从 stack 信息中找到 panic 的那一行。

举一个例子来说明:

package mainimport ("fmt""runtime"
)func a() {fmt.Println("a")b()
}func b() {fmt.Println("b")c()
}type Student struct {Name int
}func c() {defer RecoverFromPanic("fun c")fmt.Println("c")var a *Studentfmt.Println(a.Name)
}func main() {a()
}func RecoverFromPanic(funcName string) {if e := recover(); e != nil {buf := make([]byte, 64<<10)buf = buf[:runtime.Stack(buf, false)]fmt.Printf("[%s] func_name: %v, stack: %s", funcName, e, string(buf))}return
}

这是一个有几层调用关系的例子,假装我们年幼无知直接解引用了一个空指针,导致 panic,但被 recover 了,输出的调用栈信息如下:

goroutine 1 [running]:
main.RecoverFromPanic(0x4c4551, 0x5)/home/raoquancheng/go/src/hello/test.go:36 +0xb5
panic(0x4a9340, 0x55b8d0)/usr/local/go/src/runtime/panic.go:679 +0x1b2
main.c()/home/raoquancheng/go/src/hello/test.go:26 +0xd4
main.b()/home/raoquancheng/go/src/hello/test.go:15 +0x7a
main.a()/home/raoquancheng/go/src/hello/test.go:10 +0x7a
main.main()/home/raoquancheng/go/src/hello/test.go:30 +0x20

栈信息中,首先是 runtime.Stack 函数那一行;接着是 /usr/local/go/src/runtime/panic.go:679,也就是 runtime 里的 gopanic 函数;下一行就是真正引起 panic 的使用空指针的那一行代码,这是罪魁祸首,panic blame 机器人主要关注这个;之后的信息就是调用链关系,会一直追溯到 main 函数里调用 a() 的源头。

分析出来这些信息后,向 IM 提供的机器人 webhook 地址发送 panic 消息,并顺带 @ 刚才找到的代码提交人,老哥,你又写出 panic 了:

机器人通知

这样是不是就是万事大吉了?

并不是,还有一些关键问题需要考虑。首先业务进程不能阻塞在发送 panic 信息的过程中,且发送 panic 信息的代码不能再发次发生 panic,以免给业务进程带来二次伤害。这样就需要以异步的方式发送消息,并且最好是通过消息队列或者 UDP 这种“我发完了就不管了”的姿态发送。

机器人服务端用生产者消费者的形式来解析业务进程发送上来的消息。无论业务进程是以 HTTP,还是 UDP 或者消息队列发过来的 panic 报告请求最终都要进入一个“池子”,HTTP、UDP、消息队列也就是所谓的生产者,消费者协程则从“池子”里取出 panic 报告请求,解析、发送报警@人处理。

还有一个需要考虑的是机器人服务端不要被打跨了,尤其是考虑到一些业务跑在几千个实例上的时候,更要注意了。

分别从客户端和服务端两方面来看。

对于客户端,在一个进程生命周期内,同时发生多“种” panic 的情况并不多见,因此我们只需要在进程生命周期内发送一次就行了,用 sync.Once

在服务端,对同一个业务发送的请求进行限流和聚合,例如每秒只处理同一个业务的一个请求,对被限流的请求做聚合,报告一个总的 panic 数量就行了。

另一个可能需要考虑的是如果 panic 代码提交者离职了怎么办?或者说我只是做了一下 format,真实的提交者并不是我,怎么办?

我们并不能做到 100% 的准确,现实有很多的边角没法解决。比如代码提交者并没有离职,但他转岗了……有个可以考虑的方法是看 panic 那一行代码附近的最近修改过代码的人是谁,找他,或者直接找服务负责人好了。不求完美,只要能解决大部分问题就行了。

实现一个 panic blame 机器人比较简单,但考虑服务稳定性的话,还是有一些点要注意的。

写一个 panic blame 机器人相关推荐

  1. 手把手教你写一个中文聊天机器人

    本文来自作者 赵英俊(Enjoy) 在 GitChat 上分享 「手把手教你写一个中文聊天机器人」,「阅读原文」查看交流实录. 「文末高能」 编辑 | 哈比 一.前言 发布这篇 Chat 的初衷是想和 ...

  2. 从 Forces 开始分析责任链模式:「写一个 Discord 对话机器人」

    目录 前言 你收到了一份需求 面向对象分析 (OOA) 初版程式实作 察觉 Forces 套用责任链模式 (OOD) 封装变动之处 (Encapsulate what varies) 萃取共同行为 ( ...

  3. 如何用76行代码写一个AI微信机器人......

    本期博客主要介绍如何使用 微信SDK 和 AI聊天接口 ,实现 微信机器人功能. 准备 电脑需要安装Go环境,这个可以直接参考菜鸟教程:Go 语言环境安装,知道CSDN的同学基本能在半小时内装好吧- ...

  4. 写一个福利 Telegram 机器人

    官网 创建 bot 根据文档,在 telegram 里面添加 @BotFather, 然后跟他聊天来创建机器人 拿到 token 测试 在浏览器中(翻墙)输入 https://api.telegram ...

  5. 7-3 降价提醒机器人 (10 分)小 T 想买一个玩具很久了,但价格有些高,他打算等便宜些再买。但天天盯着购物网站很麻烦,请你帮小 T 写一个降价提醒机器人,当玩具的当前价格比他设定的价格便宜时发

    #include<stdio.h> int main() {     int N,M;     float P;     scanf("%d %d",&N,&a ...

  6. 用wxpy做一个微信聊天机器人(详解)

    用python写一个微信聊天机器人可以利用python中的wxpy库或者itchat模块,我在网上看到好多都是使用的itchat,但是我这里使用的是wxpy库,wxpy 在 itchat 的基础上,通 ...

  7. 基于WebQQ3.0协议写一个QQ机器人

    最近公司需要做个qq机器人获取qq好友列表,并且能够自动向选定的qq好友定时发送消息.没有头绪,硬着头皮上 甘甜的心情瞬间变得苦涩了 哇 多捞吆 1.WEBQQ3.0登陆协议 进入WEBQQ, htt ...

  8. [译] 如何用 Python 写一个 Discord 机器人

    原文地址:How to write a Discord bot in Python 原文作者:Junpei Shimotsu 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/go ...

  9. 用python写聊天机器人_用Python 写一个机器人陪你聊天(文尾有彩蛋)

    工作一忙,原来秉烛夜谈的好友现在都很少聊天,微信都成了微信群的天下,鲜有微信好友给你发消息,想要主动发却也找不到开题话题,怎么办?用Python写一个机器人陪自己聊聊天吧.以下是源码及解析,小白都看得 ...

最新文章

  1. js实现Form表单submit提交截获数据(各浏览器通用)
  2. 这次是在没有外网yum仓库的情况下搭建内网yum仓库和无人值守pxe装机
  3. Linux操作系统中,*.zip、*.tar、*.tar.gz、*.tar.bz2、*.tar.xz、*.jar、*.7z等格式的压缩与解压...
  4. 31.绿豆蛙的归宿(拓扑排序)
  5. 阿波罗数据集怎么下载_从2D images 到3D估计:现有最大规模数据集 ApolloCar3D
  6. 北京师范大学网络教育期末考试计算机,北京师范大学网络教育———《计算机应用基础》第二章同步练习题(4)...
  7. golang []byte 和 string相互转换
  8. Chapter 5:Spectral-Subtractive Algorithms
  9. php设置html全局路径_PHPCMS V9 URL去掉或修改/html路径的方法
  10. F5 root密码恢复
  11. 【知识图谱系列】自适应深度和广度图神经网络模型
  12. 并行计算 SLIC超像素算法(一) 大致描述
  13. vue两列数据 合并成一列
  14. 290万人考研:所有的不平凡,从不认命开始
  15. 云学python (第二章用编程改造世界·小练习)《vamei-从Python开始学编程》
  16. 工程师的浪漫:用机械臂画一个爱心
  17. 亚马逊、速卖通、沃尔玛、阿里国际、煤炉、wish、eBay、Lazada、Shopee测评自养号,listing流量不高,导致转化率低该怎么办?
  18. 深度学习:ResNet(残差网络)
  19. Jmeter基础-配置原件
  20. 复印机维修保养的常识

热门文章

  1. 图解 Java 线程的生命周期,看完再也不怕面试官问了
  2. spring ioc原理解析
  3. 简单谈谈Docker镜像的使用方法_docker
  4. [译] 原生 JavaScript 值得学习吗?答案是肯定的
  5. 【Redis】使用Redis Sentinel实现Redis HA
  6. HTML5 — 让拖放变的流行起来
  7. Linux文件和目录属性
  8. 在定义SharePoint列表的SPD数据视图的时候需要注意的问题
  9. CH - 4901 关押罪犯(二分图判定+二分/并查集)
  10. POJ - 2342 Anniversary party(树形dp入门)