diff --git a/Dockerfile b/Dockerfile index f53ca9e..05a3376 100644 --- a/Dockerfile +++ b/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"] \ No newline at end of file +# 使用 tini 作为 init 进程(PID 1) +ENTRYPOINT ["/tini", "--"] +CMD ["/lolly", "-c", "/etc/lolly/lolly.yaml"] \ No newline at end of file diff --git a/internal/app/app.go b/internal/app/app.go index 1db10b0..52d58b7 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -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("服务器已停止") diff --git a/internal/server/pool.go b/internal/server/pool.go index bb3420b..262bedc 100644 --- a/internal/server/pool.go +++ b/internal/server/pool.go @@ -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 提交任务到池。 diff --git a/internal/server/upgrade.go b/internal/server/upgrade.go index b01cf28..4c798a5 100644 --- a/internal/server/upgrade.go +++ b/internal/server/upgrade.go @@ -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 }