feat(ops): 健康检查端点,并把上传超时与服务端读超时对齐
1) /healthz 与 /readyz
改动前站内唯一的「探活」手段是 curl /,而首页只渲染模板、不查数据库:
DB 挂了首页照样 200(漏报),Redis 挂了首页又可能 500(误报)。
监控、CDN 源站健康检查、滚动重启的摘流判定都没有可靠依据。
→ /healthz 永远 200,刻意不碰任何依赖:进程活着 ≠ 能对外服务,
把 DB 检查塞进存活探针会导致数据库抖动时进程被反复重启,对恢复毫无帮助;
→ /readyz 查 DB(2s 超时),失败 503;响应体只回组件状态,
绝不回错误详情 —— 这是公开端点,DB 报错常带库名/主机名/账号。
实测:停掉 PostgreSQL 后 /healthz 仍 200、/readyz 503,恢复后 200。
2) ReadTimeout 30s → 120s
站内允许单图 8MB、请求体上限 12MB。30s 传完 8MB 需要 ≥273 KB/s 上行,
而手机 4G 上行常在 1–2 Mbps(125–250 KB/s)—— 一张 8MB 图必然超时,
且用户已写好的正文会一起丢(POST 失败不回填)。120s 对应约 68 KB/s,
偏远地区 3G 也能过。
放宽它不会让 slowloris 重新可行:那类攻击卡在「发不完 header」,
由 10s 的 ReadHeaderTimeout 拦住,ReadTimeout 只影响读完 header 之后传 body 的时长。
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
// Copyright 2026 tamakyi. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package route
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/tamakyi/TamaBox/internal/context"
|
||||
"github.com/tamakyi/TamaBox/internal/db"
|
||||
)
|
||||
|
||||
// readyzTimeout 就绪探针等待数据库的最长时间。
|
||||
// 刻意短:探针本身不该成为慢请求,超时就视为「未就绪」并由上层重试。
|
||||
const readyzTimeout = 2 * time.Second
|
||||
|
||||
// Healthz 存活探针:进程还能处理请求就返回 200。
|
||||
//
|
||||
// 刻意不碰任何外部依赖。「进程还活着」和「能对外服务」是两个问题:
|
||||
// 前者挂了才该重启,后者挂了该摘流 —— 后者看 Readyz。
|
||||
// 把 DB 检查塞进存活探针是常见反模式:数据库抖动会导致进程被反复重启,
|
||||
// 而重启对恢复数据库毫无帮助,只会让在途请求全部失败。
|
||||
func Healthz(ctx context.Context) {
|
||||
w := ctx.ResponseWriter()
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
// Readyz 就绪探针:数据库可用才返回 200,否则 503。
|
||||
//
|
||||
// 为什么需要这个端点:改动前站内唯一的「探活」手段是 curl /,而首页只渲染模板、
|
||||
// 不查数据库 —— DB 挂了首页照样 200(漏报),Redis 挂了首页又可能 500(误报)。
|
||||
// 于是监控、CDN 源站健康检查、滚动重启的摘流判定都没有可靠依据。
|
||||
//
|
||||
// 响应体只回组件状态,绝不回错误详情:这是公开端点,数据库报错里常带库名、
|
||||
// 主机名甚至账号信息。错误详情只写日志。
|
||||
func Readyz(ctx context.Context) {
|
||||
reqCtx, cancel := stdctx.WithTimeout(ctx.Request().Context(), readyzTimeout)
|
||||
defer cancel()
|
||||
|
||||
w := ctx.ResponseWriter()
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
|
||||
if err := db.Ping(reqCtx); err != nil {
|
||||
logrus.WithContext(reqCtx).WithError(err).Warn("就绪探针:数据库不可用")
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
_, _ = w.Write([]byte(`{"status":"unavailable","database":"down"}`))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"status":"ok","database":"up"}`))
|
||||
}
|
||||
Reference in New Issue
Block a user