feat(docker,server): 改进容器信号处理和优雅关闭
- 使用 tini 作为 PID 1 init 进程,处理僵尸进程回收和信号转发 - 多次 SIGINT (3次) 支持强制退出 - GoroutinePool.Stop() 添加 5s 超时等待,防止无限阻塞 - GracefulUpgrade 启动 goroutine 等待子进程,避免僵尸进程 - 关闭父进程文件描述符副本,防止泄漏 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1ed56bd9c4
commit
ada7357f7d
12
Dockerfile
12
Dockerfile
@ -32,6 +32,10 @@ RUN CGO_ENABLED=0 GOOS=linux go build \
|
||||
-o /build/lolly \
|
||||
main.go
|
||||
|
||||
# ---- Tini stage ----
|
||||
FROM alpine:3.19 AS tini-stage
|
||||
RUN apk add --no-cache tini-static
|
||||
|
||||
# ---- Runtime stage ----
|
||||
FROM scratch
|
||||
|
||||
@ -41,11 +45,15 @@ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
# 二进制文件
|
||||
COPY --from=builder /build/lolly /lolly
|
||||
|
||||
# tini 静态版本(处理 PID 1 僵尸进程回收和信号转发)
|
||||
COPY --from=tini-stage /sbin/tini-static /tini
|
||||
|
||||
# 优雅关闭:SIGQUIT 触发 30s graceful stop
|
||||
STOPSIGNAL SIGQUIT
|
||||
|
||||
# HTTP/1.1, HTTP/2, HTTP/3 (QUIC)
|
||||
EXPOSE 8080/tcp 443/tcp 443/udp
|
||||
|
||||
ENTRYPOINT ["/lolly"]
|
||||
CMD ["-c", "/etc/lolly/lolly.yaml"]
|
||||
# 使用 tini 作为 init 进程(PID 1)
|
||||
ENTRYPOINT ["/tini", "--"]
|
||||
CMD ["/lolly", "-c", "/etc/lolly/lolly.yaml"]
|
||||
@ -328,6 +328,9 @@ func (a *App) Run() int {
|
||||
}
|
||||
}()
|
||||
|
||||
// SIGINT 计数器,用于强制退出
|
||||
sigintCount := 0
|
||||
|
||||
// 等待信号或启动错误
|
||||
for {
|
||||
select {
|
||||
@ -335,6 +338,14 @@ func (a *App) Run() int {
|
||||
a.logger.Error().Err(err).Msg("服务器启动失败")
|
||||
return 1
|
||||
case sig := <-sigChan:
|
||||
// 多次 SIGINT 强制退出
|
||||
if sig == syscall.SIGINT {
|
||||
sigintCount++
|
||||
if sigintCount >= 3 {
|
||||
a.logger.LogShutdown("收到 3 次 SIGINT,强制退出")
|
||||
return 1
|
||||
}
|
||||
}
|
||||
if !a.handleSignal(sig) {
|
||||
// 返回 false 表示退出
|
||||
a.logger.LogShutdown("服务器已停止")
|
||||
|
||||
@ -126,12 +126,26 @@ func (p *GoroutinePool) Start() {
|
||||
|
||||
// Stop 停止 Goroutine 池。
|
||||
//
|
||||
// 取消所有 worker,等待它们退出完成。
|
||||
// 取消所有 worker,等待它们退出完成(最多等待 5 秒)。
|
||||
// 调用后池将不再接受新任务。
|
||||
func (p *GoroutinePool) Stop() {
|
||||
p.running.Store(false)
|
||||
p.cancel()
|
||||
p.wg.Wait()
|
||||
|
||||
// 使用超时等待,防止 wg.Wait() 无限阻塞
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
p.wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// 所有 worker 正常退出
|
||||
case <-time.After(5 * time.Second):
|
||||
// 超时,强制退出(worker 会在收到 ctx.Done() 后自行退出)
|
||||
// 不再等待,直接返回
|
||||
}
|
||||
}
|
||||
|
||||
// Submit 提交任务到池。
|
||||
|
||||
@ -223,6 +223,18 @@ func (u *UpgradeManager) GracefulUpgrade(newBinary string) error {
|
||||
_ = os.WriteFile(u.pidFile, []byte(fmt.Sprintf("%d", newPid)), 0644)
|
||||
}
|
||||
|
||||
// 启动 goroutine 等待子进程结束,避免产生僵尸进程
|
||||
// cmd.Wait() 会回收子进程资源,确保不会产生 defunct 进程
|
||||
go func() {
|
||||
_ = cmd.Wait()
|
||||
}()
|
||||
|
||||
// 关闭父进程中的文件描述符副本(子进程已继承)
|
||||
// 避免文件描述符泄漏
|
||||
for _, file := range files {
|
||||
_ = file.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user