配置中心设计

2020年某电商平台双十一前夜,运维同学在凌晨2点更新了优惠券的配置,把"满100减10"的门槛误写成了"满10减1"。

凌晨2点15分,第一个用户发现了这个bug:用1分钱买了一台iPhone。

到凌晨2点30分,已经有3000个用户薅走了价值500万的商品。

配置中心本应是保障系统稳定运行的工具,却成了这场事故的放大器——一个配置错误,在15分钟内影响了3000个用户。

如果配置中心有灰度发布变更审核快速回滚的能力,这场事故完全可以避免。

【架构权衡】

配置中心的核心价值不是"存储配置",而是配置变更的管控。一个好的配置中心,应该能让配置变更可追溯、可灰度、可回滚。理解了这一点,你才能设计出真正有用的配置中心。

一、配置中心的核心问题 🔴

1.1 四大核心问题

配置中心的四座大山:

1. 配置存储
   - 配置如何存储?
   - 如何支持亿级配置项?
   - 如何保证高可用?

2. 配置推送
   - 配置变更后如何实时推送到客户端?
   - 如何处理推送失败?
   - 如何避免推送风暴?

3. 版本管理
   - 配置变更历史如何追溯?
   - 如何快速回滚?
   - 如何做灰度发布?

4. 权限管控
   - 谁可以修改配置?
   - 修改需要审核吗?
   - 如何防止误操作?

1.2 量化指标

配置中心的关键数字:

性能要求:
- 配置读取延迟:P99 < 10ms
- 配置推送延迟:P99 < 5s
- 支持配置项:亿级
- 客户端SDK:< 5MB

可用性要求:
- 配置中心可用性:99.99%
- 推送成功率:> 99.9%
- 回滚时间:< 1分钟

1.3 面试核心问题

面试官:配置中心怎么实现配置变更的实时推送?

候选人:用长连接。客户端和配置中心维护一个WebSocket/T长连接,配置变更时,服务端主动推送到客户端。

面试官:如果推送失败了怎么办?

候选人:客户端本地缓存配置,推送失败时用缓存。如果缓存也没有,就从配置中心拉取。

面试官:怎么避免配置变更导致的服务雪崩?

候选人:灰度发布。先推10%的机器,观察没问题再全量推送。如果有问题,立即回滚。

【面试官心理】

配置中心的追问方向通常围绕"推送机制"和"灰度发布"。能回答出长连接推送的候选人,说明理解了配置中心的核心价值;能说出灰度发布和快速回滚的候选人,说明有运维意识。

二、配置存储设计 🔴

2.1 配置模型

// 配置模型
public class ConfigItem {
    private String namespace;     // 命名空间,如 "order-service"
    private String key;          // 配置key,如 "coupon.threshold"
    private String value;        // 配置值,如 "100"
    private String type;        // 类型:TEXT / JSON / YAML / PROPERTIES
    private long version;       // 版本号
    private long timestamp;      // 更新时间
    private String operator;     // 操作人
}

// 配置分组
public class ConfigGroup {
    private String namespace;    // 命名空间
    private String group;        // 分组,如 "dev" / "test" / "prod"
    private List<ConfigItem> configs;
}

2.2 存储架构

MySQL存储层:
- 配置元数据(namespace、key、version、operator)
- 配置变更历史
- 审计日志

Redis缓存层:
- 热点配置缓存
- 命中率 > 95%

配置仓库:
- Git仓库存储配置文本
- 支持配置版本化
- 可追溯、可回滚

配置推送:
- 长连接网关
- WebSocket/T长连接推送

2.3 数据库表设计

-- 配置表
CREATE TABLE config (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    namespace VARCHAR(128) NOT NULL,
    `group` VARCHAR(128) NOT NULL,  -- dev/test/prod
    config_key VARCHAR(256) NOT NULL,
    config_value TEXT,
    config_type VARCHAR(32) NOT NULL,  -- TEXT/JSON/YAML
    version BIGINT NOT NULL DEFAULT 1,
    status TINYINT NOT NULL DEFAULT 1,  -- 1=生效,0=删除
    create_time DATETIME,
    update_time DATETIME,
    operator VARCHAR(64),
    UNIQUE KEY uk_namespace_group_key (namespace, `group`, config_key),
    INDEX idx_namespace (namespace),
    INDEX idx_update_time (update_time)
) ENGINE=InnoDB;

-- 配置变更历史表
CREATE TABLE config_history (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    config_id BIGINT NOT NULL,
    old_value TEXT,
    new_value TEXT,
    version BIGINT NOT NULL,
    change_type VARCHAR(32) NOT NULL,  -- CREATE/UPDATE/DELETE/ROLLBACK
    operator VARCHAR(64),
    reason VARCHAR(512),
    create_time DATETIME,
    INDEX idx_config_id (config_id),
    INDEX idx_create_time (create_time)
) ENGINE=InnoDB;

