题干与适用场景
设计一个供多支研发团队使用的指标监控与告警系统。它监控 5 万个服务实例,每个实例暴露 200 条活跃时间 序列,每 15 秒采集一次。系统要支持计数器、仪表、直方图,按标签筛选和聚合,展示看板,并按规则发送告警。 新故障从进入采集路径到告警进入通知路由的 p99 不超过 60 秒;最近 6 小时的常用看板查询 p95 不超过 2 秒。原始样本保留 7 天,1 分钟聚合保留 90 天,1 小时聚合保留 13 个月。
题目假设单地域、三个可用区部署,租户身份由可信认证信息给出。通知渠道可以复用现有邮件、即时消息和电话 服务,本题负责可靠地生成、去重、分组和路由通知。日志与链路追踪不在主设计范围内。面试中需要重点解释 高基数标签、重复和迟到样本、缺失数据、规则状态、查询隔离,以及监控平台自身故障时如何被发现。
2026 年的中英文公开材料都直接把 metrics monitoring and alerting system 作为系统设计面试题,讨论时间序列 写入、查询、告警和高可用。Prometheus 官方资料提供了时间序列身份、拉取采集、WAL、告警 pending/firing、 分组与抑制等一手语义;Google SRE 则给出低噪声告警和黑盒监控原则。因此,本题既有当前代表性,也有可核验 的技术依据。
面试官考察点
第一,候选人能否把“样本吞吐”和“活跃时间序列数”分开。写入量由实例数、每实例序列数和采集周期决定; 内存、索引和查询扇出往往先被基数击穿。把 userid 或 requestid 放进标签,可能在流量不变时制造数量级 更高的新序列。
第二,写入确认、看板查询和告警发现是否形成闭环。采集器拿到数据不代表数据已经耐久;告警规则也不能依赖 一个被重查询拖垮的共享资源池。强答案会给写入持久边界,为告警保留独立预算,并明确迟到、重复、空值和 部分查询结果的语义。
第三,存储结构是否匹配访问模式。最近数据会被高频追加和查询,历史数据适合不可变块、压缩、索引和对象 存储。降采样不能简单地对平均值再次求平均;至少要保留 sum、count、min、max,直方图也要按兼容 桶边界合并。
最后,告警系统要在事故期间仍可工作。事故会同时增加错误指标、查询和通知数量。候选人需要处理规则分片、 状态恢复、通知去重、告警风暴、租户噪声、监控系统自监控,以及独立于本平台的端到端存活探针。
回答前需要澄清的问题
- 采集对象是否稳定? 大部分是长生命周期服务,由服务发现提供目标;短任务和受限网络通过推送网关接入。
- 规模是全局还是单地域? 题设数字按单地域峰值计算,跨地域扩展时各地域独立采集,再提供受控的全局查询。
- 确认边界是什么? 写入网关只有在样本进入跨可用区复制的耐久日志后才返回成功。
- 允许重复和乱序吗? 允许网络重试带来的重复,也允许有限窗口内的迟到;同序列同时间戳采用确定性冲突规则。
- 缺失样本等于零吗? 不等于。缺失可能是目标宕机、采集失败或网络分区,必须与真实数值零区分。
- 查询是否都要 2 秒完成? 只承诺有界的最近 6 小时常用查询;超大基数、跨月原始查询受配额和异步分析限制。
- 告警如何判定恢复? 规则定义评估周期、持续时间、缺失数据策略和恢复条件,不能由通知服务猜测。
- 租户能否自定义任意标签? 可以在配额内使用,但受标签数量、长度、活跃序列和新序列速率限制。
- 是否要求 exactly-once 通知? 不要求。通知采用至少一次投递,接收端以告警指纹和状态转换做幂等。
- 原始与聚合数据的关系? 聚合作业可从仍在保留期内的原始块重算,聚合块记录分辨率和覆盖水位。
30 秒回答框架
“先算规模:活跃序列是 1000 万条,写入约 66.7 万样本/秒、576 亿样本/天。采集器通过服务发现分片拉取, 短任务走推送网关;标准化后的样本按租户和序列指纹进入跨可用区耐久日志,再写入最近数据层和不可变时间块。 标签索引支持查询,基数预算限制 user_id 一类危险标签。规则引擎用独立资源按固定时间点评估,维护 inactive、pending、firing 状态,告警管理器负责去重、分组、静默、抑制和路由。最后用十倍新序列冲击、重复 迟到样本、可用区故障和外部 canary 验证写入、查询及告警链路。”
分步骤深入解答
先确定六条不变量:租户身份不能由样本标签声明;已经确认的样本进入了耐久日志;同一重试不会制造不同 结果;缺失样本不被偷偷改写成零;看板压力不能饿死告警;所有拒绝、丢弃、延迟和降级都有可观测计数。
第一步:复算规模,把基数当成一等公民。
活跃序列数是:
50,000 instances × 200 series/instance = 10,000,000 active series
10,000,000 ÷ 15 seconds ≈ 666,667 samples/second
666,667 × 86,400 ≈ 57.6 billion samples/dayPrometheus 官方存储文档给出的本地样本粗略起点是平均 1–2 字节/样本。按这个量级,压缩样本块约为 57.6–115.2 GB/天;三副本约为 172.8–345.6 GB/天。这个估算不含标签索引、WAL、head、对象元数据、 副本以外的安全余量,也不能替代对实际指标分布的压测。容量评审必须把这些开销分别列出。
更危险的是新序列。如果请求级 user_id 进入标签集合,序列身份会随标签值变化。系统要同时限制每租户和 每指标的活跃序列数、新序列创建速率、标签数量、标签长度与允许键,并提供先告警、再隔离或拒绝的策略。
第二步:选择拉取为主、推送为辅的采集模型。
服务发现把目标列表交给采集器,控制面用一致性哈希或 rendezvous hashing 分配目标。采集器每 15 秒通过 HTTP 拉取,给样本附加可信的租户、集群、任务和实例信息,同时生成 up、采集耗时、样本数和抓取错误。 拉取模型天然知道“目标存在但抓取失败”,也便于控制节奏。
短生命周期批任务、无入站网络或只支持 OTLP 推送的环境通过区域网关接入。网关认证身份、限制批量大小、 标准化字段后进入同一写入契约。推送成功只能表示进入本平台的耐久边界,不能把客户端发送成功当成最终存储 成功。采集器断线时使用有界本地 spool;空间耗尽要按策略拒绝并计数,不能无限吃满磁盘。
第三步:定义时间序列身份和写入契约。
一条序列由 tenantid + metricname + canonical_labels 唯一标识;标签按键排序并规范编码后计算指纹。指纹碰撞 时仍须比较完整身份,不能只相信哈希。样本至少包含序列身份、源时间、接收时间、值或直方图、数据类型和 采集来源。
WriteBatch {
tenantId, sourceId, requestId,
samples: [{ metric, labels, sourceTimestamp, value }]
}网关依次执行认证、标签规范化、类型校验、基数预算、时间范围和批量限制,然后按 tenantid + seriesfingerprint 分区写入跨可用区复制日志。日志提交是 ack 边界。同一 requestId 可安全 重试;存储层再以 (seriesid, sourcetimestamp) 去重。同时间戳不同值要采用固定规则并暴露冲突计数,不能 依赖到达顺序悄悄覆盖。
第四步:把最近数据、历史块和索引分层。
最近数小时保存在可追加的 head 中,支持迟到窗口内的有限修改。后台把数据冻结成按时间和分片组织的不可变 块,块内使用时间戳差分和值压缩,随后上传对象存储。块元数据记录最小/最大时间、租户、分辨率、校验和与 覆盖水位;压缩器合并小块并删除已经被完整覆盖的旧块。
倒排索引从标签键值映射到序列 ID,再由序列 ID 定位数据块。高频标签和序列元数据可缓存,但租户边界必须 进入缓存键。降采样按 1 分钟和 1 小时生成新块,保留 sum/count/min/max;计数器还要处理重置,直方图只在 桶 Schema 兼容时合并。原始块保留 7 天,1 分钟块 90 天,1 小时块 13 个月。
第五步:让查询成本可预测。
查询协调器解析时间范围、标签选择器、聚合和步长,先鉴权,再用索引找序列并向相关块并行读取。只有查询的 步长和函数允许时才选聚合块;例如平均值用 sum/count 重算,分位数不能从已有分位数再次聚合。最近数据要 合并 head 与已经落盘的块,并按统一水位去重。
每次查询都有最大序列数、扫描点数、并发、内存和截止时间。普通看板使用固定步长、预聚合和短时结果缓存, 目标是最近 6 小时 p95 低于 2 秒;跨月高基数探索可转为异步任务。分片失败时 API 必须返回 partial=true 和缺失范围,告警规则默认不接受部分结果。
QueryRange {
tenantId, expression, start, end, step, maxSeries
}
QueryResult { data, resolution, watermark, partial, warnings }第六步:把规则评估和通知路由拆开。
规则调度器按租户和规则组分片,每 15 或 30 秒在明确的评估时间点运行。告警实例由规则名和结果标签生成稳定 指纹,状态在 inactive -> pending -> firing 之间转换。for 持续时间过滤瞬时尖峰;keepfiringfor 或明确 回滞避免短暂缺数导致反复恢复。规则还要选择缺失数据是正常、告警还是保留未知状态。
两个高可用评估器可以向告警管理器发送相同指纹,由后者去重。告警管理器按团队、服务和严重级别路由,把同一 事故的实例分组;集群不可达时抑制其下游实例告警;维护窗口使用有期限静默。通知状态保存在复制存储中,投递 采用至少一次语义,以 alertfingerprint + statetransition + receiver 做幂等。告警计算、状态和通知队列使用 独立于临时看板查询的资源池。
第七步:处理故障、租户隔离和自监控。
采集器失效后,目标租约转移到健康实例;短暂重叠采集依靠去重吸收。日志或存储背压时,网关降低批量并明确 拒绝,不能在内存中无限排队。对象存储故障时继续写复制日志和有界 head,并暂停块回收;超过安全水位后按 租户配额保护告警关键指标。查询故障不应阻止规则从已确认水位读取。
租户身份来自 mTLS 或服务端令牌,不能相信样本中的 tenant 标签。写入、查询和规则分别设置限额;RBAC 控制 看板与告警配置;审计规则、静默和路由变更。系统自身监控写入延迟、日志积压、head 水位、块年龄、查询扫描 量、规则评估延迟、pending/firing 数量和通知失败。
自监控仍有共同故障域,因此另设外部黑盒 canary:从独立环境持续写入已知指标,等待规则转为 firing 并收到 通知,再自动恢复。外部 dead-man 检查若未按期收到心跳,直接走独立通知路径。这能发现“监控平台完全沉默”。
第八步:用压力与故障注入验证承诺。
稳态压测至少达到 66.7 万样本/秒,并验证两倍突发;随后制造十倍新序列速率,检查危险租户是否被隔离、其他 租户和规则评估是否仍满足 SLO。数据正确性测试覆盖重复批次、同时间戳冲突、迟到窗口、计数器重置、直方图 合并、聚合重算和块压缩前后结果一致。
故障测试依次杀死采集器、一个可用区的日志副本、head 节点、查询分片、规则评估器和通知提供商。验证 ack 样本不丢、租约转移可去重、规则状态可恢复、通知可重试且不会形成无界风暴。最后测量 canary 从写入、规则 持续时间、分组等待到通知到达的端到端延迟,确认 p99 不超过 60 秒或明确是哪一段耗尽预算。
高质量示范回答
“我先把 5 万实例乘以每实例 200 条序列,得到 1000 万活跃序列;再除以 15 秒,写入约 66.7 万样本/秒, 一天 576 亿样本。容量上必须同时预算样本块和标签索引,并限制每租户活跃序列与新序列速率,防止 user_id 把基数打爆。
主路径使用服务发现和分片采集器拉取;短任务走推送网关。样本经过认证、标签规范化和配额检查后,按租户与 序列指纹写入跨可用区复制日志,提交后才 ack。最近数据进入 head,随后冻结为不可变压缩块和标签倒排索引, 上传对象存储;后台生成 1 分钟和 1 小时聚合,平均值由 sum/count 重算。
查询协调器按时间、标签和分辨率裁剪块,并用序列数、扫描点、内存和截止时间控制成本。规则引擎使用独立资源 在固定时间点评估,维护 inactive、pending、firing 状态;告警管理器按指纹去重,再分组、抑制、静默和路由。 缺失数据有显式策略,通知采用至少一次投递。
我会用 66.7 万样本/秒稳态、两倍突发和十倍新序列冲击压测,再注入重复迟到样本、可用区故障、查询分片失败 与通知商故障。独立环境的 canary 持续完成写入到通知的全链路,既验证 60 秒 p99,也能发现监控平台整体 失声。”
常见错误
- 只计算样本/秒 → 标签索引和活跃序列可能先耗尽内存 → 同时计算 1000 万基数和新序列速率。
- 允许任意
user_id标签 → 每个取值都会生成新序列 → 设置标签策略、基数预算和隔离队列。 - 收到请求就 ack → 进程或可用区故障会丢掉已确认数据 → 以复制日志提交作为确认边界。
- 把缺失样本当成零 → 采集失败会伪装成业务归零 → 保留 stale/unknown,并由规则选择缺失策略。
- 对平均值再次求平均 → 不同样本量的时间桶会产生偏差 → 保存
sum/count后重算。 - 声称所有查询都能 2 秒返回 → 无界标签与时间范围无法控制成本 → 限定 SLO,设置查询预算和异步路径。
- 规则查询允许部分结果 → 少一个分片可能把事故误判为恢复 → 告警默认要求完整水位或进入未知状态。
- 每个实例单独发通知 → 大故障会制造告警风暴 → 按事故分组,并使用抑制、静默和速率限制。
- 承诺 exactly-once 通知 → 超时重试无法区分已送达与未送达 → 采用至少一次并用稳定键幂等。
- 只用本系统监控自己 → 整个平台故障时没有信号 → 增加独立黑盒 canary 和 dead-man 路径。
追问及应对
追问一:为什么不让所有客户端直接推送?
稳定服务适合拉取:平台掌握目标清单和频率,能区分业务数值与抓取失败,也能统一附加可信资源标签。短任务、 无入站网络和现成 OTLP 客户端适合推送。两条入口最终使用同一验证与耐久写入契约,避免产生两套查询语义。
追问二:如何控制高基数而不误伤正常业务?
同时看存量和增量:每租户、每指标限制活跃序列数,并限制每分钟新序列率。上线前提供标签基数预览;接近 阈值先告警,超限后可丢弃指定危险标签、隔离整个指标或拒绝新序列,策略由租户等级确定。平台保留丢弃计数 和代表样本,方便定位来源,但不能把原始敏感标签写入普通日志。
追问三:迟到样本如何影响告警?
规则在固定评估时间点读取一个已确认水位,并可设置小的评估延迟吸收正常迟到。水位之后到达的旧样本可以 更新历史查询,但默认不回滚已经发送的人类通知;若业务需要更正,生成带版本的修正事件。迟到窗口、最大 时钟偏差和冲突规则必须公开且可测试。
追问四:为什么不能直接保存分位数?
多个实例或时间桶的 p95 不能通过再次取平均或再求 p95 得到全局 p95。应保存可合并的直方图桶或草图,查询 时合并分布后计算分位数。桶边界或草图参数不兼容时必须分开查询或显式迁移,不能静默拼接。
追问五:如何避免高可用规则引擎重复通知?
两个评估器可以都计算和发送同一告警实例,实例指纹由规则和结果标签稳定生成。告警管理器在复制状态上按 指纹、状态和接收端去重、分组并路由。这样不需要把规则评估强行做成单点;网络分区时仍接受极少量重复通知, 接收端继续以同一幂等键防重。
追问六:对象存储不可用时怎么办?
继续把新样本写入复制日志和有界 head,暂停块上传、压缩删除与超出安全水位的历史接纳。优先保障告警使用的 关键指标,并通过租户配额阻止噪声租户占满空间。如果预计无法在日志保留窗口内恢复,系统必须开始明确拒绝 并通知值班人员,不能假装全部写入成功。
追问七:全局多地域查询怎么做?
每个地域独立采集和告警,避免跨地域网络故障让本地告警失声。全局查询协调器只读取各地域已经发布的不可变 块或受控查询端点,返回每个地域的水位和 partial 状态。真正需要全局告警的少数指标先在地域内聚合,再把 低基数结果复制到独立规则域。
追问八:如何判断 60 秒告警目标被哪一段拖慢?
在 canary 样本上记录源时间、接收时间、日志提交水位、规则评估时间、pending 起止、分组等待和通知接收时间。 端到端直方图按阶段拆分,且由外部探针验证最终到达。若规则配置了 5 分钟 for,它属于产品条件,不应混进 “平台发现延迟”;SLO 要分别报告平台处理时间与规则要求的持续时间。