Go的优雅退出

原因

为什么需要优雅退出?

服务总是要升级的,升级意味老版本的服务退出,这时候如果还有请求未完成,理论上会导致一些错误产生,如502等,常见HTTP错误码模拟

随着现在大模型使用越来越频繁,一个接口可能持续几十秒甚至几分钟,如果立即退出,会给用户带来很差的体验。

所以,我们需要一种机制,能在程序退出前做一些事情,而不是粗暴的被系统杀死回收,这就是所谓的优雅退出。

比较好的解决方案是新流量不再进入这台机器(一般通过服务发现浅谈微服务微服务之服务框架和注册中心),然后给应用发终止信号,应用收到终止信号后,等待一段时间退出。

终止进程

在Linux中,操作系统要终止某个进程的时候,会向它发送退出信号:

  • 比如上面你在终端中按 CTRL+C 后,程序会收到 SIGINT 信号。
  • 打开的终端被关机,会收到 SIGHUP 信号。
  • kill 8120 杀死某个进程,会收到 SIGTERM 信号。

信号(Signal):是一种软中断,是进程间通信的方式,采用【异步通信】的方式

其中,程序不可捕获、阻塞或忽略的信号:SIGKILL(9) SIGSTOP(19)

捕获信号

我们先来看代码:

go 复制代码
package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func forcePanic() {
	defer beforeExit()
	time.Sleep(time.Second * 10)
	panic("panic")
}

func main() {
	go interrupt()
	fmt.Println("run")
	forcePanic()
	time.Sleep(time.Minute * 10)
}

func beforeExit() {
	if r := recover(); r != nil {
		tmp := "Panic err : " + r.(string)
		fmt.Println("panic:", tmp)
	}
}

// 接收中断
func interrupt() {
	signals := make(chan os.Signal, 1)
	signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
	sign, ok := <-signals
	if ok {
		tmp := "OS Signal received: " + sign.String()
		fmt.Println(tmp)
		os.Exit(0)
	}
}

其实主要是通过signal.Notify接收中断,如果收到做后续处理,然后退出。

这里之所以加了defer beforeExit(),是因为如果没有recover的话,程序会直接退出,不会走到优雅退出逻辑。由于painc机制,如果没有被处理,它会调用 os.Exit(2) 退出进程,所以算是进程主动退出,故操作系统不会发送kill信号,也就无法进入优雅退出机制。

cloudwego

对于cloudwego,整个流程如下图所示,Kitex收到TERM信号后,在等待处理完毕旧连接。

某个 Pod 将要被销毁时,K8s 会以此做以下事情:

1.kube-proxy 删除上游 iptables 中的目标 IP:这一步虽然一般来说会是一个相对比较快的操作,不会像图里所示这么夸张,但它的执行时间依然是不受保障的,取决于集群的实例规模,变更繁忙程度等多重因素影响,所以用了虚线表示。

这一步执行完毕后,只能确保新建立的连接不再连接到老容器 IP 上,但是已经存在的连接不会受影响。

2.kubelet 执行 preStop 操作

由于后一步操作会立刻关闭 listener,所以这一步,我们最好是在 preStop 中,sleep N 秒的时间(这个时间取决于你集群规模),以确保 kube-proxy 能够及时通知所有上游不再对该 Pod 建立新连接。

3.kubelet 发送 TERM 信号

此时才会真正进入到 Kitex 能够控制的优雅关闭流程:

a.停止接受新连接:Kitex 会立刻关闭当前监听的端口,此时新进来的连接会被拒绝,已经建立的连接不影响。所以务必确保前面 preStop 中配置了足够长的等待服务发现结果更新的时间。

b.等待处理完毕旧连接: b.1非多路复用下(短连接/长连接池):

  • 每隔 1s 检查所有连接是否已经都处理完毕,直到没有正在处理的连接则直接退出。

b.2多路复用:

  • 立即对所有连接发送一个 seqID 为 0 的 thrift 回包(控制帧),并且等待 1s(等待对端 Client 收到该控制帧 )

  • Client 接收到该消息后标记当前连接为无效,不再复用它们(而当前正在发送和接收的操作并不会受到影响)。这个操作的目的是,client 已经存在的连接不再继续发送请求。

  • 每隔 1s 检查所有存量连接是否已经都处理完毕,直到没有活跃连接则直接退出

  • 达到 Kitex 退出等待超时时间(ExitWaitTime,默认 5s)则直接退出,不管旧连接是否处理完毕。

4.达到 K8s terminationGracePeriodSeconds 设置的超时时间(从 Pod 进入 Termination 状态开始算起,即包含了执行 PreStop 的时间),则直接发送 KILL 信号强杀进程,不管进程是否处理完毕。

资料

  1. 优雅退出在Golang中的实现
  2. 优雅退出
  3. 优雅停机

最后

大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

我的个人博客为:shidawuhen.github.io/

往期文章回顾:

  1. 设计模式

  2. 招聘

  3. 思考

  4. 存储

  5. 算法系列

  6. 读书笔记

  7. 小工具

  8. 架构

  9. 网络

  10. Go语言

相关推荐
洗澡水加冰1 小时前
n8n搭建多阶段交互式工作流
后端·llm
陈随易1 小时前
Univer v0.8.0 发布,开源免费版 Google Sheets
前端·后端·程序员
六月的雨在掘金1 小时前
通义灵码 2.5 | 一个更懂开发者的 AI 编程助手
后端
朱龙凯2 小时前
MySQL那些事
后端
Re2752 小时前
剖析 MyBatis 延迟加载底层原理(1)
后端·面试
Victor3562 小时前
MySQL(63)如何进行数据库读写分离?
后端
Cache技术分享2 小时前
99. Java 继承(Inheritance)
前端·后端
M1A12 小时前
Python数据结构操作:全面解析与实践
后端·python
程序员蜗牛2 小时前
Controller层代码瘦身70%!5招打通任督二脉,效率飙升
后端
DemonAvenger2 小时前
Go sync.Pool 最佳实践:复用对象降低 GC 压力的技术文章
性能优化·架构·go