从 dgraph-io/dgraph 了解 etcd/raft 的使用 0x01

本次代码阅读基于commit 189fdd3

1. raftwal

godoc

之前提到, etcd/raft提供了 MemoryStorage + wal 的方式 来对 raft 中的 HardState, Snapshot 和 Entry 进行持久化. wal 将数据直接写入文件.

而对于 dgraph 来说, 它的一个物理节点上有多个 raft group, 且 raft group 会自动新建. 此时, 所有 raft group 使用同一套底层存储会相对简单一些.

本包中, dgraph 使用 badger 这个同属 dgraph-io 出品的 kv 数据库来保存所有 raft group 的日志.

1.1 Keys

既然不同 raft 的日志都存在同一个 kv 数据库中, 那么就需要对存储的 key 进行有效地区分.

对于一个 raft node 来说, 它通过节点 id RaftId(uint64) 和 组 id gid(uint32) 两层 来标识自己

相应地, raftwal 中的三类 key 都包含这两个 id

  1. snapshotKey:

    func (w *Wal) snapshotKey(gid uint32) []byte {
     b := make([]byte, 14)
     binary.BigEndian.PutUint64(b[0:8], w.id)
     copy(b[8:10], []byte("ss"))
     binary.BigEndian.PutUint32(b[10:14], gid)
     return b
    }
    

  2. hardStateKey:

    func (w *Wal) hardStateKey(gid uint32) []byte {
     b := make([]byte, 14)
     binary.BigEndian.PutUint64(b[0:8], w.id)
     copy(b[8:10], []byte("hs"))
     binary.BigEndian.PutUint32(b[10:14], gid)
     return b
    }
    
  3. entryKey:

    func (w *Wal) entryKey(gid uint32, term, idx uint64) []byte {
     b := make([]byte, 28)
     binary.BigEndian.PutUint64(b[0:8], w.id)
     binary.BigEndian.PutUint32(b[8:12], gid)
     binary.BigEndian.PutUint64(b[12:20], term)
     binary.BigEndian.PutUint64(b[20:28], idx)
     return b
    }
    

1.2 Wal

Wal 提供 raft 数据的读写.

对于 raft 数据的持久化, 最重要的是保证数据的一致性.

StoreSnapshot
func (w *Wal) StoreSnapshot(gid uint32, s raftpb.Snapshot) error {
    txn := w.wals.NewTransactionAt(1, true)
    defer txn.Discard()
    
    // ...
    
    if err := txn.Set(w.snapshotKey(gid), data); err != nil {
        return err
    }
    
    // ...
    
    // 清除 snapshot 数据之前的所有 entry
    // Delete all entries before this snapshot to save disk space.
    start := w.entryKey(gid, 0, 0)
    last := w.entryKey(gid, s.Metadata.Term, s.Metadata.Index)
    
    // 这里利用了 badger 的特性, 在遍历的时候仅读取 key 数据, 减少了读取 value 带来的开销
    opt := badger.DefaultIteratorOptions
    opt.PrefetchValues = false
    itr := txn.NewIterator(opt)
    defer itr.Close()

    // 逐一删除不再需要的 entry
    for itr.Seek(start); itr.Valid(); itr.Next() {
        // ...
    }

    // Failure to delete entries is not a fatal error, so should be
    // ok to ignore
    if err := txn.CommitAt(1, nil); err != nil {
        x.Printf("Error while storing snapshot %v\n", err)
        return err
    }
    return nil
}
Store
// Store stores the hardstate and entries for a given RAFT group.
func (w *Wal) Store(gid uint32, h raftpb.HardState, es []raftpb.Entry) error {
    txn := w.wals.NewTransactionAt(1, true)

    var t, i uint64
    // 逐一保存 entry
    for _, e := range es {
        t, i = e.Term, e.Index
        
        // ...
    }

    // 如果有必要, 保存 HardState
    if !raft.IsEmptyHardState(h) {
        // ...
    }

    // If we get no entries, then the default value of t and i would be zero. That would
    // end up deleting all the previous valid raft entry logs. This check avoids that.
    if t > 0 || i > 0 {
        // When writing an Entry with Index i, any previously-persisted entries
        // with Index >= i must be discarded.
        // Ideally we should be deleting entries from previous term with index >= i,
        // but to avoid complexity we remove them during reading from wal.
        // 有可能出现某个时间点之后, 由于网络原因, 数据分叉的情形.
        // 为了在网络恢复之后保证数据一致性, 对于每一批 entry, 需要清除逻辑上排在这批数据之后的 entry.
        start := w.entryKey(gid, t, i+1)
        prefix := w.prefix(gid)
        // ...
        
        // 逐一清除
        for itr.Seek(start); itr.ValidForPrefix(prefix); itr.Next() {
            // ...
        }
    }
    if err := txn.CommitAt(1, nil); err != nil {
        return err
    }
    return nil
}
读取
func (w *Wal) Snapshot(gid uint32) (snap raftpb.Snapshot, rerr error) {
    // ...
}
func (w *Wal) HardState(gid uint32) (hd raftpb.HardState, rerr error) {
    // ...
}
func (w *Wal) Entries(gid uint32, fromTerm, fromIndex uint64) (es []raftpb.Entry, rerr error) {
    // ...
}

