题干与适用场景
设计一个内部服务发现系统,服务于 2,000 个逻辑服务和 10 万个动态实例,实例分布在 3 个地域。容器重调度、自动扩缩和滚动发布会持续改变地址;一次大规模发布可能在 2 分钟内替换 1 万个端点。调用方包含多种语言,不能要求每个团队都维护复杂的发现 SDK。
题设把两个时限分开。实例主动进入不可接流量状态后,该变化从被控制面观测到停止新流量的 p99 不超过 3 秒。进程或节点突然消失时,硬故障检测 p99 不超过 15 秒。发现控制面即使中断 10 分钟,已有调用也应依靠最后已知端点继续;这不代表期间能发现新实例或保证旧实例仍然存活。
以上服务数、实例数、地域数、发布规模和 SLO 都是面试假设。范围包括注册、租约、健康状态、端点查询与增量传播、缓存、流量摘除、多地域和验证。请求负载均衡算法只讨论与发现相关的部分,业务 API、完整服务网格数据面和公网 DNS 不在主范围。本题归入 system-design,因为核心是跨控制面、代理、健康检查和调用链的整体架构。
面试官考察点
第一项是能否区分注册、发现、健康判断和路由。注册中心记录“谁声称在哪里”;健康系统判断“现在是否应该接流量”;发现把候选端点送给调用侧;代理或客户端才选择一个端点。把四件事画成一个数据库框,会漏掉传播延迟和失败边界。
第二项是控制面与数据面解耦。若每次业务请求都同步查询中心,中心抖动会直接变成全站故障。强方案让代理保存版本化快照,在后台接收变化;控制面不可用时,数据面继续用最后已知集合,并用短连接超时、受限重试和被动异常剔除抵御陈旧端点。
第三项是承认发现信息必然可能陈旧。DNS TTL、代理缓存、watch 延迟、失败检测和滚动退出都会制造窗口。强回答会定义“从哪个事件开始计时”,分别给主动摘除与硬故障 SLO,并计算探测周期、阈值和传播时间。宣称“强一致就不会打到坏实例”忽略了网络分区和进程在读后立即失败。
最后看规模与运维。10 万实例若每 10 秒直接续租一次,就是每秒 1 万次续租;发布期间还会有大量端点变更和 watch 扇出。候选人需要控制写放大、重连风暴、全量快照、跨地域故障域和误判健康检查,而不是只列举 Consul、etcd 或 Kubernetes 名称。
回答前需要澄清的问题
- 谁是注册事实来源? 编排器已经掌握 Pod 生命周期时,应由控制器产生注册信息;虚拟机或外部进程可以由带工作负载身份的本地代理注册。允许任意实例无认证自注册会污染目录。
- 3 秒从何时开始? 本题从控制面接受 READY 到 DRAINING 或未就绪变化开始,不从应用第一次决定退出开始。若应用在上报前卡住,要由硬故障检测路径处理。
- 15 秒硬故障容许多少误摘除? 连续三次探测失败能降低瞬时丢包误判,但 5 秒周期加超时接近整个预算。若业务更怕误摘除,需要被动错误率、区域容量和更长确认窗口共同决定。
- 调用方需要实例 IP 还是稳定服务地址? 只需稳定 VIP 时,DNS 加平台负载均衡最简单;需要版本、地域或分片元数据时,代理或客户端发现更合适。
- 控制面中断时是 fail-open 还是 fail-closed? 普通内部服务可用最后快照继续;涉及权限撤销或强隔离的决策不能依赖陈旧发现缓存,应由独立身份与授权层拒绝。
- 是否允许跨地域自动故障转移? 无状态读服务可以按策略切换;带地域数据主权、单主写入或高跨区成本的服务必须在元数据中显式禁止或限定。
30 秒回答框架
“我会把发现做成区域化控制面和本地数据面。编排器或认证代理把实例写入分片目录,状态经历 STARTING、READY、DRAINING、UNHEALTHY、EXPIRED。只有 READY 进入可路由集合;主动退出先变为 DRAINING,再排空连接。区域 leader 对变更排序并生成单调 revision,分发层向 5,000 个节点代理推送增量;代理发现 revision 缺口就拉全量快照。
业务请求只查询本地代理或稳定 VIP,不同步依赖中心。中心中断时代理保留最后快照,同时用连接超时、受限重试和被动异常临时剔除坏端点。健康检查分启动、就绪、存活和被动信号;5 秒主动探测加三次失败用于约 15 秒硬故障检测。最后用滚动替换、网络分区、watch 断线、重连风暴和错误探针做故障注入,量化摘除延迟、陈旧请求和恢复收敛。”
分步骤深入解答
第一步:定义数据模型和状态机
目录键至少是命名空间、服务名和端口名,避免不同环境或协议碰撞。端点记录包含稳定实例 ID、地址、区域、可用区、版本、权重、能力标签、状态、租约到期时间和修订号。标签只允许预先定义的维度,禁止把任意高基数数据带进发现面。
状态机比一个布尔 healthy 更能表达生命周期:STARTING 不接流量;READY 可接新流量;DRAINING 停止新请求但允许既有连接排空;UNHEALTHY 是探测或被动信号判定失败;EXPIRED 表示租约未续。每次状态变化都带原因、来源和单调 revision,便于审计乱序更新。
同一端点更新以实例 ID 和启动世代去重。旧进程延迟到达的续租不能让已被替换的地址复活。若编排器是事实来源,控制器观察期望状态和实际就绪状态;若使用自注册,写入端必须以工作负载身份认证,并限制它只能修改自己的服务与实例记录。
第二步:选择发现模式,不把复杂度复制到每种语言
DNS 加稳定 VIP 适合调用方只需服务名、平台已处理端点和健康状态的场景。DNS 直返实例地址虽然简单,但 TTL 会在查询量与陈旧时间之间取舍;缓存不尊重短 TTL 的客户端还会扩大风险。
客户端发现能按版本、区域和负载选择端点,代价是每种语言都要实现 watch、缓存、负载均衡、重试和安全升级。题设包含多语言调用方,因此推荐服务器侧发现:每个节点运行本地代理,或使用已有平台代理。应用调用稳定本地地址,代理维护端点集合并选择后端。多一跳的成本换来统一语义与快速升级。
若已经全部运行在 Kubernetes 中,Service、DNS 和 EndpointSlice 通常已经覆盖基础发现,不应重复造注册中心。独立控制面只在跨虚拟机、跨集群、复杂版本路由或统一策略确有需求时引入,并优先消费编排器端点事实。
第三步:让写路径可排序,让读路径可陈旧
每个地域部署 3 或 5 个目录副本,以共识 leader 接受注册与状态变更;按服务键或租户分片,避免全局单 leader 承担 10 万实例。跨地域不做每次写入的同步共识,区域故障不应阻塞其他区域。全局层只同步服务策略和允许的故障转移目标。
一次写成功表示该地域目录以 revision 接受了变化,不表示所有代理已经看到。分发器把有序增量推给订阅者;代理落盘最后完整快照和 revision。收到连续 revision 就应用增量,发现缺口、校验失败或保留日志已截断就拉该服务全量快照。快照原子替换,不能让半份新列表与半份旧列表混合。
读路径允许有界陈旧来换可用性。代理记录快照年龄、最后控制面联系时间和 watch lag。超过普通陈旧阈值时报警,但控制面 10 分钟中断期间仍用最后集合。若所有已知端点都失败,返回明确的无可用后端,而不是绕过策略请求任意地域。
第四步:把主动摘除与硬故障检测分开
优雅退出由应用先将就绪状态撤回,目录进入 DRAINING,分发到代理后停止选择该端点;然后等待最长连接排空时间,再终止进程。应用自身还要停止接受新工作,因为发现传播不是瞬时的,旧代理或长连接可能仍持有地址。
硬故障没有主动信号。假设主动探测每 5 秒一次、连续 3 次失败才摘除,故障恰好发生在一次成功探测后,单是采样就可能接近 15 秒,再加单次超时和传播。要满足 15 秒 p99,探测超时、调度抖动和分发必须一并预算,或缩短周期。代理观察到连接拒绝、超时或高错误率时,可以对本地端点做短期被动剔除,但不能凭一个调用方的局部网络问题全局注销实例。
启动、就绪和存活也不能混用。启动信号保护慢启动;就绪失败只停止流量;存活失败才触发重启。把共享数据库故障放进所有实例的存活检查,会同时重启整个服务并加重依赖压力。关键依赖可以影响就绪,但要用短超时、抖动和容量保护避免探针风暴。
第五步:计算写入、变更和扇出规模
10 万实例若每 10 秒直接续租,稳态为每秒 1 万次续租。可以由编排器 watch 代替每实例心跳,或让节点代理聚合续租并加随机抖动;租约到期仍是遗留记录的安全网,不应成为正常摘除的唯一机制。
两分钟替换 1 万端点,相当于平均每秒约 83 次新增和 83 次移除,即约 167 次成员变更。若每个变化都向 5,000 个节点代理独立发送,最坏会形成每秒约 83.5 万次投递。实际订阅应按代理所需服务过滤,短窗口合并同服务变化,并通过分层分发节点扇出。不能为了减少消息把 3 秒主动摘除 SLO 合并成一分钟批次。
全量快照同样需要预算。每个端点若序列化后粗估 256 字节,10 万端点全局快照约 24.4 MiB;正常代理只拉所订阅服务,不拉全球目录。重连采用指数退避与随机抖动,分发层提供短期增量日志,避免控制面恢复时 5,000 个代理同时请求全量。
第六步:定义跨地域和安全边界
实例默认只注册到所在地域,调用优先同地域同可用区的 READY 端点。区域目录失去 quorum 时禁止接受新写,但本地代理继续读缓存。其他地域不自动把该区域端点改成健康,也不依据跨区探测覆盖本地事实。
服务策略声明是否允许跨地域、只读还是可写、目标顺序、容量上限和数据边界。全局故障转移以显式策略触发;无状态读服务可以快速切换,单主数据库写代理则必须先确认主权转移。发现只回答可候选地址,不能替代业务一致性协议。
注册、注销和 watch 都要求工作负载身份与最小权限;目录变更写审计日志。代理验证控制面身份,敏感服务可结合双向 TLS。端点标签不是可信授权声明,调用端仍要在连接或请求层验证服务身份和权限。
第七步:用时间线和故障注入验收
先验证一次滚动替换:旧实例撤回就绪,记录目录接受 revision、代理应用 revision、最后一次新请求和进程退出时间;新实例只有启动完成且就绪后才进入集合。断言主动摘除传播 p99 小于 3 秒,排空中的已有请求不中断,也没有未就绪实例接到流量。
再终止进程但不发送注销,验证 5 秒探测、连续失败阈值和分发总和满足 15 秒 p99。分别注入单代理网络故障、整节点故障、目录 follower 落后、leader 切换、地域失去 quorum、增量缺口、全量快照损坏和控制面中断 10 分钟。恢复时用抖动重连,确认没有全量拉取风暴。
线上指标至少包括注册与续租写入率、目录写延迟、各服务 READY 数、状态抖动率、探测延迟、watch lag、快照年龄、revision 缺口、全量回退次数、代理重连率、对已摘除端点的新请求数、陈旧端点连接失败率和无可用后端次数。只有业务流量时间线能证明端点传播生效,控制面显示健康并不足够。
高质量示范回答
“我先把注册事实、健康判断、发现传播和请求路由分开。区域目录由共识组接收带身份的实例变更,每条记录有服务键、实例 ID、启动世代、地址、区域、版本、状态、租约和 revision。实例只有 READY 才进入可路由集合;退出时先变 DRAINING、停止新流量、排空后再终止。硬故障走主动探测和租约,被动错误只让单个代理临时剔除,不能直接代表全局事实。
题设有多语言调用方,所以我不会让每个进程直接 watch 注册中心。5,000 个节点代理按需订阅服务,保存全量快照与单调 revision,连续应用增量,遇到缺口再拉该服务快照。业务请求只走本地代理,不同步查询控制面。控制面中断 10 分钟时使用最后已知端点,配合短连接超时、受限重试和本地异常剔除;代价是新实例不可见,旧地址可能陈旧,这必须通过指标暴露。
10 万实例每 10 秒续租会有每秒 1 万次写,因此优先消费编排器 watch 或由节点代理聚合。两分钟替换 1 万端点约产生每秒 167 次增删变更,按订阅过滤、短窗口合并和分层扇出,仍保留 3 秒摘除预算。每 5 秒探测且连续 3 次失败已接近 15 秒,超时和传播也要算进去。
我会以流量时间线验收:主动撤回后 3 秒内不再有新请求,硬崩溃 15 秒内退出候选集合;leader 切换、地域分区和控制面停 10 分钟时已有流量继续。最后让 5,000 个代理带抖动重连,验证 revision 缺口会全量恢复且不会把中心打垮。”
常见错误
- 每个请求先查注册中心 → 控制面延迟或故障直接进入业务路径 → 本地缓存版本化端点,后台接收变化。
- 只有一个
healthy布尔值 → 启动、接流量、排空和需要重启的语义混在一起 → 使用明确状态机并记录转换来源。 - 把就绪失败当作重启理由 → 下游依赖故障会触发全服务重启风暴 → 就绪负责摘流量,存活只判断本进程不可恢复故障。
- 认为短 DNS TTL 消除陈旧 → 客户端缓存、传播和失败检测仍有窗口,查询量也会上升 → 量化 TTL 权衡并保留数据面容错。
- 让实例无认证自注册 → 错误或恶意端点可进入内部流量 → 使用编排器事实或受限工作负载身份。
- 所有代理直接订阅全球目录 → 发布和重连造成写放大与全量风暴 → 按服务过滤、分层扇出、增量 revision 与抖动重连。
- 全局强一致复制每个心跳 → 跨地域延迟或分区阻塞正常区域 → 区域内排序写,跨区只同步策略和允许的故障转移信息。
- 发现中心返回端点就视为调用成功 → 端点可能在读取后立即失败 → 连接超时、受限重试、熔断和幂等仍属于调用协议。
- 健康检查依赖所有下游 → 一个共享依赖故障让全部实例同时退出 → 只检查接流量所需的关键条件,并给探针短超时与抖动。
追问及应对
追问一:为什么不让 10 万个实例直接使用客户端发现?
客户端发现可以少一跳,也能做细粒度路由,但会把 watch、缓存、revision 补洞、负载均衡、异常剔除和升级复制到每种语言与每个进程。题设有多语言团队,节点代理把 10 万个客户端连接收敛到约 5,000 个数据面实例。若只有一种成熟运行时且延迟预算极严,统一 SDK 可能更合适,但必须保留同样的协议一致性测试与强制升级机制。
追问二:控制面中断时为什么敢用旧端点?
最后快照让仍然健康的既有实例继续服务,代价是无法获知新增、摘除和故障转移变化。代理必须暴露快照年龄,用短超时和被动错误减少对坏地址的请求,并限制重试预算。对于安全撤权或禁止访问的变化,不应依赖发现缓存传播;独立身份和授权层应 fail-closed。超过业务允许的最大快照年龄后,可以按服务策略降级或拒绝,而不是全局统一处理。
追问三:5 秒探测、三次失败真的能满足 15 秒吗?
不一定。故障若发生在探测刚成功之后,三个失败采样可能接近 15 秒,单次 timeout、调度抖动和端点传播会让总时长更长。要满足 p99,可以把周期缩短、让 timeout 小于周期、并利用连接拒绝等被动信号加速本地剔除。然后用故障注入测从最后成功请求到最后一次新路由的分布,不能只用配置乘法宣称通过。
追问四:滚动发布为什么需要 DRAINING,而不是直接删除?
直接删除只能阻止看到新列表的调用方,无法处理旧缓存、keep-alive、长 RPC 和已经排队的工作。DRAINING 先从新请求候选集合移除,同时保留进程一段排空时间。应用还应停止接受新工作,并让超时边界小于平台终止宽限期。验证要同时统计最后一次新请求、在途完成率和被强杀请求数。
追问五:如果一个地域失去 quorum,是否应从另一个地域接管注册写入?
不能自动接管同一地域的实例事实。跨区观察可能把网络分区误判为实例全部死亡,两个控制面也可能分别接受冲突状态。失去 quorum 的地域停止目录写,代理继续读缓存;全局层只依据预先声明的服务策略把新调用切到允许的地域。原地域恢复后以 revision 和启动世代重新同步,不能让过期续租覆盖新状态。