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

只读节点是否可以指定一个节点读取数据 #989

Open
LinHuiG opened this issue May 18, 2023 · 3 comments
Open

只读节点是否可以指定一个节点读取数据 #989

LinHuiG opened this issue May 18, 2023 · 3 comments

Comments

@LinHuiG
Copy link

LinHuiG commented May 18, 2023

背景是这样的:
我在甲地部署了A、B两个节点,在乙地部署了C,D两个节点。
我让A、B、C作为一个raft集群,D是这个集群的只读节点,并且将C的优先级设置为0,这样就可以保证在服务可用的情况下这个集群主节点一定在甲地。
同时我让C、D、A作为另一个raft集群,B是这个集群的只读节点,并且将A的优先级设置为0,这样就可以保证在服务可用的情况下这个集群主节点一定在乙地。
现在遇到一个小问题,数据同步的方向是主节点向从节点同步(包括只读节点),跨地区网络流量翻倍,有没有办法配置让只读节点从本地的从节点读数据,(第一个集群D从C读数据,第二个集群B从A读数据)。这样降低跨地区的网络开销。

@killme2008
Copy link
Contributor

目前的 learner 都是从主节点读取,还不支持从其他节点读取,这是一个好的 proposal,可能需要想下怎么设计。

@dbl-x
Copy link

dbl-x commented Nov 4, 2024

一、背景

解决类似 #issue_989 的问题,支持通过配置指定Learner节点从特定的Leader或者Follower节点获取日志,尽可能减少跨数据中心的网络流量,节约网络带宽。

image

如上图,共有4个节点组成一个Raft Group,其中Node1、Node2在数据中心A,Node3、Node4在数据中心B。当Node1为Leader,Node2、Node3为Follower,Node4为Learner节点,在SOFAJRaft的实现中,所有的数据流向都是从Leader->Follower/Learner,那么会存在两份跨数据中心的流量:

  • 从Node1->Node3:Leader到Follower的数据同步
  • 从Node1->Node4:Leader到Learner的数据同步

跨数据中心的带宽是较为昂贵的,希望能支持配置实现Learner节点可以从特定的节点(Leader/Follower)同步数据,减少跨数据中心的流量。例如实现上图中Node4从Node3复制数据。

二、目标

2.1 Goals

  1. 通过配置控制Learner节点从特定节点复制数据
  2. 默认情况下(配置缺失或默认配置)Learner依旧从Leader节点复制数据
  3. 一个Learner节点只能从一个目标节点复制数据,不同Learner节点可以从不同的目标节点复制数据
  4. 一个Follower节点可以支持复制数据到多个Learner节点

image

2.2 TradeOff

  • 当Learner采用从Follow节点复制数据后,数据链路从Leader->Learner变成了Leader->Follower->Learner,链路变长那么Learner上的数据延迟也将增加,需要用户按需配置自己的数据复制策略

三、当前数据复制实现分析

image

上图中大致罗列了JRaft实现中Learner控制流和数据流相关的组件:

  • 控制链路上:
    • 管理员通过CliService向Leader节点发送添加Learner的指令
    • Leader通过ReplicatorGroup创建和维护Replicator,每个Replicator代表了一个数据复制节点
  • 数据链路上:
    • Replicator通过RaftClientService持续性的发送AppendEntries请求
    • Learner的AppendEntriesReqeustProcessor接收到AppendEntries请求后将数据添加到Node中(Node应用到LogManager)

四、数据复制改造方案

根据目标和当前数据复制实现的分析,要支持Learner从特定节点复制数据需要:

  1. 支持CliService中Learner相关指令的处理,通过CliService实现向Follower节点添加Learner
  2. Follower实现数据复制到Leader
  3. 支持FO,可通过配置提供不同的FO模式,如目标节点故障后是否Learner是否自动切换到其他节点

image

如上图为Learner支持从Follower复制数据后的示意图,由Node1Node10组成了一个Raft Group分布在两个数据中心,其中Node1为Leader,Node2Node5为Follow,Node6~Node10为Learner。其中需要引入Replication Group的概念,用Replication Group来避免Learner的数据复制跨不同的数据中心。
在Learner支持从Follower复制数据后需要:

  1. 同一个Replication Group内保持尽可能的负载均衡,如在数据中心A内有2个Follower节点和2个Learner节点,则尽可能保证1个Follower节点挂载1个Learner节点,在数据中心B内有2个Follower节点和3个Learner节点,则保证不同Follower节点间挂载的Learner节点数相差不超过1
  2. Follower节点故障后,Learner节点优先切换挂在到同Replication Group的其他Follower节点,如上图中的Node2发生故障则Node6迁移挂在的Node3
  3. 若一个Replication Group内所有节点不可用,则Learner退化成默认策略,从Leader复制数据
  4. 系统需要支持动态恢复的能力,当Replication Group内有新增的节点时,Learner节点能负载均衡的挂载到新的节点上

