Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Supports learner mode. #1

Merged
merged 14 commits into from
May 29, 2024
Merged

feat: Supports learner mode. #1

merged 14 commits into from
May 29, 2024

Conversation

KKorpse
Copy link
Collaborator

@KKorpse KKorpse commented Feb 24, 2024

1. 功能概览

关键细节:

  • 不支持直接切换成 Follower:可以手动关闭主从关系,然后调用 NODE.JOIN 命令手动加入集群,这个方式对 braft 的修改最小,也不需要设计新的指令。
  • 不支持 Learner 挂载 Learner 这种套娃模式。

2024.3.2 讨论结果:

  • 换用新的解决方式:Learner 是挂载到集群,而不是挂载到 Leader,add learner 用一条新类型的日志实现。
  • 未来优化:单节点集群情况下,比如一个 Leader 挂多个 learner,可以考虑兼容 redis 哨兵模式下主备切换的逻辑。
  • 未来优化:考虑 Learner 上挂 Learner,可以减少 Leader 节点压力。

目前支持的功能:

  • Learner 是挂在在集群的,集群切主后 Learner 依旧能接收日志。
  • 增加和删除 Learner。

支持的单测:

  • 单测:Learner 能被删除。
  • 单测:Leader 能加入 Learner。
  • 单测:Learner 能收到 Leader 的日志。
  • 单测:四节点集群,一个 Learner的场景,当两个Follower关闭后,Leader 的日志无法达成共识(只有两票)。
  • 单测:发生切主后,Learner 依旧会收到集群新的日志。
  • 单测:Learner 重启后,仍是 learner 并且能收到 Leader 消息。

2. 当前的改造方式

概览

主要方式是在 ConfigurationManager 中加入了用于管理 Learner 的元数据和接口,均类似于配置变更的内容。

class ConfigurationManager {
    // ...
private:

    std::deque<ConfigurationEntry> _configurations;
    std::deque<ConfigurationEntry> _learner_configurations;
    ConfigurationEntry _snapshot;
    ConfigurationEntry _learner_snapshot;
};

而添加 Learner 也类似于配置变更,需要将变更操作封装成一条普通的日志,该日志 apply 时,如果当前节点是主节点,则添加/删除一个 learner 类型的 replicator,如果是从节点,则忽略。

与配置变更不同的是,Learner 变更无需两阶段,所以只使用了 LogEntry 中的 peers 成员,old_peers 必须为空。


### 对于主节点的改造:

原理很简单:Node 类新增了一个成员,用于存储 Learner 的地址。

```C++
Configuration _learners;

Node 上新增了一个接口,用于为当前节点添加 Learner。

void Node::add_learner(const PeerId &peer, Closure *done); 

调用该接口时,如果当前节点是 Leader,即可成功添加一个新的 learner 类型的 replicator,并向其同步日志。

Learner 对应的 replicator 不会对日志投票,这样可以不修改 ballot_box 的代码,只需要给 replicator 添加一个标志位,表示该 replicator 是 learner 即可,该标志位在 replicator 启动时设置。

        if (!r->_options.is_learner) {
            r->_options.ballot_box->commit_at(
                    min_flying_index, rpc_last_log_index,
                    r->_options.peer_id);
        }

对 Learner 节点的支持

Node 节点中新增了一个 STATE_LEARNER 状态,Node 启动时可以以 Learner 状态启动。

enum State {
    STATE_LEARNER = 0,
    // ...
};

int NodeImpl::init(const NodeOptions& options) {
    // ...
    if (options.is_learner) {
        _state = STATE_LEARNER;
    } else {
        _state = STATE_FOLLOWER;
    }
    // ...
 }

STATE_LEARNER 状态的节点不会发起选举:

void NodeImpl::handle_election_timeout() {
    // ...
    if (_state != STATE_FOLLOWER) {
        return;
    }

STATE_LEARNER 状态的节点也不会参与选举,进行投票(通常情况下也不会收到 RequestVoteRPC):

void NodeImpl::handle_request_vote_response(const PeerId& peer_id, const int64_t term,
                                            const int64_t ctx_version,
                                            const RequestVoteResponse& response) {
    // ...
    if (_state != STATE_CANDIDATE) {
        LOG(WARNING) << "node " << _group_id << ":" << _server_id
                     << " received invalid RequestVoteResponse from " << peer_id
                     << " state not in CANDIDATE but " << state2str(_state);
        return;
    }

braft 中的 Learner 不需要存储主节点的信息,主节点给 Learner 发送 AppendEntriesRPC 时,learner 会自动更新 leader ip。

3. TODO

  • fixme:Learner 在收到 term 更大的消息时,会调用 step_down() 函数,并且会被设置为 follower。(如下位置)
void NodeImpl::handle_append_entries_request(brpc::Controller* cntl,
                                             const AppendEntriesRequest* request,
                                             AppendEntriesResponse* response,
                                             google::protobuf::Closure* done,
                                             bool from_append_entries_cache) {


    // check term and state to step down
    check_step_down(request->term(), server_id);  
  • 支持删除 Learner 的操作。

@KKorpse KKorpse changed the title feat: supporting learner feat: Supports learner mode. Mar 1, 2024
@hotjump
Copy link

hotjump commented Mar 2, 2024

也就是说不允许从节点拥有从节点的场景,这个场景有需求吗?
很多数据库都支持级联复制

@hotjump
Copy link

hotjump commented Mar 2, 2024

发生切主后,Learner 不再会收到集群新的日志。
如果在raft集群外挂一个learner的场景中,是否应该能接收新主的日志。

@KKorpse
Copy link
Collaborator Author

KKorpse commented Mar 2, 2024

发生切主后,Learner 不再会收到集群新的日志。 如果在raft集群外挂一个learner的场景中,是否应该能接收新主的日志。

可以实现,可以把 add learner 操作封装成一条新类型的日志,apply == 添加learner。

src/braft/node.cpp Outdated Show resolved Hide resolved
CMakeLists.txt Outdated Show resolved Hide resolved
@KKorpse
Copy link
Collaborator Author

KKorpse commented Mar 2, 2024

发生切主后,Learner 不再会收到集群新的日志。 如果在raft集群外挂一个learner的场景中,是否应该能接收新主的日志。

也就是说不允许从节点拥有从节点的场景,这个场景有需求吗? 很多数据库都支持级联复制

如果需要的话我可以给 learner 加上 replicator。

src/braft/node.h Outdated Show resolved Hide resolved
src/braft/node.cpp Outdated Show resolved Hide resolved
ConfigurationEntry learner_entry;
learner_entry.id = LogId(meta->last_included_index(), meta->last_included_term());
learner_entry.conf = learner_conf;
_config_manager->set_snapshot(entry, learner_entry);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Learner 不用两阶段,所以 learner 只用到了 peer,没有用到 old peer。

@longfar-ncy longfar-ncy requested a review from hotjump May 11, 2024 13:52
@panlei-coder panlei-coder self-requested a review May 13, 2024 05:27
@panlei-coder panlei-coder marked this pull request as ready for review May 14, 2024 04:20
@panlei-coder
Copy link
Collaborator

解决一下冲突

@longfar-ncy longfar-ncy requested a review from hotjump May 25, 2024 11:20
@AlexStocks AlexStocks merged commit bf02d5e into pikiwidb:master May 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants