Add Member
添加节点的一般步骤
在集群里执行:
etcdctl member add <new-name> --peer-urls=https://<new-ip>:2380正常情况下会返回集群的相关信息
ETCD_NAME="new-member"
ETCD_INITIAL_CLUSTER="old1=https://old1:2380,old2=https://old2:2380,new-member=https://new-ip:2380"
ETCD_INITIAL_CLUSTER_STATE="existing"
ETCD_INITIAL_ADVERTISE_PEER_URLS="https://new-ip:2380"
- 使用上面的集群信息启动新的节点
--name=<new-name>
--initial-cluster=<返回的 initial cluster>
--initial-advertise-peer-urls=https://<new-ip>:2380
--listen-peer-urls=https://<new-ip>:2380
--initial-cluster-state=existing
--data-dir=<空目录>
raft
raft消息
新的 Member 信息包装成 raftpb.ConfChangeAddNode 类型的配置变更 ConfChange
b, err := json.Marshal(memb)
cc := raftpb.ConfChange{
Type: raftpb.ConfChangeAddNode,
NodeID: uint64(memb.ID),
Context: b,
}
如果添加的是Learn 节点,那么类型配置变更 ConfChange 的类型为 raftpb.ConfChangeAddLearnerNode
if memb.IsLearner {
cc.Type = raftpb.ConfChangeAddLearnerNode
}
这个配置变更也会被包装为 pb.MsgProp 类型的消息 Propose 到 raft 流程里
pb.Message{Type: pb.MsgProp, Entries: []pb.Entry{{Type: EntryConfChange, Data: data}}}
一样的,这条消息也会经过 raft 流程追加 entry 的方式,直到超过半数节点对这个 index 进行 commit
apply配置
消息通过了 raft 流程后,集群所有节点都会在上层 server 进行 apply;
apply 配置和 apply kv 有一点区别
apply 配置其实就是将新的节点加入到每个节点的type ProgressMap map[uint64]*Progress,可以参考ETCD《四》--成为Leader
然后再将新的节点信息保存在 boltdb 中;保存在 members 这个 Bucket中,key就是member_id,value就是 member 信息
tx.UnsafePut(Members, mkey, mvalue)
然后会添加这个新的节点的 peer_url 来作为新的 Peer;会不断尝试 dail 这个新 Peer 的地址
Learner节点
Learner节点最大的区别在于加入集群后不会主动发起投票;只会接收 Leader 节点的日志同步消息
func (r *raft) tickElection() {
r.electionElapsed++
if !pr.IsLearner {
r.electionElapsed = 0
if err := r.Step(pb.Message{From: r.id, Type: pb.MsgHup}); err != nil {
}
}
}
集群内默认情况下最多允许 1 个 Learner 节点
Leader 节点通过 HeartBeat 向这个 Learner 节点同步 commit index;这里取得是 Learner 节点反馈给 Leader 节点的 commit index 以及 Leader 节点当前的 commit index 中的较小值,Learner 节点刚加入时肯定是更小的
func (r *raft) sendHeartbeat(to uint64, ctx []byte) {
pr := r.trk.Progress[to]
commit := min(pr.Match, r.raftLog.committed)
r.send(pb.Message{
To: to,
Type: pb.MsgHeartbeat,
Commit: commit,
Context: ctx,
})
pr.SentCommit(commit)
}
Leader 节点收到 Learner 节点的 HeartBeat 响应后;如果 Learner 节点的 commit index 仍然落后,会向这个节点同步日志
case pb.MsgHeartbeatResp:
if pr.Match < r.raftLog.lastIndex() || pr.State == tracker.StateProbe {
r.sendAppend(m.From)
}
如果这个节点的 commit index 落后太多,已经被 Leader 节点 compact 了,那么就无法确定这个 index 所属的 term了,会抛出 ErrCompacted 错误
prevTerm, err := r.raftLog.term(prevIndex)
if err != nil {
// The log probably got truncated at >= pr.Next, so we can't catch up the
// follower log anymore. Send a snapshot instead.
return r.maybeSendSnapshot(to, pr)
}
- 在错误的情况下 ,Leader 会改为发送 snapshot 给该节点
pr.ResetState(StateSnapshot)
pr.PendingSnapshot = snapshoti
pr.Next = snapshoti + 1
pr.sentCommit = snapshoti
r.send(pb.Message{To: to, Type: pb.MsgSnap, Snapshot: &snapshot})
- 其它正常情况下,Learner 没有落后太多的话,就还是正常同步日志 entry
r.send(pb.Message{
To: to,
Type: pb.MsgApp,
Index: prevIndex,
LogTerm: prevTerm,
Entries: ents,
Commit: r.raftLog.committed,
})
Promote Member
用于将 Learner 节点提升为投票节点
本质上也是再次发送一次 raftpb.ConfChangeAddNode 类型配置变更;携带了IsPromote=true
b := membership.ConfigChangeContext{
Member: membership.Member{
ID: types.ID(id),
},
IsPromote: true,
}
cc := raftpb.ConfChange{
Type: raftpb.ConfChangeAddNode,
NodeID: id,
Context: b,
}
同时仅 Leader 节点能够处理 Promote 请求;Followers 收到后需要将请求通过 post http /members/promote/member_id发送给 Leader;并等待 Leader 的处理结果
for _, url := range leader.PeerURLs {
resp, err := promoteMemberHTTP(cctx, url, id, s.peerRt)
Leader Promote 节点时会检查 Learner 节点到 Leader 节点之间的日志同步进度,至少需要同步 90% 才能 Promote 这个 Learner;