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 小时前
Java 与 MySQL 性能优化:Java 实现百万数据分批次插入的最佳实践
后端·mysql·性能优化
麦兜*2 小时前
Spring Boot启动优化7板斧(延迟初始化、组件扫描精准打击、JVM参数调优):砍掉70%启动时间的魔鬼实践
java·jvm·spring boot·后端·spring·spring cloud·系统架构
大只鹅2 小时前
解决 Spring Boot 对 Elasticsearch 字段没有小驼峰映射的问题
spring boot·后端·elasticsearch
ai小鬼头2 小时前
AIStarter如何快速部署Stable Diffusion?**新手也能轻松上手的AI绘图
前端·后端·github
IT_10243 小时前
Spring Boot项目开发实战销售管理系统——数据库设计!
java·开发语言·数据库·spring boot·后端·oracle
bobz9653 小时前
动态规划
后端
stark张宇3 小时前
VMware 虚拟机装 Linux Centos 7.9 保姆级教程(附资源包)
linux·后端
亚力山大抵4 小时前
实验六-使用PyMySQL数据存储的Flask登录系统-实验七-集成Flask-SocketIO的实时通信系统
后端·python·flask
超级小忍4 小时前
Spring Boot 中常用的工具类库及其使用示例(完整版)
spring boot·后端
CHENWENFEIc5 小时前
SpringBoot论坛系统安全测试实战报告
spring boot·后端·程序人生·spring·系统安全·安全测试