当初线上服务多实例在滚动降级时总是会呈现局部工作失败,只管有失败工作交由其余实例重试的策略,然而有时候滚动降级较快,调配到的新实例又要降级,则导致二次失败,工作就彻底失败了。因为上线时总要留神下上线并发度,盯一下盘,有时候甚至要手动期待下。

为了做好平滑重启降级(悠闲吃根雪糕,泡杯咖啡),因而学习了下golang平滑重启的次要做法。

golang的http服务的平滑重启曾经有挺多相干组件,罕用的endless反对原生的http、gin等,次要就是复用socket来保障热重启更新。
然而对于一个非http服务即runner服务,须要本人来实现平滑重启,来确保在程度扩缩容、滚动更新、主从切换时不会出错。
次要依赖就是三点

  1. 信号捕获
  2. context传递敞开信号给各个子模块
  3. wait group确保各个模块可能平滑完结

次要应用如上面代码主函数

func main() {   exitChan := make(chan os.Signal, 1)   // 应用signal.Notify来捕获退出信号,个别是应用term int信号来作为敞开信号,能够依据本人须要抉择   signal.Notify(exitChan, syscall.SIGINT, syscall.SIGTERM)   // 筹备好wg和ctx来记录服务执行逻辑状态   wg := &sync.WaitGroup{}   //  cancel用于勾销,ctx用于告诉   ctx, cancel := context.WithCancel(context.Background())   // 开启业务逻辑   wg.Add(2)   go FatherServer(ctx, wg,1)   go FatherServer(ctx, wg,2)   for {      select{         // 期待退出信号         case <-exitChan:         log.Println("get exit signal")         // 告知业务处理函数该退出了         cancel()         // 期待业务处理函数全都退出         wg.Wait()         log.Println("exit main success")         return     }   }}

业务解决逻辑会须要工夫,也有并发,这里用简略的time sleep来代替,fatherServer代表业务逻辑1和2,ChildServer代表理论执行业务逻辑的函数。

func FatherServer(ctx context.Context, wg *sync.WaitGroup, num int){   defer wg.Done()   fatherWg := &sync.WaitGroup{}   i := 0 for {      select {        case <-ctx.Done():         log.Println("FatherServer wait exit")         fatherWg.Wait()         log.Println("FatherServer exit success")         return        case <-time.After(time.Second * 5):         i++         fatherWg.Add(1)         go ChildServer(fatherWg, fmt.Sprintf("FatherServer num:%d-%d", num, i))      }   }}func ChildServer(wg *sync.WaitGroup, args string){   defer wg.Done()   fmt.Printf("ChildServer will process %sn", args)   time.Sleep(time.Second * 3)   fmt.Printf("ChildServer process success%sn", args)}

运行后在ChildServer将要执行工作的时候退出

能够看到服务在解决完子工作1-3 2-3才退出服务,不会呈现解决工作到一半的状况而中断。

理论利用中,FatherServer可能为接管音讯队列的音讯,一直调用ChildServer来解决,采纳该办法,能够保障接管到的音讯不会解决到一半而因为重启导致中断,在滚动降级、扩缩容时音讯可能平滑的退出进入。

理论业务中有一次服务须要调用一个客户端来解决,因而解决逻辑中应用了exec.Command来调用,然而服务在滚动降级重启的时候发现调用客户端解决到一半的工作会被中断,然而服务曾经做了期待各个工作执行后才会重启该实例。

应用一个执行脚本代替该逻辑

sleep 3echo $1 > /tmp/hello.txt

业务解决逻辑为

func ChildServer(wg *sync.WaitGroup, args string){   defer wg.Done()   fmt.Printf("ChildServer will process %sn", args)   ExecuteCmd(args)   fmt.Printf("ChildServer process success%sn", args)}func ExecuteCmd(args string){   cmd := exec.Command("/bin/bash", "./hello.sh", args)   _, err := cmd.CombinedOutput()   if err != nil{      log.Println("err:", err)   }}

服务启动后应用kill pid在将要执行1-6的时候kill服务,发现期待工作实现后才退出,看起来实现了优雅重启

然而上线后发现不会期待子工作实现就退出了!

显示实现了工作1-2,然而查看/tmp/hello.txt发现外面是1-1的信息,也就是工作中断了。
查看日志能够发现调用脚本的cmd也收到退出信号,并且退出了,导致工作失败,然而咱们的中断信号是发给main服务的,玩什么这里子过程也收到退出信号?
这是因为ctrl+c发送的退出信号是给过程组的,而由咱们main服务调用发动的子过程属于该过程组,所以也收到了该信号,导致子过程不持续工作而退出了。
kill pid是仅对过程发信号,而应用kill -pid即可对该过程组发送信号。
线上服务应用supervisor托管,滚动降级重启的时候也是发信号给过程组,因而导致服务的子过程立即退出了。
那么只有调起的过程有本人的编号即可不接管到该退出信号,能安稳实现本人的工作再退出了。
调用cmd时应用

func ExecuteCmd(args string){   cmd := exec.Command("/bin/bash", "./hello.sh", args)   // 领有本人的过程组   cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}   _, err := cmd.CombinedOutput()   if err != nil{      log.Println("err:", err)   }}

运行服务

子过程没有收到退出信号,父过程也是期待子过程实现后,再退出,至此实现了平滑重启!

之后上线点一下,灰度局部实例实现无误后,就能够期待数十个实例滚动更新重启,也不会报工作失败了,能够安心去茶水间拿点吃的泡杯咖啡回来静静期待就好了。