Apache Ratis 常用工具与 API 详解

系统梳理 Apache Ratis 的核心 API 与管理工具:RaftPeer、RaftGroup、RaftServer.newBuilder、BaseStateMachine 与 RaftClient 读写接口均附实测代码与签名核对,ratis-shell 的组信息查询、leader 转移、快照创建命令附真实回显与踩坑提示,全部步骤在 Ubuntu 22.04 环境实测通过。

Apache RatisApache Ratis2026/10/12

Apache Ratis 常用工具与 API 详解

目标:给出一套够用的 Apache Ratis 3.3.1 开发与管理工具清单。API 部分覆盖组网、服务端构建、状态机与客户端读写;工具部分覆盖 ratis-shell 管理命令与官方 FileStore 示例脚本,全部代码与回显来自实测。

一、环境信息

项目值
操作系统Ubuntu 22.04.5 LTS
JDKOpenJDK 17.0.20.1
Maven3.6.3
Ratis 版本3.3.1
演示集群单机伪集群 n0/n1/n2,端口 6000-6002
组 ID02511d47-67ce-4664-951d-584ebac98b48

二、工具与 API 概览

Ratis 是内嵌库,没有独立守护进程,日常围绕三类入口工作:

类别入口用途
Java APIRaftServer、RaftClient、StateMachine把共识能力嵌入应用
管理命令行ratis-shell(bin/ratis)查看组信息、转移 leader、触发快照
官方示例发行包 examples/FileStore 参考实现与演练脚本

三、前置条件

  • Maven 工程已引入依赖(版本 3.3.1):ratis-server、ratis-grpc、ratis-metrics-default,管理命令行另加 ratis-shell。
  • classpath 生成(独立进程运行工具时使用):
$ mvn -B -q dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt
  • 3.3.1 的 RaftServer 关键签名(javap 核对):getId()、getDivision(RaftGroupId)、start()、getLifeCycleState()、close()、newBuilder()。

四、核心 API 详解

4.1 RaftPeer、RaftGroupId 与 RaftGroup

三者构成组网三元组:节点、组标识、组内成员集合。

final RaftPeer[] peers = new RaftPeer[]{
    RaftPeer.newBuilder().setId(RaftPeerId.valueOf("n0")).setAddress("127.0.0.1:6000").build(),
    RaftPeer.newBuilder().setId(RaftPeerId.valueOf("n1")).setAddress("127.0.0.1:6001").build(),
    RaftPeer.newBuilder().setId(RaftPeerId.valueOf("n2")).setAddress("127.0.0.1:6002").build()
};
final RaftGroup group = RaftGroup.valueOf(
    RaftGroupId.valueOf(UUID.fromString("02511d47-67ce-4664-951d-584ebac98b48")), peers);
System.out.println(group);

实测输出:

group-584EBAC98B48:[n0|127.0.0.1:6000, n1|127.0.0.1:6001, n2|127.0.0.1:6002]

要点:

  • 组 ID 必须全集群一致,推荐 UUID 固定下来写入代码或配置;组 ID 变化会让旧存储目录无法识别。
  • RaftPeer 的 setAddress 五段格式 name:host:port:dataStreamPort:clientPort:adminPort,只写 host:port 时其余端口自动按偏移生成。
  • members 变化属动态成员变更,需走 setConfiguration,不在此展开。

4.2 RaftServer.newBuilder 构建与生命周期

服务端构建四要素:serverId、group、properties、stateMachine。

final RaftProperties props = new RaftProperties();
RaftServerConfigKeys.setStorageDir(props, Collections.singletonList(new File("/tmp/ratis/n0")));
GrpcConfigKeys.Server.setPort(props, 6000);
final RaftServer server = RaftServer.newBuilder()
    .setServerId(RaftPeerId.valueOf("n0"))
    .setGroup(group)
    .setProperties(props)
    .setStateMachine(new SimpleStateMachine())
    .build();
server.start();          // 生命周期:构建、启动、运行、关闭
// ...
server.close();

启动日志(版本与修订号,每节点打印一次):

[demo.RatisDemo.main()] INFO org.apache.ratis.server.RaftServer - Starting Apache Ratis Server -- RaftServerProxy n0
[demo.RatisDemo.main()] INFO org.apache.ratis.server.RaftServer -                version: 3.3.1
[demo.RatisDemo.main()] INFO org.apache.ratis.server.RaftServer -               revision: 379c90a00291252beffa81c6d10d47ac0d015dce

4.3 状态机接口:BaseStateMachine

业务共识的核心是状态机。最简实现只需覆写 applyTransaction 与 query:

public class SimpleStateMachine extends BaseStateMachine {
    private final Map<String, String> data = new ConcurrentHashMap<>();
    private RaftPeerId selfId;

    @Override
    public void initialize(RaftServer server, RaftGroupId groupId, RaftStorage storage) throws IOException {
        super.initialize(server, groupId, storage);
        this.selfId = server.getId();
    }

