容器环境下 Go Panic 告警日志发送方案

2022年5月31日 0 条评论 3.14k 次阅读 3 人点赞

一、前言

以前服务没有运行在容器中时,我们通常使用 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 连接。

三、结语

对于普通的 ErrorWarning 级别的告警来说,日志就可以直接通过日志库提供的 Hook 钩子,来发送给 MQ ,然后通过一个中心化的 logagent 去消费这些日志,来发送告警。

上面的方案只适用于处理在容器下 panic 日志无法捕捉的问题。

兰陵美酒郁金香

大道至简 Simplicity is the ultimate form of sophistication.

文章评论(0)

你必须 登录 才能发表评论