i007.cc

i007.cc

优先队列-降维打击

05.价值资料

分布式配置管理系统

第一题

设计一个分布式配置管理系统(类似 AWS Parameter Store 或 Azure App Configuration)。

要求:

  • 支持百万级服务实例实时拉取配置
  • 配置更新后,所有实例需要在 30 秒内感知到变更
  • 系统需要高可用,不能成为单点故障

请描述你的整体架构设计。

回答你的问题:

  1. 实例数量:峰值 100 万实例同时在线,正常情况约 60-70 万。
  2. 拉取频率:服务实例启动时拉取一次全量配置,运行期间不主动轮询——配置变更由系统推送通知,实例收到通知后再拉取。配置变更频率不高,平均每天几十次。

附加信息:

  • 配置项数量级:每个服务约 10-100 个 key,单个 value 不超过 64KB
  • 读多写极少,写操作来自运维人员或 CI/CD 系统
  • 需要支持配置版本回滚
  • 不同环境(prod/staging/dev)的配置需要隔离

回答:

  • 大约 5000 种服务,每种服务平均 200 个实例
  • 每种服务有自己独立的配置集,不同实例共享同一份配置(即配置是 per-service 的,不是 per-instance 的)
  • 每个服务约 50 个 key,每个 value 平均 1KB

你现在有足够的数字了:

维度 数值
服务种类 5,000
实例总数 1,000,000
每服务 key 数 50
每 value 大小 平均 1KB
配置版本保留 最近 100 个版本

可以开始算存储,然后继续设计了。

 

分布式配置管理系统 — 完整设计

第一步:澄清需求(必做)

拿到题目先问清楚:服务种类多少、实例数峰值、配置变更频率、是否需要版本回滚、多环境隔离需求。这些数字决定后续所有容量规划。


第二步:容量规划

存储计算(per 环境):

Redis(当前版本热数据):
  5000 服务 × 100 keys × 64KB(max) = 32GB  ← 单台 Redis 可以放下

MySQL(历史版本冷数据):
  5000 × 100 keys × 100 版本 × 1KB(avg) = 50GB  ← 实际远小于极限值
  极限值:5000 × 100 × 100 × 64KB = 3.2TB

 

读 QPS:
Push 通知机制下,实例不轮询。只有配置变更时才有读请求。每天 100 次变更,每次影响 200 个实例,QPS 极低,Redis 轻松应对。

Long Poll 连接数:
每台 Config API Server 维持约 5 万连接,20 台覆盖 100 万实例。


第三步:整体架构

┌─────────────────────────────────────────────┐
│              运维 / CI/CD                    │
└──────────────────┬──────────────────────────┘
                   ↓ 写配置
┌──────────────────────────────────────────────┐
│            Write Service                     │
│  1. 写 MySQL(Source of Truth)              │
│  2. 更新 Redis(热缓存)                     │
│  3. Publish → Redis Pub/Sub                  │
└──────┬───────────────────────────────────────┘
       │ Pub/Sub 通知
┌──────▼───────────────────────────────────────┐
│      Config API Server × 20台(无状态)      │
│  - 每台维持 5 万个 Long Poll 连接            │
│  - 收到 Pub/Sub → 响应对应服务的等待连接     │
└──────┬───────────────────────────────────────┘
       │ 实例拉取配置
┌──────▼───────────────────────────────────────┐
│            Redis Cluster                     │
│         (当前版本热缓存,32GB)             │
└──────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│         MySQL(历史版本 + Source of Truth) │
│              主从复制,高可用                │
└─────────────────────────────────────────────┘

 


第四步:变更通知链路

从”运维改了一条配置”到”100 万实例感知”,完整流程:

1. 运维写入新配置
        ↓
2. Write Service 写 MySQL ✅ 确认落盘
        ↓
3. Write Service 更新 Redis(带 Retry,失败重试 3 次)
        ↓
4. Publish 到 Redis Pub/Sub:
   "service-X 配置已更新,version=42"
        ↓
5. 20 台 Config API Server 同时收到 Pub/Sub 消息
        ↓
6. 每台找到本机挂着的 service-X Long Poll 连接
   立即响应:「有新版本,来拉吧」
        ↓
7. service-X 的 200 个实例收到响应
   请求 Config API Server 拉取 v42 配置
        ↓
8. Config API Server 从 Redis 读取返回给实例
   全程 < 1 秒,远低于 30 秒要求

 


第五步:实例断线重连

实例不需要离线消息队列。配置是状态,不是事件流。

重连时只需带上当前版本号:

GET /config/service-X?current_version=41

API Server 判断:
  最新是 v42 → 立即返回新配置
  最新是 v41 → 挂住,进入 Long Poll 等待

 

这样断线 1 秒还是 1 小时,重连逻辑完全相同。


第六步:一致性保障(Reconciliation Loop)

两层防御,处理写入路径上的失败:

第一层:Retry(处理网络抖动)
  MySQL 写成功 → Redis 更新失败 → 指数退避重试 3 次

第二层:Reconciliation Loop(兜底)
  后台任务每 30 秒扫一次
  比对 MySQL 版本号 vs Redis 版本号
  发现不一致 → 同步 Redis → 触发 Pub/Sub

 


第七步:高可用

组件 方案
Config API Server 无状态,20 台,任意挂几台不影响
Redis 主从 + Sentinel,或 Redis Cluster
MySQL 主从复制,从库只读,主库故障自动切换
Write Service 多实例部署,无状态