-- 客户端订阅表
CREATE TABLE client_subscription (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    client_id VARCHAR(128) NOT NULL,  -- 客户端实例ID
    namespace VARCHAR(128) NOT NULL,
    `group` VARCHAR(128) NOT NULL,
    last_version BIGINT NOT NULL DEFAULT 0,
    last_pull_time DATETIME,
    INDEX idx_client_namespace (client_id, namespace, `group`)
) ENGINE=InnoDB;

三、配置推送机制 🟡

3.1 长连接推送

推送架构:

配置中心Server ←→ 长连接网关 ←→ 客户端SDK

流程:
1. 客户端启动时,连接长连接网关
2. 网关将连接信息注册到路由表
3. 配置变更时,配置中心通知网关
4. 网关将变更推送给对应客户端

3.2 推送失败处理

public class ConfigClient {

    private static final long PULL_INTERVAL = 30000;  // 30秒拉取一次

    private volatile long currentVersion = 0;
    private ConfigCache localCache = new ConfigCache();

    // 长连接接收推送
    @Subscribe
    public void onConfigPush(ConfigPush push) {
        if (push.getVersion() > currentVersion) {
            applyConfig(push);
            currentVersion = push.getVersion();
        }
    }

    // 兜底:定时拉取
    @Scheduled(fixedDelay = PULL_INTERVAL)
    public void pullConfig() {
        try {
            ConfigSnapshot snapshot = configCenterClient.pull(
                namespace, group, currentVersion
            );
            if (snapshot.getVersion() > currentVersion) {
                applyConfig(snapshot);
                currentVersion = snapshot.getVersion();
            }
        } catch (Exception e) {
            log.warn("拉取配置失败,使用本地缓存", e);
            // 使用本地缓存兜底
        }
    }

    // 应用配置
    private void applyConfig(ConfigSnapshot snapshot) {
        for (ConfigItem item : snapshot.getConfigs()) {
            localCache.put(item.getKey(), item.getValue());
            notifyListeners(item);
        }
    }
}

3.3 推送风暴防护

推送风暴场景:
- 10000个客户端实例
- 同时修改了1000个配置项
- 可能产生10000 × 1000 = 1000万次推送

防护策略:
1. 批量推送:合并同一客户端的多次变更
2. 推送限流:每秒最多推送N次
3. 优先级队列:高优先级配置先推送
4. 连接分组:按客户端分组,错峰推送

四、灰度发布 🟡

4.1 灰度策略

灰度维度:
1. 机器维度:先推10%的机器
2. 用户维度:先推白名单用户
3. 区域维度:先推某个机房
4. 时间维度:低峰期推送

灰度流程:
1. 创建配置变更,状态=草稿
2. 选择灰度策略:10%机器
3. 灰度发布:推送到10%机器
4. 观察监控:无异常
5. 全量发布:推送到所有机器
6. 回滚:有问题时回滚到上一版本

4.2 灰度发布实现

public class ConfigGrayRelease {

    // 灰度百分比
    public static final Map<String, Integer> GRAY_PERCENT = Map.of(
        "first", 10,
        "second", 30,
        "third", 50,
        "full", 100
    );

    // 判断是否在灰度范围内
    public boolean isInGrayScope(String clientId, int grayPercent) {
        // 使用一致性哈希,相同的clientId总是落入同一范围
        int hash = Math.abs(clientId.hashCode() % 100);
        return hash < grayPercent;
    }

    // 灰度发布
    public void grayRelease(Long configId, int grayPercent) {
        Config config = configDAO.selectById(configId);

        // 标记为灰度状态
        config.setStatus(ConfigStatus.GRAY);
        config.setGrayPercent(grayPercent);
        config.setGrayStrategy("MACHINE");
        configDAO.update(config);

        // 只推送给灰度范围内的客户端
        List<String> grayClients = clientDAO.selectByGrayScope(grayPercent);
        pushToClients(config, grayClients);

        // 通知监控
        monitorService.alert("灰度发布", configId, grayPercent);
    }

    // 全量发布
    public void fullRelease(Long configId) {
        Config config = configDAO.selectById(configId);
        config.setStatus(ConfigStatus.RELEASED);
        config.setGrayPercent(100);
        configDAO.update(config);

        // 推送给所有客户端
        List<String> allClients = clientDAO.selectAll();
        pushToClients(config, allClients);
    }

