一、前言
以前服务没有运行在容器中时,我们通常使用 supervisor
来守护一个 Go 编写的服务。并且在服务运行时,如果发生 Panic
导致容器退出,此时 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
之后通过logagent
消费该日志文件就可以发送邮件告警。
现在换成 k8s 后不再使用 `supervisor
,因此只能在程序中想办法实现。
大部分初入 Go 开发的萌新会认为 panic 报错可以通过 recover 来拦截,并发送告警邮件。
然而事实上 recover 并不能拦截所有 panic 。例如在父协程中创建的 recover 是无法捕捉到子协程中的 panic ,就好比下面这个样子。
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("拦截",err.Error())
}
}()
go func() {
// 无法被 recover 捕捉
panic(123)
}()
time.Sleep(time.Second * 5)
}
并且当容器中的 go 程序发生 panic 后,输出的信息会直接被打倒 stderr (标准错误) ,当容器重启后,上一次发生的错误将会被清空。每次看到容器有重启次数,但是却找不到问题所在,这一定是个苦恼的问题。
此外如果我还要像以前一样,将 go 运行的标准错误定向到一个文件中,那么我还需要在容器中挂载一个 logagent 边车去消费这个 Go 产生的错误日志来发告警。显然这是一个非常费力的操作.......
二、解决方案
无意中在网上看到一个方法,它可以将 Panic
抛出的标准错误定向到一个文件,具体代码如下所示
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
}
其本质上就是使用 syscall.Dup2()
方法(注:这个方法在 windows 下没有,只有在 linux 下才能调用),将标准错误重定向到一个新的文件中,不过这里用的是文件描述符罢了。但是如果我再次重定向到文件,又会回到上文所说的,我还要给每个 Go 服务新增一个 logagent 边车去消费这些错误日志,这显然很麻烦......
此时灵感来了,那么我何必不将它定向到一个网络地址中去?
说干就干,代码如下所示
tcpAddr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:9999")
var err error
conn, err = net.DialTCP("tcp", nil, tcpAddr)
if err != nil {
panic(err)
}
f, err = conn.File()
if err != nil {
panic(err)
}
// 发生 panic 时,将错误重定向到一个网络地址
if err = syscall.Dup2(int(f.Fd()), int(os.Stderr.Fd())); err != nil {
fmt.Println(err)
return err
}
当每个 Go 服务启动时,它将会与一个中心日志服务建立连接,当 Go 服务发生 panic
时,就会将它的标准错误重定向输出到该远程的日志服务上。
远程的日志服务收到错误信息后,就将发送 panic 告警,并且关闭 socket 连接。
三、结语
对于普通的 Error
和 Warning
级别的告警来说,日志就可以直接通过日志库提供的 Hook 钩子,来发送给 MQ ,然后通过一个中心化的 logagent
去消费这些日志,来发送告警。
上面的方案只适用于处理在容器下 panic 日志无法捕捉的问题。
文章评论(0)