EnerOS v0.16.0
发布日期:2025年03月09日 版本代号:Raft Git Tag:v0.16.0 支持状态:稳定(Stable) Crate 总数:42(新增 4 个) 测试用例数:5820+(新增 580)
版本概述
EnerOS v0.16.0「Raft」是 EnerOS 在「高可用(High Availability)」方向上的核心版本,本版本的核心目标是引入基于 Raft 共识算法的分布式一致性层,使 EnerOS 集群能够在节点故障时自动切换 Leader、复制日志、保持数据一致性,实现 99.99% 以上的可用性,满足电力调度系统对连续运行能力的严苛要求。
电力调度系统是典型的「7×24 不间断」系统,任何停机都可能导致电网运行风险。传统方案依赖主备双机热备,但主备切换依赖外部仲裁、数据同步存在窗口、故障检测滞后等问题突出。Raft 算法通过 Leader 选举、日志复制、多数派提交三大机制,在算法层面解决了分布式一致性问题,且比 Paxos 更易理解与实现。v0.16.0 将 Raft 深度集成进 EnerOS 内核,使拓扑状态、孪生体快照、租户配置、安全策略等关键数据均具备强一致性保证。
本版本引入了 eneros-raft(Raft 核心实现)、eneros-raft-log(日志复制)、eneros-raft-election(Leader 选举)、eneros-raft-failover(故障切换)四个新 crate。设计哲学是「多数派即真理」——任何写入必须经过集群多数派节点确认方可提交,确保在少数节点故障时数据零丢失、服务零中断。
关键数据
| 指标 | v0.15.0(单节点) | v0.16.0(5 节点 Raft) | 说明 |
|---|---|---|---|
| 集群可用性 | 99.5% | 99.995% | +0.5pp |
| 故障切换时间 | 人工 5-30min | 自动 800ms | 显著降低 |
| 数据一致性 | 弱 | 强(线性一致) | CP 系统 |
| 写入延迟 | 2ms | 4.5ms | 含复制 |
| 容忍故障节点 | 0 | 2(5 节点中) | 多数派 |
新特性
1. Raft 共识算法
引入 eneros-raft crate,实现完整的 Raft 共识算法,包括 Leader 选举、日志复制、安全性保证与成员变更。实现严格遵循 Raft 论文(Ongaro & Ousterhout, 2014),并通过 Jepsen 风格的分布式测试验证。
集群初始化
use eneros_raft::{RaftNode, RaftConfig, NodeId};
// 创建一个 5 节点 Raft 集群
let nodes = vec![
NodeId::from("node-1"),
NodeId::from("node-2"),
NodeId::from("node-3"),
NodeId::from("node-4"),
NodeId::from("node-5"),
];
let mut raft = RaftNode::new(NodeId::from("node-1"))
.config(RaftConfig {
peers: nodes.clone(),
election_timeout: Duration::milliseconds(150..300),
heartbeat_interval: Duration::milliseconds(50),
commit_quorum: Quorum::Majority,
log_storage: LogStorage::persistent("/var/eneros/raft"),
snapshot_interval: Duration::minutes(10),
snapshot_threshold: 10000,
})?;
// 启动 Raft
raft.start().await?;
Raft 节点状态
| 状态 | 职责 | 触发条件 | 持续时间 |
|---|---|---|---|
| Follower | 接收日志 | 默认 | 长期 |
| Candidate | 发起选举 | 选举超时 | 150-300ms |
| Leader | 处理写入 | 赢得选举 | 直至故障 |
| Observer | 只读同步 | 配置指定 | 长期 |
写入流程
// 客户端写入(自动路由到 Leader)
let proposal = RaftProposal::new("update-topology")
.payload(Command::UpdateBus {
id: 1,
voltage: 1.025,
});
match raft.submit(proposal).await {
Ok(commit) => {
// 已在多数派节点提交
println!("提交索引: {}", commit.index);
println!("提交时间: {:?}", commit.timestamp);
}
Err(RaftError::NotLeader(leader_hint)) => {
// 重定向到 Leader
let leader = leader_hint.unwrap();
redirect_to(leader, proposal).await?;
}
}
2. Leader 选举
引入 eneros-raft-election crate,实现 Raft Leader 选举算法。当 Leader 故障或网络分区时,剩余节点在选举超时后自发发起选举,获得多数派投票的节点成为新 Leader,整个选举过程在 1 秒内完成。
选举流程
use eneros_raft_election::{Election, ElectionResult};
// 监听选举事件
raft.on_election(|result| {
match result {
ElectionResult::Elected { term, votes } => {
log::info!("新 Leader 当选: term={}, 票数={}/{}",
term, votes.for, votes.total);
}
ElectionResult::Lost { term, winner } => {
log::info!("选举失败: term={}, 胜者={}", term, winner);
}
ElectionResult::Split { term } => {
log::warn!("选举脑裂: term={}", term);
}
}
});
选举参数调优
| 参数 | 默认值 | 推荐范围 | 影响 |
|---|---|---|---|
| 选举超时 | 150-300ms | 100-500ms | 检测速度 vs 误触发 |
| 心跳间隔 | 50ms | 25-100ms | 网络负载 vs 检测速度 |
| 任期递增 | +1 | 固定 | 防止过期 Leader |
| 投票策略 | 先到先得 | FCF / 优先级 | 公平 vs 偏好 |
优先级选举
// 为不同节点配置不同优先级,优先选举高配节点为 Leader
let raft = RaftNode::new(NodeId::from("node-1"))
.config(RaftConfig {
peers: nodes,
election_priority: Priority::High, // 高优先级
// ...
})?;
// node-2: 优先级 Medium
// node-3: 优先级 Medium
// node-4: 优先级 Low
// node-5: 优先级 Low
3. 日志复制
引入 eneros-raft-log crate,实现 Raft 日志复制机制。Leader 将所有写入操作封装为日志条目,并行复制到所有 Follower,多数派确认后提交。日志采用 LSM 树存储,支持快速查找与压缩。
日志结构
use eneros_raft_log::{LogEntry, LogIndex, Term};
// 日志条目结构
let entry = LogEntry {
index: LogIndex(1024),
term: Term(5),
entry_type: EntryType::Normal,
data: Command::UpdateBus { id: 1, voltage: 1.025 }.encode()?,
timestamp: now(),
};
// 追加日志
let index = raft.log().append(entry).await?;
// 复制到 Follower
raft.log().replicate(index, &followers).await?;
// 提交日志
raft.log().commit(index).await?;
日志压缩与快照
use eneros_raft_log::snapshot;
// 当日志超过阈值时自动快照压缩
let snapshot = snapshot::create(&raft)
.last_included_index(5000)
.last_included_term(4)
.state(topology.current_state()?)
.build()?;
raft.log().install_snapshot(snapshot).await?;
// 日志压缩效果
// 压缩前: 10000 条日志,大小 120MB
// 压缩后: 1 个快照 + 5000 条日志,大小 8MB + 6MB = 14MB
复制性能指标
| 集群规模 | 复制延迟 | 写入吞吐 | 日志压缩比 |
|---|---|---|---|
| 3 节点 | 2.1ms | 5 万/秒 | 8:1 |
| 5 节点 | 4.5ms | 3 万/秒 | 8:1 |
| 7 节点 | 6.8ms | 2 万/秒 | 8:1 |
| 9 节点 | 9.2ms | 1.5 万/秒 | 8:1 |
4. 故障自动切换
引入 eneros-raft-failover crate,实现节点故障的自动检测与切换。当 Leader 节点故障时,集群在选举超时后自动选举新 Leader,客户端请求自动重定向,整个过程对上层应用透明。
故障检测
use eneros_raft_failover::{FailureDetector, DetectorConfig};
let detector = FailureDetector::new(DetectorConfig {
heartbeat_timeout: Duration::milliseconds(300),
failure_threshold: 3, // 连续 3 次未心跳判定故障
suspicion_level: SuspicionLevel::PhiAccrual(phi_threshold: 8.0),
auto_demote: true, // 故障节点自动降级
});
raft.install_failure_detector(detector)?;
自动切换流程
// 监听故障切换事件
raft.on_failover(|event| {
log::warn!("故障切换: {:?}", event);
match event {
FailoverEvent::LeaderDown { old_leader, new_leader, duration } => {
log::warn!("Leader 切换: {} -> {}, 耗时 {:?}",
old_leader, new_leader, duration);
// 通知客户端重连
notify_clients(new_leader);
}
FailoverEvent::NodeRecovered { node } => {
log::info!("节点恢复: {}", node);
// 自动重新加入集群
}
FailoverEvent::QuorumLost { available, required } => {
log::error!("多数派丢失: {}/{}, 集群不可用", available, required);
// 触发告警
}
}
});
客户端重试
use eneros_raft_failover::RaftClient;
// 客户端自动处理 Leader 重定向
let client = RaftClient::new(&cluster_endpoints)
.retry_policy(RetryPolicy::Exponential {
initial: Duration::milliseconds(50),
max: Duration::seconds(2),
multiplier: 2.0,
})
.leader_redirect(true);
// 写入自动重试
let result = client.write(proposal).await?;
// 内部自动处理 NotLeader 错误与重定向
故障切换时间分解
| 阶段 | 平均耗时 | 说明 |
|---|---|---|
| 故障检测 | 300ms | 心跳超时 |
| 选举发起 | 50ms | 随机退避 |
| 投票收集 | 100ms | 多数派响应 |
| Leader 就绪 | 150ms | 状态恢复 |
| 客户端重定向 | 200ms | 重连 |
| 总计 | 800ms | 自动完成 |
5. 成员变更
支持在线成员变更,无需停机即可添加或移除节点。成员变更采用单节点变更(Joint Consensus 简化版),每次仅变更一个节点,避免配置冲突。
在线扩容
// 添加新节点 node-6
raft.add_member(NodeId::from("node-6")).await?;
// 等待新节点追上日志
raft.wait_for_catch_up(NodeId::from("node-6"), Duration::minutes(5)).await?;
// 移除节点 node-5
raft.remove_member(NodeId::from("node-5")).await?;
成员变更安全性
| 变更类型 | 安全性保证 | 备注 |
|---|---|---|
| 添加节点 | 不影响多数派 | 先同步再投票 |
| 移除节点 | 维持多数派 | 逐个移除 |
| Leader 移除 | 自动转移 | 先降级再移除 |
| 替换节点 | 添加 + 移除 | 串行执行 |
改进
- 拓扑引擎:拓扑状态变更现在通过 Raft 复制,所有节点拓扑视图一致
- 孪生体:孪生体快照支持 Raft 复制,故障切换后状态不丢失
- 租户配置:租户配额与 RBAC 策略通过 Raft 同步,配置变更全局生效
- 守卫系统:安全策略通过 Raft 复制,确保所有节点策略一致
Bug 修复
- 修复
eneros-raft在网络分区恢复后日志冲突的问题(#1608) - 修复
eneros-raft-log快照安装期间写入阻塞的问题(#1614) - 修复
eneros-raft-election预投票(pre-vote)未正确处理的问题(#1620) - 修复
eneros-raft-failover客户端重试在 Leader 频繁切换时风暴的问题(#1626)
破坏性变更
Topology::update_bus:现在返回RaftResult,包含提交索引信息Tenant::set_quota:改为 async,需.await等待 Raft 提交Guardian::add_policy:通过 Raft 复制,需多数派确认
性能提升
- 集群可用性从 99.5% 提升至 99.995%
- 故障切换时间从人工 5-30 分钟降至自动 800ms
- 写入延迟仅增加 2.5ms(含复制开销)
- 5 节点集群写入吞吐达 3 万/秒
贡献者
本版本由 25 位贡献者共同完成,提交 432 次。特别感谢:
- @raft-implementer:Raft 算法核心实现
- @election-master:Leader 选举与预投票优化
- @log-replication:日志复制与压缩
- @failover-guru:故障检测与自动切换
升级指南
从 v0.15.0 升级
1. 更新依赖
# Cargo.toml
[dependencies]
eneros-raft = { version = "0.16" }
eneros-raft-log = { version = "0.16" }
eneros-raft-election = { version = "0.16" }
eneros-raft-failover = { version = "0.16" }
2. 部署 Raft 集群
# eneros.toml
[raft]
enabled = true
cluster_id = "eneros-prod"
peers = ["node-1", "node-2", "node-3", "node-4", "node-5"]
[raft.election]
timeout_ms = [150, 300]
heartbeat_ms = 50
[raft.log]
storage = "/var/eneros/raft"
snapshot_interval_minutes = 10
snapshot_threshold = 10000
3. 单节点到集群迁移
use eneros_raft::migrate;
// 将单节点数据迁移到 Raft 集群
migrate::from_single_node("/var/eneros/data", &raft_cluster).await?;
// 验证数据一致性
migrate::verify(&raft_cluster).await?;
4. 集群规模建议
根据可用性需求选择合适的集群规模:
| 集群规模 | 容忍故障 | 可用性 | 适用场景 |
|---|---|---|---|
| 3 节点 | 1 | 99.99% | 地市调度 |
| 5 节点 | 2 | 99.995% | 省级调度 |
| 7 节点 | 3 | 99.999% | 区域调度 |
生产环境推荐 5 节点起步,兼顾可用性与性能。跨机房部署时建议 3 机房 5 节点(2-2-1 分布),确保单机房故障不丢数据。