关键设计原则总结

  • MySQL 是 Source of Truth,Redis 是缓存,写入顺序永远 MySQL 先
  • Long Poll 解决实时推送,避免无效轮询
  • 版本号 解决断线重连,不需要离线消息队列
  • Reconciliation Loop 保证最终一致性
  • Config API Server 作为连接层,实例不直连 Redis

 

整个系统redis只用了两台么?

好问题,我在那里写得比较模糊,来梳理清楚。


Redis 在这个系统里承担两个职责:

一是存当前配置(32GB 热数据),二是 Pub/Sub 通知。这两个可以用同一套 Redis,也可以分开部署做隔离,避免通知流量影响配置读取。


需要几台?

32GB 数据量对 Redis 来说很小,不需要 Cluster 分片,一台主节点完全放得下。读 QPS 也很低(Push 模型,不轮询),不需要大量读副本。

所以最简配置是:

Redis Primary(主,处理写 + Pub/Sub publish)
    ↓ 主从同步
Redis Replica(从,处理读请求)

+ 3 个 Sentinel 节点(监控主从,主挂了自动切换)

 

Sentinel 节点很轻量,通常和其他服务混跑在同一台机器上,不占额外资源。


和原来你说的”20台Redis”对比

那是你最初短轮询方案里算出来的——因为 200K QPS 需要多台扛压。换成 Long Poll 之后读 QPS 极低,根本不需要那么多。这也是为什么通知机制的选择对整个系统的资源消耗影响这么大。

所以答案是:2 台数据节点(1主1从)+ 3 个 Sentinel,足够了。

有个专业名词:Reconciliation Loop(对账循环),Kubernetes 的 Controller Manager 就是用这个模式工作的——不断比对期望状态和实际状态,发现不一致就修正。


两层防御组合起来就很健壮:

第一层:写入路径上加 Retry
MySQL 写成功 → 更新 Redis 失败 → 立即重试 3 次(指数退避)
→ 大部分网络抖动在这里就恢复了

第二层:Reconciliation Loop(兜底)
每 30 秒扫一次:比对 MySQL 版本号 vs Redis 版本号
发现不一致 → 同步 Redis → Pub/Sub publish
→ 处理 Retry 也没救回来的极端情况

 

Reconciliation 间隔设为 30 秒,刚好满足题目的 30 秒内通知要求。


这道题到这里,整体评分

你的设计覆盖了核心要素,我给一个综合评价:

做得好的:

  • 主动澄清需求(问服务数量、QPS),面试里加分项
  • 冷热数据分离(Redis + MySQL)思路正确
  • 被引导后能快速理解 Long Poll、版本号对比、Reconciliation Loop

需要加强的:

  • 容量计算要更严谨,别漏掉关键维度(key 数量)
  • 通知机制是分布式配置系统的核心难点,需要主动想到,不能靠引导
  • 对 Kafka 等中间件的适用场景要更熟悉(fan-out vs queue 的区别)

总体: 相当于 L4-L5 水平,基础扎实,系统设计经验还需要多练。Apple 这类公司期望你能主动推进设计,而不是被面试官一步步引出来。

方向对了,Redis Pub/Sub 检测变更 + 通知实例拉取,这个链路是正确的。现在帮你细化一个关键问题。


不能让 100 万实例直接连 Redis

Redis 是单线程的,直接承接 100 万个持久连接会崩。需要在中间加一层:

正确架构:

100万实例
    ↓  Long Poll (HTTP)
Config API Server × N 台(无状态,水平扩展)
    ↓  内部订阅
Redis Pub/Sub(只有 N 个连接,N = API Server 台数)
    ↑
Write Service(配置写入时 publish)

 

每台 Config API Server 可以维持约 5 万个 Long Poll 连接,20 台就能支撑 100 万实例。Config API Server 是无状态的,随时可以横向扩展。


完整变更链路

1. 运维改了 service-X 的配置
        ↓
2. Write Service 写入 MySQL(持久化)
        ↓
3. Write Service 更新 Redis(热缓存)
        ↓
4. Write Service 向 Redis Pub/Sub publish:
   "service-X 配置已更新,版本号 v42"
        ↓
5. 所有 Config API Server 收到 Pub/Sub 消息
        ↓
6. 各 API Server 找到本机挂着的 service-X 的 Long Poll 连接
   立即响应:「有新版本,去拉吧」
        ↓
7. service-X 的 200 个实例收到响应
   各自向 Config API Server 请求新配置
        ↓
8. Config API Server 从 Redis 读取返回
   (整个过程 < 1 秒,远低于 30 秒要求)

Long Polling(长轮询)


工作原理

短轮询(你的方案):
实例 → "有变更吗?" → 服务端立即回 "没有" → 5秒后再问
实例 → "有变更吗?" → 服务端立即回 "没有" → 5秒后再问
实例 → "有变更吗?" → 服务端立即回 "有!" → 实例拉取配置

长轮询:
实例 → "有变更吗?" → 服务端挂住这个连接,不回应
                              ↓(等待,可能等 30 秒)
                         配置发生变更
                              ↓
                    服务端立即响应 "有变更!"
实例收到响应 → 立即拉取配置 → 立即发起下一个 Long Poll

 


对比收益

短轮询(5s) 长轮询(30s timeout)
QPS 200K 33K
无效请求 99.99% 接近 0%
变更延迟 最多 5 秒 接近 0 秒(实时)

Long Polling 是 Consul、Apollo 配置中心的标准实现方式。

发表回复