跳到主内容

v0.16.0 版本说明

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 系统
写入延迟2ms4.5ms含复制
容忍故障节点02(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-300ms100-500ms检测速度 vs 误触发
心跳间隔50ms25-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.1ms5 万/秒8:1
5 节点4.5ms3 万/秒8:1
7 节点6.8ms2 万/秒8:1
9 节点9.2ms1.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 节点199.99%地市调度
5 节点299.995%省级调度
7 节点399.999%区域调度

生产环境推荐 5 节点起步,兼顾可用性与性能。跨机房部署时建议 3 机房 5 节点(2-2-1 分布),确保单机房故障不丢数据。