V7: 系统故障 —— 「半夜挂了没人知道」
周六凌晨 3 点,PostgreSQL 数据磁盘写满了,系统开始返回 500 错误。Redis 缓存还能撑一会儿,但写操作全部失败。周一早上 9 点,财务打开页面发现白屏,找到你。系统已经挂了整整两天,没有人收到任何告警。
老板很生气:“系统挂了两天才知道?以后故障必须第一时间通知到我。”
当前状态 (V6):系统有完整功能和安全加固,但日志是 fmt.Println 散落在代码里,没有健康检查端点,没有监控指标,没有告警机制。出了问题只能事后翻服务器日志。
问题分析(5 层框架)
Section titled “问题分析(5 层框架)”| 层级 | 问题 | 影响 |
|---|---|---|
| 表象 | 系统挂了两天无人知晓 | 周末报销数据丢失,用户体验极差 |
| 直接原因 | 没有健康检查和告警 | 故障发生后无人感知 |
| 系统原因 | 没有监控指标采集 | 磁盘逐渐写满的趋势无法预判 |
| 设计缺失 | 日志非结构化,无法检索和分析 | 排障靠 grep 翻日志,效率极低 |
| 根本原因 | 可观测性为零(no observability) | 系统是个黑盒,出了事只能猜 |
可观测性三支柱
Section titled “可观测性三支柱”- 日志 (Logs):结构化 JSON,带 request_id 串联请求链路
- 指标 (Metrics):Prometheus 格式,量化系统健康度
- 追踪 (Traces):本阶段暂不引入(单服务场景 request_id 足够)
graph TD User["用户"] --> Nginx --> Go["Go/Gin"] Go --> DB["PostgreSQL"] Go --> Logs["日志收集<br/>(结构化 JSON)"] DB --> Logs Logs --> Monitor["Prometheus<br/>+ Grafana"] Monitor -->|"告警"| Alert["📱 手机/邮件通知"]新增:结构化日志 + 健康检查 + Prometheus + Grafana + 告警 解决:半夜挂了 5 分钟内收到通知
| 决策点 | 选项 A | 选项 B | 选择 | 理由 |
|---|---|---|---|---|
| 日志格式 | 文本日志(人类可读) | JSON 结构化日志 | B | 方便 ELK/Loki 检索,可程序解析 |
| 日志库 | 标准库 log | zerolog / zap | B | 高性能 + 结构化 + 级别过滤 |
| 指标采集 | 自定义上报 | Prometheus client_golang | B | 业界标准,Grafana 原生支持 |
| 可视化 | 终端看日志 | Grafana Dashboard | B | 图形化趋势,非技术人员也能看 |
| 告警 | 人工巡检 | Grafana Alerting 规则 | B | 7x24 自动检测,支持多渠道通知 |
可观测性体系:├── 应用层│ ├── 结构化日志(zerolog):每条日志带 request_id, method, path, status, latency│ ├── Prometheus 指标:/metrics 端点│ └── 健康检查:/health 端点(检查 DB + Redis 连通性)├── 采集层│ └── Prometheus server:定时拉取 /metrics├── 可视化层│ └── Grafana:Dashboard + Alerting└── docker-compose 新增:prometheus + grafana 容器核心指标设计
Section titled “核心指标设计”| 指标名 | 类型 | 说明 |
|---|---|---|
http_requests_total | Counter | 请求总数(按 method, path, status 分标签) |
http_request_duration_seconds | Histogram | 请求延迟分布 |
http_requests_in_flight | Gauge | 当前正在处理的请求数 |
db_connections_active | Gauge | 数据库活跃连接数 |
db_connections_idle | Gauge | 数据库空闲连接数 |
cache_hits_total | Counter | Redis 缓存命中次数 |
cache_misses_total | Counter | Redis 缓存未命中次数 |
给 AI 的 Prompt
Section titled “给 AI 的 Prompt”我有一个 Go(Gin) + GORM + PostgreSQL + Redis 的团队记账工具,目前没有任何监控。周末系统挂了两天没人知道,需要加上完整的可观测性体系。
请帮我实现以下功能:
1. **结构化日志中间件** - 使用 rs/zerolog 库 - 新建 server/middleware/logger.go - 每个请求生成唯一 request_id(UUID v4),写入 context 和响应头 X-Request-ID - 请求完成时输出 JSON 日志,字段包括: timestamp, request_id, method, path, status, latency_ms, client_ip, user_id(如有JWT), error(如有) - 日志级别:2xx=info, 4xx=warn, 5xx=error - 启动时根据环境变量 LOG_LEVEL 设置级别(默认 info)
2. **健康检查端点** - 新建 server/handlers/health.go - GET /health 返回 JSON: { "status": "ok" | "degraded" | "down", "checks": { "database": {"status": "ok", "latency_ms": 5}, "redis": {"status": "ok", "latency_ms": 2} }, "timestamp": "2024-01-01T00:00:00Z" } - DB 检查:执行 SELECT 1,超过 5 秒算 down - Redis 检查:执行 PING,超过 3 秒算 down - 任一组件 down 则整体 status = "down" - 该接口不需要认证
3. **Prometheus 指标** - 使用 prometheus/client_golang 库 - 新建 server/middleware/metrics.go - 注册以下指标: - http_requests_total (Counter): labels=[method, path, status] - http_request_duration_seconds (Histogram): labels=[method, path] buckets=[0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10] - http_requests_in_flight (Gauge) - 在 main.go 暴露 GET /metrics 端点(使用 promhttp.Handler()) - 新建 server/middleware/db_metrics.go: - 定时(10s)采集 DB 连接池状态:active, idle, max_open - 注册为 Gauge 指标
4. **Grafana Dashboard + Prometheus 配置** - docker-compose.yml 新增 prometheus 和 grafana 服务 - prometheus.yml 配置: scrape_interval: 15s targets: ["host.docker.internal:8080"](或 app 容器名) - grafana provisioning: - datasource: Prometheus - dashboard JSON:4 个面板 (1) QPS 折线图 (rate(http_requests_total[1m])) (2) P50/P95/P99 延迟 (histogram_quantile) (3) 错误率 (status=~"5.." / total) (4) DB 连接池使用率
5. **告警规则** - 在 Grafana 或 Prometheus alerting rules 中配置: - 错误率 > 5% 持续 2 分钟 → 告警 - 健康检查失败持续 1 分钟 → 告警 - DB 活跃连接数 > 80% 最大连接数持续 5 分钟 → 告警 - 告警通知先配置为 webhook(后续可接钉钉/飞书)
6. **请求日志与指标中间件注册** - 在 Gin 路由初始化时,按顺序注册: (1) Recovery 中间件 (2) Request ID 中间件 (3) Metrics 中间件 (4) Logger 中间件 (5) 业务路由
请给出完整代码文件,包括 docker-compose 新增部分和配置文件。- 启动服务后请求 API,终端输出 JSON 格式日志
- 日志包含 request_id,且与响应头 X-Request-ID 一致
- 请求报错时日志级别为 error,包含 error 字段
- 设置
LOG_LEVEL=debug后可看到更多日志
-
curl http://localhost:8080/health返回{"status": "ok", ...} - 停止 PostgreSQL 后再请求,database 状态变为 down
- 停止 Redis 后再请求,redis 状态变为 down
- 整体状态相应变为 down
-
curl http://localhost:8080/metrics返回 Prometheus 格式文本 - 发几个请求后,
http_requests_total计数增加 -
http_request_duration_seconds有 bucket 数据 -
db_connections_active有值
Grafana 验证
Section titled “Grafana 验证”-
docker-compose up后访问http://localhost:3000(admin/admin) - Prometheus 数据源连接正常(绿色)
- Dashboard 四个面板有数据展示
- 手动触发一些请求,图表实时更新
- 停止数据库,1 分钟内告警触发
- 用压测工具打出 5xx 错误,错误率告警触发
- 告警恢复后状态变回 OK
你学到了什么
Section titled “你学到了什么”| 主题 | 对应模块 |
|---|---|
| 可观测性 = 日志 + 指标 + 追踪,三者缺一不可 | → Module 15 (可观测性) |
| 结构化日志是可检索日志的前提 | → Module 15 |
| request_id 串联一次请求的所有日志 | → Module 15 |
| Prometheus 拉模型 vs 推模型的权衡 | → Module 15 |
| RED 方法:Rate, Error, Duration 三个黄金指标 | → Module 15 |
| docker-compose 编排多服务本地开发环境 | → Module 16 (Docker Compose) |
1. /metrics 端点暴露了敏感信息
Section titled “1. /metrics 端点暴露了敏感信息”现象:/metrics 是公开的,攻击者可以看到系统内部状态。 原因:指标端点没有做访问控制。 解决:生产环境 /metrics 只允许 Prometheus 服务器 IP 访问,或放在内部端口(如 :9090)不对外暴露。
2. 日志输出太多打满磁盘
Section titled “2. 日志输出太多打满磁盘”现象:INFO 级别日志每天几 GB,磁盘又满了(和故障原因一样)。
原因:日志没有做轮转(rotation)和清理策略。
解决:使用 lumberjack 做日志轮转(按大小/天数),或输出到 stdout 由 Docker 管理日志。生产环境日志级别设为 WARN。
3. Prometheus 标签基数爆炸
Section titled “3. Prometheus 标签基数爆炸”现象:Prometheus 内存暴涨,查询越来越慢。
原因:把 URL path 里的动态参数(如 /api/expenses/123)当标签,每个 ID 一个时间序列。
解决:path 标签用路由模板(/api/expenses/:id),不用实际 URL。用 Gin 的 c.FullPath() 而不是 c.Request.URL.Path。
4. 健康检查太重拖慢系统
Section titled “4. 健康检查太重拖慢系统”现象:Kubernetes/负载均衡每秒调健康检查,每次查询 DB 和 Redis。 原因:健康检查做了真实的依赖查询,频率又高。 解决:区分 liveness 和 readiness:liveness 只检查进程存活(直接返回 200),readiness 检查依赖组件。或者缓存健康状态(每 10 秒实际检查一次)。
5. Grafana Dashboard 导入后没数据
Section titled “5. Grafana Dashboard 导入后没数据”现象:面板显示 “No data”。
原因:Prometheus 抓取目标配错了地址,容器内 localhost 不等于宿主机 localhost。
解决:Docker 内部使用服务名通信(如 app:8080),或使用 host.docker.internal(Mac/Windows)。用 Prometheus 的 Status > Targets 页面确认抓取状态。