全栈工程课 · 一个相册系统的上线之路
课程平台Chapter 10 · 故障定位

CHAPTER 10 · OBSERVABILITY

10

上线之后,用户说“相册打不开了”,怎么排查?

系统上线只是开始。真正的工程能力,是能够发现问题、定位问题,并尽快恢复服务。

第一反应不能是猜

“打不开”不是一个具体错误。

用户反馈 “相册打不开了。”

是前端静态资源加载失败吗?

是 Nginx 转发失败吗?

是 Go 服务挂了吗?

是 JWT 失效了吗?

是 MySQL 连接失败吗?

是 OSS 签名失败吗?

是某一个用户失败,还是所有用户都失败?

是哪一个版本发布后出现的?

上线后必须具备 发现问题 + 定位问题 + 恢复服务

日志

日志不是随手写一句 fmt.Println

没法定位

query failed

可以定位

request_id=abc123
user_id=1001
path=/api/my/albums
status=500
duration=320ms
error=mysql timeout

日志的目的不是“证明代码执行过”,而是让你在故障发生后能回答:谁、什么时候、访问了什么、用了多久、失败在哪里。

普通日志不要记录

完整密码、完整 JWT、OSS 签名 URL、数据库密码、AccessKeySecret、敏感请求体

Request ID

一次请求经过多个组件,需要一个统一标识。

浏览器NginxGo 服务MySQLOSS

如果每一层日志都没有共同字段,就很难把它们串成同一次请求。request_id 就是这条链路的线索。

位置做什么说明

Nginx

生成或传递

如果请求中已有 Request ID,就继续传递;没有就生成一个。

Go 中间件

读取或生成

写入上下文、响应头和后端日志。

用户报错

可提供

错误响应中带回 requestId,方便开发者快速查日志。

error-response.json
{
  "code": "INTERNAL_ERROR",
  "message": "服务暂时不可用",
  "requestId": "abc123"
}

Gin 中间件

日志中间件负责统一记录请求上下文。

不要在每个 Handler 里手写一套日志。请求开始、请求结束、状态码、耗时、用户身份和错误信息,都适合由中间件统一处理。

应该记录

request_id
method
path
status
duration
client_ip
user_id
error

需要审查

是否记录密码?
是否记录完整 Token?
是否记录签名 URL?
是否把所有日志都打成 error?

AI 可以实现日志中间件,但字段设计和敏感信息边界必须由开发者决定。

健康检查

存活检查和就绪检查不是一回事。

GET /health/live

进程还活着吗?

只检查服务进程是否可以响应。它应该轻量,不依赖复杂外部系统。

GET /health/ready

现在能处理请求吗?

可以检查 MySQL Ping、必要配置是否加载、服务是否完成初始化。

检查项是否适合 ready原因

MySQL Ping

适合

数据库不可用时,核心接口通常无法正常工作。

检查必要配置

适合

缺少 JWT Secret、数据库地址等配置时,不应该接流量。

查询全部相册 / 上传 OSS 图片

不适合

健康检查不能执行复杂业务,也不能因为外部服务偶发波动导致实例频繁重启。

监控指标

不要等用户反馈,系统应该先告诉你异常。

Traffic

流量

每秒请求数、登录次数、上传次数。

Errors

错误

4xx、5xx、登录失败率、OSS 上传失败率。

Latency

延迟

平均响应时间、P95、P99、慢接口。

Resources

资源

CPU、内存、Goroutine、数据库连接池、磁盘和网络。

平均响应时间正常,不代表所有用户都正常。P95、P99 可以帮助你看到尾部慢请求,而不是只看“平均还行”。

分层排查

前端错误和后端错误要分开看。

前端可能失败

JavaScript 运行错误
静态资源 404
API 请求失败
页面白屏
数据结构不匹配

后端可能失败

500
数据库超时
OSS 调用失败
Panic
配置错误

浏览器 Network浏览器 ConsoleNginx 日志Go 服务日志MySQL / OSS 状态

不要一上来就改代码。先确认请求有没有发出、发到哪里、返回什么、哪个组件记录了异常。

错误信息分层

错误不能只打印,也不能直接返回内部细节。

对用户返回

response.json
{
  "code": "INTERNAL_ERROR",
  "message": "服务暂时不可用",
  "requestId": "abc123"
}

对开发者记录

app.log
request_id=abc123
path=/api/my/albums
user_id=1001
error=mysql connection timeout

用户需要稳定、可理解的信息;开发者需要完整上下文。不要把数据库错误、SQL、密钥、签名 URL 直接暴露给用户。

Panic 与 Recovery

Recovery 是防止单次异常拖垮服务,不是掩盖缺陷。

Handler panic 后应该发生什么?

返回 500,而不是让用户看到堆栈。

记录 panic 信息和堆栈,带上 request_id。

不要把所有 panic 都当作“已经处理”。

修复根因,而不是依赖 Recovery 长期兜底。

Recovery 中间件保护进程稳定性,但 panic 仍然代表代码存在严重路径,需要追踪和修复。

发布版本信息

线上报错时,要知道当前运行的是哪一个版本。

没有版本信息,就很难判断问题是否由刚发布的代码引起,也很难确认回滚是否生效。

GET /version
{
  "version": "1.3.0",
  "commit": "a8f921c",
  "buildTime": "2026-07-17T10:30:00+08:00",
  "env": "prod"
}

Git Commit

精确定位当前运行代码。

构建时间

判断镜像何时生成。

环境名称

区分 dev、test、prod。

本章总结

  1. 系统上线后必须具备发现和定位问题的能力。
  2. 日志要结构化,并包含 Request ID 等关键上下文。
  3. 密码、Token 和签名 URL 不能进入普通日志。
  4. 健康检查应区分存活和就绪。
  5. 监控需要关注流量、错误、延迟和资源。
  6. 前端、Nginx、Go、MySQL 和 OSS 都可能成为故障点。
  7. 用户错误信息和内部日志应该分离。
  8. Recovery 能保护进程,但不能掩盖程序缺陷。
  9. 每次发布都应该能够关联到明确版本。
  10. AI 可以生成监控和日志代码,但开发者必须决定记录什么、告警什么以及如何排查。
已经能观察系统之后,下一步才是优化系统。
下一章:系统为什么越来越慢?