文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

揭开 Raft 的神秘面纱,和ApacheRatis 了解Raft 组件的使用

2024-12-24 16:23

关注

相比 Paxos, Raft 一直以来就是以易于理解著称。今天我们以一年 Raft 使用者的角度,来看一下,别人根据 Raft 论文实现了之后,我们一般要怎么样使用。

俗话说,要想知道梨子的味道,就要亲口尝一尝,没吃过猪肉,也要见一见猪跑。否则别人再怎么样形容,你可能还以为是像猫狗一类毛茸茸。

在 Raft 官网里长长的列表就能发现,实现 Raft 的框架目前不少。Java 里我大概看了蚂蚁的 SOFARaft 和 Apache 的 Ratis。这次我们以 Ratis 为例,揭开面纱,来看看到底要怎样使用。

当然,下面具体提到的例子,也是这些组件中自带的 example。

一、编译

github下载 Ratis 直接 mvn clean package 即可,如果编译过程中出错,可以先clean install ratis-proto

二、示例

Ratis 自带的示例有三个:

在 ratis-examples 模块中,对于 arithmetic 和 filestore比较方便,可以通过main/bin目录下的 shell 脚本快速启动 Server 和 Client 来进行测试。

对于Raft,咱们都知道是需要多实例组成集群才能测试,你启动一个实例没啥用,连选主都成问题。Bin 目录下的 start-all 支持 example 的名称以及对应的命令。比如 filestore server 代表是启动 filestore 这个应用的server。对应的命令参数会在相应example里的 cli 中解析。同时会一次性启动三个server,组成一个集群并在周期内完成选举。

而对于 counter 这个示例,并没有相应的脚本来快速启动三个server,这个我们可以通过命令行或者在IDE里以参数的形式启动。

三、分析

下面我们来示例里看下 Raft Server 是怎样工作的。

对于 counter 示例来说,我们启动的时候,需要传入一个参数,代表当前的server是第几个,目的在于,要从 peers 列表中得知该用哪个IP + 端口去启动它。这里我们能发现,这个 peers 列表,是在代码内提前设置好的。当然你说动态配置啥的,也没啥问题,另外两个示例是通过shell 脚本里common 中的配置传入的。

所以,第一步我们看到, Raft Server 在启动的时候,会通过「配置」的形式,来知道 peer 之间的存在,这样才能彼此通信,让别人给自己投票或者给别人投票,完成 Term 内的选举。另外,才能接收到 Leader 传过来的 Log ,并且应用到本地。

第二步,我们来看下 Client 和 集群之间是如何通信的。整个 Raft 集群可能有多个实例,我们知道必须通过 Leader 来完成写操作。那怎样知道谁是Leader?有什么办法?

一般常见的思路有:

当然方式二这样试下去效率不太高。所以会在这个随机试一次之后,集群会将当前的 Leader 信息返回给 Client,然后 Client 直接通过这个建立连接进行通信即可。

在 Ratis 里, Client 调用非 Leader 节点会收到 Server 抛出的一个异常,异常中会包含一个称为 suggestLeader 的信息,表示当前正确的 Leader,按这个连上去就行。当然,如果如果在此过程中发生的 Leader 的变更,那就会有一个新的suggestLeader 返回来,再次重试。

我们来看 Counter 这个示例中的实现。

