系统设计:配置中心
- 1 系统设计:分布式唯一 ID 生成器
- 2 系统设计:配置中心
本文是「系统设计专栏」的第 8 篇,聚焦于配置中心的设计与实现。配置中心是微服务架构中不可或缺的组件,负责集中管理所有服务的配置信息,支持动态更新和灰度发布。
背景与问题
在没有配置中心之前,应用的配置通常以以下方式存在:
- 硬编码在代码中:修改配置需要重新编译、打包、发布,效率极低
- 散落在配置文件中:每个应用都有自己的
application.properties、config.yaml,难以统一管理 - 环境差异难以维护:开发、测试、生产环境的配置各不相同,手动维护容易出错
- 动态更新困难:修改配置后必须重启服务才能生效,无法实现热更新
- 缺乏版本管理:配置变更历史不可追溯,出问题后难以回滚
- 权限控制缺失:任何人都可以修改生产环境配置,存在严重安全隐患
随着微服务架构的普及,服务数量从几个增长到几百甚至几千个,上述问题被急剧放大。配置中心应运而生,它通过集中式管理、动态推送、版本控制等能力,彻底解决了配置管理的痛点。
需求分析
功能需求
| 需求 | 说明 |
|---|---|
| 配置 CRUD | 支持配置的创建、读取、更新、删除 |
| 多环境管理 | 支持开发、测试、预发、生产等多环境隔离 |
| 命名空间 | 支持按应用、按集群划分配置作用域 |
| 实时推送 | 配置变更后,客户端能够实时感知并生效 |
| 灰度发布 | 支持按 IP、百分比、用户维度进行灰度配置下发 |
| 版本管理 | 记录配置变更历史,支持版本对比和回滚 |
| 权限控制 | 支持 RBAC 权限模型,控制配置的读写权限 |
| 配置审计 | 记录所有配置操作日志,便于问题追溯 |
非功能需求
| 需求 | 目标 |
|---|---|
| 推送延迟 | 配置变更后 1 秒内推送到 99% 的客户端 |
| 可用性 | 服务可用性达到 99.99% |
| 吞吐量 | 支持 10 万+ 客户端同时连接 |
| 数据一致性 | 配置数据在服务端和客户端保持一致 |
| 安全性 | 敏感配置加密存储和传输 |
| 扩展性 | 支持水平扩展,应对客户端数量增长 |
核心指标估算
假设一个中等规模的互联网公司:
| 指标 | 估算值 |
|---|---|
| 应用数量 | 500 个 |
| 每个应用配置数 | 50 个 |
| 总配置数 | 25,000 个 |
| 活跃客户端(实例) | 50,000 个 |
| 日均配置变更次数 | 200 次 |
| 配置推送 QPS(峰值) | 5,000 |
| 配置数据总大小 | ~100 MB |
架构设计
graph TB
subgraph "管理端"
Portal[配置管理 Portal]
Admin[Admin Service]
end
subgraph "服务端"
ConfigService[Config Service<br/>配置推送服务]
MetaService[Meta Service<br/>元数据服务]
end
subgraph "存储层"
ConfigDB[(Config DB<br/>MySQL)]
Cache[(本地缓存)]
end
subgraph "客户端"
Client1[App Client 1]
Client2[App Client 2]
ClientN[App Client N]
end
Portal -->|管理操作| Admin
Admin -->|读写配置| ConfigDB
ConfigService -->|读取配置| ConfigDB
ConfigService -->|缓存| Cache
ConfigService -->|长连接推送| Client1
ConfigService -->|长连接推送| Client2
ConfigService -->|长连接推送| ClientN
Client1 -->|心跳/拉取| ConfigService
Client2 -->|心跳/拉取| ConfigService
ClientN -->|心跳/拉取| ConfigService
组件说明:
- 配置管理 Portal:提供 Web UI,供开发/运维人员进行配置管理操作
- Admin Service:处理配置的管理类请求(CRUD、权限校验、审计日志)
- Config Service:处理配置的查询和推送请求,与客户端保持长连接
- Meta Service:提供服务发现,客户端通过它获取 Config Service 地址列表
- Config DB:持久化存储配置数据,通常使用 MySQL
- Client:嵌入在应用中的 SDK,负责从服务端拉取配置并监听变更
配置推送流程
sequenceDiagram
participant User as 运维人员
participant Portal as 管理 Portal
participant Admin as Admin Service
participant DB as Config DB
participant Config as Config Service
participant Client as App Client
User->>Portal: 修改配置并发布
Portal->>Admin: 提交配置变更
Admin->>Admin: 权限校验
Admin->>DB: 写入新配置版本
Admin->>Config: 通知配置变更 (ReleaseMessage)
Config->>DB: 拉取最新配置
Config->>Config: 更新本地缓存
Config->>Client: 推送配置变更 (长连接)
Client->>Client: 更新本地配置
Client->>Client: 触发回调/刷新上下文
Client-->>Config: ACK 确认
核心功能详解
配置存储模型
配置中心需要支持多维度的配置隔离,典型的存储模型如下:
Namespace(命名空间)
└── App(应用)
└── Cluster(集群)
└── Env(环境:dev/test/prod)
└── Config Item(配置项)
配置 Key 的层级设计:
{namespace}.{app}.{cluster}.{env}.{key}
# 示例
production.order-service.default.prod.mysql.host
production.order-service.default.prod.mysql.port
数据库表设计:
-- 应用表
CREATE TABLE app (
id BIGINT PRIMARY KEY,
app_id VARCHAR(64) NOT NULL UNIQUE,
name VARCHAR(128),
owner VARCHAR(64),
created_at TIMESTAMP
);
-- 命名空间表(配置分组)
CREATE TABLE namespace (
id BIGINT PRIMARY KEY,
app_id VARCHAR(64),
name VARCHAR(64),
format VARCHAR(16), -- properties / yaml / json / xml
UNIQUE (app_id, name)
);
-- 配置项表
CREATE TABLE config_item (
id BIGINT PRIMARY KEY,
namespace_id BIGINT,
key VARCHAR(128) NOT NULL,
value TEXT,
comment VARCHAR(256),
created_by VARCHAR(64),
updated_by VARCHAR(64),
updated_at TIMESTAMP
);
-- 配置发布历史表(用于版本管理和回滚)
CREATE TABLE release_history (
id BIGINT PRIMARY KEY,
namespace_id BIGINT,
release_key VARCHAR(64),
configurations TEXT, -- JSON 格式的完整配置快照
released_by VARCHAR(64),
released_at TIMESTAMP,
is_rollback BOOLEAN DEFAULT FALSE,
previous_release_id BIGINT
);
-- 灰度规则表
CREATE TABLE gray_rule (
id BIGINT PRIMARY KEY,
namespace_id BIGINT,
rule_type VARCHAR(16), -- ip / percentage / user
rule_value VARCHAR(256),
config_snapshot TEXT,
status VARCHAR(16) -- active / inactive
);
配置推送机制
配置推送是配置中心的核心能力,主流实现方案有三种:
方案一:长轮询(Long Polling)
原理:客户端发起请求后,服务端不立即返回,而是 hold 住连接,直到配置变更或超时(通常 30 秒)。
Client Config Service
| |
|--- 1. 发起请求 (携带当前版本号) -->|
| |
|<-- 2. hold 连接 (30s 超时) ---|
| |
| | 配置变更
| |
|<-- 3. 立即返回新配置 ---------|
| |
|--- 4. 立即发起下一次请求 ---->|
优点:实现简单,兼容性好(基于 HTTP),无额外端口要求 缺点:每次超时后需要重新建立连接,有一定开销
方案二:WebSocket
原理:客户端与服务端建立 WebSocket 全双工连接,配置变更时服务端主动推送。
优点:实时性最好,连接建立后零额外开销 缺点:需要额外端口,某些网络环境(如企业防火墙)可能限制 WebSocket
方案三:服务端推送(gRPC Stream / SSE)
原理:基于 HTTP/2 的多路复用能力,建立双向流式连接。
优点:性能优异,支持多路复用 缺点:实现复杂度较高
实际选型建议:
| 场景 | 推荐方案 |
|---|---|
| 通用 Web 应用 | 长轮询 |
| 内部网络环境可控 | WebSocket |
| 高性能要求的服务间通信 | gRPC Stream |
灰度发布
灰度发布是配置中心的重要能力,支持逐步放量验证配置变更的正确性。
灰度维度:
- 按 IP 灰度:指定特定 IP 范围的客户端生效新配置
- 按百分比灰度:随机选择一定比例(如 10%)的客户端生效
- 按用户灰度:根据用户 ID、地域、设备等信息定向灰度
灰度实现逻辑:
public class GrayReleaseService {
public Configuration getConfiguration(ClientContext ctx, String namespace) {
// 1. 获取正式发布的配置
Configuration releaseConfig = getReleasedConfig(namespace);
// 2. 检查是否存在活跃的灰度规则
List<GrayRule> grayRules = grayRuleRepository.findActiveByNamespace(namespace);
for (GrayRule rule : grayRules) {
if (matches(ctx, rule)) {
// 客户端命中灰度规则,返回灰度配置
return rule.getGrayConfiguration();
}
}
// 3. 未命中灰度,返回正式配置
return releaseConfig;
}
private boolean matches(ClientContext ctx, GrayRule rule) {
switch (rule.getType()) {
case IP:
return IpUtils.isInRange(ctx.getClientIp(), rule.getValue());
case PERCENTAGE:
return hash(ctx.getClientId()) % 100 < Integer.parseInt(rule.getValue());
case USER:
return rule.getValue().contains(ctx.getUserId());
default:
return false;
}
}
}
配置变更监听与回调
客户端获取配置后,需要能够感知变更并做出响应。典型的回调机制:
// 1. 注册配置变更监听器
ConfigService.addListener("mysql.host", new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent event) {
String oldValue = event.getOldValue();
String newValue = event.getNewValue();
// 动态刷新数据源连接池
DataSourceManager.refreshConnectionPool(newValue);
log.info("配置变更: {} = {} -> {}", event.getKey(), oldValue, newValue);
}
});
// 2. 客户端内部维护配置缓存和变更检测
public class ConfigClient {
private Map<String, String> localCache = new ConcurrentHashMap<>();
private List<ConfigChangeListener> listeners = new CopyOnWriteArrayList<>();
public void onServerPush(Map<String, String> newConfigs) {
for (Map.Entry<String, String> entry : newConfigs.entrySet()) {
String key = entry.getKey();
String newValue = entry.getValue();
String oldValue = localCache.get(key);
if (!Objects.equals(oldValue, newValue)) {
localCache.put(key, newValue);
// 触发回调
fireChangeEvent(key, oldValue, newValue);
}
}
}
}
配置版本管理与回滚
每次配置发布都生成一个版本快照,支持随时回滚到任意历史版本。
版本管理流程:
- 用户修改配置并点击「发布」
- 系统将当前命名空间下的所有配置序列化为 JSON,存入
release_history表 - 生成唯一的
release_key(如时间戳 + 随机数) - 更新
namespace表的当前发布版本指向
回滚实现:
@Transactional
public void rollback(Long namespaceId, Long targetReleaseId) {
// 1. 获取目标版本的配置快照
ReleaseHistory target = releaseHistoryRepository.findById(targetReleaseId);
// 2. 将历史配置还原到 config_item 表
List<ConfigItem> historicalConfigs = parseSnapshot(target.getConfigurations());
configItemRepository.batchUpdate(namespaceId, historicalConfigs);
// 3. 创建一条新的发布记录,标记为回滚操作
ReleaseHistory rollbackRelease = new ReleaseHistory();
rollbackRelease.setNamespaceId(namespaceId);
rollbackRelease.setConfigurations(target.getConfigurations());
rollbackRelease.setRollback(true);
rollbackRelease.setPreviousReleaseId(getCurrentReleaseId(namespaceId));
releaseHistoryRepository.save(rollbackRelease);
// 4. 通知 Config Service 推送变更
releaseMessageSender.sendMessage(namespaceId);
}
主流方案对比
| 特性 | Apollo | Nacos | Consul | etcd |
|---|---|---|---|---|
| 开发方 | 携程 | 阿里巴巴 | HashiCorp | CoreOS |
| 定位 | 配置中心 | 配置中心 + 服务发现 | 服务发现 + 配置中心 | KV 存储(可作为配置中心底座) |
| 配置推送 | 长轮询 | 长轮询 | 长轮询 | Watch 机制 |
| 灰度发布 | 支持 | 支持 | 不支持 | 需自行实现 |
| 版本管理 | 完善 | 基础支持 | 不支持 | 不支持 |
| 权限控制 | 完善(RBAC) | 基础支持 | ACL | TLS + RBAC |
| 多环境 | 原生支持 | 支持 | 需自行规划 | 需自行规划 |
| 配置格式 | Properties/YAML/JSON | YAML/JSON/Properties/Text | Key-Value | Key-Value |
| 客户端 SDK | Java/.NET/Go/Python/Node.js | Java/Go/Python/Node.js/C++ | 多语言 | 多语言 |
| 部署复杂度 | 较高(需 MySQL) | 较低(内置存储) | 中等 | 较低 |
| 适用场景 | 大规模企业级配置管理 | 云原生、K8s 环境 | 服务网格、Consul 生态 | K8s 原生、简单配置 |
选型建议:
- Apollo:对配置管理有严格要求的大型企业,需要完善的灰度、审计、权限功能
- Nacos:Spring Cloud / Dubbo 生态,希望配置中心和服务发现统一管理的场景
- Consul:已经使用 Consul 做服务发现,配置管理需求较简单的场景
- etcd:K8s 原生环境,或者需要自研配置中心时使用 etcd 作为存储底座
面试追问点
Q1: 配置中心挂了,客户端还能正常工作吗?
答:可以,但需要客户端实现本地缓存和降级机制。
- 本地缓存:客户端在启动时从配置中心拉取全量配置,缓存在本地内存和文件中
- 降级策略:当配置中心不可用时,客户端使用本地缓存的配置继续运行
- 容错设计:配置中心恢复后,客户端自动重新连接并同步最新配置
public String getConfig(String key) {
// 1. 优先从本地缓存读取
String value = localCache.get(key);
if (value != null) {
return value;
}
// 2. 本地缓存未命中,尝试从服务端获取
try {
value = remoteConfigService.get(key);
localCache.put(key, value);
return value;
} catch (Exception e) {
// 3. 服务端不可用,使用本地文件兜底
return localFileCache.get(key);
}
}
Q2: 配置推送如何保证所有客户端都收到?如果有客户端离线怎么办?
答:采用「推送 + 拉取」的双保险机制。
- 推送阶段:配置变更时,通过长连接向所有在线客户端推送
- ACK 机制:客户端收到推送后返回 ACK,服务端记录未 ACK 的客户端
- 重试机制:对未 ACK 的客户端进行一定次数的重试
- 兜底拉取:客户端定时(如每 5 分钟)主动拉取配置,与本地版本对比,发现不一致则更新
- 离线补偿:客户端重新上线时,立即对比本地配置版本和服务端最新版本,如有差异则同步
Q3: 配置变更的即时性 vs 一致性怎么权衡?
答:配置中心通常选择最终一致性,优先保证高可用。
- 即时性:通过长连接推送,大部分客户端在 1 秒内收到变更
- 一致性:不要求所有客户端在同一时刻看到完全一致的配置,允许短暂的配置版本不一致
- 权衡原因:配置变更不像金融交易那样要求强一致性,短暂的配置不一致通常不会导致严重问题
- 优化手段:对于要求强一致的场景,可以在配置中附带生效时间戳,客户端在指定时间统一生效
Q4: 敏感配置(如数据库密码)怎么安全存储和传输?
答:需要多层安全机制:
- 存储加密:敏感配置在数据库中加密存储(AES-256 加密)
- 传输加密:客户端与服务端通信使用 TLS/HTTPS
- 访问控制:敏感配置单独放在受控的命名空间,限制访问权限
- 密钥分离:加密密钥存储在独立的密钥管理系统(KMS)中,与配置数据分离
- 客户端解密:服务端传输密文,由客户端使用本地密钥解密,避免服务端持有明文
- 审计日志:所有敏感配置的访问都记录审计日志
// 服务端存储时加密
String encryptedValue = AESCipher.encrypt(plainPassword, kmsKey);
configRepository.save(key, encryptedValue);
// 传输时保持密文
// 客户端收到后解密
String plainPassword = AESCipher.decrypt(encryptedValue, clientLocalKey);
Q5: 如果配置推送导致服务出问题,怎么快速回滚?
答:配置中心需要提供一键回滚能力。
- 版本快照:每次发布自动保存完整配置快照
- 一键回滚:在管理界面选择历史版本,点击回滚立即生效
- 自动回滚:可配置变更后的观察期(如 5 分钟),期间监控业务指标,异常自动触发回滚
- 紧急开关:提供「全局回滚」功能,将所有配置恢复到指定时间点状态
- 熔断机制:客户端 SDK 可配置变更熔断策略,如连续 N 次配置更新失败则拒绝后续变更
// 客户端熔断示例
public class ConfigCircuitBreaker {
private int consecutiveFailures = 0;
private static final int THRESHOLD = 3;
public void onConfigChange(Config newConfig) {
if (consecutiveFailures >= THRESHOLD) {
log.warn("配置变更熔断,拒绝应用新配置");
return;
}
try {
applyConfig(newConfig);
consecutiveFailures = 0;
} catch (Exception e) {
consecutiveFailures++;
log.error("配置应用失败,当前连续失败次数: {}", consecutiveFailures);
}
}
}
扩展思考
配置中心和服务发现能否合并?
从功能上看,配置中心和服务发现都涉及「元数据的管理和分发」,但两者有本质区别:
| 维度 | 配置中心 | 服务发现 |
|---|---|---|
| 数据特点 | 数据量较大,变更频率较低 | 数据量较小,变更频率高(实例上下线) |
| 一致性要求 | 最终一致性即可 | 通常要求更高的一致性 |
| 推送模式 | 配置变更后推送 | 实例状态变化实时推送 |
| 数据格式 | 结构化配置(YAML/Properties) | 简单的地址列表 |
Nacos 的设计哲学:Nacos 将两者合并,核心理念是「统一的服务治理平台」。其优势在于:
- 统一模型:服务和配置都抽象为「命名空间 + 分组 + 数据 ID」的模型
- 统一推送通道:复用同一套长连接推送机制
- 降低运维成本:减少组件数量,简化部署和维护
- 云原生友好:与 Spring Cloud、Dubbo、K8s 深度集成
但也存在权衡:
- 合并后架构复杂度增加
- 配置中心和服务发现的性能特征不同,混合部署可能需要更精细的资源隔离
- 故障域扩大,一个组件的问题可能影响两个功能
结论:对于中小型系统,合并可以简化架构;对于超大规模系统,分离可以获得更好的独立扩展性和故障隔离。
专栏导航
系统设计专栏 系列文章:
上一篇:系统设计:分布式缓存
下一篇:系统设计:秒杀系统
本文是第 8 篇,共 12 篇。
© 本文著作权归作者所有,转载请注明出处。