在JRaft的实现中,系统的控制指令和数据指令都由Leader进行操作,并通过日志复制到Follower和Learner节点,完成系统初始化状态保持一致并通过应用相同顺序的一系列的指令达到另一个一致状态的原则,所以在本改造方案中依旧保持该原则的实现,所有的操作需要通过Leader节点之后应用到各个子节点,保持状态的一致。

4.1 基础配置改造

按上文分析,Follower节点需要增加Replication Group的基础配置,而Follower节点时可以升级为Leader节点的,Learner节点也可以升级为Follower节点,那么Replication Group应当成为系统的一个基础配置,所有节点都需要有Replication Group配置。

/**
 * Add a new peer into the replicating group which consists of |conf|.
 * Return OK status when success.
 *
 * @param groupId the raft group id
 * @param conf    current configuration
 * @param peer    peer to add
 * @return operation status
 */
Status addPeer(final String groupId, final Configuration conf, final PeerId peer);

系统通过CliService#addPeer方法添加节点,用PeerId指代一个Raft Group中的参与者。采用拓展PeerId的方式,向PeerId添加replicationGroup属性,表示节点所属的复制组。

public class PeerId {
    private Endpoint endpoint;
    private int idx;
    private int priority;
    private String replicationGroup; // 在当前的属性下添加replicationGroup表示所属的复制组
}

当前PeerId#toString的格式需要进行修改(且保证解析和读取方式向前兼容):

  • ip:port:idx:priority => ip:port:idx:priority:replicationGroup
    若节点的repcationGroup属性为空,则其不能被选择为数据复制来源节点,除非是它是Leader节点。

4.2 Learner相关控制流改造

4.2.1 Add/Remove Learners

image

JRaft添加Learner节点的流程如上图,其中在NodeImpl#unsafeRegisterConfChange之前会将新增的Learner保存到Configuration中。之后通过一系列操作实现在ReplicatorGroup添加一个新的Replicator,实现从Leader节点复制数据。
在支持向特定节点添加Learner后,Replicator并不是Leader节点的ReplicatorGroup添加,而是需要根据配置决定的目标节点添加,因此需要:

  1. 避免在Leader节点添加Learner
  2. 将配置变更作为日志追加到Log中
  3. Leader/Follower应用日志时根据配置决定是否在自身节点启动Learner的Replicator

当前的配置类Configuration包含peers和learners两个列表,在支持将Learner挂载到Follower后需要在配置中指定Learner对应的Follower节点,因此需要修改Configuration类,将Learner和Follower的对应关系记录到Configuration中:

  • LinkedHashSet learners => Map<PeerId,PeerId> // Key为Learner,Value为Follower

同时为了实现负载均衡,需要在Leader节点拿到所有节点的Learner挂载信息,并将选择的目标节点作为Entry的一部分写入到Log中。如第三章节的分析,在JRaft的实现中通过ReplicatorGroup维护了一个节点的所有Replicator(当前是Leader节点的ReplicatorGroup维护了所有代表Follower和Learner节点的Replicator),那么需要在节点上增加RPC接口和实现,允许调用获取当前节点的Replicator信息。

// 增加如下两个RPC相关请求和响应
message GetReplicatorsRequest {
    required string group_id = 1;
    optional string leader_id = 2;
}

message GetReplicatorsResponse {
    repeated string replicators = 1;
    optional ErrorResponse errorResponse = 99;
}

// ReplicatorGroup已经拥有了listReplicators接口
public interface ReplicatorGroup extends Describer {
    /**
     * Returns all replicators.
     */
    List<ThreadId> listReplicators();
}

增加如上协议以及对应的Processor,Leader向目标Replication Group下的节点发起请求获取挂载情况信息,并从中按策略选出目标节点,将目标节点即对应Learner信息作为一个Entry写入到日志中。Entry的data字段包含的内容为目标节点的PeerId#toString结果。

