题干与适用场景
设计一个多租户 Feature Flag(功能开关)系统,供 3 个地域的 20,000 个服务端实例使用。系统跨项目存储 50,000 个开关,每秒处理 500 万次进程内求值,新增延迟 p99 低于 1 毫秒,已发布变更在 5 秒内传播到 p99 实例,并在控制面中断 15 分钟期间继续求值。系统需要支持有类型的变体、定向规则、确定性百分比灰度、审计日志、审批和紧急关闭。
这些数字是面试假设,不是产品基准。假设每个运行实例最多订阅 500 个相关开关,单个开关定义序列化后平均 2 KB,控制面写入峰值为每秒 100 次,服务端求值规则可能包含敏感属性。客户端与移动端交付、实验统计和通用配置中心属于追问,不进入基础范围。
这道题适用于高级后端、平台、基础设施、发布工程和系统设计岗位。可复用的判断是:管理路径可以实行强治理,请求路径仍应保持本地与高可用。后续大部分设计都由这项分离决定。
面试官考察点
第一个信号是能否分开部署与发布以及控制面与数据面。控制台和数据库管理开关定义,应用 SDK 在进程内对版本化规则集求值。如果每次检查开关都远程调用服务,管理系统的故障和网络延迟就会进入每个产品请求。
第二个信号是语义是否精确。一次求值需要开关键、环境、有类型的回退值和求值上下文。顺序规则、显式目标、百分比分配与默认规则必须具有确定优先级。“每个请求随机抽 10% 开启”会让同一用户跨请求和地域得到不稳定体验。
第三个信号是故障设计。最后已知正确版本可以在控制面中断时维持求值,同时也把陈旧配置变成明确风险。高质量回答会定义首次启动、最大可接受陈旧时间、版本缺口修复、无效更新拒绝、紧急关闭,以及展示类改动和危险写路径采用不同默认值的规则。
第四个信号是运营归属。开关编辑就是生产变更。认证、授权、环境隔离、乐观并发、校验、审批策略、不可变审计、分阶段发布、回滚、负责人和下线流程都属于系统设计。
回答前需要澄清的问题
- 在哪里执行求值? 服务端 SDK 可以接收完整规则并本地求值。浏览器和移动端不能安全接收敏感定向规则,需要过滤后的规则或远程求值模型。
- 5 秒目标从哪里量到哪里? 定义为发布成功到 99% 的健康已订阅服务端实例应用该版本。离线实例和超过支持新鲜度窗口的客户端需要另行定义语义。
- 回退契约是什么? SDK 从未加载有效快照时,返回应用代码提供的有类型默认值。完成初始化后,可以在约定陈旧窗口内使用最后已知正确版本。
- 同一主体是否必须留在同一组? 百分比灰度需要稳定定向键和确定性哈希输入。需要稳定体验的匿名会话也要有持久标识。
- 规则能否包含敏感数据? 优先使用分群 ID 和非敏感属性。服务端交付可以携带受保护规则,浏览器交付不能暴露密钥、内部白名单或授权逻辑。
- 紧急关闭有多关键? 5 秒传播 SLO 仍不等于瞬时完成。真正危险的操作还需要服务端授权和独立安全控制。
30 秒回答框架
“我会把平台拆成受治理的控制面和本地求值面。控制面对变更做校验、版本化和审计,再发布不可变快照与补丁。服务端 SDK 保留最后有效规则,并在进程内执行显式目标、顺序规则、灰度和默认值;初始化前或状态无效时返回代码回退值。确定性灰度把稳定主体键与环境、开关键和盐值一起哈希。流推送更新,轮询修复缺口,控制面中断不会进入请求路径。最后用故障注入验证传播、跨 SDK 结果、群体稳定性、陈旧行为和回滚。”
分步骤深入解答
第一步:把题目转换为明确契约
功能需求包括创建、编辑、审批、发布、关闭和下线开关;定义布尔、字符串、数值或结构化变体;按主体或分群定向;做百分比分配;返回便于诊断的求值详情。非功能需求包括本地 p99 低于 1 毫秒、传播 p99 为 5 秒、跨地域结果确定,以及管理系统中断 15 分钟仍能求值。
画组件前先确定三条边界:
- 已发布版本不可变,后续编辑会生成新的草稿与版本。
- 传播 SLO 约束健康且已连接的服务端实例,不包括离线设备。
- 最终回退值由应用负责;类型校验、初始化或求值失败时,平台不能自行猜值。
基础设计中,最后已知正确状态至少在题目要求的 15 分钟内有效。超过开关发布的 staleafter 边界后,staleaction 决定继续使用最后已知正确版本还是返回代码回退值,SDK 同时返回陈旧原因。安全敏感或破坏性路径必须选择回退,不能静默执行旧的开启规则。
这样可以避免把产品策略藏进基础设施。结账迁移可以默认走旧路径,安全敏感能力可以默认关闭。两者使用同一平台,但应用契约不同。
第二步:分离控制面和求值数据面
控制面包含管理 API 与控制台、身份与角色校验、schema 和规则校验器、审批工作流、关系型事实源、追加式审计日志、快照构建器和发布器。写流量远低于求值流量,因此正确性、可审查性和可恢复性比微秒级延迟更重要。
数据面包含地域更新中继、快照存储、轮询端点,以及 SDK 本地存储和求值器。服务端 SDK 连接就近地域流,安装不可变快照,应用带版本的补丁,并且不经网络即可求值。流中断后继续使用最后有效状态,并用带抖动的轮询恢复。如果补丁跳过版本或校验和失败,SDK 丢弃该补丁并请求完整快照。
数据面交付不能依赖控制面的主数据库。发布器先写入持久的版本化产物,再通知中继。这样数据库或控制台故障只会阻止新编辑,不会停止现有求值。
第三步:定义数据模型和 API
开关定义至少需要:
FlagDefinition {
tenant_id, project_id, environment, flag_key
version, value_type, variations[], off_variation
ordered_rules[], default_rule, salt
stale_after, stale_action
state, owner, expires_at
}
Rule {
rule_id, conditions[], outcome
}
Outcome = fixed_variation | weighted_variations[]分群需要独立版本,因为多个开关可能引用同一组成员。审计条目记录操作人、时间、理由、预期旧版本、变更前后引用、审批和发布结果。草稿与已发布产物分离,避免未审批编辑泄漏到求值路径。
代表性 API 为:
PUT /v1/projects/{project}/environments/{env}/flags/{key}
body: draft definition, expectedVersion
POST /v1/projects/{project}/environments/{env}/flags/{key}:publish
body: draftVersion, reason, approvalToken
GET /v1/sdk/bootstrap?project={project}&env={env}&after={version}
GET /v1/sdk/stream?project={project}&env={env}预期版本可以防止两个编辑者静默覆盖彼此。管理凭证不能同时作为 SDK 凭证。租户、项目、环境和允许的交付模式都必须参与授权判断。
第四步:让求值和百分比灰度保持确定性
所有 SDK 使用一套公开顺序:
- 检查开关是否存在、环境、类型以及定向是否启用。
- 应用显式主体或分群目标。
- 按顺序执行规则,第一个匹配项获胜。
- 解析固定变体或加权灰度。
- 没有规则匹配时使用默认规则。
- 无法得到合法类型结果时,返回应用回退值与错误原因。
加权灰度从稳定输入计算分桶:
bucket = H(tenant_id || environment || flag_key || salt || targeting_key) mod 100000把分桶映射到变体的累计区间。相同输入在每个实例和地域都会得到相同结果。把盐值与算法版本写进已发布定义,才能复现行为。扩大已有连续区间时可以保留原来已进入该区间的主体,但任意重新分配权重或修改哈希输入都可能移动用户;审核时必须展示这个后果。
两个具有相同比例的独立开关不一定选择同一批主体,因为开关键参与哈希。多个开关需要共同移动时,使用共享版本化分群或显式实验键。存在稳定主体 ID 时,不能用会变化的邮箱等字段做哈希。
第五步:安全交付快照与增量变更
快照构建器按项目和环境解析范围,校验引用的分群与前置开关,确定性排序规则,序列化为规范产物,并附加版本与校验和。一个事务提交已发布元数据和 outbox 记录;异步发布器存储产物并通知地域中继,避免开关已提交却没有安排交付。
SDK 启动顺序为:
- 配置了持久存储时,加载有效的最后已知正确快照。
- 从最近中继获取最新完整产物或增量产物。
- 通过类型、版本和校验和检查后,原子替换内存快照。
- 把提供方标为就绪,再提供本地求值。
- 保持更新流连接,并以轮询作为修复路径。
不能原地修改活跃规则集。构建新的不可变快照并原子替换一个引用,使并发请求只会看到完整旧版本或完整新版本。诊断详情需要记录实际应用版本与求值原因。
第六步:先设计故障语义
| 故障 | 求值行为 | 恢复方法 | |---|---|---| | 控制 API 或数据库不可用 | 在 stale_after 前使用最后有效快照,随后执行该开关的陈旧策略 | 阻止编辑,恢复控制面,校验后再发布新版本 | | 更新流断开 | 使用本地状态并标记更新状态陈旧 | 带抖动重连;轮询并在发现缺口时请求完整快照 | | SDK 启动时没有快照 | 返回代码中的有类型回退值 | 重试启动加载,但不能无限阻塞应用其他启动流程 | | 补丁无效或乱序 | 保留当前版本 | 拒绝补丁、报警并拉取规范快照 | | 错误规则已发布 | 求值内部一致,但业务结果错误 | 停止灰度,把已知正确定义作为新的已审计版本发布 | | 地域中继不可用 | 继续本地求值,并尝试其他中继或轮询端点 | 限制重连流量,避免同步启动风暴 |
紧急关闭应走同一条持久发布路径,只是使用优先通道,不能另建无审计的可变旁路。预授权的 break-glass 策略可以跳过普通审批等待,但仍要记录操作人、理由、旧版本和结果。5 秒传播目标不能保证所有断连实例已经切换,危险操作仍需要独立强制控制。
第七步:防止配置平台变成授权漏洞
按租户、项目和环境拆分管理角色。生产编辑可以要求第二人审批,开发环境可直接发布。凭证加密并轮换,SDK 密钥不能编辑开关,管理调用需要限流。追加式审计日志和不可变产物用于还原事故过程。
客户端交付与服务端交付要分开。浏览器或移动端只接收明确允许公开的开关,最好只获得针对当前上下文求值后的值。用户可以检查或修改客户端状态,所以开关可以改变展示,不能授予权限、绕过支付或替代服务端权益校验。求值上下文应尽量减少个人信息,遥测主体标识需要假名化。
每个临时发布开关都要有负责人和下线条件。灰度完成后,先把胜出路径固化,再停止求值,等代码引用消失后删除定义。否则旧分支和开关组合会无限扩张测试矩阵。
第八步:复算容量并证明设计
每个实例订阅 500 个平均 2 KB 的开关时,单份快照约 1 MB。20,000 个实例同时启动会传输约 20 GB,尚未计算协议和副本开销。规范产物需要由地域中继或对象分发承载,重连要加抖动并限制启动并发,同时通过条件版本和增量交付减少流量。单个 2 KB 变更扇出到 20,000 个实例的逻辑载荷约 40 MB,远低于完整刷新。
每秒 500 万次求值不能同步发送 500 万条网络事件。即使每条只有 200 字节,也约为每秒 1 GB,复制前每天 86.4 TB。普通诊断应缓冲、批量、采样或聚合。只有实验契约确实需要时才保存完整曝光事件;遥测使用有界队列,并允许在不延迟求值的前提下丢弃非关键事件。
验证不能只看吞吐量:
- 用相同规则与上下文的黄金用例运行所有 SDK,对比值、变体、原因和错误行为。
- 对分桶确定性、近似分布以及扩大已有灰度区间时的稳定性做性质测试。
- 按规则数和分群规模测量本地 p50 与 p99,另行测量发布到应用的传播延迟。
- 断开更新流、停止控制数据库、破坏补丁、跳过版本、让凭证过期,并让全部实例同时重启。
- 先向小型金丝雀群体发布错误规则,触发健康告警,验证回滚会生成并传播新的已审计版本。
- 验证租户隔离、生产审批、客户端暴露过滤、审计完整性和开关下线。
高质量示范回答
“我先把基础范围限定为服务端求值。系统有 3 个地域的 20,000 个实例,但控制面峰值只有每秒 100 次写入,两条流量路径不应共享可用性依赖。管理服务把草稿和不可变已发布版本保存在关系型数据库。每次发布都要校验类型、规则引用和权限,检查预期旧版本,写审计与 outbox,再构建规范的项目与环境产物。
地域中继分发产物。SDK 加载最后已知正确快照,通过流接收更新,并用轮询修复缺口。它原子安装新的不可变快照,在本地求值,使控制面中断时仍能满足低于 1 毫秒的 p99 目标。最后已知正确状态至少有效 15 分钟;超过已发布陈旧边界后,开关按风险策略继续使用该值或改用有类型的代码回退值。本地版本、新鲜度和求值原因都可观测。
求值顺序是关闭状态、显式目标、顺序规则、加权结果和默认规则。百分比灰度把租户、环境、开关、盐值和稳定定向键哈希到 100,000 个桶,使所有实例得出相同选择。修改哈希输入属于迁移;多个开关需要同一群体时使用共享分群,不能依赖比例恰好相同。
最大突发是实例集体启动:1 MB 范围快照乘以 20,000 个实例约为 20 GB。我会在各地域提供快照、发送增量、打散重连并限制重试。求值遥测必须批量与采样,因为每秒同步记录 500 万个事件会威胁产品路径。最后验证跨 SDK 一致性、传播 p99、启动风暴、陈旧求值、损坏与缺失版本、金丝雀回滚、紧急审计、租户隔离和客户端暴露。成功标准是请求求值保持本地与确定,同时每次配置变更都受治理且可恢复。”
常见错误
- 每次求值都调用中心服务 → 产品延迟和可用性依赖开关服务 → 把版本化规则交付给服务端 SDK,在进程内求值。
- 每个请求重新随机选择用户 → 同一用户跨变体跳转,实验数据被污染 → 用稳定定向键和明确的开关专属输入做哈希。
- 把最后已知正确版本当作永久正确 → 断连实例可能无限期执行危险旧规则 → 定义陈旧状态观测、重连修复和应用安全回退。
- 把服务端规则集发给浏览器 → 用户可以查看敏感分群与凭证 → 过滤客户端安全开关或远程求值,并在服务端强制授权。
- 原地更新共享规则对象 → 并发请求可能看到半应用配置 → 校验完整不可变快照,再原子替换引用。
- 同步记录每次求值 → 遥测成为请求路径最高流量依赖 → 批量、采样、聚合并允许丢弃非关键事件。
- 紧急改动不留审计 → 最快恢复路径变成不可追踪的生产后门 → 使用预授权 break-glass 策略和带不可变审计的优先发布通道。
- 永不下线开关 → 过期分支与开关组合持续增加测试成本 → 指定负责人,在胜出路径固化后删除定义。
追问及应对
追问一:如何支持浏览器和移动端 SDK?
不要交付完整服务端规则集。标记哪些开关允许客户端暴露,认证应用但不向其提供管理凭证,返回过滤后的值或当前上下文的已求值结果。离线缓存需要带版本与新鲜度状态。客户端开关只能作为体验提示,服务端仍要独立检查权限、购买状态、配额和其他安全决策。
追问二:多个开关如何保持完全相同的灰度群体?
每个开关键参与哈希时,相同比例并不足够。创建共享版本化分群或以公共实验 ID 建立实验分配,再由每个开关引用。分群与依赖开关应一致发布,扩大暴露前先验证群体成员保持稳定。
追问三:一个分群有一千万成员怎么办?
不能把完整成员列表嵌进每份快照。可以把成员关系表示为紧凑版本化产物,按主体哈希分片,或预计算求值器可消费的属性。布隆过滤器可以减少负查询,但存在误判,不能单独承担授权。内存、查询延迟、更新放大和成员陈旧需要与普通规则分别测量。
追问四:紧急关闭能做到瞬时生效吗?
不能。任何分布式发布都无法瞬时到达断连进程。紧急变更走优先通道,保持地域流连接,统计确认并对落后版本报警。危险操作还要叠加服务端强制控制,例如关闭写端点、撤销能力或在网关阻止任务。开关可以加快恢复,不能替代硬安全边界。
追问五:如何迁移哈希算法?
每个已发布开关都保存算法版本和盐值。先以影子方式同时运行新旧求值器,测量分组移动比例。移动可接受时分阶段发布;不可接受时,把既有主体分配保存在分群或迁移表中直到灰度结束。不能静默修改 SDK 哈希实现,否则不同版本会产生分歧。
追问六:如何防止开关依赖形成环?
发布时构建前置依赖图,用深度优先遍历检测到环就拒绝该版本。还要限制最大依赖深度和总求值工作量,避免无环但病态的图破坏延迟目标。产物中包含被引用开关版本,跨 SDK 测试求值顺序,并在诊断详情中展示依赖链。