这是我参与「第四届青训营 」笔记创作活动的第1天
1 关于分布式共识算法Raft
不同于Paxos算法直接从分布式一致性问题出发推导出来,Raft算法则是从多副本状态机的角度提出,用于管理多副本状态机的日志复制。Raft实现了和Paxos相同的功能,它将一致性分解为多个子问题:Leader选举(Leader election)、日志同步(Log replication)、安全性(Safety)、日志压缩(Log compaction)、成员变更(Membership change)等。同时,Raft算法使用了更强的假设来减少了需要考虑的状态,使之变的易于理解和实现。
Raft将系统中的角色分为领导者(Leader)、跟从者(Follower)和候选人(Candidate):
- Leader:接受客户端请求,并向Follower同步请求日志,当日志同步到大多数节点上后告诉Follower提交日志。
- Follower:接受并持久化Leader同步的日志,在Leader告之日志可以提交之后,提交日志。
- Candidate:Leader选举过程中的临时角色。
1、1 Leader选举
Raft 使用心跳(heartbeat)触发Leader选举。当服务器启动时,初始化为Follower。Leader向所有Followers周期性发送heartbeat。如果Follower在选举超时时间内没有收到Leader的heartbeat,就会等待一段随机的时间后发起一次Leader选举。
1、2 日志同步
Leader选出后,就开始接收客户端的请求。Leader把请求作为日志条目(Log entries)加入到它的日志中,然后并行的向其他服务器发起 AppendEntries RPC (RPC细节参见八、Raft算法总结)复制日志条目。当这条日志被复制到大多数服务器上,Leader将这条日志应用到它的状态机并向客户端返回执行结果。
其他的比如安全性,日志压缩等等就不过多介绍了,本篇主要介绍golang分布式存储系统如何引入raft。
2 Raft库调用
hashicorp/raft是github上star数比较高的golang实现的raft算法,Raft gRPC Example是一种使用grpc结合raft使用的一种示例,在这种示例中,还有其他的一些库方便我们调用,具体可以访问源代码查看,具体可查看使用教程。
2、1 新建Raft结点
func newRaft(master bool, myID, myAddress string, fsm raft.FSM) (*raft.Raft, *transport.Manager, error) {
c := raft.DefaultConfig()
isLeader := make(chan bool, 1)
c.NotifyCh = isLeader
c.LocalID = raft.ServerID(myID)
baseDir := filepath.Join(config.RaftCfg.RaftDataDir, myID)
err := os.MkdirAll(baseDir, 0755)
if err != nil {
return nil, nil, err
}
ldb, err := boltdb.NewBoltStore(filepath.Join(baseDir, "logs.dat"))
if err != nil {
return nil, nil, fmt.Errorf(`boltdb.NewBoltStore(%q): %v`, filepath.Join(baseDir, "logs.dat"), err)
}
sdb, err := boltdb.NewBoltStore(filepath.Join(baseDir, "stable.dat"))
if err != nil {
return nil, nil, fmt.Errorf(`boltdb.NewBoltStore(%q): %v`, filepath.Join(baseDir, "stable.dat"), err)
}
fss, err := raft.NewFileSnapshotStore(baseDir, 3, os.Stderr)
if err != nil {
return nil, nil, fmt.Errorf(`raft.NewFileSnapshotStore(%q, ...): %v`, baseDir, err)
}
tm := transport.New(raft.ServerAddress(myAddress), []grpc.DialOption{grpc.WithInsecure()})
r, err := raft.NewRaft(c, fsm, ldb, sdb, fss, tm.Transport())
if err != nil {
return nil, nil, fmt.Errorf("raft.NewRaft: %v", err)
}
log.Println("master", master)
//根据命令行传入的参数决定是否以boostrap入口启动cluster,没有的话就是正常启动节点
if master {
cfg := raft.Configuration{
Servers: []raft.Server{
{
Suffrage: raft.Voter,
ID: raft.ServerID(myID),
Address: raft.ServerAddress(myAddress),
},
},
}
f := r.BootstrapCluster(cfg)
if err := f.Error(); err != nil {
return nil, nil, fmt.Errorf("raft.Raft.BootstrapCluster: %v", err)
}
}
return r, tm, nil
}
代码具体解释可以观看这篇博客,这篇博客给我们详细地介绍了各个配置的含义与作用。
2、2 将从节点跟随主节点
先go get一下这个仓库github.com/Jille/rafta… 然后go build一下就可以得到raftadmin命令
./raftadmin localhost:50051 add_voter nodeB localhost:50052 0
可以先参考使用教程,运行示例以观察日志变化,关掉某个服务器之后日志是怎么样变化的,然后结合起来用。
2、3 查找Leader
// FindLeader 查找NameNode的Raft集群的Leader
func FindLeader(addrList string) (string, error) {
split := strings.Split(addrList, "///")
nameNodes := strings.Split(split[1], ",")
var res = ""
for _, n := range nameNodes {
conn, err := grpc.Dial(n, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
//表明连接不上,继续遍历节点
continue
}
resp, err := namenode_pb.NewNameNodeServiceClient(conn).FindLeader(context.Background(), &namenode_pb.FindLeaderReq{})
if err != nil {
continue
}
res = resp.Addr
break
}
if res == "" {
return "", errors.New("there is no alive name node")
}
return res, nil
}
主要是给正在活着的namenode发送grpc调用,raft结构体带有获取当前Leader的方法,返回这个Leader的ServerAddress即可。
通过以上流程即可把NameNode集群搭建成raft集群,实现元数据一致性,并且通过FindLeader方法来获取主节点并返回给Datanode或者client去发起grpc调用请求。