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语言

相关推荐
Java爱好狂.5 小时前
Java初学者如何一周速通SpringBoot!
java·spring boot·后端·spring·高并发·架构师·java面试
马良神笔7 小时前
2026 年Rag最近技术前沿总结
后端·python·django
程序猿DD7 小时前
同样10美元,OpenDesign和Command Code的DeepSeek V4.1 Flash额度到底有多少?
后端
高频因子挖掘机7 小时前
A股量化从日线升级到5分钟线:数据量增加后最先暴露哪些工程问题?
后端·github·api
高频因子挖掘机7 小时前
历史行情返回顺序变了,为什么按行号计算指标很危险?
后端·github·api
团子哒哒9 小时前
数组、切片和Map
后端
专注仿真9 小时前
ThinkPHP6 API接口开发实例:从零搭建RESTful风格接口
后端·php·restful
开心猴爷9 小时前
应对iOS审核4.3:可以使用 IpaGuard 来给属性名混淆的技术细节与平衡之道
后端·ios
1m555555559 小时前
RAG 工程实战(五):正确知识已经进入 Prompt,为什么大模型还是会答错?
后端·面试
易小尊9 小时前
我的模型在 65% 以上时准得可怕,在 50% 时像在掷硬币
后端