    // 回滚
    public void rollback(Long configId) {
        Config config = configDAO.selectById(configId);
        Config previous = configHistoryDAO.selectPrevious(configId);

        // 恢复上一版本
        config.setConfigValue(previous.getConfigValue());
        config.setVersion(previous.getVersion());
        config.setStatus(ConfigStatus.ROLLED_BACK);
        configDAO.update(config);

        // 通知所有客户端
        pushToClients(config, clientDAO.selectAll());
    }
}

五、配置变更管控 🟡

5.1 权限控制

权限模型:
1. 操作人:谁可以修改配置
2. 审批流:哪些配置需要审批
3. 告警:配置变更需要通知谁

审批流程:
1. 草稿创建:创建者写配置变更
2. 提交审批:提交审批请求
3. 审批通过:审批者审核
4. 灰度发布:按灰度策略发布
5. 全量发布:审批者确认后全量

5.2 审计日志

审计日志内容:
1. 操作人
2. 操作时间
3. 操作类型(创建/修改/删除/回滚)
4. 变更前值
5. 变更后值
6. 审批人(如果有)
7. 变更原因
8. 工单号(如果有)

六、生产避坑 🟡

6.1 配置中心的五大坑

坑1:配置推送顺序问题

问题:多个配置同时变更,推送顺序不确定
场景:配置A依赖配置B,但B先推送完成
影响:应用读取到错误的配置组合
解决方案:
- 配置关联依赖管理:按依赖顺序推送
- 或者:应用重启后再加载配置

坑2:本地缓存导致配置不一致

问题:应用本地缓存配置,更新后没有生效
场景:配置变更推送成功,但应用还在用旧配置
影响:配置不生效
解决方案:
- 应用监听配置变更事件,立即刷新缓存
- 或者:重启应用
- 或者:配置SDK定期拉取最新配置

坑3:灰度发布时监控缺失

问题:灰度发布后没有及时观察
场景:灰度10%的机器有异常,但没发现
影响:全量发布后问题扩大
解决方案:
- 灰度发布必须设置观察窗口(如30分钟)
- 观察窗口内必须看监控
- 告警必须发送给配置变更负责人

坑4:回滚不及时

问题:发现问题后回滚太慢
场景:双十一零点配置出错,但回滚花了10分钟
影响:损失扩大
解决方案:
- 一键回滚:按钮即可回滚
- 自动回滚:发现异常时自动回滚
- 预演回滚:每次发布前先测试回滚

坑5:配置加密泄漏

问题:数据库中的密码等敏感配置被泄漏
场景:MySQL密码存在配置中心,被开发人员看到
影响:数据库泄漏
解决方案:
- 敏感配置加密存储
- 访问日志记录
- 最小权限原则

【架构权衡】

配置中心的设计哲学是变更可控。一个好的配置中心,应该让每一次配置变更都可追溯、可灰度、可回滚。技术实现只是基础,更重要的是流程管控和运维意识。

七、真实面试回放 🟡

面试官:Apollo和Nacos配置中心的区别是什么?

候选人(配置架构师):核心区别在于架构和适用场景。

Apollo是携程开源的,架构更重,强调多环境隔离和权限管控,适合大型企业。

Nacos是阿里开源的,架构更轻,配置和服务发现一体,适合中小型项目。

推送机制上,Apollo用HTTP长轮询,Nacos用轮询 + 推送混合。

面试官:HTTP长轮询和长连接推送有什么区别?

候选人:HTTP长轮询是客户端主动拉,服务器有变更就返回,没有就等30秒再拉。

长连接推送是服务端主动推,变更时立即推送,延迟更低。

长轮询的优点是简单,不占用服务端连接数;长连接的优点是实时性更好。

面试官:如果配置中心挂了怎么办?

候选人:配置中心挂了分两种情况:

一是配置中心可读不可写:应用还能读取配置,只是不能修改。这个影响不大。

二是配置中心完全不可用:应用本地有配置缓存,可以继续用旧配置。最多影响配置变更,不影响正常运行。

关键设计是:配置SDK必须有本地缓存兜底。

【面试官手记】

配置架构师这场面试的亮点:

  1. 知道Apollo和Nacos的区别:架构定位不同

  2. 知道长轮询和长连接的区别:拉 vs 推

  3. 知道配置SDK必须有本地缓存:容灾设计

这场面试属于P6+级别,配置中心是架构师必备知识。

配置中心设计的核心是变更管控,记住三个要点:

  1. 推送机制:长连接推送 + HTTP长轮询兜底
  2. 灰度发布:按机器/用户/区域灰度,有观察窗口
  3. 快速回滚:一键回滚 + 自动回滚

配置中心不只是存储配置,更是保障系统稳定运行的工具。