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

2022年5月31日 0 条评论 4.32k 次阅读 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 连接。

三、结语

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

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

兰陵美酒郁金香

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

文章评论(0)

你必须 登录 才能发表评论