# 运维排查手册 (Runbook) 本文档覆盖 tmpl-api 服务最常见的故障场景。每个场景包含:触发条件、排查步骤、 恢复操作和事后改进建议。 > **适用角色**:SRE、运维工程师、值班开发人员。 > **前置条件**:拥有 Kubernetes 集群 `kubectl` 权限、Grafana/Prometheus 访问权限、 > 数据库与 Redis CLI 访问权限。 --- ## RB-01 数据库连接池耗尽 ### 触发条件 告警 `DatabaseConnectionExhaustion` 触发(`hikaricp_connections_pending > 5` 持续 1 分钟) 或应用日志出现 `HikariPool-1 - Connection is not available, request timed out after 30000ms`。 ### 排查步骤 **Step 1 — 确认连接池状态** ```bash curl -s http://:9090/actuator/metrics/hikaricp.connections.active | jq . curl -s http://:9090/actuator/metrics/hikaricp.connections.pending | jq . ``` 若 `active` 接近 `max`(默认 10)且 `pending` 持续增长,确认连接池耗尽。 **Step 2 — 检查数据库慢查询** ```sql SELECT pid, now() - pg_stat_activity.query_start AS duration, query, state FROM pg_stat_activity WHERE (now() - pg_stat_activity.query_start) > interval '5 seconds' AND state != 'idle' ORDER BY duration DESC; ``` **Step 3 — 检查是否存在长事务** ```sql SELECT pid, age(clock_timestamp(), xact_start) AS xact_age, query FROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY xact_age DESC LIMIT 10; ``` **Step 4 — 确认数据库最大连接数** ```sql SHOW max_connections; SELECT count(*) FROM pg_stat_activity; ``` ### 恢复操作 | 严重程度 | 操作 | |----------|------| | 中等 | 终止长事务:`SELECT pg_terminate_backend();` | | 严重 | 重启受影响的 Pod:`kubectl rollout restart deployment/tmpl-api -n ` | | 持续 | 提升 HikariCP `maximumPoolSize`(配合数据库 `max_connections` 调整) | ### 事后改进 检查是否有缺少索引的查询导致全表扫描;确认 `@Transactional` 范围是否过大;考虑引入连接池监控 Dashboard 面板和更低的超时阈值。 --- ## RB-02 Redis 连接抖动 / 缓存降级 ### 触发条件 告警 `RedisDown` 触发,或日志中出现 `GracefulCacheErrorHandler` 的 WARN 级别条目 (`Cache 'order' get failed`),health 端点显示 `redisPing: DOWN`。 ### 排查步骤 **Step 1 — 确认 Redis 实例状态** ```bash redis-cli -h -p 6379 PING redis-cli -h -p 6379 INFO server | grep uptime redis-cli -h -p 6379 INFO clients | grep connected_clients ``` **Step 2 — 检查网络连通性** ```bash kubectl exec -it -- nc -zv 6379 ``` **Step 3 — 检查 Redis 内存与驱逐** ```bash redis-cli INFO memory | grep -E "used_memory_human|maxmemory_human|evicted_keys" ``` **Step 4 — 查看应用日志确认降级行为** ```bash kubectl logs --since=5m | grep -i "cache.*fail\|redis\|GracefulCacheErrorHandler" ``` ### 恢复操作 | 场景 | 操作 | |------|------| | Redis 实例宕机 | 重启 Redis Pod / 故障转移到从节点 | | 网络问题 | 检查 NetworkPolicy、Service 配置 | | 内存满 | 调整 `maxmemory` 或优化 key TTL | | 应用侧连接池满 | 调整 `spring.data.redis.lettuce.pool.max-active` | > **关键认知**:当 `app.cache.enabled=true` 且 Redis 不可用时,`GracefulCacheErrorHandler` > 会记录 WARN 并回退到数据库查询。服务不会宕机,但 DB 负载会上升。若 DB 也接近瓶颈, > 应优先恢复 Redis。 ### 事后改进 考虑引入 Redis Sentinel / Cluster 高可用方案;评估是否需要 Resilience4j CircuitBreaker 包裹缓存调用;确认缓存 key TTL 设置合理。 --- ## RB-03 Flyway 迁移失败 ### 触发条件 应用启动失败,日志出现 `FlywayMigrateException` 或 `Migration checksum mismatch`。 Pod 处于 CrashLoopBackOff 状态。 ### 排查步骤 **Step 1 — 查看 Flyway 日志** ```bash kubectl logs | grep -i "flyway\|migration" ``` **Step 2 — 检查迁移历史表** ```sql SELECT version, description, success, checksum, installed_on FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 10; ``` **Step 3 — 确认失败类型** | 类型 | 日志关键词 | 说明 | |------|------------|------| | Checksum 不匹配 | `checksum mismatch` | 已应用的迁移文件被修改 | | SQL 语法错误 | `ERROR: syntax error` | 迁移脚本有 SQL 错误 | | 冲突版本号 | `Found more than one migration with version` | 版本号重复 | | 手动修改了库 | `Detected resolved migration not applied` | 库结构被手动改动 | ### 恢复操作 | 场景 | 操作 | |------|------| | SQL 语法错误 | 修复脚本,手动清理 `flyway_schema_history` 中失败记录:`DELETE FROM flyway_schema_history WHERE success = false;` | | Checksum 不匹配 | **切勿修改已部署的迁移文件!** 使用 `flyway repair` 仅在确认数据一致时更新 checksum | | 手动修改了库 | 创建补偿迁移脚本(新版本号),使 schema 回到预期状态 | > **重要**:Flyway 迁移本质上是不可逆的(forward-only)。每次修复后必须创建新的迁移脚本 > 而非修改历史脚本。回滚需通过编写反向 DDL 的新迁移实现。 ### 事后改进 所有迁移脚本必须在 CI 中通过 Testcontainers 验证;禁止在 `main` 分支合并后修改迁移文件;命名规范参见 [database.md](database.md)。 --- ## RB-04 P95 延迟飙升 ### 触发条件 告警 `HighLatencyP95` 触发(P95 > 500ms 持续 3 分钟),或 Grafana 延迟面板出现 突增尖峰。 ### 排查步骤 **Step 1 — 定位慢端点** 在 Prometheus 中查询: ```promql topk(5, histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri) ) ) ``` **Step 2 — 排除基础设施因素** 检查 HikariCP 等待连接数(→ RB-01)、Redis 延迟(→ RB-02)、Pod CPU/内存使用率、 GC 暂停时间: ```promql rate(jvm_gc_pause_seconds_sum[5m]) / rate(jvm_gc_pause_seconds_count[5m]) ``` **Step 3 — 检查上游依赖** 若服务调用外部 API 或消息队列,检查对应 Timer 指标或日志中的耗时。 **Step 4 — 检查缓存命中率** ```promql sum(rate(cache_gets_total{result="hit"}[5m])) / sum(rate(cache_gets_total[5m])) ``` 命中率骤降可能导致大量 DB 查询,引发延迟升高。 ### 恢复操作 | 根因 | 操作 | |------|------| | DB 慢查询 | 添加索引、优化 SQL、终止长事务 | | GC 抖动 | 检查堆内存配置、排查内存泄漏 | | 缓存击穿 | 恢复 Redis、预热缓存 | | 流量突增 | HPA 扩容、启用限流 | ### 事后改进 设置 P99 延迟告警;在慢查询日志中启用 `log_min_duration_statement`;考虑在 API 层引入超时和熔断。 --- ## RB-05 容器反复重启 (CrashLoopBackOff) ### 触发条件 `kubectl get pods` 显示 Pod 状态为 `CrashLoopBackOff`,Restart count 持续增加。 ### 排查步骤 **Step 1 — 查看 Pod 事件和日志** ```bash kubectl describe pod -n kubectl logs -n --previous ``` **Step 2 — 分类退出原因** | Exit Code | 含义 | 常见原因 | |-----------|------|----------| | 0 | 正常退出 | 主线程结束(配置错误,未启动 Web 服务器) | | 1 | 应用异常 | Bean 创建失败、数据库不可达、Flyway 失败 | | 137 | OOMKilled | 容器内存超出 `resources.limits.memory` | | 143 | SIGTERM | 被 kubelet 杀死(探针超时) | **Step 3 — 针对性检查** 若 Exit Code = 137:检查 `resources.limits.memory` 是否过小,JVM `-Xmx` 是否超出容器限制,堆外内存(Metaspace、DirectBuffer)是否泄漏。 若 Exit Code = 1:检查 `application.yml` 配置是否正确,数据库/Redis 连接信息是否正确注入(Secret 挂载),Flyway 是否失败(→ RB-03)。 若 Exit Code = 143:检查 startup/liveness 探针配置,`initialDelaySeconds` 是否足够。 ### 恢复操作 | 根因 | 操作 | |------|------| | OOMKilled | 提升 `resources.limits.memory`,调整 JVM 参数 `-XX:MaxRAMPercentage=75` | | DB 不可达 | 确认 Secret 中连接 URL 正确,DB 实例正常,NetworkPolicy 允许流量 | | 探针超时 | 延长 `initialDelaySeconds`、`timeoutSeconds` | | Flyway 失败 | 参见 RB-03 | ### 事后改进 建立 Pod 资源 baseline(通过 VPA 推荐或历史监控数据);在 CI/CD 中验证配置文件完整性;为 startup 探针留足应用冷启动时间(至少 60s)。 --- ## 通用排查工具速查 | 用途 | 命令 | |------|------| | Pod 日志 | `kubectl logs -n -f --since=10m` | | 前一次崩溃日志 | `kubectl logs -n --previous` | | 进入 Pod | `kubectl exec -it -n -- /bin/sh` | | 端口转发 | `kubectl port-forward 9090:9090 -n ` | | 健康检查 | `curl http://localhost:9090/actuator/health | jq .` | | Prometheus 指标 | `curl http://localhost:9090/actuator/prometheus` | | Redis CLI | `redis-cli -h -p 6379 INFO` | | PostgreSQL | `psql -h -U -d -c "SELECT 1;"` |