Go的优雅退出

原因

为什么需要优雅退出?

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

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

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

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

终止进程

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

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

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

在这里插入图片描述

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

在这里插入图片描述

捕获信号

我们先来看代码:

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. 优雅停机

最后

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

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

往期文章回顾:

  1. 设计模式

  2. 招聘

  3. 思考

  4. 存储

  5. 算法系列

  6. 读书笔记

  7. 小工具

  8. 架构

  9. 网络

  10. Go语言

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员麻辣烫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值