前言

之前把Go服务都迁到Kubernetes上后有些服务的某个 Pod总是时不时的重启一下,通过查业务日志根本查不到原因,我分析了一下肯定是哪里代码不严谨造成引用空指针导致Go发送运行时panic才会挂掉的,但是容器重启后之前输出到stderrpanic是会被清空的,所以才有了这篇文章里后面的分析和方案解决。

解决思路分析

在Go编写的应用程序里无论是在主协程(main goroutine)还是其他子协程里,一旦出了运行时panic错误后,整个程序都会宕掉。一般的部署Go项目的时候都会使用supervisor监控应用程序进程,一旦应用程序发生panic停掉后supervisor会把进程再启动起来。

那么在把项目部署到Kubernetes集群后,因为每个节点上的kubelet会对主进程崩溃的容器进行重启,所以就再引入supervisor就有些功能重叠。但是Gopanic信息是直接写到标准错误的,容器重启后之前的panic错误就没有了,没法排查导致容器崩溃的原因。所以排查容器重启的关键点就变成了:怎么把panicstderr重定向到文件,这样就能通过容器的volume持久化日志文件的目录方式保留程序崩溃时的信息。

那么以前在supervisor里可以直接通过配置stderr_logfile把程序运行时的标准错误设置成一个文件:

[program: go-xxx...]
directory=/home/go/src...
environment=...
command=/home/go/src.../bin/app
stderr_logfile=/home/xxx/log/..../app_err.log

现在换成了Kubernetes,不再使用supervisor后就只能是想办法在程序里实现了。针对在Go里实现记录panic到日志文件你可能首先会考虑:recover里把导致panic的错误记录到文件里,不过引用的第三方包里也有可能panic,这个不现实。而且Go 也没有其他语言那样的Exception,未捕获的异常能由全局的ExceptionHandler捕获到的机制,实现不了用一个recover捕获所有的panic的功能。

最后就只有一个办法了,想办法把程序运行时的标准错误替换成日志文件,这样Gopanic的时候它还是往标准错误里写,只不过我们偷偷把标准错误的文件描述符换成了日志文件的描述符(在系统眼里stderr也是个文件,Unix系统里一切皆文件)。

方案试错

按照这个思路我先用下面的例子试了一下:

package mainimport ("fmt""os"
)const stdErrFile = "/tmp/go-app1-stderr.log"func RewriteStderrFile() error {file, err := os.OpenFile(stdErrFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)if err != nil {fmt.Println(err)return err}os.Stderr = filereturn nil
}func testPanic() {panic("test panic")
}func main() {RewriteStderrFile()testPanic()
}

这个例子,我们尝试使用 os.Stderr = file 来强制转换,但运行程序后,发现不起作用,/tmp/go-app1-stderr.log没有任何信息流入,panic信息照样输出到标准错误里。

最终方案

关于原因,搜索了一下,幸运的是 Rob Pike有专门对类似问题的解答,是这样说的:

把高层包创建的变量直接赋值到底层的runtime是不行的,我们用syscall.Dup2实现替换描述符再试一次,并且增加一个全局变量对日志文件描述符的引用,避免常驻线程运行中文件描述符被GC回收掉:

var stdErrFileHandler *os.Filefunc RewriteStderrFile() error {if runtime.GOOS == "windows" {return nil}file, err := os.OpenFile(stdErrFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)if err != nil {fmt.Println(err)return err}stdErrFileHandler = file //把文件句柄保存到全局变量,避免被GC回收if err = syscall.Dup2(int(file.Fd()), int(os.Stderr.Fd())); err != nil {fmt.Println(err)return err}// 内存回收前关闭文件描述符runtime.SetFinalizer(stdErrFileHandler, func(fd *os.File) {fd.Close()})return nil
}

因为Windows系统不支持的syscall.Dup2这个函数,所以我加了个判读,Windows环境下的Go运行时加载系统的一个dll文件也能实现这里的功能,不过我们服务器环境都是Linux的,所以我认为这部分要兼容Windows是无用功,保证项目在Windows下能跑不受影响就行了。

再次运行程序后,打开日志文件/tmp/go-app1-stderr.log后就能看到刚才程序崩溃时的panic信息,以及导致panic时整个调用栈的信息:

➜  ~ cat /tmp/go-app1-stderr.log
panic: test panicgoroutine 1 [running]:
main.testPanic(...)/Users/kev/Library/Application Support/JetBrains/GoLand2020.1/scratches/scratch_4.go:39
main.main()/Users/kev/Library/Application Support/JetBrains/GoLand2020.1/scratches/scratch_4.go:44 +0x3f
panic: test panicgoroutine 1 [running]:
main.testPanic(...)/Users/kev/Library/Application Support/JetBrains/GoLand2020.1/scratches/scratch_4.go:39
main.main()/Users/kev/Library/Application Support/JetBrains/GoLand2020.1/scratches/scratch_4.go:44 +0x3f

方案实施后的效果

目前这个方案已经在我们线上运行一个月了,已发现的Pod重启事件都能把程序崩溃时的调用栈准确记录到日志文件里,帮助我们定位了几个代码里的问题。其实问题都是空指针相关的问题,这些问题我在之前的文章《如何避免用动态语言的思维写Go代码》也提到过,项目一旦复杂起来谁写的代码也不能保证说不会发生空指针,不过我们事先做好检查很多都是能够避免的明显错误,对于特别细微条件下引发的错误只能靠分析事故当时的日志来解决啦。

- END -

关注公众号,获取更多精选技术原创文章

Go服务迁到K8s后老抽风重启? 记一次完整的线上问题解决过程相关推荐

  1. mysql事务在提交后才发送给数据库执行_从一个线上问题分析binlog与内部XA事务提交过程...

    1. 问题 业务上新增一条订单记录,用户接收到BinLake拉取的MySQL从库数据消息后,马上根据消息内的订单号去查询同一个MySQL从库,发现有些时候无法查到该条数据,等待大约500ms-1000 ...

  2. HH SaaS电商系统的线上服务商品库存和采购设计

    文章目录 线上服务商品库存和采购整体思路 线上服务商品的采购单状态 线上服务商品的出库单状态 商家完成服务 线上服务商品库存和采购整体思路 线上服务商品直接在商品编辑页面编辑库存数量即可,服务端自动生 ...

  3. AI+音视频双引擎驱动,保司线上服务能力全面升级 | 爱分析报告

    报告编委 张扬 爱分析联合创始人&首席分析师 孙文瑞 爱分析高级分析师 廖耘加 爱分析分析师 外部专家(按姓氏拼音排序) 段磊 容联云音视频负责人 徐靖辰 声网数字化转型政企行业总监 特别鸣谢 ...

  4. 我在暴躁同事小张的胁迫下学会了Go的交叉编译和条件编译

    今天继续关于Go开发经验的分享,这次的主题是关于Go的交叉编译和条件编译,伴随着我对自己打不过.惹不起的壕同事小张还有运维们的碎碎念. 交叉编译 交叉编译是用来在一个平台上生成另一个平台的可执行程序. ...

  5. k8s停止服务_使用 K8S 几年后,这些技术专家有话要说

    9 月 7 日下午,在深圳南山软件产业基地,腾讯云 K8S & 云原生技术开放日成功落幕,来自腾讯.灵雀云.超参数科技.虎牙等资深技术专家与现场开发者共同探讨企业落地 K8S 的过程中遇到的难 ...

  6. dubbo k8s 服务发现_将Dubbo微服务迁移到k8s集群环境中前的思考与落地

    将Dubbo微服务迁移到k8s中的思考与落地 说到容器化,不得不提kubernetes这个集群编排系统,它是一个开源系统,用于容器化应用的自动部署.扩缩和管理. Kubernetes 将构成应用的容器 ...

  7. linux mysql端口启动失败怎么办,Linux下apache mysql等服务修改默认端口后无法正常启动解决办法...

    Linux下apache mysql等服务修改默认端口后无法正常启动解决办法 linux下 apache 等服务修改默认端口后无法正常启动解决办法 服务器上装了两个webserver,一个是nginx ...

  8. 【rabbitmq】Queueingconsumer被废止后老代码如何做的解决方案

    [rabbitmq]Queueingconsumer被废止后老代码如何做的解决方案 参考文章: (1)[rabbitmq]Queueingconsumer被废止后老代码如何做的解决方案 (2)http ...

  9. 小程序和钉钉发版后老版缓存的问题调研

    小程序发版后老版缓存方式 1. 小程序运行机制 (1)小程序启动 小程序启动会有两种情况,一种是冷启动,一种是热启动. 热启动:假如用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时无需重 ...

最新文章

  1. pthread_cond_singal condition
  2. 静态链接库与动态链接库的优缺点
  3. Python Django URL逆向解析(通过Python代码逆向访问)代码示例
  4. python装饰器应用论文_python 装饰器应用
  5. pagerTabStrip例子
  6. 数据库读写分离这个坑,你应该踩过吧?
  7. 51单片机外设篇:红外通信
  8. Android将网页转为pDf,UrlToPDF 输入网址直接将网页转存为 PDF 档(Android)
  9. 您有一张H5新年贺卡未领取
  10. Edge浏览器打不开网页解决方法教学
  11. Unity3D学习日记2
  12. 技术经理成长复盘-处理线上问题
  13. VI退出 退出VIM 适用新手
  14. C语言实现strcpy和strcmp
  15. 关于电子科技大学学生阅读情况调查报告
  16. 怎样剪切视频中的一段音频
  17. 深入了解Windows句柄到底是什么
  18. Jmeter并发测试 - 设置集合点
  19. 姚期智领衔开设清华人工智能班,北大机器人专业开始招生
  20. google 外链寻找方法

热门文章

  1. pytorch教程龙曲良21-25
  2. 【SpringBoot零基础案例03】【IEDA 2021.1】SpringBoot框架核心配置文件application.properties的使用
  3. 用css3实现ps蒙版效果+动画
  4. 51CTO学院四周年-成长之路
  5. Linux命令:MySQL系列之十一--MySQL日志管理
  6. Open*** 安装脚本
  7. jQuery Sizzle选择器(一)
  8. 运用工具优化数据库设计(Database Engine Tuning Advisor)
  9. 牛客多校2 - Keyboard Free(几何)
  10. UVA - 11806 Cheerleaders(组合数学+容斥原理)