当初线上服务多实例在滚动降级时总是会呈现局部工作失败,只管有失败工作交由其余实例重试的策略,然而有时候滚动降级较快,调配到的新实例又要降级,则导致二次失败,工作就彻底失败了。因为上线时总要留神下上线并发度,盯一下盘,有时候甚至要手动期待下。
为了做好平滑重启降级(悠闲吃根雪糕,泡杯咖啡),因而学习了下 golang 平滑重启的次要做法。
golang 的 http 服务的平滑重启曾经有挺多相干组件,罕用的 endless 反对原生的 http、gin 等,次要就是复用 socket 来保障热重启更新。
然而对于一个非 http 服务即 runner 服务,须要本人来实现平滑重启,来确保在程度扩缩容、滚动更新、主从切换时不会出错。
次要依赖就是三点
- 信号捕获
- context 传递敞开信号给各个子模块
- 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 3
echo $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)
}
}
运行服务
子过程没有收到退出信号,父过程也是期待子过程实现后,再退出,至此实现了平滑重启!
之后上线点一下,灰度局部实例实现无误后,就能够期待数十个实例滚动更新重启,也不会报工作失败了,能够安心去茶水间拿点吃的泡杯咖啡回来静静期待就好了。