    @Override
    public CompletableFuture<Message> applyTransaction(TransactionContext trx) {
        final LogEntryProto entry = trx.getLogEntry();
        final String content = entry.getStateMachineLogEntry().getLogData().toStringUtf8();
        final String[] kv = content.split("=", 2);
        if (kv.length == 2) {
            data.put(kv[0], kv[1]);
        }
        updateLastAppliedTermIndex(entry.getTerm(), entry.getIndex());
        return CompletableFuture.completedFuture(
                Message.valueOf("OK: " + selfId + " applied index=" + entry.getIndex() + " -> " + content));
    }

    @Override
    public CompletableFuture<Message> query(Message request) {
        return CompletableFuture.completedFuture(
                Message.valueOf(data.getOrDefault(request.getContent().toStringUtf8(), "(nil)")));
    }
}

接口分类速查:

方法触发点返回注意
initialize服务启动时回调一次void捕获节点 ID、初始化存储
applyTransactionleader 与 follower 应用已提交日志CompletableFuture<Message>必须更新 lastAppliedTermIndex
query只读请求(任一节点)CompletableFuture<Message>不走复制日志
takeSnapshot触发快照时CompletableFuture<Long>返回快照索引

4.4 RaftClient 读写

try (RaftClient client = RaftClient.newBuilder()
        .setProperties(new RaftProperties())
        .setRaftGroup(group)
        .build()) {
    // 写:send 返回 RaftClientReply
    final RaftClientReply reply = client.io().send(Message.valueOf("user=alice"));
    System.out.println(reply.getMessage().getContent().toStringUtf8());

    // 读:sendReadOnly 由状态机 query 处理
    final RaftClientReply r = client.io().sendReadOnly(Message.valueOf("user"));
    System.out.println(r.getMessage().getContent().toStringUtf8());
}

实测输出:

OK: n1 applied index=1 -> user=alice
alice

读写接口速查(javap 核对,BlockingApi 即 client.io() 的类型):

方法语义说明
client.io().send(Message)写入并等提交阻塞至多数派落盘并应用
client.io().sendReadOnly(Message)本地读leader 默认线性一致读,开销低于写
client.send(Message)异步写返回 CompletableFuture<RaftClientReply>
RaftClientReply.getMessage()取响应内容getContent() 为 ByteString,toStringUtf8 还原

异常语义:请求打到 follower 时抛 NotLeaderException(携带 suggested leader);leader 刚上任未就绪抛 LeaderNotReadyException。两种情况客户端默认自动重试,实测首个请求出现过上述日志并最终成功。

4.5 集群状态查询

final RaftServer leader = servers.stream()
    .filter(s -> s.getLifeCycleState().isRunning())
    .filter(s -> s.getDivision(group.getGroupId()).getInfo().isLeader())
    .findFirst().orElse(null);
System.out.println("leader = " + (leader == null ? "none" : leader.getId()));

实测输出:

[Demo] leader = n1

五、ratis-shell 命令详解

5.1 命令入口与调用方式

发行包脚本(推荐):

/opt/ratis/apache-ratis-3.3.1-bin/bin/ratis sh <command> [options]

无参运行可见全部命令组:

$ ./apache-ratis-3.3.1-bin/bin/ratis sh
Usage: ratis sh [generic options]
         [election [transfer] [stepDown] [pause] [resume]]
         [group [info] [list]]
         [local [raftMetaConf]]
         [peer [add] [remove] [setPriority]]
         [snapshot [create]]

Maven 工程也可用 java -cp 直接调用主类 org.apache.ratis.shell.cli.sh.RatisShell(ratis-shell-3.3.1.jar 无 Main-Class 清单项,必须显式指定),两种方式回显一致。

5.2 sh group info:查看组与 leader

$ java -cp "target/classes:$(cat /tmp/cp.txt)" org.apache.ratis.shell.cli.sh.RatisShell group info \
    -peers "127.0.0.1:6000,127.0.0.1:6001,127.0.0.1:6002" \
    -groupid "02511d47-67ce-4664-951d-584ebac98b48"
group id: 02511d47-67ce-4664-951d-584ebac98b48
leader info: n1(127.0.0.1:6001)

[server {
  id: "n0"
  address: "127.0.0.1:6000"
  startupRole: FOLLOWER
}
, server {
  id: "n1"
  address: "127.0.0.1:6001"
  startupRole: FOLLOWER
}
, server {
  id: "n2"
  address: "127.0.0.1:6002"
  startupRole: FOLLOWER
}
]

巡检脚本可直接取 leader info 行判断主节点位置。

5.3 sh group list:查询节点所属组

$ java -cp "target/classes:$(cat /tmp/cp.txt)" org.apache.ratis.shell.cli.sh.RatisShell group list \
    -peers "127.0.0.1:6000,127.0.0.1:6001,127.0.0.1:6002" \
    -groupid "02511d47-67ce-4664-951d-584ebac98b48" -peerId n0
The peerId n0 (server 127.0.0.1:6000) is in 1 groups, and the groupIds is: [group-584EBAC98B48]

注意定位节点必须用 -peerId;实测用 -serverAddress 127.0.0.1:6000 会报 NullPointerException: Server 127.0.0.1_6000 not found。

5.4 sh election transfer:转移 leader

$ java -cp "target/classes:$(cat /tmp/cp.txt)" org.apache.ratis.shell.cli.sh.RatisShell election transfer \
    -peers "127.0.0.1:6000,127.0.0.1:6001,127.0.0.1:6002" \
    -groupid "02511d47-67ce-4664-951d-584ebac98b48" -address 127.0.0.1:6000
Transferring leadership to peer n0 with address 127.0.0.1:6000
Transferring leadership initiated

转移完成后复查 group info:

group id: 02511d47-67ce-4664-951d-584ebac98b48
leader info: n0(127.0.0.1:6000)

典型场景:版本升级前把 leader 平滑挪离待重启节点。

5.5 sh snapshot create:手动触发快照

$ java -cp "target/classes:$(cat /tmp/cp.txt)" org.apache.ratis.shell.cli.sh.RatisShell snapshot create \
    -peers "127.0.0.1:6000,127.0.0.1:6001,127.0.0.1:6002" \
    -groupid "02511d47-67ce-4664-951d-584ebac98b48" -peerId n0
Successful take snapshot on peerId n0, the latest snapshot index is 0

注意 3.3.1 必须带 -peerId 指定目标节点,否则报 NullPointerException at RaftClientImpl.getSnapshotManagementApi(RaftClientImpl.java:324)。官方 usage:

Usage: create -peers <PEER0_HOST:PEER0_PORT,PEER1_HOST:PEER1_PORT,PEER2_HOST:PEER2_PORT> [-groupid <RAFT_GROUP_ID>] [-snapshotTimeout <timeoutInMs>] [-peerId <raftPeerId>]
Make a ratis server take snapshot

六、官方 FileStore 示例工具

发行包 examples/ 内置 FileStore 参考实现(ratis-examples-3.3.1.jar),server.sh 与 client.sh 封装了全部参数:

# 启动三副本(每实例一条命令,--storage 各自独立)
JAVA_TOOL_OPTIONS="-Xms256m -Xmx512m" bin/server.sh filestore server \
  --id n0 --peers "n0:localhost:6000,n1:localhost:6001,n2:localhost:6002" --storage /tmp/filestore/n0

# 写入 2 个 1024 字节随机文件
bin/client.sh filestore loadgen --numFiles 2 --size 1024 --numClients 1 \
  --peers "n0:localhost:6000,n1:localhost:6001,n2:localhost:6002" --storage /tmp/loadgen-client
Total files written: 2

写入后对三节点存储做 md5 核验,结果一致即复制生效:

36210cb5a0968876183da67427d9e141  /tmp/filestore/n0/n0/file-654a7e8a-2378-454c-a17e-0a357b7c4686-0
36210cb5a0968876183da67427d9e141  /tmp/filestore/n1/n1/file-654a7e8a-2378-454c-a17e-0a357b7c4686-0
36210cb5a0968876183da67427d9e141  /tmp/filestore/n2/n2/file-654a7e8a-2378-454c-a17e-0a357b7c4686-0

已知问题:3.3.1 的 filestore read 实测报 NullPointerException: target == null,读取验证用存储目录 md5 替代。

七、注意事项

  • ratis-shell 的 -peers 传的是地址列表,group info 的结果显示 startupRole 均为 FOLLOWER 属正常,运行时角色以 leader info 行为准。
  • Java API 与 CLI 的 -groupid 写法不同:API 用 UUID 对象,CLI 用 UUID 字符串;组 ID 前缀 group- 是显示层附加的。
  • 客户端要处理 NotLeaderException 自行换节点;CLI 内部已自动重定向,无需干预。
  • state machine 的 query 只反映本节点应用进度,读旧值场景须确认线性一致语义(见配置篇 read.option)。
  • 发行包脚本会自动注入 OTEL agent,包布局变化时可能打印 agent 安装失败告警,不影响功能。

八、常见问题

  • group info 卡住无输出:目标节点未启动或端口不通,先用 jps 与端口探活确认。
  • election transfer 报 Already leader:目标节点已是 leader,无需转移。
  • snapshot create 报 NullPointerException:补 -peerId 参数。
  • javap 提示找不到类:RaftServerConfigKeys 在 ratis-server-api,DivisionInfo 在 ratis-server,javap 时把两个 jar 一起加入 classpath。
  • 客户端重试产生重复写入:Ratis 内置 retrycache 按客户端会话去重,自研客户端保持 clientId 稳定即可。

九、参考链接