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:
xfy 2026-04-10 18:29:42 +08:00
parent 1ed56bd9c4
commit ada7357f7d
4 changed files with 49 additions and 4 deletions

View File

@ -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"]

View File

@ -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("服务器已停止")

View File

@ -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 提交任务到池。

View File

@ -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
}