fix(ops): 访问日志脱敏、优雅关闭、Redis 可降级并启动即探活
三处都属于「故障时才发现」的运维缺口,一起改。
1) 访问日志泄露凭证
flamego.Classic() 内部 Use(Logger()),而 Logger 记的是 RequestURI —— 含 query。
实测日志出现 path=/_/tblocal/4?t=SECRETTOKEN123:?t= 是私密提问钥匙,
?code= 是一次性改密凭证,?q= 是搜索词,全都会被明文落盘,凡是能看日志的人
都能直接读私密提问或重置密码。internal/tracing 早就为同一原因只取 URL.Path
并留了注释,漏的是框架自带那层。
→ 改 flamego.New() + accessLogMiddleware,只记 Method/Path/Status/Duration/IP;
静态资源成功请求不记(噪音),4xx/5xx 照记(排查裂图靠它)。
2) 没有优雅关闭
直接 ListenAndServe,Go 对 SIGTERM 是立即退出,每次部署都会把正在写库的 POST
就地处死,留下「提问已 INSERT、图片没传完」这类半成品。
→ signal.NotifyContext(SIGTERM/SIGINT) + srv.Shutdown(15s),超时才 Close;
退出前关连接池。实测 8 个并发分享图请求中发 SIGTERM,8/8 全部成功。
3) Redis 单点
session 建在 Redis 上,load 失败 flamego/session 会 panic → 每请求 500(含匿名访客);
而 cache.Cacher 没判空 Addr,go-redis 会回退 localhost:6379,于是「不配 Redis」
这种被允许的部署里找回密码整体不可用、私信邮件节流失效,且启动零提示。
→ cache 跟随开关(未配则 MemoryIniter);启动 PingRedis fail-fast,显式 2s 超时。
实测停掉 Redis 后启动立即失败并明确指出是 Redis。
验证:单测 2 项(query 永不入日志 / 静态资源过滤口径);重启日志出现
「收到退出信号」→「HTTP 服务已关闭」;启动打「Redis 连接正常」。
This commit is contained in:
+64
-3
@@ -5,8 +5,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -70,10 +74,18 @@ func runWeb(ctx *cli.Context) error {
|
||||
}
|
||||
conf.Database.DSN = dsn
|
||||
|
||||
_, err := db.Init(dbType, dsn)
|
||||
gdb, err := db.Init(dbType, dsn)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "connect to database")
|
||||
}
|
||||
// 退出前把连接池关掉,避免优雅关闭期间连接还在池里挂着。
|
||||
if sqlDB, dbErr := gdb.DB(); dbErr == nil {
|
||||
defer func() {
|
||||
if err := sqlDB.Close(); err != nil {
|
||||
logrus.WithError(err).Warn("关闭数据库连接池失败")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 蜜罐命中记录保留期清理:honeypot_hits 是只追加的审计表,写入完全由
|
||||
// 外部扫描流量驱动(不可预期、不可节流)。没有自动清理的话,表体积只增不减。
|
||||
@@ -94,6 +106,20 @@ func runWeb(ctx *cli.Context) error {
|
||||
defer close(stopRetention)
|
||||
}
|
||||
|
||||
// Redis 配了就必须连得上:session 建在它上面,宕机等于全站 500(含匿名访客)。
|
||||
// 启动期 fail fast,别让运维在「站点全 500」的现场去猜是不是 Redis。
|
||||
if conf.Redis.Addr != "" {
|
||||
if err := shared.PingRedis(conf.Redis.Addr, conf.Redis.Password); err != nil {
|
||||
return errors.Wrapf(err,
|
||||
"无法连接 Redis(%s)。session 依赖它,连不上会导致全站 500,启动已中止。"+
|
||||
"请检查 [redis] addr/password,或清空 addr 改用进程内 session 与缓存",
|
||||
conf.Redis.Addr)
|
||||
}
|
||||
logrus.WithField("addr", conf.Redis.Addr).Info("Redis 连接正常")
|
||||
} else {
|
||||
logrus.Info("未配置 [redis] addr:session 与缓存均使用进程内实现(单副本部署可用)")
|
||||
}
|
||||
|
||||
logrus.WithContext(ctx.Context).WithField("external_url", conf.App.ExternalURL).Info("Starting web server")
|
||||
|
||||
// 站点默认语言初始化(default_lang 配置,未配置时回退 zh-CN)
|
||||
@@ -142,8 +168,43 @@ func runWeb(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
logrus.WithContext(ctx.Context).WithField("port", conf.Server.Port).Info("Listening on")
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
return errors.Wrap(err, "start http server")
|
||||
|
||||
// 优雅关闭:收到 SIGTERM / SIGINT 后停止接受新连接,给在途请求最多 15 秒收尾。
|
||||
//
|
||||
// 改动前是直接 ListenAndServe:Go 对 SIGTERM 的处理就是立即退出进程,
|
||||
// 于是每次部署 / 重启都会把正在写库的 POST 就地处死,留下
|
||||
// 「提问已 INSERT、图片还没传完」「回答已写入、配额还没扣」这类半成品。
|
||||
// systemd Restart=always 与 docker restart 都救不了这个。
|
||||
runCtx, stop := signal.NotifyContext(ctx.Context, syscall.SIGTERM, os.Interrupt)
|
||||
defer stop()
|
||||
|
||||
serveErr := make(chan error, 1)
|
||||
go func() {
|
||||
err := server.ListenAndServe()
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
// 正常的 Shutdown 路径,不算失败
|
||||
err = nil
|
||||
}
|
||||
serveErr <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-serveErr:
|
||||
// 端口被占 / 监听失败:没有在途请求要收尾,直接返回。
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "start http server")
|
||||
}
|
||||
return nil
|
||||
case <-runCtx.Done():
|
||||
logrus.Info("收到退出信号,正在优雅关闭(最多 15 秒)")
|
||||
}
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
if err := server.Shutdown(shutdownCtx); err != nil {
|
||||
logrus.WithError(err).Warn("优雅关闭超时,强制断开剩余连接")
|
||||
_ = server.Close()
|
||||
}
|
||||
logrus.Info("HTTP 服务已关闭")
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user