1.3 关于badger

badger 来源于这篇论文 WiscKey: Separating Keys from Values in SSD-conscious Storage. .

知乎上仅有的评论里, 对它的评价不甚高 如何评价 Badger (fast key-value storage) 😂.

但不论怎样, 它在一些情况下确实比较 , 也可能非常适合 dgraph 的使用场景.

2. conn

godoc

conn 充当了 etcd/raft 的网络传输层, 基于 gRPC 在 raft 节点之间同步信息.

2.1 Pool

看名字是个连接池, 实际上其中的 *grpc.ClienctConn 是复用的.

一旦创建, 会每隔 10 秒尝试 ping 一下, 根据结果判断当前连接是否可用.

// "Pool" is used to manage the grpc client connection(s) for communicating with other
// worker instances.  Right now it just holds one of them.
type Pool struct {
    sync.RWMutex
    
    // 这段注释说明了 *grpc.ClientConn 可以服用的原因
    // A "pool" now consists of one connection.  gRPC uses HTTP2 transport to combine
    // messages in the same TCP stream.
    conn *grpc.ClientConn

    // 上一次 ping 请求成功的时间
    lastEcho time.Time
    
    // 目标节点的地址
    Addr     string
    
    // 发起 ping 请求的 ticker
    ticker   *time.Ticker
}

2.2 Pools

Pools 维护了不同地址的 Pool.

这里是一个单例.

type Pools struct {
    sync.RWMutex
    all map[string]*Pool
}

var pi *Pools

func init() {
    pi = new(Pools)
    pi.all = make(map[string]*Pool)
}

2.3 Node

Node 的用于维护 raft 成员节点, 以及在各节点之间传输信息.职责包括:

初始化 / 读取 当前的 raft.Node
// SetRaft would set the provided raft.Node to this node.
// It would check fail if the node is already set.
func (n *Node) SetRaft(r raft.Node) {
    // ...
}

// Raft would return back the raft.Node stored in the node.
func (n *Node) Raft() raft.Node {
    // ...
}
维护 ConfState 即节点 id 列表
// SetConfState would store the latest ConfState generated by ApplyConfChange.
func (n *Node) SetConfState(cs *raftpb.ConfState) {
    // ...
}

// ConfState would return the latest ConfState stored in node.
func (n *Node) ConfState() *raftpb.ConfState {
    // ...
}
维护 节点 id - 地址 的对应关系
func (n *Node) Peer(pid uint64) (string, bool) {
    // ...
}

// addr must not be empty.
func (n *Node) SetPeer(pid uint64, addr string) {
    // ...
}

func (n *Node) DeletePeer(pid uint64) {
    // ...
}

// Connects the node and makes its peerPool refer to the constructed pool and address
// (possibly updating ourselves from the old address.)  (Unless pid is ourselves, in which
// case this does nothing.)
func (n *Node) Connect(pid uint64, addr string) {
    // ..
}
加入和移除节点
  • 加入节点

    // 这个函数是可以认为是 AddCluster 的回调
    // 由使用者在收到 ConfChange 成功 apply 时主动调用
    // dgraph 中调用这个方法的地方 err 都传入了 nil
    func (n *Node) DoneConfChange(id uint64, err error) {
      n.Lock()
      defer n.Unlock()
      ch, has := n.confChanges[id]
      if !has {
          return
      }
      delete(n.confChanges, id)
      ch <- err
    }
    
    func (n *Node) AddToCluster(ctx context.Context, pid uint64) error {
      addr, ok := n.Peer(pid)
      // ...
      rcBytes, err := rc.Marshal()
      // ...
    
      ch := make(chan error, 1)
      // 这个函数中, 将 channel 和一个随机生成的 id 映射起来
      // 并在向其他节点同步的信息中带上这个 id
      id := n.storeConfChange(ch)
      err = n.Raft().ProposeConfChange(ctx, raftpb.ConfChange{
          ID:      id,
          Type:    raftpb.ConfChangeAddNode,
          NodeID:  pid,
          Context: rcBytes,
      })
      if err != nil {
          return err
      }
      
      // 等待 ConfChange apply 成功的回调
      err = <-ch
      return err
    }
    
    
  • 移除节点

    func (n *Node) ProposePeerRemoval(ctx context.Context, id uint64) error {
      // ...
      
      // 和 AddToCluster 类似, 这里需要等待 ConfChange 完成
      ch := make(chan error, 1)
      pid := n.storeConfChange(ch)
      err := n.Raft().ProposeConfChange(ctx, raftpb.ConfChange{
          ID:     pid,
          Type:   raftpb.ConfChangeRemoveNode,
          NodeID: id,
      })
      
      // ...
    
      return err
    }
    

