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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

程序员麻辣烫

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

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

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

打赏作者

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

抵扣说明:

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

余额充值