Go 限流
Go 服务限流:令牌桶与漏桶、x/time/rate、HTTP 中间件、按键限流与分布式限流边界。
[!info] 关联笔记
Go 限流
这个概念为什么会出现
服务容量有限。突发流量、爬虫、误循环客户端会把 goroutine、连接池、下游 API 配额与延迟 SLA 全部打穿。限流在入口按速率拒绝或排队,把过载变成可预期的 429,而不是雪崩 500。
Go 官方扩展库 golang.org/x/time/rate 实现令牌桶,是进程内限流的默认选择。
[!abstract] 一句话理解 令牌桶以速率
r产令牌、容量b允许突发;请求取令牌失败则等待或直接 429。HTTP 中间件按全局限流或按 IP/用户键限流。
最小可运行示例
先把示例放进业务场景,再看代码:
场景:公开 API /work 全局限流,过载直接 429
对外提供 GET /work 做轻量计算。
爬虫或误循环客户端会把进程打满;入口用令牌桶限流:
- 稳定速率 5 req/s
- 桶容量 10 允许短突发
- 没令牌 → 立即 429 +
Retry-After,不排队拖垮延迟
依赖:golang.org/x/time/rate。
package main
import (
"encoding/json"
"net/http"
"time"
"golang.org/x/time/rate"
)
// rateLimitMiddleware:HTTP 入口全局限流中间件。
func rateLimitMiddleware(lim *rate.Limiter) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Allow:立刻判断;失败不阻塞,快速失败
if !lim.Allow() {
// 善意客户端可按 Retry-After 退避
w.Header().Set("Retry-After", "1")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests)
_ = json.NewEncoder(w).Encode(map[string]string{"error": "rate limit exceeded"})
return
}
next.ServeHTTP(w, r)
})
}
}
func main() {
// rate.Limit(5) = 每秒 5 个令牌;burst=10 允许瞬时尖峰
lim := rate.NewLimiter(rate.Limit(5), 10)
mux := http.NewServeMux()
// 业务接口:被限流保护
mux.HandleFunc("GET /work", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("ok"))
})
srv := &http.Server{
Addr: ":8080",
Handler: rateLimitMiddleware(lim)(mux),
ReadHeaderTimeout: 5 * time.Second,
}
_ = srv.ListenAndServe()
}
建议运行:
go run .
# 终端 2:快速打 20 次,观察后半段 429
for i in $(seq 1 20); do curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/work; done
期望:前若干次 200,突发耗尽后出现 429。
结合场景再看三个关注点
rate.Limit(5)是每秒 5 个。Allow()不阻塞;Wait(ctx)会阻塞到有令牌或 ctx 取消。- 全局限流保护进程;多租户要按键多个 Limiter。
核心概念与准确模型
令牌桶 vs 漏桶
| 令牌桶 | 漏桶 | |
|---|---|---|
| 突发 | 允许至桶容量 | 输出更平滑 |
| 空闲 | 积攒令牌 | 队列以恒定速率漏 |
| 典型用途 | API 配额 | 流量整形 |
x/time/rate = 令牌桶。rate.Every(100*time.Millisecond) 约 10/s。
API 选择
| 方法 | 行为 |
|---|---|
Allow / AllowN | 立即是/否 |
Wait / WaitN | 阻塞到可过或 ctx Done |
Reserve | 拿到 Reservation,可 Delay/Cancel |
HTTP 入口多用 Allow → 429;后台消费可用 Wait。
按键限流
type keyedLimiter struct {
mu sync.Mutex
m map[string]*rate.Limiter
r rate.Limit
burst int
}
func (k *keyedLimiter) get(key string) *rate.Limiter {
k.mu.Lock()
defer k.mu.Unlock()
if lim, ok := k.m[key]; ok {
return lim
}
lim := rate.NewLimiter(k.r, k.burst)
k.m[key] = lim
return lim
}
生产还需淘汰久未使用的 key,否则 map 泄漏。
并发信号量 vs 速率
- 限流:单位时间事件数
- 信号量(有界 channel):同时进行中数量
二者常组合:既限 QPS 又限 in-flight。
分布式限流
单进程 limiter 不能跨实例共享计数。多副本需 Redis/网关,或接受“每实例各自限额”。
设计动机
- 保护尾部延迟:过载时快速失败优于全体变慢。
- 公平性:按用户/IP 隔离吵闹邻居。
- 合同配额:对外 API 的计费与 SLA。
边界情况与反直觉行为
1. 突发耗尽后的行为
burst=10, rate=5/s:可瞬间 10 个,然后约每 200ms 一个。客户端重试风暴会持续 429——配合指数退避。
2. Wait 与超时
Wait 必须用 r.Context(),避免客户端已断开仍空等。
3. 限流位置
只限业务 handler、不限 /healthz,避免探针被误杀。
4. 与鉴权顺序
先限流可挡无 token 洪水;先鉴权可按用户精确配额。常见:边缘粗限流 + 鉴权后细限流。
常见误区
[!warning] 常见误区:把
rate.Every与Limit搞混
二者都可表达每秒速率,先算清单位。
[!warning] 常见误区:无限增长的 per-IP map
必须 TTL 清理或 LRU。
[!warning] 常见误区:429 不带 Retry-After
善意客户端无法退避。
[!warning] 常见误区:只限入不限出
仍可能打爆依赖。
与相邻概念对比
| 概念 | 差异 |
|---|---|
| 熔断 | 看错误率/延迟开路;限流看速率 |
| 负载均衡 | 分散流量;不替代入口配额 |
| 信号量 | 限制并发数;限流限制速率 |
| 缓存 | 减负载;限流在过载时丢弃 |
工程实践
- 分层限额:IP 粗、用户细、全局兜底。
- 可观测:429 计数、按路由限额命中。
- 配置化:速率来自配置。
- 测试:注意 flaky;可调大 burst 测逻辑分支。
- 中间件链:限流 → 认证 → 校验 → handler(按威胁模型调整)。
可验证实验
实验 1:突发
burst=2, rate=1,连发 5 次,观察前 2 成功后 429。
实验 2:Wait
用 Wait 写小客户端,确认总耗时符合速率。
实验 3:健康检查豁免
对 /healthz 不套中间件,压测业务路径时探针仍 200。
实验 4:按键隔离
用户 A 打满不影响用户 B。
本节总结
- 本质:用算法把无限到达变成有界处理速率。
- 关键规则:令牌桶参数、
AllowvsWait、按键与分布式边界。 - 最易错:限死探针、map 泄漏、多实例误以为全局限额。
- 下一步:中间件 组装;缓存 降低真实负载。
自测题
概念题
rate.NewLimiter(10, 20)的长期速率与突发上限?- HTTP API 为何常用
Allow而非Wait? - 三副本服务每实例 100 rps,理论集群上限?
代码推理题
中间件里 lim.Wait(r.Context()) 成功后 handler 很慢——限流保护的是到达速率还是并发?
工程思考题
登录接口如何防撞库?仅全局限流够吗?
参考答案
展开
- 长期 10/s,突发 20。
- 连接与 goroutine 有限,与其占着等令牌不如 429 让客户端退避。
- 约 300 rps(若无共享计数)。
代码题:主要是到达取令牌的速率;并发仍可能高,需信号量/超时另控。
工程题:按 IP/账号键限流 + 验证码/锁定 + 监控;全局限流无法挡住单 IP 慢速扫号。
延伸阅读与资料来源
| 资料 | 类型 | 支撑 |
|---|---|---|
| golang.org/x/time/rate | 库文档 | Limiter API |
| net/http | 标准库 | 中间件与 429 |
| RFC 6585 — 429 | 标准 | Too Many Requests |
| go-http-middleware · go-context | 本库 | 组合与取消 |