One self-hosted console to run your entire business — commerce, ERP, HRM, CRM & manufacturing

高并发系统的可观测性:真正能指导行动的日志、指标与链路追踪

请求 ID、写入 OpenSearch 的结构化日志、四个黄金信号、采样的链路追踪,以及与用户影响挂钩的告警:几分钟内定位慢的那一层所需的最小集合,外加压测和活动当天该怎么盯。

Author

Anichur Rahaman

6 天前12 min read1 views
高并发系统的可观测性:真正能指导行动的日志、指标与链路追踪

“用户说结账页卡住了。”这条消息在发售当天早上 10:01 送到一家票务网站的平台负责人手里。10:00 开售,大约一千人在同一分钟点了进来,预案按每秒 440 个请求来算。监控面板上 CPU 才 40%,没有任何红色告警。

到底哪一层慢?是某个路由、某个节点,还是某一个用户?只靠控制台和猜测,接下来十分钟都会花在争论上。有了请求 ID 和几项挑出来的指标,这十分钟只需要一次搜索。(这一幕只是示例,并非真实事故。)

这并不需要庞大的平台。你需要的是请求 ID、结构化日志、四个黄金信号,以及与用户影响挂钩的告警。我曾为一次定时高峰做准备,预计约一千名用户在同一分钟内操作;服务器没有问题,险些出事的是没人能很快说清哪一层慢,也说不清那个吓人的数字是不是真的。本文把能回答这些问题的最小集合搭起来,再说明如何用它盯住一次压测和活动当天。

本文是“高并发系统工程”系列五篇中的第 4 篇。第 1 篇讲到底什么能伸缩,第 2 篇讲队列与 worker,第 3 篇讲数据层。这一篇的主题是:把这一切看清楚。

用一段话讲清可观测性

监控告诉你出了问题;可观测性让你不必先上线新代码,就能追问“为什么”。它建立在三类数据之上,通常称为信号:

  • 日志(log):每个事件一条记录,带细节。最适合回答“这个请求到底发生了什么?”
  • 指标(metric):随时间采样的数字。最适合回答“情况是不是在恶化,恶化得多快?”
  • 链路追踪(trace):一个请求穿过各个服务的完整路径,以及每一步花了多少时间。最适合回答“时间花在哪了?”

第一天不必三样齐备,也不需要昂贵的平台。关键是让它们彼此相连,这样你能从一个变红的指标,直接跳到某个慢请求的日志和 trace。把它们串起来的,就是请求 ID(request ID)。

先做一个贯穿每一跳的请求 ID

我知道的最便宜的改进,就是一个从边缘一路跟到最后一个队列任务的标识。在我运行过的这套部署里,nginx 会沿用传入的 X-Request-Id 请求头,没有就自己生成,再交给 PHP-FPM,两边都把它写进日志。应用随后把它加到每一行日志里,并复制进它派发的任务载荷,这样后台任务就能对应回触发它的那次点击。

也把同一个 ID 放进响应头返回。用户或客服反馈问题时,凭这一串字符就能还原整件事。

示意图:用请求 ID 追踪一个请求,从 CDN、负载均衡、nginx、PHP-FPM、应用到队列任务,在 OpenSearch 中一次搜索即可看全
每一层都写下同一个请求 ID,五份分散的日志就变成一条时间线。

ID 必须出现在哪些位置

层级怎么做
边缘 / CDN转发传入的 ID,或交给下一层生成
宿主机 nginx 与容器内 nginx沿用传入的 ID,缺失就生成,并写进访问日志
PHP-FPM 与应用从请求头读取,作为共享上下文附加到每行日志
队列任务存入任务载荷,任务运行时再恢复
对外 HTTP 调用继续传递,让合作方和内部服务也能记录
响应放进响应头,方便客服索取

结构化日志,送进 ELK 或 OpenSearch

普通文本日志是写给人一条条读的。在高并发下,你需要搜索、统计、过滤,这就要求每行一个 JSON 对象,字段有名字。刚开始,一小组稳定的字段就够了。

字段为什么值得保留
time排序,并与指标对齐
request_id把各层串起来的那根线
route 或 path(不含查询字符串)按接口而不是原始 URL 归类慢请求
status按接口统计错误率
duration计算 p95、p99 的原始数据
upstream time区分“nginx 慢”还是“PHP 慢”
user 或 tenant id(内部 ID,绝不用邮箱)判断问题是不是只出在某一个客户身上

