登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go 服务器优雅退出为什么仍有请求被截断

来源:17golang原创

时间:2026-10-23 03:29:01 451浏览 收藏

Go 服务器调用 Shutdown 后仍有请求被截断,首先要查的是进程有没有等它完成。Shutdown 关闭监听后,Serve 或 ListenAndServe 会很快返回 ErrServerClosed;这只表示服务不再接收新连接,不表示存量请求已经结束。主协程此时直接返回,排空中的请求仍会随进程退出而中断。

官方地址:https://pkg.go.dev/net/http

把“停止接入”和“排空完成”分开:主协程等待 Shutdown 返回,排空使用独立且尚未取消的 Context,业务依赖活到请求结束。超时强制关闭必须记录为未完成排空,不能算优雅退出成功。

请求被截断时,先还原退出顺序

设想一个三秒后才返回结果的下载准备接口,在它运行期间发布新进程。下面是为解释问题构造的场景,不是线上事故复盘或实测记录:请求进入处理器;进程收到退出信号;后台协程调用 Shutdown;监听关闭,主协程的 ListenAndServe 返回;主协程退出,但处理器尚未写回结果。

这里即使日志写了“received signal”,也只能证明接收到了信号。即使日志写了“server closed”,也只能证明监听循环停止。要判断排空是否完成,需要记录 Shutdown 的返回值,以及它与主函数退出之间的先后。不要把客户端 EOF 全部归因于网络或负载均衡。

哪些操作会让排空提前失效?

第一种是用收到信号时已经取消的 Context 再创建 Shutdown 的超时子上下文。子上下文不会重新获得十秒寿命,可能立即取消等待。第二种是把信号 Context 直接用作所有请求的 BaseContext:信号一到,请求中的数据库查询或远端调用先被取消,Shutdown 也不能让这些业务操作自动恢复。

第三种是先关闭数据库连接池、消息客户端等业务依赖,再等待 HTTP 请求完成。处理器虽然还活着,完成响应所需的资源却已经被移走。第四种是直接使用 Server.Close,它是立即关闭连接的动作,不等于等待存量请求自然结束。

信号上下文、排空上下文与请求依赖的静态边界
图1:退出职责结构图。信号通知、排空等待和请求依赖应分别管理,不是实际执行时间线。

还有超时以外的边界:WebSocket 等被接管的连接不在 Shutdown 自动等待范围内;请求处理器自己启动的后台任务,也不能只靠 HTTP 活跃连接数量判断已经完成。这些资源需要另外通知、等待和停止。

让主协程持有排空的控制权

下面的原创最小程序只演示本机普通 HTTP 请求,适用于 Unix 信号场景。它没有数据库、流量摘除接口或 WebSocket 管理器,不能当作完整生产发布框架。代码未在本文制作过程中运行。

package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "net"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func slow(w http.ResponseWriter, r *http.Request) {
    log.Print("handler started")
    timer := time.NewTimer(3 * time.Second)
    defer timer.Stop()
    select {
    case 

这段代码的重要取舍不是把时间写成十秒,而是让 sigCtx 只负责通知,让 drainCtx 从独立根创建,并在主协程同步等待 Shutdown。若排空期限到达,示例明确调用 Close 并返回错误;这一分支接受可能截断的代价,不把它伪装成请求全都成功。

Shutdown 返回超时,不意味着它自己已经强制关闭所有活跃连接;示例的强制动作来自后续 Close。Close 也不保证所有自建后台任务随之结束,因此错误分支仍需要生产应用自己的协作取消和任务回收设计。

主协程、HTTP 服务与排空结果之间的依赖关系
图2:主协程等待结构图。Serve 的返回通知与 Shutdown 的完成结果不是同一信号,图片不是运行证明。

怎样用一条在途请求观察差别?

把程序保存为 main.go,先构建再直接运行二进制,避免把信号发送给 go run 的外层工具进程。以下均为供读者手动执行的步骤,文章没有启动服务或发送信号。

# 终端一:构建并前台运行,日志会给出实际服务进程 PID
go build -o drain-demo main.go
./drain-demo
# 终端二:保持请求前台运行,在服务日志出现 handler started 后触发退出
curl -v --max-time 20 http://127.0.0.1:8089/slow
# 终端三:只填入终端一日志明确输出的服务 PID,不使用模糊进程匹配
kill -TERM 服务PID

在三秒处理器刚开始、十秒排空预算仍足够且客户端未断开的条件下,预期请求能收到 finished,然后服务记录 shutdown complete 并退出。这是预期行为,不是实测数据。若读者把排空预算改得比剩余处理时间更短,预期出现未完成排空日志和强制关闭;客户端具体报错由响应是否已经写出及链路状态决定,不应硬编码为某一种错误文本。

如果没有看到 handler started 就发送信号,测到的可能是新请求被拒绝,不能证明存量请求截断。若客户端本身先达到超时,也要与服务端主动停止区分。生产中应结合请求标识、处理器结束原因和响应写入错误,而不是仅凭一条关闭日志下结论。

应用等待了,为什么发布时仍可能失败?

Go 的排空等待无法延长外部进程管理器的强制终止期限,也不能控制负载均衡何时停止向实例派发流量。交付时要把停止新流量所需时间、存量请求预算、资源收尾时间和安全余量放进同一份退出预算,外部强制期限必须容纳整体过程。这里没有通用的固定秒数,应该根据真实请求时长及部署配置决定。

看到的现象需要对齐的证据修正方向
服务关闭日志后立即断流主函数退出是否早于 Shutdown 返回等待排空完成信号
刚收到信号查询就取消请求父上下文是否绑定信号取消区分通知与存量请求生命周期
排空期间依赖报已关闭数据库等资源关闭时机普通在途请求完成后再收尾依赖
应用还在等待却被终止外部终止期限与总预算协调应用和部署侧策略

对于永不自然结束的流式接口,不能无限增大 Shutdown 时间来“保证成功”。需要协议级结束通知、客户端重连及消息重放等机制;已发出的业务请求是否可重试,还取决于幂等设计,不能因为连接断开就假定服务端没有执行。

RegisterOnShutdown 回调里关闭依赖就够了吗?

不够。回调可以用于通知,但不应当作“存量请求已经完成”的标志,更不能据此过早关闭仍被处理器使用的资源。长期连接的完成仍应由专门机制等待。

收到第二次信号要怎么处理?

可以设计明确的快速退出策略,但要把强制停止和正常排空区分记录。示例在排空期间保留信号处理,未实现第二次信号立即强杀;不要从示例推断它具备这项能力。

参考:Server.Shutdown;Server.Close;signal.NotifyContext。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>