乐趣区

关于golang:记一次-gomicro-服务异常退出问题的根因分析

origin: https://github.com/x1nchen/bl…

Table of Contents

  1. 前情提要
  2. 问题形容
  3. 剖析起因

    1. 容器进行这个操作到底执行的是什么?
    2. go-micro 如何解决 linux signal?
    3. 通过二分法代码测试查看,发现过程 gops 后的问题
  4. 解决方案
  5. 经验总结

遇到一个很有意思的问题,在此记录一下。

<span class=”underline”>TLDR: golang 服务通过 signal.Notify 注册 signal handler 的行为有且只有一次,否则会呈现不可预知的行为。</span>

前情提要

公司开发微服务用的 go-micro 框架,这个框架提供了一个及其有价值的个性:micro.AfterStop 函数容许服务退出之后执行一段自定义的逻辑,通常用于清理资源,期待 goroutine 退出等。

另一方面,咱们在生产环境也对每个服务集成了 gops,这个工具极大了晋升了服务的可察看性。

问题形容

某天,某位仔细的共事告知 micro.AfterStop 自定义函数在容器重启时,并没有执行。现场复现确认问题存在。

上面是一个 minimal reproducible example

package main

import (
  "fmt"
  "log"

  "github.com/micro/go-micro/v2"
  grpcServer "github.com/micro/go-micro/v2/server/grpc"
  "github.com/google/gops/agent"
)

func main() {
  if err := agent.Listen(agent.Options{
    Addr:            "0.0.0.0:0",
    ShutdownCleanup: true}); err != nil {log.Fatal(err)
  }
  defer agent.Close()

  srv.Init(micro.Name("service.name"),
    micro.Server(grpcServer.NewServer()),
    micro.AfterStop(func() error {fmt.Println("after stop")
      return nil
    }),
  )

  if err := srv.Run(); err != nil {fmt.Println("server run err", err)
  }
}

https://docs.docker.com/engin…

The main process inside the container will receive SIGTERM, and after a grace period, SIGKILL. The first signal can be changed with the STOPSIGNAL instruction in the container’s Dockerfile, or the –stop-signal option to docker run.

简而言之,默认状况下会给容器对应的过程发送一个 SIGTERM 信号,而后在一段时间后(默认是 10s)发送一个 SIGKILL 信号

go-micro 如何解决 linux signal?

go-micro 在 service.Run 办法注册了一个 signal handler,其中就包含了对 SIGTERM 的解决

func (s *service) Run() error {
  ......

  if err := s.Start(); err != nil {return err}

  ch := make(chan os.Signal, 1)
  if s.opts.Signal {
    signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, 
      syscall.SIGQUIT, syscall.SIGKILL)
  }

  select {
  // wait on kill signal
  case sig := <-ch:
    // wait on context cancel
  case <-s.opts.Context.Done():}

  return s.Stop()}

到目前为止,没有什么异样,察看到的景象是

  1. Stop() 每次都未执行完,但每次执行完结的中央都不同。
  2. 每次退出之后用 echo $? 查看过程退出的 code 也不雷同

通过二分法代码测试查看,发现过程 gops 后的问题

gops 在 gracefulShutdown 办法也注册了一个 signal handler

func gracefulShutdown() {c := make(chan os.Signal, 1)
  gosignal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
  go func() {
    // cleanup the socket on shutdown.
    sig := <-c
    Close()
    ret := 1
    if sig == syscall.SIGTERM {ret = 0}
    os.Exit(ret)
  }()}

不言而喻,os.Exit(ret) 会比 go-micro 更快执行完,导致整个过程退出

解决方案

晓得 root cause 之后就好解决了,移除 gops 的 signal handler 就好了。

恰好 gops 为此提供了一个 option 选项,那咱们 disable 掉。留神退出 main goroutine 前,被动调用 agent.Close()

if opts.ShutdownCleanup {gracefulShutdown()
}

看起来之前就有人提过相似的 issue

  • https://github.com/google/gop…
  • https://github.com/google/gop…

经验总结

  1. 同一个过程不要注册复数个 signal handler,这可能会导致不可预知的行为;debug 相似景象的问题时,留神查看第三方库和集成的性能 (监控 /pprof/metric-report 等) 是否存在这种状况
  2. 对于一个 sdk lib or integration lib 来说,尽量避免本人注册 signal handler,特地是不要随便调用 os.Exit() 自行终止过程
  3. 如果 2 不可避免,那么提供一个选项让调用者能够从内部管制这个行为,否则肯定会被喷得狐疑人生。
退出移动版