先说名称:ELK 是三个工具的旧称,即 Elasticsearch、Logstash 和 Kibana;后来的 Elastic Stack 又加入了名为 Beats 的轻量采集器。OpenSearch 于 2021 年由 AWS 发起,是 Elasticsearch 和 Kibana 的 Apache 许可分支,并在 2024 年 9 月转入 Linux Foundation 旗下的 OpenSearch Software Foundation。两者如今已不完全相同,但处理日志的流程一致:采集、索引、搜索、画图,所以人们才说“兼容 ELK”。

不需要大集群。在我的部署里,一个 2 GB 内存、1 个 vCPU 的小型 OpenSearch 节点,就存下了一个要应对陡峭高峰的平台的日志。这只是一套配置,不是基准测试,但它说明成本很小,远低于第一次把故障从几小时缩短到几分钟所换来的价值。

哪些内容不该记录

日志会被复制、被索引、被保存数月,所以它本身就是数据保护风险。在任何一行离开应用之前,先脱敏密码、令牌、银行卡数据和完整的个人信息。记录内部 ID,而不是姓名、邮箱或手机号。路径里的查询字符串也要去掉,因为里面常带令牌。

保留期与索引生命周期

按天滚动索引并配上自动生命周期,小节点也能保持健康:近几天可搜索,更早的索引压缩或迁移,超过保留期的直接删除。保留期要和负责隐私与安全的同事一起定,不要凭感觉。详细日志保留三十天是常见的起点,具体取决于你适用的规定。

指标:四个黄金信号

Google 的《Site Reliability Engineering》一书在讲分布式系统监控的那一章里,提出了四个几乎适用于所有面向用户的服务的信号。如果你只能量四样东西,就量这四样。

信号回答的问题在 Web 加 worker 平台上怎么量
Latency(延迟)请求要花多久?每个 route 的 p50、p95、p99;失败请求单独统计
Traffic(流量)需求有多大?边缘的每秒请求数,每秒派发的任务数
Errors(错误)多大比例失败?5xx 比例、超时、失败任务
Saturation(饱和度)最紧的那项资源有多满?FPM 忙碌 worker 数、队列深度与时长、数据库连接、Redis 内存

两点提醒。第一,不要按平均值设告警或做规划:看起来健康的平均值会掩盖可怕的长尾。在我对一个服务端渲染前端做的压测里,单个进程在每秒约 220 到 250 个请求时就饱和了,p95 从约 260 ms 跳到约 3 秒,而主机上还有空闲的核。平均值有一阵子看上去还能接受,说出真相的是分位数。

第二,麻烦从饱和度开始,而它很少是 CPU。对这类平台,我最先看的是:

  • PHP-FPM 的忙碌 worker 数,对照进程池上限。一旦触顶,请求就在 nginx 里排队。
  • 每个队列的队列深度与队列时长。深度说明有多少在等;时长说明你已经晚了多久。
  • 数据库连接数和写入吞吐,不只是 CPU。
  • 按用途(缓存、会话、队列)区分的 Redis 内存与淘汰次数。
  • 每个容器的内存,因为内存溢出被杀,从外面看就像一个随机错误。

用 OpenTelemetry 做链路追踪

日志和指标告诉你请求慢了。trace 则告诉你:它在 nginx 花了 40 ms,在 PHP 花了 90 ms,等一条查询等了 1.2 秒,在 Redis 花了 30 ms。对于有前端层、后端层和 worker 的系统,这是找出问题环节最快的办法。

OpenTelemetry 是产出这类数据的厂商中立标准。按其规范状态页的说法,追踪和日志信号已稳定,指标的 API、协议和数据模型也已稳定,但各语言 SDK 的成熟度不一。PHP 项目的文档称,追踪、指标和日志均已可用,并提供可选扩展来实现自动埋点。动手前,请先核对你实际要用的那些库的状态。

我的建议是按这个顺序引入。先确保请求 ID 已就位。然后只追踪关键路径,比如结账或提交,并且要做采样:高并发下保留全部 trace,花的钱比学到的东西多。错误请求和慢请求的 trace 全部保留,其余只留一小部分。再把 trace ID 写进日志,放在请求 ID 旁边,让两个工具互相跳转。

SLO 与真正有意义的告警

一条告警只应该回答一个问题:现在是否需要有人动手?CPU 到 85% 很少需要;“2% 的用户结账失败”则一定需要。服务级别目标(SLO)把它具体化:比如“30 天内,99% 的结账请求在 2 秒内成功”,其余部分就是错误预算。

