华为云 LTS + CES:把“线上出事了”变成“提前知道要出事”
一、一次失败的排查
先看一个真实场景。
用户反馈"提交订单一直转圈"。值班的人打开监控面板:CPU 28%、内存 51%、磁盘 34%、网络正常。所有面板都是绿色。
于是开始翻日志,找到应用的 app.log,grep 关键词"error",搜到 200 多行,全是各种无关的告警。筛了 20 分钟,才在一个角落里发现:
2026-10-15 03:12:44 WARNING retry 3/3 failed for payment gateway, cost 30012ms
问题在支付网关超时,重试三次全失败,但因为代码里 catch 住了没抛异常,所以既没有 error 日志,也没有影响任何监控指标。
这个案例暴露了三个可观测性的经典缺陷:
- 指标没覆盖到关键路径。 看的是机器指标,出问题的是外部依赖调用。
- 日志不可检索。 非结构化文本,只能 grep 关键词,噪音淹没了信号。
- 失败被吞掉了。 异常被 catch 后只打了个 WARNING,没进任何统计。
二、可观测性的三个支柱
标准说法是"日志、指标、追踪"三支柱,但更重要的是理解它们各自回答什么问题:
| 支柱 | 回答的问题 | 典型工具 | 数据特征 |
|---|---|---|---|
| 指标 Metrics | “现在整体健康吗?” | CES(云监控) | 数值、时序、聚合、便宜 |
| 日志 Logs | “具体发生了什么?” | LTS(日志服务) | 文本、明细、贵、有上下文 |
| 追踪 Traces | “慢在哪一步?” | APM / 链路追踪 | 调用链、跨服务、关联性强 |
三者的关系不是并列,而是层层下钻:
指标告警(订单接口 P99 > 2s)
↓ 下钻
日志检索(同一时段的错误日志)
↓ 下钻
链路追踪(某个具体请求的耗时分解)
↓
定位到:支付网关调用超时 30s
理解了这个下钻路径,就知道建设顺序应该是:先指标(最快发现问题),再日志(定位原因),最后追踪(解决跨服务问题)。反过来做,往往会先建一套很漂亮但没人看的日志系统。
三、第一支柱:指标该盯什么
大部分团队的问题不是"没监控",而是监控了太多没用的东西,漏掉了关键的。
3.1 三个层次,缺一不可
| 层次 | 指标 | 为什么重要 | 常见错误 |
|---|---|---|---|
| 基础设施 | CPU、内存、磁盘、网络 | 排除资源瓶颈 | 只盯这一层 |
| 应用 | QPS、延迟分位、错误率、饱和度 | 反映用户真实感受 | 只看平均值 |
| 业务 | 下单量、支付成功率、登录数 | 直接对应用户价值 | 完全没有 |
只盯基础设施是最大的误区。 就像开头那个故障:CPU 28% 一切正常,但业务已经瘫了一半。基础设施指标健康不等于服务健康——它只能回答"机器有没有问题",不能回答"用户能不能用"。
3.2 延迟必须看分位,不能看平均
这个数字对比足以说明问题:
| 指标 | 数值 | 含义 |
|---|---|---|
| 平均延迟 | 120 ms | 看起来很好 |
| P50 | 80 ms | 一半用户很快 |
| P95 | 400 ms | 5% 的用户明显慢 |
| P99 | 3000 ms | 1% 的用户等了 3 秒 |
平均值 120ms 隐藏了"1% 请求要等 3 秒"这个事实。而真实业务里,这 1% 往往是最活跃、最有价值的用户(比如数据量最大的商户)。
所以延迟监控至少要盯 P95 和 P99,平均值可以作为参考但不是决策依据。
3.3 应用埋点示例
import time
import logging
from functools import wraps
from collections import defaultdict
import threading
# 简单的分位统计(生产环境建议用专业库或直接打点到 CES)
_latency_buckets = defaultdict(list)
_lock = threading.Lock()
def timed(metric_name: str):
"""装饰器:记录函数耗时分位"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
status = "success"
try:
return func(*args, **kwargs)
except Exception:
status = "error"
raise
finally:
cost_ms = (time.time() - start) * 1000
with _lock:
_latency_buckets[f"{metric_name}.{status}"].append(cost_ms)
if len(_latency_buckets[f"{metric_name}.{status}"]) > 1000:
_latency_buckets[f"{metric_name}.{status}"].pop(0)
# 关键:超过阈值的慢请求单独打日志,带完整上下文
if cost_ms > 1000:
logging.warning(
"slow_request metric=%s cost_ms=%.1f status=%s",
metric_name, cost_ms, status
)
return wrapper
return decorator
@timed("order.create")
def create_order(user_id, items):
...
注意那个"超阈值慢请求单独打日志"的设计。它把指标和日志关联起来了——平时靠指标看趋势,出问题时从指标告警下钻到具体日志,不用在海量日志里大海捞针。
四、第二支柱:日志必须结构化
4.1 从文本到 JSON
这是提升日志价值最大的一步。对比一下:
# 改造前(文本)
2026-10-15 03:12:44 WARNING retry 3/3 failed for payment gateway, cost 30012ms
# 改造后(JSON)
{"ts":"2026-10-15T03:12:44.123+08:00","level":"WARNING","service":"order",
"trace_id":"a1b2c3d4","user_id":10086,"action":"payment.retry",
"attempt":3,"cost_ms":30012,"gateway":"alipay","result":"failed"}
结构化之后,才能做这些事:
- 按
cost_ms > 5000检索慢请求 - 按
result=failed统计失败率 - 按
gateway分组看哪个支付渠道有问题 - 用
trace_id串起一个请求在所有服务里的完整轨迹
文本日志做不到这些,只能靠关键词碰运气。
4.2 日志级别要用对
| 级别 | 用途 | 生产环境 |
|---|---|---|
| DEBUG | 详细调试信息 | 默认关闭,排障时临时开 |
| INFO | 关键业务节点 | 保留,量大时采样 |
| WARNING | 可恢复的异常 | 全部保留 |
| ERROR | 业务失败 | 全部保留并告警 |
| FATAL | 服务无法继续 | 立即告警 |
最常见的问题是INFO 打成 DEBUG,ERROR 打成 WARNING。开头那个案例就是:支付重试三次全失败,这是明确的业务失败,应该是 ERROR,结果打了 WARNING,既不告警也检索不到。
判断标准很简单:这件事如果发生了,需不需要有人知道? 需要,就是 ERROR。
4.3 一个必须遵守的纪律:不打印敏感信息
# 绝不这样写
logging.info(f"user login: phone={phone}, password={password}")
# 正确做法:脱敏
def mask(phone: str) -> str:
return phone[:3] + "****" + phone[-4:] if len(phone) >= 7 else "***"
logging.info(json.dumps({
"action": "user.login",
"phone": mask(phone),
"result": "success",
}))
日志一旦集中收集,访问权限往往比数据库宽松得多。一条包含明文密码的日志,等于给所有能看日志的人发了一份凭证。
4.4 LTS 的实用配置
在 LTS 上,几个配置决定了好不好用:
| 配置 | 建议 | 理由 |
|---|---|---|
| 日志组/流 | 按服务分,按环境分 | 检索时能缩小范围 |
| 结构化解析 | 开 JSON 解析 | 才能按字段检索 |
| 保留期 | 生产 30 天,归档到 OBS | 成本与追溯需求的平衡 |
| 关键词告警 | 只对 ERROR 和特定关键词 | 告警太多等于没有 |
| 日志转储 | 长期归档到 OBS | LTS 存储比 OBS 贵 |
第四条值得展开。告警疲劳是运维最大的敌人。如果每天收到 50 条告警,第 51 条真故障一定被忽略。所以告警必须"稀有且准确"——每条告警都应该对应一个需要人即刻行动的场景。
五、第三支柱:链路追踪解决什么
有了指标和日志,还有一类问题解不了:跨服务调用。
一个下单请求经过:网关 → 订单服务 → 用户服务 → 库存服务 → 支付服务。总耗时 3 秒,每个服务的日志都显示"我很正常",那慢在哪?
链路追踪就是把它们串起来:
trace_id: a1b2c3d4 总耗时 3120ms
├─ gateway 12ms
├─ order-service 120ms
│ ├─ user-service 45ms
│ ├─ stock-service 28ms
│ └─ payment-svc 2900ms ← 问题在这
│ └─ http-call 2880ms
实现的关键只有一件事:trace_id 要贯穿全链路。
import uuid
import logging
def get_trace_id():
"""优先从请求头取(上游传下来的),没有就新生成"""
return request.headers.get("X-Trace-Id") or uuid.uuid4().hex[:16]
@app.before_request
def inject_trace_id():
g.trace_id = get_trace_id()
@app.after_request
def log_request(response):
logging.info(json.dumps({
"trace_id": g.trace_id,
"path": request.path,
"method": request.method,
"status": response.status_code,
"duration_ms": round((time.time() - g.start) * 1000, 1),
}))
return response
def call_downstream(url, **kwargs):
"""调下游时把 trace_id 传下去"""
headers = kwargs.setdefault("headers", {})
headers["X-Trace-Id"] = g.trace_id
return requests.request(**kwargs, url=url, timeout=5)
那个 timeout=5 是另一个必须加的东西。没有超时的下游调用是最危险的——下游卡住,你的线程池会被慢慢占满,最终整个服务不可用。开头那个案例里 30 秒超时,就是因为没设超时,只能等系统默认的 30 秒。
六、五个真实排查路径
这套体系建好之后,排查变成了"按图索骥"。举五个典型场景:
6.1 接口变慢
① CES 看接口 P99 → 确认变慢的时间点
② LTS 检索该时段的 slow_request → 拿到 trace_id 和耗时
③ 看 trace 分解 → 定位到具体下游
④ 查下游的 CES 指标 → 是它自己慢还是网络慢
6.2 偶发报错
① LTS 按 level=ERROR + 时间范围检索
② 按 error_type 分组统计 → 找出占比最大的那类
③ 用 trace_id 关联同请求的完整日志 → 看前置操作
6.3 内存缓慢上涨
① CES 看内存曲线斜率 → 是阶梯上涨(泄漏)还是锯齿(正常 GC)
② LTS 检索 OOM 前的日志 → 看最后一次大对象操作
③ 结合 trace 看那段时间的请求特征(是不是某个接口被刷了)
6.4 数据库连接打满
① CES 看连接数曲线 → 被打满的时间点
② 对比同时段 QPS → 是流量涨了还是连接泄漏
③ LTS 检索连接获取失败日志 → 拿到调用方
④ 查该调用的 trace → 是不是有长事务没释放连接
6.5 磁盘突然写满
① CES 看磁盘曲线 → 陡增还是缓增
② 按日志组统计写入量 → 哪个服务在猛写
③ 检索该服务的日志内容 → 是不是进入死循环狂打日志
④ 检查日志保留期配置 → 是不是忘了设
七、告警怎么配才不会被静音
这是最实际的问题。我们经历过"每天 200 条告警,团队集体静音"的阶段,后来按三条原则重构:
原则一:只告警"需要人行动"的事。
| 该告警 | 不该告警 |
|---|---|
| 错误率突增 3 倍 | 单次请求失败 |
| P99 超过 SLA | 某个 WARNING 出现 |
| 磁盘 > 85% | CPU 短时飙到 80% |
| 证书 7 天内过期 | 定时任务执行完成 |
原则二:每条告警必须带上下文和处置建议。
不要只发"订单服务错误率高"。要发:
【告警】订单服务错误率 8.3%(阈值 2%),持续 5 分钟
影响:下单接口约 8% 失败
可能原因:支付网关超时(近 10 分钟 gateway=alipay 失败 47 次)
排查入口:<LTS 检索链接>
Runbook:https://wiki/runbook/order-error
带上下文的告警,值班的人第一时间就知道该干什么;没有上下文的告警,只是把人叫醒然后让他自己查。
原则三:分级,而不是一刀切。
- P0:电话 + 短信 + 群消息,5 分钟内响应
- P1:短信 + 群消息,30 分钟内响应
- P2:群消息,当天处理
- P3:日报汇总,排期处理
分级的价值是让人的注意力分配给正确的事情。
八、常见问题 Q&A
Q1:可观测性建设从哪开始?
从应用指标开始,不要从日志开始。指标最便宜、最直观、最能快速发现"有事发生了"。有指标之后,再加日志解决"出了什么事",最后加追踪解决"跨服务慢在哪"。反过来的顺序会先陷入日志海洋。
Q2:日志量太大,存储成本怎么控?
三层策略:一是级别控制(生产环境关 DEBUG),二是采样(INFO 级别按 1% 采样,ERROR 全留),三是分层存储(LTS 保留 30 天用于检索,超过的转 OBS 归档,成本降一个数量级)。
Q3:trace_id 怎么在异步任务里传下去?
不能依赖请求上下文。做法是显式传递:把 trace_id 作为参数传给异步任务,或者在消息体里带上。线程池场景可以用 contextvars,但跨进程(消息队列、定时任务)必须靠数据传递。
Q4:监控和告警的关系是什么?
监控是"看",告警是"叫"。很多人只建了监控没配告警,等于装了摄像头但没人看屏幕。反过来只配告警没有可下钻的面板,则告警来了也无从查起。两者要配套。
Q5:小团队有必要搞这么全吗?
不必一次全上,但指标和错误日志这两样必须有。最小可用方案:CES 盯 CPU/内存/磁盘 + 应用自报 QPS/延迟/错误率,LTS 收集 ERROR 日志并配一条错误率告警。这套东西配置量很小,但能覆盖 80% 的线上问题。
Q6:为什么我的告警总在事情发生后才来?
因为告警的往往是"结果指标"(错误率、延迟)。更好的做法是加先行指标:队列积压长度、连接池使用率、线程池活跃数、缓存命中率。这些指标恶化时,业务还没受影响,但已经能预判。先行指标的价值就是买时间。
九、总结
可观测性的目标,不是"出事之后能查",而是**“在用户投诉之前就知道”**。
这两者的差距,取决于你有没有在正确的地方埋点。开头那个案例之所以排查 20 分钟,不是因为工具不行,而是因为关键路径(支付网关调用)的状态没有被任何指标覆盖,失败也没有进任何统计。
所以建设的正确顺序是:先想清楚"这个系统的关键路径有哪些,每条路径失败会怎样",再去选工具。工具是次要的——CES、LTS、APM 都能做到,难的是知道该采集什么。
最后一条经验:每次故障复盘,都要问一句"这次为什么没有提前发现?" 如果答案是"没监控到这一点",那就补上。故障是最好的需求来源,一次补齐一个盲区,半年之后体系就相当完整了。
- 点赞
- 收藏
- 关注作者
评论(0)