节点间同步信息
func (n *Node) Send(m raftpb.Message) {
    // ...
    select {
    case n.messages <- sendmsg{to: m.To, data: data}:
        // pass
        
    // - -0 为什么这边会有 ignore... 仅仅是为了不阻塞调用者么?
    default:
        // ignore
    }
}

// 所有通过 n.messages 传递的信息都会积累到一定程度后一起发送
// 发往同一个节点的信息也会整合
func (n *Node) BatchAndSendMessages() {
    // 对同一个目标 id, 始终复用一个 *bytes.Buffer
    batches := make(map[uint64]*bytes.Buffer)
    for {
        totalSize := 0
        sm := <-n.messages
    slurp_loop:
        for {
            // 如有必要, 初始化 *bytes.Buffer
            var buf *bytes.Buffer
            
            // ...
            
            // 先将当前 data 的长度写入, 用做 message 之间的分隔.
            // 再写入 data 本体
            // 因此每条 message 占用 4 + len(sm.data)
            totalSize += 4 + len(sm.data)
            x.Check(binary.Write(buf, binary.LittleEndian, uint32(len(sm.data))))
            x.Check2(buf.Write(sm.data))

            // 如果累积的数据量足够大, 中断此次汇集, 执行发送
            if totalSize > messageBatchSoftLimit {
                // We limit the batch size, but we aren't pushing back on
                // n.messages, because the loop below spawns a goroutine
                // to do its dirty work.  This is good because right now
                // (*node).send fails(!) if the channel is full.
                break
            }

            // 如果没有新的 message 传入, 同样中断汇集执行发送
            select {
            case sm = <-n.messages:
            default:
                break slurp_loop
            }
        }

        // 执行发送
        for to, buf := range batches {
            if buf.Len() == 0 {
                continue
            }
            data := make([]byte, buf.Len())
            copy(data, buf.Bytes())
            go n.doSendMessage(to, data)
            
            // 重置 buf 供下一轮 message 汇集循环使用
            buf.Reset()
        }
    }
}

func (n *Node) doSendMessage(to uint64, data []byte) {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()

    // 获取到指定节点的连接
    addr, has := n.Peer(to)
    pool, err := Get().Get(addr)
    if !has || err != nil {
        x.Printf("No healthy connection found to node Id: %d, err: %v\n", to, err)
        // No such peer exists or we got handed a bogus config (bad addr), so we
        // can't send messages to this peer.
        return
    }
    client := pool.Get()

    // ...

    ch := make(chan error, 1)
    go func() {
        _, err = c.RaftMessage(ctx, p)
        if err != nil {
            x.Printf("Error while sending message to node Id: %d, err: %v\n", to, err)
        }
        ch <- err
    }()

    // 超时或发送完成
    select {
    case <-ctx.Done():
        return
    case <-ch:
        // We don't need to do anything if we receive any error while sending message.
        // RAFT would automatically retry.
        return
    }
}
保存/恢复 raft 数据
func (n *Node) SaveSnapshot(s raftpb.Snapshot) {
    // ...
}

func (n *Node) SaveToStorage(h raftpb.HardState, es []raftpb.Entry) {
    // ...
}

func (n *Node) InitFromWal(wal *raftwal.Wal) (idx uint64, restart bool, rerr error) {
    // ...
}

WaitForMinProposal

这里应该是 LinearRead 相关, 用来确认 Read 对应的 message 已经 apply

func (n *Node) WaitForMinProposal(ctx context.Context, read *api.LinRead) error {
    if read == nil || read.Ids == nil {
        return nil
    }
    gid := n.RaftContext.Group
    min := read.Ids[gid]
    return n.Applied.WaitForMark(ctx, min)
}

2.4 RaftServer

RaftServer 是 gRPC service Raft 的实现, 内部是对 Node 的操作.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容