需要呼叫人的情况发工单或群消息即可的情况
结账错误率超过 SLO 燃烧阈值磁盘在缓慢变满
关键路由的 p95 连续数分钟高于目标某个节点 CPU 偏高,但用户未受影响
提交队列的时长在增长某个任务重试一次后成功了
失败任务快速上升证书三周后到期

举个算得出来的例子。假设结账 30 天内收到 3,000,000 个请求,目标是 99% 成功,那么错误预算就是 30,000 个失败或过慢的请求。在每秒 440 个请求的十分钟高峰里,你会处理 264,000 个请求。其中 2% 失败,就是 5,280 次失败:整月预算的约 18% 在十分钟内烧掉。不管 CPU 怎么说,这都该呼叫人。(示例数字。)

每一条无谓吵醒人的告警,都在教团队无视下一条。每次活动之后复盘告警,把吵的删掉或降级。

活动当天的看板

针对定时高峰,我只做一块屏幕。最上面是关键路由的黄金信号,下面是饱和度数字,旁边是队列深度与时长,再加一条目标请求速率线。除此之外什么都不要。需要滚动的看板,没有人会在高峰那一分钟去看。

活动当天看板线框图:延迟、流量、错误、饱和度面板,以及队列深度与时长、数据库连接、Redis 内存
高峰那一分钟只看一块屏:先看四个黄金信号,再看会被耗尽的资源。

在每张图上加发布标记。一半的“神秘回退”,其实是十分钟前上线的一次发布。

如何观察一次压测

压测是你能拿到的最便宜的演练,但看不进去就白做了。要重放上一次高峰的真实请求组合,而不是只打一个 URL。我的压测用 k6 并设了中止阈值:p95 超过 3 秒,或出现任何 5xx 或超时,就停止。再加一个小的守卫脚本,一旦触及 CPU 上限就终止运行,免得压测变成一次真正的故障。

  1. 在第一次运行前定好中止阈值,并写下来。
  2. 运行活动当天要用的那块看板,不要另做测试专用视图。
  3. 给压测流量加一个请求头,方便在日志里过滤。
  4. 把自己的指标与云服务商的指标对照。我的几次测试里两者相差在百分之几以内;如果你的对不上,说明其中一个有问题。
  5. 找出第一个饱和的资源,修好,再按目标速率重跑。
  6. 把结果和代码放在一起,让下一次活动有基线可依。

先核实吓人的告警,再动手

压力之下,人容易对着所有红色的东西动手。请先核实。有些控制台会把容器标错,“CPU 100%”的告警可能指向的并不是你正准备重启的那个进程。先看真实的进程列表,用第二个来源交叉验证,再做决定。

流程图:告警触发;若没有面向用户的信号佐证,就核实真实进程和第二来源;若有佐证,则在 15 分钟内有发布时回滚,否则找出第一个饱和的资源,修复后再看看板
红色告警下一步去哪:大部分工作,是判断它到底是不是真的。

对自己的后台任务同样如此。健康检查和调度器也会消耗资源;有一次,一个每次运行都会拉起新进程的健康检查,在高负载下留下了孤儿进程,让一个数据库容器变得不稳定。给监控本身也配上指标,并不是多疑。

回到 10:01。有了这套东西,客服的消息会带着请求 ID 一起到。一次搜索显示 nginx 等了 PHP 1.31 秒,应用在同一个请求里记录了一条慢查询。看板也印证了这一点:p95 在爬升,60 个 FPM worker 里有 48 个在忙。唯一那条红色的 CPU 告警属于另一个容器,所以没人去重启任何东西。负责人用大约两分钟而不是十分钟找到原因,修复也落在正确的地方。(同样是示例。)

本系列最后一篇讲保持在线的另一半:加固边缘,以及零停机发布变更。

核心要点

  • 在边缘加上请求 ID,并带过 nginx、PHP-FPM、应用日志和队列任务。这是成本最低的大收益。
  • 用一小组稳定字段写结构化 JSON 日志,脱敏个人数据,并有意识地设定保留期。
  • 量四个黄金信号,用分位数而不是平均值,并盯住饱和度:worker、队列时长、连接数、内存。
  • 只对关键路径引入 OpenTelemetry 追踪,并做采样,再把 trace ID 关联到日志。
  • 只为与 SLO 挂钩的用户影响呼叫人,每次活动后清理吵闹的告警。
  • 带着中止阈值演练,与服务商的指标对照,对吓人的告警先核实再行动。

Anichur Rahaman 是一名软件架构师,也是 StoreConsole 的创建者。他为成长型企业设计电商与 ERP 系统,专注于事件驱动架构、数据完整性和自托管部署。

About the Author

Anichur Rahaman

Continue Reading