Go 限流

Go 服务限流:令牌桶与漏桶、x/time/rate、HTTP 中间件、按键限流与分布式限流边界。

#type / concept #status / growing #tech / dev / backend #resource / go

[!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

结合场景再看三个关注点

  1. rate.Limit(5)每秒 5 个
  2. Allow() 不阻塞;Wait(ctx) 会阻塞到有令牌或 ctx 取消。
  3. 全局限流保护进程;多租户要按键多个 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/网关,或接受“每实例各自限额”。

设计动机

  1. 保护尾部延迟:过载时快速失败优于全体变慢。
  2. 公平性:按用户/IP 隔离吵闹邻居。
  3. 合同配额:对外 API 的计费与 SLA。

边界情况与反直觉行为

1. 突发耗尽后的行为

burst=10, rate=5/s:可瞬间 10 个,然后约每 200ms 一个。客户端重试风暴会持续 429——配合指数退避。

2. Wait 与超时

Wait 必须用 r.Context(),避免客户端已断开仍空等。

3. 限流位置

只限业务 handler、不限 /healthz,避免探针被误杀。

4. 与鉴权顺序

先限流可挡无 token 洪水;先鉴权可按用户精确配额。常见:边缘粗限流 + 鉴权后细限流。

常见误区

[!warning] 常见误区:把 rate.EveryLimit 搞混
二者都可表达每秒速率,先算清单位。

[!warning] 常见误区:无限增长的 per-IP map
必须 TTL 清理或 LRU。

[!warning] 常见误区:429 不带 Retry-After
善意客户端无法退避。

[!warning] 常见误区:只限入不限出
仍可能打爆依赖。

与相邻概念对比

概念差异
熔断看错误率/延迟开路;限流看速率
负载均衡分散流量;不替代入口配额
信号量限制并发数;限流限制速率
缓存减负载;限流在过载时丢弃

工程实践

  1. 分层限额:IP 粗、用户细、全局兜底。
  2. 可观测:429 计数、按路由限额命中。
  3. 配置化:速率来自配置。
  4. 测试:注意 flaky;可调大 burst 测逻辑分支。
  5. 中间件链:限流 → 认证 → 校验 → handler(按威胁模型调整)。

可验证实验

实验 1:突发

burst=2, rate=1,连发 5 次,观察前 2 成功后 429。

实验 2:Wait

Wait 写小客户端,确认总耗时符合速率。

实验 3:健康检查豁免

/healthz 不套中间件,压测业务路径时探针仍 200。

实验 4:按键隔离

用户 A 打满不影响用户 B。

本节总结

  • 本质:用算法把无限到达变成有界处理速率。
  • 关键规则:令牌桶参数、Allow vs Wait、按键与分布式边界。
  • 最易错:限死探针、map 泄漏、多实例误以为全局限额。
  • 下一步中间件 组装;缓存 降低真实负载。

自测题

概念题

  1. rate.NewLimiter(10, 20) 的长期速率与突发上限?
  2. HTTP API 为何常用 Allow 而非 Wait
  3. 三副本服务每实例 100 rps,理论集群上限?

代码推理题

中间件里 lim.Wait(r.Context()) 成功后 handler 很慢——限流保护的是到达速率还是并发?

工程思考题

登录接口如何防撞库?仅全局限流够吗?

参考答案

展开
  1. 长期 10/s,突发 20。
  2. 连接与 goroutine 有限,与其占着等令牌不如 429 让客户端退避。
  3. 约 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本库组合与取消

创建于 2026/6/25 更新于 2026/7/15