在微服务架构和云原生环境中,应用容器化部署已经成为标准操作。然而,在每次版本发布或扩缩容过程中,常常会出现短暂的请求报错,比如接口超时或连接重置。这往往不是代码逻辑本身的缺陷,而是应用没有正确处理容器的停止信号,导致服务被强行终止。理解并实现优雅停机,是保障线上服务高可用性的关键一环。

容器停机时究竟发生了什么?SIGTERM信号的传递机制
当我们在Kubernetes或Docker环境中停止一个容器时,系统并不会立刻杀死进程。以Kubernetes为例,当Pod被删除或滚动更新时,kubelet会向容器内的主进程(PID为1的进程)发送一个SIGTERM信号。同时,Pod的端点会从Service的Endpoints列表中被剔除,这意味着新的流量不会再被路由到这个即将停止的Pod上。
此时,容器内的应用进程会进入一个宽限期。默认情况下,Kubernetes的宽限期是30秒。在这个时间段内,应用应当完成正在处理的请求,释放数据库连接、关闭文件句柄并清理临时资源。如果30秒后进程仍未退出,kubelet会发送SIGKILL信号,这个信号无法被捕获或忽略,操作系统会直接强制结束该进程。
问题的核心在于,如果应用没有专门监听并处理SIGTERM信号,或者容器的主进程不是直接运行应用代码(例如通过shell脚本启动),SIGTERM信号就无法正确传递给业务代码。这会导致应用在宽限期内毫无反应,最终被SIGKILL强制杀死,造成正在执行的事务中断。
为什么强制退出会导致请求中断?未处理信号的代价
许多开发者可能认为,进程被杀掉后,客户端重试一下就行了。但在高并发场景下,强制退出的代价是巨大的。当一个处理耗时较长的HTTP请求正在进行时,如果容器突然被销毁,TCP连接会被重置,客户端会收到Connection Reset错误或502 Bad Gateway。如果此时刚好在执行数据库写入操作,还可能导致数据不一致或事务回滚不完整。
此外,未处理停机信号还会引发资源泄漏问题。应用与Redis、RabbitMQ等中间件建立的连接如果没有被正常关闭,会在服务端留下大量处于TIME_WAIT或CLOSE_WAIT状态的僵尸连接,直到服务端超时清理。这不仅浪费了系统资源,还可能影响后续实例的连接池容量。
更隐蔽的问题是线程池中的任务堆积。如果应用在退出时没有等待线程池执行完队列中的任务,那些尚未落盘的缓存数据、未发送的日志或者未完成的异步通知都会丢失,对业务造成不可逆的影响。
如何在代码中捕获并处理SIGTERM信号?多语言实战指南
要实现优雅停机,核心思路是让应用监听操作系统的信号,并在收到SIGTERM时触发一段平滑关闭的逻辑。这通常包括停止接收新请求、等待当前请求处理完成、释放资源等步骤。
对于Go语言来说,可以利用标准库中的os/signal包和context来实现。下面是一个典型的Go程序处理SIGTERM信号的代码示例,通过创建一个context,在收到信号时取消context,从而通知各个协程停止工作。
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second) // 模拟耗时请求
w.Write([]byte("请求处理完成"))
})
server := &http.Server{Addr: ":8080", Handler: mux}
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("服务启动失败: %v", err)
}
}()
// 监听系统信号
quit := make(chan os.Signal, 1)
// SIGTERM 信号对应 syscall.SIGTERM
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
<-quit // 阻塞等待信号到来
log.Println("收到停机信号,开始优雅停机...")
// 设置停机超时时间,给正在处理的请求留出时间
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Printf("停机超时或出错: %v", err)
}
log.Println("服务已安全退出")
}
对于Java/Spring Boot应用,处理起来同样方便。Spring Boot从特定版本开始内置了优雅停机的支持,只需要在配置文件中开启相关设置即可。当收到SIGTERM信号时,Spring Boot会关闭Web服务器并等待活动请求完成。
# application.yml 配置
server:
shutdown: graceful # 开启优雅停机
spring:
lifecycle:
timeout-per-shutdown-phase: 30s # 设置最大等待时间
如果是基于Node.js的应用,可以使用process模块监听信号。需要注意的是,Node.js默认在收到SIGTERM时如果未绑定监听器就会直接退出,因此绑定监听器是让服务平滑关闭的前提。在监听器内部,应当关闭HTTP服务器并断开数据库连接。
const http = require('http');
const server = http.createServer((req, res) => {
setTimeout(() => {
res.end('请求处理完成');
}, 2000); // 模拟耗时操作
});
server.listen(8080, () => {
console.log('服务已启动');
});
// 监听 SIGTERM 信号
process.on('SIGTERM', () => {
console.log('收到 SIGTERM 信号,准备关闭服务');
// 停止接收新连接
server.close(() => {
console.log('所有请求已处理完毕,服务退出');
process.exit(0);
});
});
除了业务代码层面的处理,容器编排层面的配置也至关重要。在Kubernetes中,务必合理设置Pod的terminationGracePeriodSeconds参数。如果应用需要更长时间来处理存量请求,比如需要等待长连接中的消息消费完毕,可以适当调大这个值,但要注意不能超过上游网关的超时时间,否则网关可能先于Pod返回超时错误。同时,使用preStop钩子也是一个常见的最佳实践,它可以在容器收到SIGTERM之前执行一段命令,比如先主动向注册中心注销自己,等待几秒让流量完全摘除,然后再让容器进入正常的停机流程。