一个示例阐述 Go 应用的优雅中止
敬维 · · 1400 次点击 · · 开始浏览写在前面
按照一般的设计原则, 每个 HTTP 请求都是无状态的,因此大多情况下 Web 应用都很容易做水平扩展。"无状态"也意味着 HTTP 请求发起重试的成本是很低的,从而使得 Web 接口的开发很少关注优雅中止(一部分也因为 Web 框架做了这部分的考虑)。
不过,业务中 1 总会存在对中止比较敏感的接口(比如支付相关),并且 2 总会存在一些带状态的服务,此时优雅中止就显得比较重要了。
本文通过一个Go 定时任务示例来简单介绍 Go 技术栈中优雅中止的处理思路。
适用人群
入门——初级√——中级——高级;本文适应初级及以上。
代码级支持优雅中止是必要的
优雅中止的含义
所谓"优雅中止",是指应用接收到特定的中止信号(比如 INT、TERM)后,不再接受外部的新请求,也不再创建内部的新任务,保持应用进程运行直到旧需求和旧任务执行完成后再终止退出。
Kubernetes 中 Pod 的终止机制
作为高可靠的服务平台,k8s 定义了终止 Pod (业务进程在 Pod 中运行)的基本步骤:当主动删除 pod 时,系统会在强制终止 Pod 之前将 TERM 信号发送到每个容器中的主进程,过一段时间后(默认为 30 秒),再把 KILL 信号发送到这些进程。除此之外, k8s 还通过钩子方法提供了对 容器生命周期 的管理能力,允许用户通过自定义的方式配置容器启动后或终止前执行的操作。
当打包进镜像的应用运行在 k8s 中的时候,如果应用实现了优雅中止的机制,就可以充分利用上面提到的 k8s 的能力,在升级应用(发新版本)和管理 Pod (宿主机维护时把 Pod 漂移到另一个宿主机,或者在闲时动态地收缩 Pod 数量从而把资源省出来另作他用)的过程中实现服务的零中断。
优雅中止的 Go 代码示例
下面的代码定义了两个定时任务:mySecondJobs 每秒钟会触发一次,每次持续约 1 秒钟;myMinuteJobs 每分钟会触发一次,每次持续约 2 秒钟。具体地可以阅读下面的代码(可以直接复制下面的代码到自己的环境中运行):
packagemainimport("fmt""os""os/signal""syscall""time")funcmain(){c:=make(chanos.Signal)// Go 不允许监听 SIGKILL/SIGSTOP 信号// 参考 https://github.com/golang/go/issues/9463signal.Notify(c,syscall.SIGINT,syscall.SIGTERM)second:=time.NewTicker(time.Second)minute:=time.NewTicker(time.Minute)A:// 由于 for-select 嵌套使用,设置跳出 for 循环的标记for{select{cases:=<-c:// 收到 SIGTERM/SIGINT 信号,跳出 for 循环结束进程fmt.Printf("get signal %s, graceful ending...\n",s)breakAcase<-second.C:gomySecondJobs()case<-minute.C:gomyMinuteJobs()}}fmt.Println("graceful ending")// 做一些操作让异步任务正常结束,这里偷懒地采取简单等待的方式 ????time.Sleep(time.Second*10)fmt.Println("graceful ended.")}funcmySecondJobs(){tS:=time.Now().String()fmt.Printf("starting second job: %s \n",tS)time.Sleep(time.Second*1)// 假设每个任务消耗 1 秒时间fmt.Printf("second job %s are done. \n",tS)}funcmyMinuteJobs(){tS:=time.Now().String()fmt.Printf("starting minute job: %s \n",tS)time.Sleep(time.Second*2)// 假设每个任务消耗 2 秒时间fmt.Printf("minute job %s are done. \n",tS)}源码解读-优雅中止的处理思路
- 通过
signal.Notify捕获特定的信号; - 通过
for + select来实现循环任务,同时检测上步中欲捕获的信号; - 如果定时器被触发,则执行对应的任务;
- 如果发现收到了指定的信号,则跳出
for循环,并采取一定措施结束异步任务。
源码解读-值得关注的几个点
- 代码中采用了
go mySecondJobs()和go myMinuteJobs()异步任务的方式;如果采用同步的方式将无法捕获信号,因为此时主线程在处理业务逻辑,没有空闲处理信号捕获逻辑。 - 源码中偷懒地采取简单等待的方式来保证异步任务正常结束,非普适方法,实际开发中需要根据情况做定制。
time.Ticker的使用是有注意事项的,当select语句中同一时刻有多个分支满足条件时会随机取一个执行,从而导致信息丢失(参考文献中最后一篇有讲到),不过本文的代码不会触发这个问题,大家可以思考一下原因。
小结
默认情况下,Go 应用在接收到 TERM 信号后直接退出主进程,如果此时有过程没处理完(比如 接收到外部请求后尚未返回响应,或者内部的异步任务尚未结束),则会导致过程的异常中断,影响服务质量。通过在代码中显式地捕获 TERM 信号及其他信号,感知操作系统对进程的处理,可以主动采取措施优雅地结束应用进程。
随着 k8s 的普及,考虑到其对进程生命周期的规范化管理,应用支持代码级的优雅中止(尤其是容器化的应用)有必要成为一种开发规范,值得引起每一位开发者的注意。
参考
- 信号(LINUX信号机制)_百度百科 介绍 Linux 中的信号(比如 SIGINT、SIGTERM 等)
- Pods - Kubernetes Kubernetes 官网对 pod 的介绍,包含对 pod 生命周期的介绍
- Container Lifecycle Hooks - Kubernetes Kubernetes 官方对容器生命周期中的钩子的介绍
- 优雅地关闭kubernetes中的nginx - 简书 介绍了信号、 k8s 中 pod 的终止流程,以及 nginx 的优雅终止
- golang信号signal的处理 - 快乐编程
- Golang的 signal - 蝈蝈俊 - 博客园
- os/signal: Prevent developers from catching SIGKILL and SIGSTOP · Issue #9463 · golang/go · GitHub Go 中不允许捕获 SIGKILL 和 SIGSTOP 信号
- go里面select-case和time.Ticker的使用注意事项 - CSDN博客 虽然本文的示例不会触发,不过 time.Ticker 使用时还是要注意一下这个小坑
- how to get a graceful shutdown of an server. · Issue #1329 · gin-gonic/gin · GitHub Go 的 Web 框架 Gin 对优雅中止的支持示例
有疑问加站长微信联系(非本文作者)
入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889
关注微信- 请尽量让自己的回复能够对别人有帮助
- 支持 Markdown 格式, **粗体**、~~删除线~~、
`单行代码` - 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
- 图片支持拖拽、截图粘贴等方式上传
收入到我管理的专栏 新建专栏
写在前面
按照一般的设计原则, 每个 HTTP 请求都是无状态的,因此大多情况下 Web 应用都很容易做水平扩展。"无状态"也意味着 HTTP 请求发起重试的成本是很低的,从而使得 Web 接口的开发很少关注优雅中止(一部分也因为 Web 框架做了这部分的考虑)。
不过,业务中 1 总会存在对中止比较敏感的接口(比如支付相关),并且 2 总会存在一些带状态的服务,此时优雅中止就显得比较重要了。
本文通过一个Go 定时任务示例来简单介绍 Go 技术栈中优雅中止的处理思路。
适用人群
入门——初级√——中级——高级;本文适应初级及以上。
代码级支持优雅中止是必要的
优雅中止的含义
所谓"优雅中止",是指应用接收到特定的中止信号(比如 INT、TERM)后,不再接受外部的新请求,也不再创建内部的新任务,保持应用进程运行直到旧需求和旧任务执行完成后再终止退出。
Kubernetes 中 Pod 的终止机制
作为高可靠的服务平台,k8s 定义了终止 Pod (业务进程在 Pod 中运行)的基本步骤:当主动删除 pod 时,系统会在强制终止 Pod 之前将 TERM 信号发送到每个容器中的主进程,过一段时间后(默认为 30 秒),再把 KILL 信号发送到这些进程。除此之外, k8s 还通过钩子方法提供了对 容器生命周期 的管理能力,允许用户通过自定义的方式配置容器启动后或终止前执行的操作。
当打包进镜像的应用运行在 k8s 中的时候,如果应用实现了优雅中止的机制,就可以充分利用上面提到的 k8s 的能力,在升级应用(发新版本)和管理 Pod (宿主机维护时把 Pod 漂移到另一个宿主机,或者在闲时动态地收缩 Pod 数量从而把资源省出来另作他用)的过程中实现服务的零中断。
优雅中止的 Go 代码示例
下面的代码定义了两个定时任务:mySecondJobs 每秒钟会触发一次,每次持续约 1 秒钟;myMinuteJobs 每分钟会触发一次,每次持续约 2 秒钟。具体地可以阅读下面的代码(可以直接复制下面的代码到自己的环境中运行):
packagemainimport("fmt""os""os/signal""syscall""time")funcmain(){c:=make(chanos.Signal)// Go 不允许监听 SIGKILL/SIGSTOP 信号// 参考 https://github.com/golang/go/issues/9463signal.Notify(c,syscall.SIGINT,syscall.SIGTERM)second:=time.NewTicker(time.Second)minute:=time.NewTicker(time.Minute)A:// 由于 for-select 嵌套使用,设置跳出 for 循环的标记for{select{cases:=<-c:// 收到 SIGTERM/SIGINT 信号,跳出 for 循环结束进程fmt.Printf("get signal %s, graceful ending...\n",s)breakAcase<-second.C:gomySecondJobs()case<-minute.C:gomyMinuteJobs()}}fmt.Println("graceful ending")// 做一些操作让异步任务正常结束,这里偷懒地采取简单等待的方式 ????time.Sleep(time.Second*10)fmt.Println("graceful ended.")}funcmySecondJobs(){tS:=time.Now().String()fmt.Printf("starting second job: %s \n",tS)time.Sleep(time.Second*1)// 假设每个任务消耗 1 秒时间fmt.Printf("second job %s are done. \n",tS)}funcmyMinuteJobs(){tS:=time.Now().String()fmt.Printf("starting minute job: %s \n",tS)time.Sleep(time.Second*2)// 假设每个任务消耗 2 秒时间fmt.Printf("minute job %s are done. \n",tS)}源码解读-优雅中止的处理思路
- 通过
signal.Notify捕获特定的信号; - 通过
for + select来实现循环任务,同时检测上步中欲捕获的信号; - 如果定时器被触发,则执行对应的任务;
- 如果发现收到了指定的信号,则跳出
for循环,并采取一定措施结束异步任务。
源码解读-值得关注的几个点
- 代码中采用了
go mySecondJobs()和go myMinuteJobs()异步任务的方式;如果采用同步的方式将无法捕获信号,因为此时主线程在处理业务逻辑,没有空闲处理信号捕获逻辑。 - 源码中偷懒地采取简单等待的方式来保证异步任务正常结束,非普适方法,实际开发中需要根据情况做定制。
time.Ticker的使用是有注意事项的,当select语句中同一时刻有多个分支满足条件时会随机取一个执行,从而导致信息丢失(参考文献中最后一篇有讲到),不过本文的代码不会触发这个问题,大家可以思考一下原因。
小结
默认情况下,Go 应用在接收到 TERM 信号后直接退出主进程,如果此时有过程没处理完(比如 接收到外部请求后尚未返回响应,或者内部的异步任务尚未结束),则会导致过程的异常中断,影响服务质量。通过在代码中显式地捕获 TERM 信号及其他信号,感知操作系统对进程的处理,可以主动采取措施优雅地结束应用进程。
随着 k8s 的普及,考虑到其对进程生命周期的规范化管理,应用支持代码级的优雅中止(尤其是容器化的应用)有必要成为一种开发规范,值得引起每一位开发者的注意。
参考
- 信号(LINUX信号机制)_百度百科 介绍 Linux 中的信号(比如 SIGINT、SIGTERM 等)
- Pods - Kubernetes Kubernetes 官网对 pod 的介绍,包含对 pod 生命周期的介绍
- Container Lifecycle Hooks - Kubernetes Kubernetes 官方对容器生命周期中的钩子的介绍
- 优雅地关闭kubernetes中的nginx - 简书 介绍了信号、 k8s 中 pod 的终止流程,以及 nginx 的优雅终止
- golang信号signal的处理 - 快乐编程
- Golang的 signal - 蝈蝈俊 - 博客园
- os/signal: Prevent developers from catching SIGKILL and SIGSTOP · Issue #9463 · golang/go · GitHub Go 中不允许捕获 SIGKILL 和 SIGSTOP 信号
- go里面select-case和time.Ticker的使用注意事项 - CSDN博客 虽然本文的示例不会触发,不过 time.Ticker 使用时还是要注意一下这个小坑
- how to get a graceful shutdown of an server. · Issue #1329 · gin-gonic/gin · GitHub Go 的 Web 框架 Gin 对优雅中止的支持示例