Server 和 Client 的共用的Common 代码中,包含 peers 的声明

  1. public final class CounterCommon { 
  2.   public static final List PEERS = new ArrayList<>(3); 
  3.  
  4.   static { 
  5.     PEERS.add(new RaftPeer(RaftPeerId.getRaftPeerId("n1"), "127.0.0.1:6000")); 
  6.     PEERS.add(new RaftPeer(RaftPeerId.getRaftPeerId("n2"), "127.0.0.1:6001")); 
  7.     PEERS.add(new RaftPeer(RaftPeerId.getRaftPeerId("n3"), "127.0.0.1:6002")); 
  8.   } 

这里声明了三个节点。

通过命令行启动时,会直接把index 传进来, index 取值1-3。

  1. java -cp *.jar org.apache.ratis.examples.counter.server.CounterServer {serverIndex} 

然后在Server 启动的时候,拿到对应的配置信息。

  1. //find current peer object based on application parameter 
  2.     RaftPeer currentPeer = 
  3.         CounterCommon.PEERS.get(Integer.parseInt(args[0]) - 1); 

再设置存储目录

  1. //set the storage directory (different for each peer) in RaftProperty object 
  2.     File raftStorageDir = new File("./" + currentPeer.getId().toString()); 
  3.     RaftServerConfigKeys.setStorageDir(properties, 
  4.         Collections.singletonList(raftStorageDir)) 

重点看这里,每个 Server 都会有一个状态机「CounterStateMachine」,平时我们的「业务逻辑」都放到这里

  1. //create the counter state machine which hold the counter value 
  2.     CounterStateMachine counterStateMachine = new CounterStateMachine(); 

客户端发送的命令,会在这个状态机中被执行,同时这些命令又以Log 的形式复制给其它节点,各个节点的Log 又会在它自己的状态机里执行,从而保证各个节点状态的一致。

 

最后根据这些配置,生成 Raft Server 实例并启动。

  1. //create and start the Raft server 
  2.     RaftServer server = RaftServer.newBuilder() 
  3.         .setGroup(CounterCommon.RAFT_GROUP) 
  4.         .setProperties(properties) 
  5.         .setServerId(currentPeer.getId()) 
  6.         .setStateMachine(counterStateMachine) 
  7.         .build(); 
  8.     server.start(); 

CounterStateMachine 里,应用计数的这一小段代码,我们看先检查了命令是否合法,然后执行命令

  1. //check if the command is valid 
  2.     String logData = entry.getStateMachineLogEntry().getLogData() 
  3.         .toString(Charset.defaultCharset()); 
  4.     if (!logData.equals("INCREMENT")) { 
  5.       return CompletableFuture.completedFuture( 
  6.           Message.valueOf("Invalid Command")); 
  7.     } 
  8.     //update the last applied term and index 
  9.     final long index = entry.getIndex(); 
  10.     updateLastAppliedTermIndex(entry.getTerm(), index); 
  11.  
  12.     //actual execution of the command: increment the counter 
  13.     counter.incrementAndGet(); 
  14.  
  15.     //return the new value of the counter to the client 
  16.     final CompletableFuture f = 
  17.         CompletableFuture.completedFuture(Message.valueOf(counter.toString())); 
  18.  
  19.     //if leader, log the incremented value and it's log index 
  20.     if (trx.getServerRole() == RaftProtos.RaftPeerRole.LEADER) { 
  21.       LOG.info("{}: Increment to {}"index, counter.toString()); 
  22.     } 

我们再来看 Client 的实现。

和 Server 类似,通过配置属性,创建一个实例

  1. private static RaftClient buildClient() { 
  2.     RaftProperties raftProperties = new RaftProperties(); 
  3.     RaftClient.Builder builder = RaftClient.newBuilder() 
  4.         .setProperties(raftProperties) 
  5.         .setRaftGroup(CounterCommon.RAFT_GROUP) 
  6.         .setClientRpc( 
  7.             new GrpcFactory(new Parameters()) 
  8.                 .newRaftClientRpc(ClientId.randomId(), raftProperties)); 
  9.     return builder.build(); 
  10.   } 

然后就可以向Server发送命令开工了。

  1. raftClient.send(Message.valueOf("INCREMENT")); 

Counter 的状态机支持INCREMENT 和 GET 两个命令。所以example 最后执行了一个 GET 的命令来获取最终的计数结果

  1. RaftClientReply count = raftClient.sendReadOnly(Message.valueOf("GET")); 

四、内部部分实现

RaftClientImpl 里,初期会从peers列表中选一个,当成leader 去请求。

  1. RaftClientImpl(ClientId clientId, RaftGroup group, RaftPeerId leaderId, 
  2.       RaftClientRpc clientRpc, RaftProperties properties, RetryPolicy retryPolicy) { 
  3.     this.clientId = clientId; 
  4.     this.clientRpc = clientRpc; 
  5.     this.peers = new ConcurrentLinkedQueue<>(group.getPeers()); 
  6.     this.groupId = group.getGroupId(); 
  7.     this.leaderId = leaderId != null? leaderId 
  8.         : !peers.isEmpty()? peers.iterator().next().getId(): null
  9.     ... 
  10.   } 

之后,会根据server 返回的不同异常分别处理。

  1. private RaftClientReply sendRequest(RaftClientRequest request) throws IOException { 
  2.     RaftClientReply reply; 
  3.     try { 
  4.       reply = clientRpc.sendRequest(request); 
  5.     } catch (GroupMismatchException gme) { 
  6.       throw gme; 
  7.     } catch (IOException ioe) { 
  8.       handleIOException(request, ioe); 
  9.     } 
  10.     reply = handleLeaderException(request, reply, null); 
  11.     reply = handleRaftException(reply, Function.identity()); 
  12.     return reply; 
  13.   } 

比如在 handleLeaderException 中,又分几种情况,因为通过Client 来和 Server 进行通讯的时候,会随机从peers里选择一个,做为leader去请求,如果 Server 返回异常,说它不是leader,就用下面的代码,随机从另外的peer里选择一个再去请求。

  1. final RaftPeerId oldLeader = request.getServerId(); 
  2.     final RaftPeerId curLeader = leaderId; 
  3.     final boolean stillLeader = oldLeader.equals(curLeader); 
  4.     if (newLeader == null && stillLeader) { 
  5.       newLeader = CollectionUtils.random(oldLeader, 
  6.           CollectionUtils.as(peers, RaftPeer::getId)); 
  7.     } 
  8.  
  9.  static  T random(final T given, Iterable iteration) { 
  10.     Objects.requireNonNull(given, "given == null"); 
  11.     Objects.requireNonNull(iteration, "iteration == null"); 
  12.  
  13.     final List list = StreamSupport.stream(iteration.spliterator(), false
  14.         .filter(e -> !given.equals(e)) 
  15.         .collect(Collectors.toList()); 
  16.     final int size = list.size(); 
  17.     return size == 0? null: list.get(ThreadLocalRandom.current().nextInt(size)); 
  18.   } 

是不是感觉很低效。如果这个时候,server 返回的信息里,告诉client 谁是 leader,那client 直接连上去就可以了是吧。

  1.  
  2.   RaftClientReply handleLeaderException(RaftClientRequest request, RaftClientReply reply, 
  3.                                         Consumer handler) { 
  4.     if (reply == null || reply.getException() instanceof LeaderNotReadyException) { 
  5.       return null
  6.     } 
  7.     final NotLeaderException nle = reply.getNotLeaderException(); 
  8.     if (nle == null) { 
  9.       return reply; 
  10.     } 
  11.     return handleNotLeaderException(request, nle, handler); 
  12.   }
  1. RaftClientReply handleNotLeaderException(RaftClientRequest request, NotLeaderException nle, 
  2.       Consumer handler) { 
  3.     refreshPeers(nle.getPeers()); 
  4.     final RaftPeerId newLeader = nle.getSuggestedLeader() == null ? null 
  5.         : nle.getSuggestedLeader().getId(); 
  6.     handleIOException(request, nle, newLeader, handler); 
  7.     return null
  8.   } 

我们会看到,在异常的信息中,如果能够提取出一个 suggestedLeader,这时候就会做为新的leaderId来使用,下次直接连接了。

本文转载自微信公众号「Tomcat那些事儿」,可以通过以下二维码关注。转载本文请联系Tomcat那些事儿公众号。

 

来源:Tomcat那些事儿内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