Revel框架健康检查不能用普通Controller,因其强制执行JWT验证、日志等拦截器链,导致探针返回401/500;必须通过revel.Router.Add注册裸handler,手动控制状态码与纯文本响应,并用context.WithTimeout做依赖检查。

Revel 框架本身不提供内置健康检查端点,也不能靠 app.Init() 或拦截器自动注入——必须手动注册一个轻量、绕过中间件、带超时控制的裸 handler,否则会被日志、认证、CORS 等拦截,导致探针返回 401/403/500。
为什么不能用普通 Controller 写健康检查
Revel 的 Controller 默认走完整拦截器链(INTERCEPTOR_ROUTER → INTERCEPTOR_FILTER → INTERCEPTOR_ACTION),而健康检查接口必须跳过所有业务级中间件:
- JWT 验证拦截器会拒绝无 token 的
/health请求,返回 401 - 日志拦截器在高频率探测下会刷爆磁盘 I/O
- 请求上下文可能被提前取消或污染,导致
c.Params或c.Render()panic - Controller 的
Render()会强制写入text/htmlContent-Type,但 Kubernetes 探针期望text/plain
如何注册一个真正可用的 /health 路由
在 app/init.go 的 init() 函数末尾,直接调用 Revel 的 revel.BuiltinHTTPHandler 注册裸 HTTP handler,绕过所有框架层:
func init() {
// ... 其他初始化代码
revel.InterceptFunc(func(c *revel.Controller, fc revel.FilterConfig) revel.Result {
if c.Request.URL.Path == "/health" || c.Request.URL.Path == "/readyz" {
return nil // 不执行任何拦截器
}
return nil
}, revel.BEFORE)
// 注册裸 handler,不经过任何 Revel 中间件
revel.Router.Add(&revel.Route{
Method: "GET",
Pattern: "/health",
Action: "Health.Check",
})
// 在 controllers/health.go 中定义
// func (c Health) Check() revel.Result { ... }
}
注意:revel.Router.Add() 是唯一能确保路由不被拦截器干扰的方式;别用 revel.InterceptFunc 去“拦截后放行”,那只是掩耳盗铃。
Health.Check() 方法里该检查什么、怎么返回
Revel 的 Controller 对象在裸路由下仍可使用,但必须放弃 Render() 和模板,手动控制状态码与响应体:
- 只检查进程存活:能接收请求、goroutine 未 panic、主线程未卡死(
runtime.NumGoroutine() 可作粗略参考) - 依赖检查(如 DB/Redis)必须用
context.WithTimeout(c.Request.Context(), 2*time.Second)包裹,否则 probe 超时(K8s 默认 1s)会误判 - 失败时调用
c.Response.Status = 503,并用c.Response.Out.Write([]byte("service unavailable"))直接写响应,避免c.RenderText()触发额外 header 或模板逻辑 - 不要返回 JSON —— Revel 的
c.RenderJSON()会加Content-Type: application/json,但很多负载均衡器(如 Nginx upstream)只认纯文本ok或down
如何安全地做依赖探测而不拖慢主线程
Revel 没有原生 errgroup 或 context-aware DB ping 支持,所以得自己封装:
- 数据库检查别用
db.Ping(),改用db.QueryRowContext(ctx, "SELECT 1").Scan(&dummy),并设ctx, cancel := context.WithTimeout(c.Request.Context(), 1500*time.Millisecond) - Redis 检查用
redisClient.Ping(ctx).Result(),不是Ping().Result() - 所有依赖检查必须并发启动(用
sync.WaitGroup+ goroutine),但结果聚合要在主线程完成;别让 goroutine 持有*revel.Controller引用,防止内存泄漏 - 缓存探测结果(比如 5 秒 TTL),避免每秒数十次探测都触发真实连接 —— Revel 没有全局 cache,可用
sync.Map存map[string]struct{ ok bool; ts time.Time }
最易被忽略的是:Revel 的 c.Request.Context() 在裸路由下默认不带 cancel,必须显式传入超时 context;否则依赖挂起,整个 probe 就卡死,K8s 会反复重启容器。


















