故事的开始:一条看起来很吓人的告警
某天,我的 Grafana 突然推来一条告警:
⚠️ 站点
sqlforge.dengshu.ovh当前错误率达到 100%,超过阈值 10%。
100%?! 整个站挂了?
我赶紧打开 ClickHouse 查日志,发现真相是这样的:
| 时间段 | 总请求 | 错误数 | 错误率 |
|---|---|---|---|
| 13:10 | 5 | 3 | 60% |
| 13:20 | 12 | 2 | 16.67% |
| 12:10 | 12 | 3 | 25% |
所谓的「100% 错误率」,其实是在某个 5 分钟窗口内,只有几个请求,而且全是 404(Not Found)——一些爬虫在请求早已不存在的旧版静态资源:
GET /assets/js/auth.js → 404GET /assets/js/qr_modal.js → 404GET /assets/js/message.js → 404GET /assets/GanjingAbout-0xqqgv2B.js → 404站点本身完全正常。是告警规则把 404 也算作了「错误」,导致误报。
HTTP 状态码:三位数的大学问
HTTP 状态码是服务器对客户端请求的标准化回答。三位数字,第一位决定分类:
1xx → 信息性:「我收到了,还在处理」2xx → 成功:「搞定了」3xx → 重定向:「你要的东西搬家了」4xx → 客户端错误:「你的请求有问题」5xx → 服务端错误:「我这边出问题了」1xx:信息响应(你几乎见不到)
| 状态码 | 名称 | 大白话 |
|---|---|---|
| 100 | Continue | 「你先发的请求头我看了,没问题,继续发 body 吧」 |
| 101 | Switching Protocols | 「好的,我们切换到 WebSocket 协议了」 |
| 103 | Early Hints | 「正式响应还没好,但你可以先加载这些资源」 |
1xx 是中间状态,浏览器自动处理,你在开发和运维中基本不会直接碰到它们。
2xx:成功(好消息)
| 状态码 | 名称 | 大白话 | 常见场景 |
|---|---|---|---|
| 200 | OK | 「给你,一切正常」 | 最常见,页面正常加载 |
| 201 | Created | 「资源已创建」 | POST 创建新用户后 |
| 204 | No Content | 「操作成功了,但没有内容返回」 | DELETE 删除操作 |
| 206 | Partial Content | 「给你请求的那一部分」 | 视频拖动、断点续传 |
3xx:重定向(搬家通知)
| 状态码 | 名称 | 大白话 | 区别 |
|---|---|---|---|
| 301 | Moved Permanently | 「永久搬家了,更新你的书签」 | 搜索引擎会更新索引 |
| 302 | Found | 「临时搬家,下次还来这个地址」 | 搜索引擎保留原地址 |
| 304 | Not Modified | 「跟上次一样,用你的缓存就行」 | 节省带宽 |
| 307 | Temporary Redirect | 「临时跳转,保持原请求方法」 | POST 不会变成 GET |
| 308 | Permanent Redirect | 「永久跳转,保持原请求方法」 | 301 的严格版 |
实际应用:当你改了博客的 URL 结构,用 301 把旧链接指向新链接,这样搜索引擎的排名不会丢失。
4xx:客户端错误(是你的问题,不是我的)
这是最容易被误会的一类。很多人把 4xx 当成「服务出错了」,但其实它的意思是 “客户端发了一个有问题的请求” ——服务器本身是正常的。
| 状态码 | 名称 | 大白话 | 该紧张吗? |
|---|---|---|---|
| 400 | Bad Request | 「请求格式不对,我看不懂」 | 看情况,可能是前端 bug |
| 401 | Unauthorized | 「你谁?先登录」 | 正常,认证机制在工作 |
| 403 | Forbidden | 「我知道你是谁,但你没权限」 | ⚠️ 大量 403 需要排查 |
| 404 | Not Found | 「这个地址没有东西」 | 😴 通常无需关注 |
| 405 | Method Not Allowed | 「这个接口不支持 GET/POST」 | API 配置问题 |
| 422 | Unprocessable Entity | 「格式对但内容不合法」 | 正常,表单验证在工作 |
| 429 | Too Many Requests | 「你请求太频繁了,休息一下」 | 正常,限流在工作 |
5xx:服务端错误(我的锅)
这才是真正需要你紧张的:
| 状态码 | 名称 | 大白话 | 严重程度 |
|---|---|---|---|
| 500 | Internal Server Error | 「出 bug 了,我也不知道怎么回事」 | 🔴 立即排查 |
| 502 | Bad Gateway | 「我作为代理,后端服务没回应」 | 🔴 上游服务挂了 |
| 503 | Service Unavailable | 「服务暂时不可用(过载/维护)」 | 🟡 可能是重启中 |
| 504 | Gateway Timeout | 「等后端服务等太久了,超时了」 | 🔴 后端性能问题 |
在我的 Nginx 反向代理架构里:
- 502 通常意味着 Docker 容器挂了或没启动
- 504 说明应用处理太慢(比如大查询卡住了)
- 503 可能是在
docker compose restart的过程中
这三个才是告警应该关注的。
错误 ≠ 故障:运维监控的正确姿势
经过这次误报,我重新审视了告警规则。核心问题是之前的 SQL:
-- ❌ 之前的写法:把所有 4xx 都当「错误」countIf(status >= 400) * 100.0 / count()优化后的写法:
-- ✅ 只监控 5xx(真正的服务端故障)SELECT toStartOfMinute(now()) as time, host, round(countIf(status >= 500) * 100.0 / count(), 2) as error_rateFROM default.nginx_access_logWHERE timestamp >= now() - INTERVAL 10 MINUTEGROUP BY hostHAVING count() >= 30 -- 最低请求量:防止低流量误报 AND countIf(status >= 500) >= 2 -- 最低错误数:排除偶发异常ORDER BY error_rate DESC对比一下改动的逻辑:
| 维度 | 改前 | 改后 | 为什么 |
|---|---|---|---|
| 错误范围 | status >= 400 | status >= 500 | 4xx 是客户端的问题,不应告警 |
| 时间窗口 | 5 分钟 | 10 分钟 | 低流量站点需要更大窗口 |
| 最低样本量 | 10 个请求 | 30 个请求 | 防止几个请求就触发 |
| 最低错误数 | 无 | 2 个 5xx | 单个偶发错误不报 |
速查表
最后,一张图总结不同状态码在监控中的处理策略:
┌─────────────────────────────────────────────────────┐│ HTTP 状态码监控策略 │├──────────┬────────────────────────────────────────────┤│ 2xx ✅ │ 正常,不需要监控(可以统计趋势) ││ 3xx ↗️ │ 正常,不需要告警(过多 301 可能是配置问题) ││ 4xx ⚠️ │ 不告警,Dashboard 观察即可 ││ └ 404 │ 忽略(爬虫、旧链接、正常现象) ││ └ 401 │ 忽略(认证机制正常工作) ││ └ 403 │ 关注趋势(大量可能=权限配置异常) ││ └ 429 │ 关注趋势(大量可能=被攻击或限流太严) ││ 5xx 🔴 │ 必须告警!每一个都应该被调查 ││ └ 500 │ 应用 bug,查日志 ││ └ 502 │ 上游服务挂了,查容器状态 ││ └ 503 │ 过载或重启中,查负载 ││ └ 504 │ 超时,查上游性能 │└──────────┴────────────────────────────────────────────┘总结
三位数的 HTTP 状态码,背后是一整套客户端—服务端协作的设计哲学:
- 2xx 是好消息
- 3xx 是搬家通知
- 4xx 是「你的请求有问题」——服务端在正常工作
- 5xx 是「我的服务出问题了」——需要立刻行动
搞清楚这个区别,你就能写出更精准的告警规则,减少无意义的告警噪音,把注意力放在真正重要的事情上。