现在在线服务多实例在滚动升级时总会出现部分任务失败。虽然有一种让失败的任务由其他实例重试的策略,但有时滚动升级更加困难。很快,分配的新实例将再次升级,这将导致第二次失败,任务将完全失败。因为上线的时候要时刻注意离线和在线的并发情况,时刻关注磁盘,有时甚至要手动等待下载。为了做平滑重启升级(悠闲地吃冰淇淋,泡杯咖啡),所以学习了golang平滑重启的主要方法。golang的http服务的平滑重启有很多相关的组件。常用的endless支持原生的http、gin等,主要是复用sockets保证热重启更新。但是对于一个非http服务,也就是runner服务,需要自己实现平滑重启,保证水平伸缩、滚动更新、主从切换时不会出错。主要依赖三点信号捕获上下文将关闭信号传递给各个子模块等待组,保证各个模块顺利完成。主要使用下面的代码mainfunctionfuncmain(){exitChan:=make(chanos.Signal,1)//使用signal.Notify来捕获退出信号,一般使用intsignal这个词作为关闭信号,可以选择signal.Notify(exitChan,syscall.SIGINT,syscall.SIGTERM)根据自己的需要//准备wg和ctx,用于记录服务执行逻辑Statuswg:=&sync.WaitGroup{}//cancel用于取消,ctx用于通知ctx,cancel:=context.WithCancel(context.Background())//打开业务逻辑wg.Add(2)goFatherServer(ctx,wg,1)goFatherServer(ctx,wg,2)for{select{//等待退出信号case<-exitChan:log.Println("getexitsignal")//通知业务处理函数该退出cancel()//等待所有业务处理函数退出wg.Wait()log.Println("exitmainsuccess")return}}}业务处理逻辑会耗时并且有并发,这里用simpletimesleep代替,fatherServer代表业务逻辑1和2,ChildServer代表实际执行的函数商业逻辑。funcFatherServer(ctxcontext.Context,wg*sync.WaitGroup,numint){deferwg.Done()fatherWg:=&sync.WaitGroup{}i:=0for{选择{case<-ctx.Done():log.Println("FatherServerwaitexit")fatherWg.Wait()log.Println("FatherServerexitsuccess")返回情况<-time.After(time.Second*5):i++fatherWg.Add(1)goChildServer(fatherWg,fmt.Sprintf("FatherServernum:%d-%d",num,i))}}}funcChildServer(wg*sync.WaitGroup,argsstring){deferwg.Done()fmt.Printf("ChildServer将process%sn",args)time.Sleep(time.Second*3)fmt.Printf("ChildServerprocesssuccess%sn",args)}运行后,在ChildServer即将执行任务时退出,可以看到serviceisprocessingthechildTask1-32-3会退出服务,处理任务中途不会中断。在实际应用中,FatherServer可能会不断调用ChildServer来处理从消息队列中接收到的消息。使用这种方法,可以保证接收到的消息不会因为处理到一半而重启而中断,并且可以在滚动升级和扩缩容的过程中处理消息。顺利退出进入。在实际业务中,一个service需要调用一个client来处理,所以在处理逻辑中使用了exec.Command来调用。但是滚动升级重启服务的时候,发现调用客户端执行到一半的任务会被打断,但是服务已经在等待每个任务执行完成后才会重启实例。用一个执行脚本代替逻辑sleep3echo$1>/tmp/hello.txt业务处理逻辑为funcChildServer(wg*sync.WaitGroup,argsstring){deferwg.Done()fmt.Printf("ChildServer将处理%sn",args)ExecuteCmd(args)fmt.Printf("ChildServer进程成功%sn",args)}funcExecuteCmd(argsstring){cmd:=exec.Command("/bin/bash","./hello.sh",args)_,err:=cmd.CombinedOutput()iferr!=nil{log.Println("err:",err)}}服务启动后,使用killpidkill服务时正要执行1-6,发现是在等待任务完成才退出。貌似实现了优雅重启,但是上线后发现不会等子任务完成就退出了!显示任务1-2已经完成,但是查看/tmp/hello.txt发现有1-1的信息,即任务中断。查看日志可以发现调用脚本的cmd也收到退出信号退出,导致任务失败。然而,我们的中断信号被发送到主服务。为什么子进程也收到退出信号?这是因为ctrl+c发出的退出信号是给进程组的,而我们主服务调用发起的子进程属于进程组,所以也收到信号,导致子进程没有继续任务就退出了。killpid只向进程发送信号,使用kill-pid向进程组发送信号。在线服务由supervisor托管,滚动升级重启时,也会向进程组发送信号,因此服务的子进程立即退出。那么,只要被调用的进程有自己的序号,就不会收到退出信号,就可以安全地完成自己的工作再退出。使用funcExecuteCmd(argsstring){cmd:=exec.Command("/bin/bash","./hello.sh",args)调用cmd//自己的进程组时cmd.SysProcAttr=&syscall.SysProcAttr{Setpgid:true}_,err:=cmd.CombinedOutput()iferr!=nil{log.Println("err:",err)}}正在运行的服务子进程没有收到退出信号,父进程也在等待为子进程完成后,再次退出,至此顺利重启!然后上网点击。灰度实例正确完成后,可以等待几十个实例滚动更新重启,任务不会报失败。你可以去茶水间拿点吃的,泡杯咖啡回来静静等着。向上。
