中间件中需用gopsutil定期采样cpu负载并缓存,限流逻辑置于middlewarefunc开头且在c.next()前;推荐令牌桶而非计数器,并注意容器环境、健康接口白名单、retry-after头及分布式场景。

中间件里怎么拿到当前系统负载
Go 标准库不直接暴露 CPU/内存使用率,echo.Context 本身也不带负载数据。你得自己采集——常用方式是调用 /proc/stat(Linux)或用 gopsutil 这类第三方包。别在每次请求里都去读 /proc/stat,开销大还不准;建议用 goroutine 定期采样(比如每 2 秒),把结果缓存在全局变量或原子值里,中间件只读不写。
示例关键点:
- 用
github.com/shirou/gopsutil/v3/cpu的CPUPercent获取最近 1 秒平均使用率 - 采样 goroutine 启动后,首次调用可能返回
0.0,需等至少一次完整周期才有效 - 避免在中间件里调
runtime.NumGoroutine()做唯一判断——它反映的是协程数,不是真实负载,高并发场景下容易误判
限流逻辑该放在中间件的哪个位置
必须放在 echo.MiddlewareFunc 的函数体开头,且在 c.Next() 之前。否则请求已经进到业务 handler 里了,再拦没意义。
典型结构是:
func LoadLimitMiddleware(threshold float64) echo.MiddlewareFunc {
return func(next echo.Handler) echo.Handler {
return echo.HandlerFunc(func(c echo.Context) error {
// ← 在这里检查负载、判断是否超阈值
if currentLoad > threshold {
return c.JSON(http.StatusTooManyRequests, map[string]string{
"error": "system overloaded",
})
}
// ← 只有没超限才继续
return next.ServeHTTP(c.Response(), c.Request())
})
}
}
注意:next.ServeHTTP 是底层调用,c.Next() 是封装后的语义糖,二者效果一致,但前者更明确控制流。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
为什么用令牌桶比简单计数器更合适
单纯统计「过去 1 秒内请求数」会放大毛刺影响:比如某秒前 10ms 突然来 100 个请求,后面 990ms 空闲,但计数器已满,新请求全被拒。而令牌桶允许短时突发,更贴合真实流量特征。
推荐组合方案:
- 用
golang.org/x/time/rate.Limiter实现令牌桶,每秒补充 N 个 token - 把 limiter 实例挂到
echo.Echo的Map字段里(e.Map["limiter"] = limiter),避免中间件闭包捕获导致内存泄漏 - 不要为每个请求 new 一个 limiter——创建开销大,且无法跨请求共享状态
- 阈值建议设为 CPU 使用率 75% + 令牌桶速率 80% QPS 上限,双保险
容易被忽略的边界情况
负载检测和限流不是一劳永逸的事。几个硬伤点常被跳过:
- 容器环境(如 Kubernetes)下,
/proc/stat返回的是宿主机数据,不是容器 cgroup 限制值;得改用github.com/docker/go-units解析/sys/fs/cgroup/cpu/cpu.stat - 健康检查接口(如
/health)不该被限流,否则探针失败触发误扩缩容;加个路径白名单判断:if c.Path() == "/health" { c.Next(); return } - 限流响应里没带
Retry-Afterheader,前端重试策略失控;记得手动设置:c.Response().Header().Set("Retry-After", "1") - 多个实例部署时,单机限流失效——这时得上 Redis + Lua 做分布式令牌桶,但代价是延迟增加 2–5ms,得权衡
最麻烦的其实是负载指标滞后:CPU 上升到触发限流,中间有 1–2 秒延迟。真要保稳,得结合连接数、pending request 队列长度这些更实时的信号一起看。










