原因
为什么需要优雅退出?
服务总是要升级的,升级意味老版本的服务退出,这时候如果还有请求未完成,理论上会导致一些错误产生,如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 信号强杀进程,不管进程是否处理完毕。
资料
最后
大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)
我的个人博客为:shidawuhen.github.io/
往期文章回顾: