RAFT是一种新型易于理解的分布式一致性复制协议,由斯坦福大学的Diego Ongaro和John Ousterhout提出,作为RAMCloud项目中的中心协调组件。Raft是一种Leader-Based的Multi-Paxos变种,相比Paxos、Zab、View Stamped Replication等协议提供了更完整更清晰的协议描述,并提供了清晰的节点增删描述。RAFT是一种新型易于理解的分布式一致性复制协议,由斯坦福大学的Diego Ongaro和John Ousterhout提出,作为RAMCloud项目中的中心协调组件。Raft是一种Leader-Based的Multi-Paxos变种,相比Paxos、Zab、View Stamped Replication等协议提供了更完整更清晰的协议描述,并提供了清晰的节点增删描述。
一、使用braft
1.1注册并启动Server
braft需要运行在具体的brpc server里面,你可以让braft和你的业务共享同样的端口, 也可以将braft启动到不同的端口中。 brpc允许一个端口上注册多个逻辑Service, 如果你的Service同样运行在brpc Server里面,你可以管理brpc Server并且调用以下任意一个接口将braft相关的Service加入到你的Server中。这样能让braft和你的业务跑在同样的端口里面, 降低运维的复杂度。
int add_service(brpc::Server* server, const butil::EndPoint& listen_addr);
int add_service(brpc::Server* server, int port);
int add_service(brpc::Server* server, const char* const butil::EndPoint& listen_addr);
add_service的部分代码如下,负责把braft相关的service添加到brpc server里面。RaftServiceImpl主要和raft协议有关,有一些选举、append_entries和快照相关的接口。CliServiceImpl负责管理braft相关的工作,比如add_peer,get_leader,transfer_leader等操作。
int NodeManager::add_service(brpc::Server* server,
const butil::EndPoint& listen_address) {
...
if (0 != server->AddService(
new RaftServiceImpl(listen_address),
brpc::SERVER_OWNS_SERVICE)) {
LOG(ERROR) << "Fail to add RaftService";
return -1;
}
...
if (0 != server->AddService(new CliServiceImpl, brpc::SERVER_OWNS_SERVICE)) {
LOG(ERROR) << "Fail to add CliService";
return -1;
}
{
BAIDU_SCOPED_LOCK(_mutex);
_addr_set.insert(listen_address);
}
return 0;
}
1.2实现业务状态机
需要继承braft::StateMachine并且实现里面的接口
#include
class YourStateMachineImple : public braft::StateMachine {
protected:
// on_apply是*必须*实现的
// on_apply会在一条或者多条日志被多数节点持久化之后调用, 通知用户将这些日志所表示的操作应用到业务状态机中.
// 通过iter, 可以从遍历所有未处理但是已经提交的日志, 如果你的状态机支持批量更新,可以一次性获取多
// 条日志提高状态机的吞吐.
//
void on_apply(braft::Iterator& iter) {
for (; iter.valid(); iter.next()) {
// This guard helps invoke iter.done()->Run() asynchronously to
// avoid that callback blocks the StateMachine.
braft::AsyncClosureGuard closure_guard(iter.done());
// Parse operation from iter.data() and execute this operation
// op = parse(iter.data());
// result = process(op)
// The purpose of following logs is to help you understand the way
// this StateMachine works.
// Remove these logs in performance-sensitive servers.
LOG_IF(INFO, FLAGS_log_applied_task)
<< "Exeucted operation " << op
<< " and the result is " << result
<< " at log_index=" << iter.index();
}
}
// 当这个braft节点被shutdown之后, 当所有的操作都结束, 会调用on_shutdown, 来通知用户这个状态机不再被使用。
// 这时候你可以安全的释放一些资源了.
virtual void on_shutdown() {
// Cleanup resources you'd like
}
1.3构造braft::Node
一个Node代表了一个RAFT实例, Node的ID由两个部分组成:GroupId: 为一个string, 表示这个复制组的ID.
PeerId, 结构是一个EndPoint表示对外服务的端口, 外加一个index(默认为0). 其中index的作用是让不同的副本能运行在同一个进程内, 在下面几个场景中,这个值不能忽略:
Node(const GroupId& group_id, const PeerId& peer_id);
启动这个节点:
// Starts this node
int start() {
butil::EndPoint addr(butil::my_ip(), FLAGS_port);
braft::NodeOptions node_options;
if (node_options.initial_conf.parse_from(FLAGS_conf) != 0) {
LOG(ERROR) << "Fail to parse configuration `" << FLAGS_conf << '\'';
return -1;
}
node_options.election_timeout_ms = FLAGS_election_timeout_ms;
node_options.fsm = this;
node_options.node_owns_fsm = false;
node_options.snapshot_interval_s = FLAGS_snapshot_interval;
std::string prefix = "local://" + FLAGS_data_path;
node_options.log_uri = prefix + "/log";
node_options.raft_meta_uri = prefix + "/raft_meta";
node_options.snapshot_uri = prefix + "/snapshot";
node_options.disable_cli = FLAGS_disable_cli;
braft::Node* node = new braft::Node(FLAGS_group, braft::PeerId(addr));
if (node->init(node_options) != 0) {
LOG(ERROR) << "Fail to init raft node";
delete node;
return -1;
}
_node = node;
return 0;
}initial_conf只有在这个复制组从空节点启动才会生效,当有snapshot和log里的数据不为空的时候的时候从其中恢复Configuration。initial_conf只用于创建复制组,第一个节点将自己设置进initial_conf,再调用add_peer添加其他节点,其他节点initial_conf设置为空;也可以多个节点同时设置相同的inital_conf(多个节点的ip:port)来同时启动空节点。
RAFT需要三种不同的持久存储, 分别是:RaftMetaStorage, 用来存放一些RAFT算法自身的状态数据, 比如term, vote_for等信息.
LogStorage, 用来存放用户提交的WAL
SnapshotStorage, 用来存放用户的Snapshot以及元信息. 用三个不同的uri来表示, 并且提供了基于本地文件系统的默认实现,type为local, 比如 local://data 就是存放到当前文件夹的data目录,local:///home/disk1/data 就是存放在 /home/disk1/data中。libraft中有默认的local://实现,用户可以根据需要继承实现相应的Storage。
1.4将操作提交到复制组
你需要将你的操作序列化成IOBuf, 这是一个非连续零拷贝的缓存结构。构造一