Go 服务背压设计拒绝请求比拖垮全链路更负责一、服务不能无限接请求Go 后端很容易写出高并发服务一个请求一个 goroutine看起来吞吐很高。但下游数据库、模型服务、队列和第三方接口都有容量上限。入口无限接内部排队无限长最后用户等到超时服务也被拖垮。背压的本质是在系统还有理智时说不。我经历过一次典型事故数据库连接池设置了 50 个连接但业务高峰期网关没做限流每秒进来 300 个请求。每个请求都要查数据库连接池瞬间满后续请求都在等连接。等连接的请求把 goroutine 占满了内存飙升最终 OOM killed。如果入口做了背压在连接池快满时就对额外请求返回 429数据库和业务服务都不会挂。那次之后我们给所有入口都加了 inflight 限制宁可拒绝 5% 的请求也不让 100% 的请求等超时。二、先找容量瓶颈flowchart TD A[入口请求] -- B[HTTP Handler] B -- C[业务队列] C -- D[下游服务] D -- E[响应] C -- F{队列是否过载} F --|是| G[拒绝或降级]背压要放在瓶颈前面。数据库连接池只有 50 个连接入口却允许 5000 个请求进入业务队列这不是高并发是延迟炸弹。backpressure_policy: max_inflight_requests: 800 queue_size: 200 queue_timeout_ms: 100 reject_status: 429队列长度和等待时间都要限制。只限制队列长度不够队列没满但等待过久也应该拒绝。用户等 5 秒拿到结果和等 5 秒收到 429后者体验更好因为用户可以立刻重试或切换操作。瓶颈分析不要靠猜。用压测找到下游每次调用平均耗时然后反推入口需要的并发度。比如下游单次调用 50ms要支撑 1000 QPS大约需要 50 个并发。给 2-3 倍缓冲入口 inflight 设置在 100-150 比较安全。超过这个数就应该背压。三、用 channel 控制入口type Limiter struct { sem chan struct{} } func (l *Limiter) TryAcquire() bool { select { case l.sem - struct{}{}: return true default: return false } } func (l *Limiter) Release() { -l.sem }这种 semaphore 方式简单直接适合限制某段业务逻辑的并发。拿不到名额就快速失败不要让请求一直挂着。但 channel semaphore 只是最基础的实现。生产环境里你还需要区分不同优先级请求的队列高优先级单独分配名额、可以动态调整的并发上限流量突增时适当放宽下游异常时收紧、以及本地限制加全局限流的组合单机 inflight 全局限流。实际项目里还要按接口或租户区分。低成本查询接口和高成本 AI 生成接口不能共用一个限额否则重请求会挤掉轻请求。比如查询接口被 AI 生成接口拖慢导致管理系统卡顿这比背压本身更影响业务。一个常见的错误是全局背压设置了 1000 inflight但没有给轻量查询保留最低保障。结果 AI 生成的 800 个请求占满了名额查询请求全部排队。应该给查询这类 P0 接口预留最少并发比如总是保留 100 个名额给查询AI 生成最多用 700。四、拒绝也要可观测背压不是静默失败。每次拒绝都要记录原因、当前 inflight、队列长度、接口和租户。否则业务方只看到 429不知道是容量不足还是限流策略太保守。backpressure_metrics: inflight_requests: true queue_wait_ms: true reject_count_by_route: true downstream_latency_ms: true背压触发时还可以返回Retry-After。这比让客户端盲目重试更友好。客户端重试也要带退避否则拒绝会被重试风暴放大。降级也是一种背压。比如 AI 接口可以在高负载时关闭重排、减少 topK、降低最大输出 token。不是每次过载都只能 429但降级后的结果要标记清楚让上层知道这不是完整质量的回答。一个有意思的细节背压发生后不要立刻把所有拒绝请求的 429 都堆到监控大盘上那样会触发连环告警。可以在短时间内对同接口、同原因做聚合减少告警风暴。最后压测要验证背压。把下游延迟调高看入口是否及时拒绝服务是否保持稳定。只测正常容量下的 QPS看不出背压设计好不好。背压参数也要能动态调整。模型服务、数据库和第三方接口的容量会随时间变化固定阈值容易在高峰期过松、低峰期过紧。可以把阈值放进配置中心但必须配合灰度和回滚。dynamic_backpressure: config_hot_reload: true min_limit: 100 max_limit: 1200 change_audit: true还要防止多个服务层层排队。入口排一次、业务队列排一次、下游 SDK 再排一次用户看到的就是长时间无响应。链路里最好只保留必要队列并让超时预算向下传递。五、总结Go 服务背压要限制并发、队列、等待时间和下游容量并把拒绝原因暴露出来。拒绝请求不是不负责。比起把全链路拖垮及时说不才是生产系统该有的边界感。能快速拒绝并让用户知道重试时间的系统比默默排队 30 秒然后超时的系统靠谱得多。