Talent Plan TinyKV Project2C RaftKV

1,606 阅读1分钟

这一部分是实现快照,测试分为两部分:

  1. ./raft
  2. ./kv/test_raftstore

首先来看raft部分的修改

首先是Append的过程中,如果要发送的Entry已经被压缩进快照了,这时候就需要发送快照。

func (r *Raft) sendAppend(to uint64) {
    // ...
    msg.LogTerm, err = r.RaftLog.Term(msg.Index)
    if err != nil {
        // ...
    } else if err == ErrCompacted {
       snapshot, err := r.RaftLog.Snapshot()
       if err != nil {
          return
       }
       msg.Snapshot = &snapshot
       msg.MsgType = pb.MessageType_MsgSnapshot
    }
    r.msgs = append(r.msgs, msg)
}

其次是handleSnapshot,更新Raft状态。

func (r *Raft) handleSnapshot(m pb.Message) {
   meta := m.Snapshot.Metadata
   if m.Term < r.Term || meta.Index <= r.RaftLog.committed {
      return
   }
   r.becomeFollower(m.Term, m.From)

   r.RaftLog.committed = meta.Index
   r.RaftLog.pendingSnapshot = m.Snapshot
   r.RaftLog.applied = meta.Index
   r.RaftLog.stabled = meta.Index
   r.RaftLog.entries = nil

   r.Prs = make(map[uint64]*Progress)
   for _, id := range meta.ConfState.Nodes {
      r.Prs[id] = &Progress{}
   }
}

最后是RawNode中HasReady、Ready、Advance的修改,添加快照信息。

func (rn *RawNode) Ready() Ready {
    // ...
    if !IsEmptySnap(rn.Raft.RaftLog.pendingSnapshot) {
        ready.Snapshot = *rn.Raft.RaftLog.pendingSnapshot
    }
    return ready
}

func (rn *RawNode) HasReady() bool {
    // ...
    if !IsEmptySnap(rn.Raft.RaftLog.pendingSnapshot) {
        return true
    }
}

func (rn *RawNode) Advance(rd Ready) {
    // ...
    rn.Raft.RaftLog.pendingSnapshot = nil
    rn.Raft.RaftLog.maybeCompact()
}

func (l *RaftLog) maybeCompact() {
    first, _ := l.storage.FirstIndex()
    if first > l.FirstIndex() {
        l.entries = l.entries[first-l.FirstIndex():]
    }
}

再就是raftstore部分的修改

先回顾一下上层状态机是怎么运行的。首先是从raftWorker.run()开始。

func (rw *raftWorker) run() {
    msg := <-rw.raftCh
    HandleMsg(msg)
    HandleRaftReady()
}

func (d *peerMsgHandler) HandleMsg(msg message.Msg) {
    switch msg.Type {
    case message.MsgTypeRaftCmd:
        d.proposeRaftCommand()
    case message.MsgTypeTick:
        d.onTick()
    }
}

d.onTick()的任务之一就是检查是否需要压缩日志,然后调用d.proposeRaftCommand(),压缩指令过一次Raft后,会在HandleReadyState中被上层状态机应用,修改storage的TruncatedState.Index,然后maybeCompact()就会截断Raft层的Entries。

if msg.AdminRequest != nil {
   switch msg.AdminRequest.CmdType {
   case raft_cmdpb.AdminCmdType_CompactLog:
      compactLog := msg.AdminRequest.GetCompactLog()
      if compactLog.CompactIndex > d.peerStorage.applyState.TruncatedState.Index {
         d.peerStorage.applyState.TruncatedState.Index = compactLog.CompactIndex
         d.peerStorage.applyState.TruncatedState.Term = compactLog.CompactTerm
         kvWB.SetMeta(meta.ApplyStateKey(d.regionId), d.peerStorage.applyState)
         kvWB.WriteToDB(d.peerStorage.Engines.Kv)
         d.ScheduleCompactLog(compactLog.CompactIndex)
      }
   }
}

这里d.ScheduleCompactLog是创建一个Task交给raftLogGCWorker,这里是要回收记录的raft log元数据。

HandleReadyState另一个任务是SaveReadyState,SaveReadyState()还会调用ApplySnapshot(),更新并保存raftState、applyState、snapState、regionState,因为这里更新了regionState,所以需要添加一个Task给regionWorker,这个Task最终会由applySnap来处理。

ch := make(chan bool, 1)
ps.regionSched <- &runner.RegionTaskApply{
   RegionId: snapData.Region.Id,
   Notifier: ch,
   SnapMeta: snapshot.Metadata,
   StartKey: snapData.Region.StartKey,
   EndKey: snapData.Region.EndKey,
}
if r := <-ch; r {
   return &ApplySnapResult{PrevRegion: ps.region, Region: snapData.Region}, nil
}
return nil, nil

关于RaftLog.Term()

只能说这个函数太精髓了,为什么RaftLog的0位就不能空出来,就没这么复杂了,都得看Test的脸色,太难受了。

func (l *RaftLog) Term(i uint64) (uint64, error) {
   // 迎合Test
   if !IsEmptySnap(l.pendingSnapshot) && i == l.pendingSnapshot.Metadata.Index {
      return l.pendingSnapshot.Metadata.Term, nil
   }
   // 篡改error,ErrCompacted说明该发送快照了
   if len(l.entries) == 0 || i < l.FirstIndex() {
      term, err := l.storage.Term(i)
      if err == ErrUnavailable && !IsEmptySnap(l.pendingSnapshot) {
         return term, ErrCompacted
      }
      return term, err
   }
   j := i - l.FirstIndex()
   if j >= uint64(len(l.entries)) {
      return 0, ErrUnavailable
   }
   return l.entries[j].Term, nil
}

总结

只能说太复杂了,还得理解他这个系统,一个基本的RaftKV没必要这么复杂的,奈何TinyKV夹带私货,把TiKV的思想也带到了里面,写的脑壳痛。

还有要说明的是,make project2c没能全过,依次运行每个Test可以过,应该是内存不足了,我的电脑是8G的,又开了个GoLand,估计不太够用。

#!/bin/bash
go test -v --count=1 --parallel=1 -p=1 ./raft -run 2C
go test -v --count=1 --parallel=1 -p=1 ./kv/test_raftstore -run TestOneSnapshot2C
go test -v --count=1 --parallel=1 -p=1 ./kv/test_raftstore -run TestSnapshotRecover2C
go test -v --count=1 --parallel=1 -p=1 ./kv/test_raftstore -run TestSnapshotRecoverManyClients2C
go test -v --count=1 --parallel=1 -p=1 ./kv/test_raftstore -run TestSnapshotUnreliable2C
go test -v --count=1 --parallel=1 -p=1 ./kv/test_raftstore -run TestSnapshotUnreliableRecover2C
go test -v --count=1 --parallel=1 -p=1 ./kv/test_raftstore -run TestSnapshotUnreliableRecoverConcurrentPartition2C