华为云 LTS + CES:把“线上出事了”变成“提前知道要出事”

举报
辣椒酱来了 发表于 2026/11/05 20:01:32 2026/11/05
【摘要】 一、一次失败的排查先看一个真实场景。用户反馈"提交订单一直转圈"。值班的人打开监控面板:CPU 28%、内存 51%、磁盘 34%、网络正常。所有面板都是绿色。于是开始翻日志,找到应用的 app.log,grep 关键词"error",搜到 200 多行,全是各种无关的告警。筛了 20 分钟,才在一个角落里发现:2026-10-15 03:12:44 WARNING retry 3/3 f...

一、一次失败的排查

先看一个真实场景。

用户反馈"提交订单一直转圈"。值班的人打开监控面板: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 日志,也没有影响任何监控指标。

这个案例暴露了三个可观测性的经典缺陷:

  1. 指标没覆盖到关键路径。 看的是机器指标,出问题的是外部依赖调用。
  2. 日志不可检索。 非结构化文本,只能 grep 关键词,噪音淹没了信号。
  3. 失败被吞掉了。 异常被 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 都能做到,难的是知道该采集什么。

最后一条经验:每次故障复盘,都要问一句"这次为什么没有提前发现?" 如果答案是"没监控到这一点",那就补上。故障是最好的需求来源,一次补齐一个盲区,半年之后体系就相当完整了。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。