安全与合规高并发系统的安全与发布:加固的边缘层与零停机部署
从 CDN、WAF 到私有网络的安全分层,以及由滚动发布、blue/green 和 expand and contract 迁移组成的交付流水线,最后附上流量尖峰当天的上线清单。
请求 ID、写入 OpenSearch 的结构化日志、四个黄金信号、采样的链路追踪,以及与用户影响挂钩的告警:几分钟内定位慢的那一层所需的最小集合,外加压测和活动当天该怎么盯。
Author
Anichur Rahaman

“用户说结账页卡住了。”这条消息在发售当天早上 10:01 送到一家票务网站的平台负责人手里。10:00 开售,大约一千人在同一分钟点了进来,预案按每秒 440 个请求来算。监控面板上 CPU 才 40%,没有任何红色告警。
到底哪一层慢?是某个路由、某个节点,还是某一个用户?只靠控制台和猜测,接下来十分钟都会花在争论上。有了请求 ID 和几项挑出来的指标,这十分钟只需要一次搜索。(这一幕只是示例,并非真实事故。)
这并不需要庞大的平台。你需要的是请求 ID、结构化日志、四个黄金信号,以及与用户影响挂钩的告警。我曾为一次定时高峰做准备,预计约一千名用户在同一分钟内操作;服务器没有问题,险些出事的是没人能很快说清哪一层慢,也说不清那个吓人的数字是不是真的。本文把能回答这些问题的最小集合搭起来,再说明如何用它盯住一次压测和活动当天。
本文是“高并发系统工程”系列五篇中的第 4 篇。第 1 篇讲到底什么能伸缩,第 2 篇讲队列与 worker,第 3 篇讲数据层。这一篇的主题是:把这一切看清楚。
监控告诉你出了问题;可观测性让你不必先上线新代码,就能追问“为什么”。它建立在三类数据之上,通常称为信号:
第一天不必三样齐备,也不需要昂贵的平台。关键是让它们彼此相连,这样你能从一个变红的指标,直接跳到某个慢请求的日志和 trace。把它们串起来的,就是请求 ID(request ID)。
我知道的最便宜的改进,就是一个从边缘一路跟到最后一个队列任务的标识。在我运行过的这套部署里,nginx 会沿用传入的 X-Request-Id 请求头,没有就自己生成,再交给 PHP-FPM,两边都把它写进日志。应用随后把它加到每一行日志里,并复制进它派发的任务载荷,这样后台任务就能对应回触发它的那次点击。
也把同一个 ID 放进响应头返回。用户或客服反馈问题时,凭这一串字符就能还原整件事。

| 层级 | 怎么做 |
|---|---|
| 边缘 / CDN | 转发传入的 ID,或交给下一层生成 |
| 宿主机 nginx 与容器内 nginx | 沿用传入的 ID,缺失就生成,并写进访问日志 |
| PHP-FPM 与应用 | 从请求头读取,作为共享上下文附加到每行日志 |
| 队列任务 | 存入任务载荷,任务运行时再恢复 |
| 对外 HTTP 调用 | 继续传递,让合作方和内部服务也能记录 |
| 响应 | 放进响应头,方便客服索取 |
普通文本日志是写给人一条条读的。在高并发下,你需要搜索、统计、过滤,这就要求每行一个 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。对这类平台,我最先看的是:
日志和指标告诉你请求慢了。trace 则告诉你:它在 nginx 花了 40 ms,在 PHP 花了 90 ms,等一条查询等了 1.2 秒,在 Redis 花了 30 ms。对于有前端层、后端层和 worker 的系统,这是找出问题环节最快的办法。
OpenTelemetry 是产出这类数据的厂商中立标准。按其规范状态页的说法,追踪和日志信号已稳定,指标的 API、协议和数据模型也已稳定,但各语言 SDK 的成熟度不一。PHP 项目的文档称,追踪、指标和日志均已可用,并提供可选扩展来实现自动埋点。动手前,请先核对你实际要用的那些库的状态。
我的建议是按这个顺序引入。先确保请求 ID 已就位。然后只追踪关键路径,比如结账或提交,并且要做采样:高并发下保留全部 trace,花的钱比学到的东西多。错误请求和慢请求的 trace 全部保留,其余只留一小部分。再把 trace ID 写进日志,放在请求 ID 旁边,让两个工具互相跳转。
一条告警只应该回答一个问题:现在是否需要有人动手?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 怎么说,这都该呼叫人。(示例数字。)
每一条无谓吵醒人的告警,都在教团队无视下一条。每次活动之后复盘告警,把吵的删掉或降级。
针对定时高峰,我只做一块屏幕。最上面是关键路由的黄金信号,下面是饱和度数字,旁边是队列深度与时长,再加一条目标请求速率线。除此之外什么都不要。需要滚动的看板,没有人会在高峰那一分钟去看。

在每张图上加发布标记。一半的“神秘回退”,其实是十分钟前上线的一次发布。
压测是你能拿到的最便宜的演练,但看不进去就白做了。要重放上一次高峰的真实请求组合,而不是只打一个 URL。我的压测用 k6 并设了中止阈值:p95 超过 3 秒,或出现任何 5xx 或超时,就停止。再加一个小的守卫脚本,一旦触及 CPU 上限就终止运行,免得压测变成一次真正的故障。
压力之下,人容易对着所有红色的东西动手。请先核实。有些控制台会把容器标错,“CPU 100%”的告警可能指向的并不是你正准备重启的那个进程。先看真实的进程列表,用第二个来源交叉验证,再做决定。

对自己的后台任务同样如此。健康检查和调度器也会消耗资源;有一次,一个每次运行都会拉起新进程的健康检查,在高负载下留下了孤儿进程,让一个数据库容器变得不稳定。给监控本身也配上指标,并不是多疑。
回到 10:01。有了这套东西,客服的消息会带着请求 ID 一起到。一次搜索显示 nginx 等了 PHP 1.31 秒,应用在同一个请求里记录了一条慢查询。看板也印证了这一点:p95 在爬升,60 个 FPM worker 里有 48 个在忙。唯一那条红色的 CPU 告警属于另一个容器,所以没人去重启任何东西。负责人用大约两分钟而不是十分钟找到原因,修复也落在正确的地方。(同样是示例。)
本系列最后一篇讲保持在线的另一半:加固边缘,以及零停机发布变更。
Anichur Rahaman 是一名软件架构师,也是 StoreConsole 的创建者。他为成长型企业设计电商与 ERP 系统,专注于事件驱动架构、数据完整性和自托管部署。
About the Author
Anichur Rahaman
Continue Reading