获取Replicator是系统实时的信息,在并发的操作Raft Group的情况下可能产生挂载Learner节点不均衡的情况,此情况不在本方案解决范围内,未来可以继续优化,实现动态的Learner节点分配。
JRaft中配置的变更流程如下:
在ConfigurationCtx#nextStage方法的STAGE_CATCHING_UP状态时会将配置写入到Log中
尝试启动3个节点,DEBUG一下当前流程,了解如何做的节点变更(记录到Conf之后,Conf是如何同步到Follower并且应用的)
NodeImpl#handleAppendEntriesRequest方法将Entry添加到LogManager中,如果Entry是配置,会被添加到ConfigManager,之后执行checkAndSetConfiguration方法会将ConfigManager的配置应用到NodeImpl中,完成节点配置的更新。

移除Learner操作和添加Learner类似:

  1. 避免Leader节点直接操作关闭自身的Learner节点
  2. 将变更作为日志追加到Log中
  3. Leader/Follower应用日志时根据配置决定是否在自身节点移除Learner的Replicator

4.2.2 Get/GetAlive Learners

Get Learners操作通过Leader去获取Learner节点。因为Learner信息依旧保存在Configuration中,Leader可以直接通过Configuration拿到Learner列表并返回,所以本方案的变更对Get Learners操作无影响。
GetAlive Learners时需要通过Replicator的最后RPC交互时间来判断节点是否存活,在Learner挂载到Follower节点之后,Leader节点上缺乏Learner节点的RPC操作时间,需要通过Follow节点去获取。因此需要Follower节点支持现有的GetAlive Learners方法,Leader在收到GetAlive Learners请求之后将请求转发给所有Follower(从配置中有被挂载Learner节点的Follower)节点,在汇总信息返回给请求端。

4.2.3 Reset Learners

Reset Learners只是将Configuration的Learner配置进行修改,然后通过ConfigurationCtx#start方法去应用生效,流程和Add/Remove Learners操作是一致的,不需要做额外的修改。

4.2.4 Learner to Follower

Learner to Follower实现上是先Remove Learner节点,然后将Learner节点作为一个新的Follower节点进行添加操作,所以实现上不需要做任何修改。

4.3 Follower支持数据复制的改造

ReplicatorGroup和Replicator已经实现了节点间的数据复制能力,在Follower上仅需根据配置决定是否启动Replicator来向Learner节点传输数据。
在NodeImpl#checkAndSetConfiguration方法中应用最新的配置:

// NodeImpl#checkAndSetConfiguration
if (this.conf != prevConf) {
    for (Entry<PeerId, PeerId> entry : conf.getLearners()) {
        if (entry.getValue().equals(getNode().getPeerId())) {
            // 找到属于自己的Learner节点,初始化对应的Replicator
            PeerId peerId = entry.getKey());
            // 新增addLearners方法
            addLearner(peerId);
        }
    }
}

public void addLearner(PeerId peerId) {
    // 校验去重
    // 调用replicatorGroup.addReplicator方法添加新的Learner节点
}

4.4 FO相关改造

4.4.1 Follower故障时的改造

当Follower故障时,挂在到该Follower的Learner节点需要自动化的迁移至同一个replicationGroup下的其他Follower节点。若此时目标replicationGroup下无可用的Follower节点则降级为挂载到Leader节点。
通过ReplicatorStateListener可以在Leader节点上感知Follower节点的状态。当Follower节点状态为不可用时Leader节点通过修改Configuration的.learners配置,重新给该Follower节点下的Learner节点选择可用的Follower节点,并通过4.1.1流程实现Learner节点挂载到新的Follower节点。
若此时选择不到目标replicationGroup下的Follower节点,则按照当前流程将Learner节点挂载到Leader下。
FO还涉及到Follower节点的恢复,暂不实现Follower节点故障恢复后的Learner节点动态重平衡,后续可进一步优化。

4.4.2 Follower提升为Leader节点时的改造

添加ReplicationGroup配置的目的是解决跨数据中心的流量问题,因此ReplicationGroup用于标识处于同一个数据中心的节点,和节点的身份状态无关。所以当Follower节点升级为Leader节点时并不需要做任何改造工作,此时Learner从Leader节点复制数据,并依旧遵循ReplicationGroup的规则,并不会出现跨数据中心的数据复制。
未来可以进一步的优化,支持通过配置来决策是否将Learner动态从Leader迁移至Follower节点,可以进一步降低Leader节点的压力。

@fengjiachun
Copy link
Contributor

fengjiachun commented Nov 4, 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

No branches or pull requests

4 participants