题干与适用场景
设计一个供多个团队使用的集中式日志系统。10 万个服务实例、容器和批处理任务在稳态下每秒产生 100 万条日志,平均每条原始日志 600 字节。一次大规模故障可能让流量持续 15 分钟升到每秒 300 万条,因为失败请求、重试和异常栈会同时增加。最近日志从产生到可搜索的 p99 目标为 30 秒; 带租户、时间、服务、环境和级别过滤的最近 15 分钟常见查询,p95 目标小于 2 秒。
用户需要按时间、服务、主机、级别和 traceId 查询,也需要受限的全文检索、下载和告警。不同日志类别 有不同保留与可靠性要求:调试日志可以短期保存并在紧急情况下采样;运行错误日志需要可靠留存;安全审计 日志不得被普通降级策略丢弃。系统还要支持租户配额、字段脱敏、访问审计、生命周期管理和历史恢复。
这些规模、延迟、保留期和可靠性目标都是面试输入,不代表任何日志产品的公开承诺。范围不包括实现一个 完整搜索引擎、在面试中重写消息队列,也不要求复制某家公司的内部架构。2026 年的公开系统设计题仍直接 要求候选人设计支持突发写入、检索、保留和多租户隔离的集中式日志平台。OpenTelemetry 的稳定日志数据 模型、Collector 的队列故障说明,以及 Elastic 对数据分层和 mapping explosion 的文档,都能为关键 工程边界提供一手依据。
面试官考察点
第一,看候选人是否把“可靠接收”与“立即可搜索”分开。网关把日志写入复制的持久消息流后,就可以确认 中央系统接收成功;搜索索引可以异步落后几十秒。如果只有写入搜索集群才算成功,索引维护或扩容会直接 反压全部生产者。
第二,看候选人能否正视事故期间的相关性故障。最需要日志时,应用错误、重试风暴和搜索集群压力往往同时 出现。答案必须包含本地磁盘缓冲、中央持久队列、背压、有限容量、分级丢弃和可观测的丢失计数,不能只说 “增加 Kafka 和 Elasticsearch”。
第三,看索引策略是否有成本与基数意识。任意 JSON 属性全部动态建索引,会让高基数字段制造大量映射、 词典和小分片。合理设计只对稳定、常查、类型明确的字段建立索引;其他属性仍可保存,但默认不参与索引。
第四,看多租户隔离是否贯穿写入与查询。租户身份必须来自认证凭据,不能相信日志体中的 tenantId。 一个高流量租户、宽泛正则查询或字段爆炸,不能耗尽共享队列、索引线程、缓存和查询并发。
最后,看容量、交付语义、安全和验证能否闭环。候选人要算原始吞吐与突发积压,明确 at-least-once 会产生重复,说明审计日志与调试日志为何需要不同策略,并能通过端到端 canary 证明 “没有静默丢失”。
回答前需要澄清的问题
- 哪些日志必须不丢? 审计与安全事件要求可靠留存;调试日志允许在明确降级策略下采样或丢弃。
- 确认成功的边界在哪里? 中央网关将事件提交到跨可用区复制的持久消息流后确认,不等待搜索索引刷新。
- 生产者能被日志系统阻塞吗? 普通应用日志不能同步阻塞业务请求;节点代理使用异步批处理和有限本地磁盘。
- 查询模式是什么? 主要是带租户和时间范围的结构化过滤、
traceId精确查找,以及有限窗口的全文检索。 - 是否要求全局顺序? 不要求。只尽量保持单个来源实例内的顺序,跨来源用时间戳和观察时间辅助排序。
- 保留多久? 假设调试日志热索引 24 小时、原始归档 7 天;运行日志热索引 7 天、归档 90 天;
审计日志可搜索 30 天并不可变归档 365 天。
- 需要跨区域主动写吗? 主设计是每个区域独立采集并写本地区域管道,控制面统一;灾备查询可以读归档。
- 是否索引所有字段? 不。稳定的顶层字段与白名单属性可索引,任意高基数属性默认只存储。
- 敏感数据怎么处理? SDK 和节点代理优先阻止或脱敏,中央处理器再次检查;密钥和令牌不应进入日志。
- 允许多大查询范围? 默认强制时间范围、分页游标、扫描预算、并发限制和取消;大范围导出走异步任务。
30 秒回答框架
“我会让应用写标准输出或异步 SDK,由每台节点代理负责批处理、脱敏和有限磁盘缓冲。区域网关完成认证、 租户配额和格式校验,再把日志写入跨可用区复制的持久消息流;成功确认以这里为边界。处理器从消息流读取, 规范化字段,并把原始记录写入对象存储,把允许检索的字段和部分正文写入热搜索索引。查询服务强制租户与 时间范围,并按时间窗口路由热、温和归档数据。稳态原始流量约 600 MB/s、51.84 TB/日;15 分钟 3 倍突发相对稳态会多积压约 1.08 TB。系统采用 at-least-once,使用 eventId 去重;队列或索引故障时 优先保留审计和错误日志,先采样调试日志,并把每次丢弃变成可告警指标。”
分步骤深入解答
先定义六个不变量:
- 日志调用不能让普通业务请求同步依赖远端日志平台。
- 中央系统只有在事件进入复制的持久缓冲后才确认接收成功。
- 租户身份由认证上下文确定,写入、存储、查询和导出都不能越过租户边界。
- 审计日志不能使用调试日志的采样与丢弃策略。
- 任意用户属性默认不动态建立索引,高基数字段不能无限扩展 schema。
- 每个丢弃、延迟、解析失败和脱敏失败都必须可计量,不能静默发生。
第一步:定义采集路径和确认边界。
应用 stdout / 异步 SDK
-> 节点代理或 sidecar:批处理、压缩、脱敏、本地磁盘 spool
-> 区域采集网关:认证、tenant 绑定、配额、强制脱敏、格式与大小限制
-> 跨可用区持久消息流
-> 规范化与路由处理器
-> 对象存储中的原始归档
-> 热搜索索引
-> 告警与流式订阅
-> 查询协调器 -> 热 / 温 / 归档读取应用优先写标准输出,让节点代理与业务进程解耦;有明确结构化事件需求时可以用异步 SDK,但 SDK 也只写 内存队列或本地代理。代理按字节和时间批量发送,限制单条大小,保存当前位置,并在网络故障时写有限磁盘 spool。磁盘满后必须执行按类别定义的策略,不能无限阻塞或无限占盘。
区域网关从 mTLS 身份、工作负载身份或短期凭据解析租户、服务和环境,覆盖日志体中伪造的同名字段。 它做限流、压缩解码、基本 schema 检查、最大事件限制,并在进入中央持久缓冲前执行必须的密钥与令牌 脱敏。事件被复制到至少跨可用区的持久消息流后返回确认。搜索索引、对象归档和告警都是下游消费者, 因此某个消费者故障不会让生产者直接依赖它。
第二步:设计统一但有边界的数据模型。
LogEvent(
eventId, timestamp, observedTimestamp,
tenantId, service, environment, instance,
severityNumber, severityText, body,
traceId, spanId, schemaVersion,
attributes, sensitivityClass
)timestamp 是事件发生时间,observedTimestamp 是采集系统首次观察到事件的时间。机器时钟偏差时,查询 仍可看到采集顺序和延迟。traceId 与 spanId 让日志关联分布式追踪。severityNumber 支持统一比较, severityText 保留来源表达。schemaVersion 允许处理器按版本升级解析规则。
稳定顶层字段使用明确类型并建立索引。attributes 保存额外结构化信息,但只有注册过的白名单字段可进入 索引映射;未知字段可以放入扁平对象、键值列或原始正文。若同一字段一会儿是数字、一会儿是对象,处理器 把它隔离为解析失败或版本化字段,不能让一次坏部署破坏整个共享索引。
eventId 可由代理按来源实例、启动纪元和本地序号生成。系统采用 at-least-once,因此代理未收到确认会 重发,消费者和索引写入以 eventId 幂等。去重窗口有限,归档中也可能保留重复;查询和统计必须知道这一 语义,不能宣称昂贵且脆弱的端到端 exactly-once。
第三步:规划分区与多租户隔离。
消息流按虚拟分区扩展。路由键可由 (tenantId, sourceInstance) 哈希生成,以维持单个来源的大致顺序, 同时把一个租户分散到多个分区。仅按 tenantId 分区会让大租户形成热点;仅按随机键又会失去来源内顺序。 大型租户可以分配独立分区池,小租户共享池,并通过控制面动态调整,而不改变事件格式。
每个租户有写入字节率、事件率、突发桶、本地与中央 backlog、索引字段数、热存储量、查询并发、扫描字节 和导出任务配额。配额拒绝和降级按租户记录。系统级过载时,优先级可以是:
- 保留审计和安全事件。
- 保留错误和关键运行事件。
- 对重复告警、信息和调试日志按已声明策略采样。
- 拒绝新的低优先级大范围查询和导出。
共享热索引适合小租户,但必须在每个文档、缓存键和查询计划中带可信 tenantId。高合规或超大租户可进入 独立索引与加密边界。不能为每个小租户每天创建大量空分片,也不能把所有租户塞进没有配额的单一索引。
第四步:把原始归档与搜索索引分开。
对象存储保存规范化后的原始事件,按区域、租户、日期、小时和可选服务进行大文件分区,并使用列式格式与 压缩。它是低成本长期真相,可用于合规导出、历史查询以及重建损坏的热索引。对象写入先聚合成合适大小的 文件,避免每条日志一个对象。
热搜索索引只保存近期和允许检索的数据。稳定字段使用倒排或列式索引;正文全文索引可以按类别启用。 高基数的请求 ID、用户 ID 或任意标签如果并不常查,就保留为存储字段,不默认建立聚合结构。traceId 虽然高基数,但有明确的精确查找价值,可以用专用精确字段和有限保留期,而不是允许任意动态字段复制它。
生命周期控制器按日志类别把数据从 hot 移到 warm、cold 或 frozen/归档层。热层为写入和低延迟查询配置 更多计算与副本;较旧数据降低成本并接受更慢访问。保留删除要同时覆盖索引、归档、缓存、导出和法律保留, 不能只删一个搜索索引就宣称完成。
第五步:设计受控查询路径。
查询 API 默认要求 tenantId 来自会话,并强制 startTime、endTime、服务或其他选择性条件。查询 协调器先看时间范围和日志类别,再把请求路由到热索引、温层或异步归档扫描。常见接口可以是:
POST /logs/search
{ startTime, endTime, services, severities, traceId, query, cursor, limit }
-> { events[], nextCursor, partial, scannedBytes }
POST /logs/exports
{ startTime, endTime, filters }
-> { jobId }结果使用稳定排序键 (timestamp, eventId) 和游标分页,避免深 offset。交互查询设置扫描字节、返回行数、 执行时间和并发上限;到达上限时返回明确的 partial 标记,而不是悄悄漏数据。宽泛正则、90 天全文搜索和 大型导出进入异步队列,用户可以取消。查询服务还要对热点条件缓存元数据,但不能跨租户复用未隔离结果。
“最近 15 分钟过滤查询 p95 小于 2 秒”只适用于选择性过滤和健康集群。没有时间范围的全库全文检索不能 沿用同一 SLO。面试中把不同查询类别写清楚,比承诺所有查询都快更可信。
第六步:完成容量估算。
稳态原始吞吐为:
1,000,000 events/s × 600 bytes = 600 MB/s
600 MB/s × 86,400 s = 51.84 TB/day15 分钟内流量从每秒 100 万升到 300 万。如果下游只能维持稳态处理能力,需要吸收的额外积压是:
(3,000,000 - 1,000,000) × 600 bytes × 900 s = 1.08 TB这 15 分钟总共进入 1.62 TB,但 0.54 TB 属于同时被稳态能力处理的基线。实际缓冲还要考虑副本、批次 开销、故障恢复和安全余量,因此不能把 1.08 TB 直接当作磁盘采购数。
若只有 20% 日志进入全文或结构化热索引,并保留 7 天,索引前原始输入仍约为:
51.84 TB/day × 20% × 7 = 72.576 TB真实索引容量取决于字段、压缩、分片和副本,需要压测测量。90 天原始归档约为 51.84 TB × 90 = 4.6656 PB,压缩前约 4.67 PB。这个数量说明分层、保留类别、压缩和减少无价值日志 比单纯扩搜索集群更重要。
分区数从每分区可持续字节率、事件率和故障时重放速度反推。除了稳态吞吐,还要验证失去一个可用区、消费者 追赶和 3 倍突发同时发生时,最老消息年龄仍能恢复。不能只用消息条数,因为异常栈会显著改变平均大小。
第七步:处理背压、日志风暴和下游故障。
每一层都有有限容量:SDK 内存队列、代理磁盘、网关连接、消息流保留、处理器并发、索引写队列和查询线程。 系统通过 retry-after、批次缩小、消费者暂停和优先级队列逐级传递压力。代理磁盘接近上限时,先按策略 采样调试日志,并发出包含丢弃类别与数量的本地计数;审计事件进入独立保留池或触发显式业务失败策略。
OpenTelemetry Collector 的故障排查文档指出,目标不可用或 Collector 配置不足都可能造成丢弃;发送队列 和重试能覆盖暂时故障,但过大的队列也会造成内存压力。这意味着“打开重试”并不等于可靠:必须同时监控 队列使用量、最老事件、拒绝、永久失败和进程内存,并在容量耗尽前告警。
索引集群不可用时,消息流继续接收,原始归档消费者与索引消费者独立推进。恢复后索引消费者按租户公平 追赶,并限制回放速度,避免把刚恢复的集群再次压垮。若索引落后,UI 显示搜索新鲜度和缺失时间窗;不能让 用户误以为“无搜索结果”等于“没有日志”。
第八步:关闭安全、隐私和删除环。
最有效的敏感信息控制是在生成端不记录。SDK 提供结构化字段白名单,代理屏蔽常见令牌、密码、Cookie 和 个人信息模式;网关在写入中央持久流前执行强制规则,后续处理器再按 schema 做语义脱敏,并把失败事件送 隔离队列。原始正文也不能因为“只进归档”就绕过安全控制。
传输使用 mTLS 或短期工作负载身份,静态数据加密,密钥按环境或合规租户隔离。RBAC 至少限制租户、环境、 服务、字段和时间范围;查询、导出、保留变更和法律保留都写不可变访问审计。高敏字段可以做字段级加密或 完全删除,不能依靠 UI 隐藏。
删除流程用可追踪工作流覆盖热索引、温层、对象分区、缓存和导出副本。审计保留与隐私删除发生冲突时, 产品和法务必须定义优先级、例外和法律保留;系统设计只能执行明确政策,不能用“日志不可变”回避删除责任。
第九步:用指标与故障注入证明系统。
核心指标包括产生到网关、网关到持久流、持久流到归档、持久流到可搜索的 p50/p95/p99;每层队列使用量、 最老事件年龄、重试和丢弃;按租户的字节率、限流、字段基数与成本;索引拒绝、mapping 增长、查询扫描量、 超时、取消和缓存命中;脱敏失败、越权拒绝和归档恢复成功率。
端到端 canary 每分钟从各区域写入带唯一 ID 的结构化日志,并分别验证持久接收、搜索可见、对象归档和 到期删除。计数对账比较代理发送、网关接受、消息流提交、归档写入和索引成功数量,允许解释过的重复, 不允许无法归因的差值。
故障测试至少覆盖:持续 15 分钟的 3 倍日志风暴、搜索集群不可用 30 分钟、对象存储限流、一个可用区 故障、热点租户、高基数字段攻击、错误 schema、机器时钟偏差、敏感数据注入、代理磁盘耗尽、重复批次、 消息重放和从归档重建热索引。关键断言是:审计事件在策略要求内可恢复;普通日志的任何丢弃都有租户、 类别、时间与数量证据;搜索新鲜度退化时用户和告警系统都能知道。
高质量示范回答
“我先把可靠接收与搜索可见分开。应用写标准输出或异步 SDK,节点代理负责批处理、脱敏和有限磁盘缓冲, 因此日志平台故障不会同步阻塞普通业务请求。区域网关从工作负载身份确定租户,执行配额和格式校验,再把 批次写入跨可用区复制的持久消息流。这个提交点就是中央接收成功;搜索索引和对象归档异步消费。
事件采用稳定顶层字段:事件时间、观察时间、租户、服务、环境、实例、级别、正文、traceId、spanId、 schema 版本和敏感级别。任意 attributes 默认只存储,只有类型稳定、确实常查的白名单字段才建索引,避免 高基数字段造成 mapping 和索引放大。系统是 at-least-once,代理用稳定 eventId 重试,索引写入幂等, 但归档可能保留可识别的重复。
对象存储保存可重放的长期原始记录,热搜索层只保留近期和允许检索的数据。查询必须带可信租户和时间范围, 按热、温、归档层路由,并限制扫描字节、并发和返回量。宽泛历史全文检索与导出进入异步任务。小租户共享 索引,大租户可独立隔离,但写入、backlog、字段和查询配额始终按租户执行。
容量上,稳态是 600 MB/s、51.84 TB/日。15 分钟 3 倍突发在下游只处理稳态流量时会多积压约 1.08 TB。 若只索引 20% 并保留 7 天,索引前仍有约 72.6 TB;90 天原始归档压缩前约 4.67 PB,所以必须做选择性 索引、分类保留和冷热分层,不能把所有字段永久放在热搜索集群。
故障时,消息流吸收短期积压,索引消费者与归档消费者独立恢复。每层容量都有上限:优先保留审计和错误 日志,先采样调试日志,并把丢弃计数、最老消息年龄和搜索新鲜度暴露给监控与 UI。最后我会用唯一 canary 贯穿生产、持久流、索引和归档,并注入 3 倍日志风暴、下游停机、高基数字段和磁盘耗尽,证明没有静默 丢失,也能从归档重建索引。”
常见错误
- 应用同步调用远端日志 API → 日志故障拖垮业务请求 → 写本地异步代理,并使用有限缓冲与明确降级。
- 写进搜索集群才确认成功 → 索引维护会反压全部生产者 → 在复制的持久消息流提交后确认。
- 所有 JSON 字段自动建索引 → 高基数字段造成 mapping explosion 和成本失控 → 只索引稳定白名单字段。
- 只按租户 ID 建一个消息分区 → 大租户成为单分区热点 → 使用租户与来源的虚拟分区并提供独立分区池。
- 随机分区后宣称全局顺序 → 跨来源时钟和并行处理无法保证 → 只维护来源内顺序,并保存观察时间。
- 用 exactly-once 掩盖重复 → 响应丢失和重放仍会发生 → at-least-once、稳定
eventId和幂等写入。 - 队列无限大就不会丢日志 → 磁盘、内存和保留最终都会耗尽 → 容量上限、最老年龄、优先级和丢弃证据。
- 日志风暴时统一采样 → 审计证据也被丢弃 → 按日志类别分别定义可靠性与降级策略。
- 所有查询都承诺 2 秒 → 90 天全文扫描会破坏交互集群 → 区分选择性热查询和异步历史扫描。
- UI 没结果就当作没有日志 → 索引延迟隐藏事故证据 → 展示搜索水位、partial 标记和缺失时间窗。
- 只删除热索引 → 归档、缓存和导出仍保留敏感信息 → 使用覆盖所有副本的可审计删除工作流。
- 只监控 Collector 是否存活 → 进程健康也可能持续丢数据 → 对账每阶段数量并监控队列、拒绝与永久失败。
追问及应对
追问一:为什么不让代理直接写搜索集群?
小规模内部工具可以这么做,组件少、搜索新鲜度也更直接。但在本题规模下,搜索集群的分片调整、mapping 问题和写入拒绝会直接传到 10 万个来源。持久消息流提供明确确认边界、突发吸收、消费者独立恢复和重放; 代价是额外运维、重复事件和几十秒异步延迟。若实测规模很小,应选择直接路径,而不是为架构图增加队列。
追问二:如何处理一个字段突然产生数百万种值?
schema 注册表记录可索引字段、类型、拥有团队和基数预算。处理器用近似去重计数监控字段基数;超过阈值 时停止为该字段创建新索引结构,把值保留为未索引属性,并通知租户。已经发生 mapping explosion 时, 先阻止新字段、修正上游格式,再把健康字段重建到新索引;仅拆分更多索引不会消除无界字段增长。
追问三:消息流和本地 spool 都满了怎么办?
先承认系统无法用有限资源保证无限突发。代理按日志类别保留独立预算:审计事件进入更强的独立通道,错误 优先于信息,调试日志先采样或丢弃。每次丢弃写本地计数并在恢复后上报。若业务规定某项审计必须可靠, 相关操作可以显式失败或进入本地事务 outbox;普通 debug 调用不能因此无限阻塞整个应用。
追问四:如何从对象存储重建搜索索引?
归档对象保存 schema 版本、时间分区、租户、校验和和对象清单。重建任务选择租户与时间窗,读取清单并 校验,按当前映射转换,使用 eventId 幂等写入新索引,再比较事件数、时间边界和 canary。完成后通过 别名或路由原子切换。重建流量有独立配额,不能抢占实时索引写入。
追问五:如何避免一个租户的查询拖垮所有人?
查询调度器按租户维护并发、CPU 时间、扫描字节和返回量令牌桶,并使用加权公平队列。交互查询优先于导出, 昂贵查询可取消并转异步。共享缓存键包含租户与权限摘要。大型或高合规租户可以使用独立索引池,但隔离前 先测量瓶颈,避免为每个小租户制造大量小分片。
追问六:审计日志为什么需要单独策略?
审计日志的价值是证明谁在何时做了什么,因此采样、可变内容和普通管理员删除都会破坏用途。它需要更严格 schema、来源身份、完整性校验、不可变归档、受限查询和访问审计。仍要遵守隐私删除与法律保留政策,所以 “不可变”表示普通路径不能修改,不表示永远不执行受授权的合规工作流。
追问七:跨区域如何设计?
每个区域先把本地日志写入本地持久流和归档,避免业务请求跨洲等待。事件携带区域和全局租户 ID,控制面 下发 schema、配额与保留策略。全局查询协调器按时间和区域扇出,并对部分区域不可用返回 partial。若监管 要求数据驻留,原始日志不跨区,只同步允许的索引或聚合。区域灾难后可从本地或合规复制的对象归档恢复。
追问八:如何证明系统没有静默丢日志?
每个批次记录来源事件数和校验和,各阶段输出接受、重复、拒绝、永久失败和写入成功计数。唯一 canary 持续穿过代理、网关、消息流、索引和归档。对账允许由 eventId 解释的重复,以及由明确配额策略解释的 丢弃;任何无法归因的差值都触发事故。再停止索引 30 分钟并恢复,验证消息年龄回落、canary 补齐和 UI 新鲜度恢复,而不只检查进程是否重新启动。