藏川线前段

--- 摄于 2017 年 9 月 藏川线前段

同步性能优化(二)

ps:感谢排序,让我远离取名烦恼

上一次说到研究方向,总体而言,分两个大方向:找到并修复潜在的问题和优化。这一篇,就说说找到的问题,这也是为什么会卡顿、同步无法正常进行的直接原因。

同步协议

首先,我们必须了解 ckb 到底是怎么同步的,即正常的逻辑,然后才能对照出异常的问题点。ckb 本身的同步协议与 Bitcoin 的 header-first 模式比较类似,但细节上有一些不一样的地方。笼统地说,ckb 同步分为两个阶段:

  1. 节点最高块的 timestamp 时间小于当前本地时间减去 24 小时,称之为 IBD(init-block-download)
  2. 节点最高块的 timestamp 时间大于等于当前本地时间减去 24 小时

节点在 IBD 状态时,relay 协议是不工作的,即不响应新块广播、不转发 Transaction,并且不响应同步协议的被同步请求,一心一意只做同步工作,争取在消除各种干扰的前提下,尽可能快地同步完成。

无论本地客户端从什么状态开始同步,同步的整体逻辑都是一致的:

  1. 发送 get_headers 消息给随机节点
  2. 根据返回的 headers 消息在内存中构建 header_map,并记录可能的 best_known_header
  3. 根据 header_map 和本地链状态,向节点发送 get_block 消息
  4. 接到返回的 block 消息,验证通过后更新本地数据库状态,同步更新对端节点状态
  5. 直到最后 best_known 等于本地 tip,同步结束
  6. 这时候 relay 协议接手后续的更新问题,偶尔需要 sync 协议辅助

问题所在

上诉流程,其实并没有什么特别的,但这里有几个技术细节实现有问题,导致了卡顿、无法同步,并且这里的问题都不是正确性问题,而是性能问题。我们根据问题进行分类说明,并给出修复的 PR 链接。

构建、查找 header_map 的性能问题

相关修复 PR:

首先,我们得理解这个 header map 是个什么东西,准确地讲,它是一个构建在内存的 skip map,也就是它是以 header hash 为 key,header + option point 为 value 的一个 hashmap,可能存在的 point 指向相对较远的 header,可能减少遍历的消耗,包括 io、hash 计算等

它在构建时的跳转逻辑如下:

fn get_skip_height(height: u64) -> u64 {
    // Turn the lowest '1' bit in the binary representation of a number into a '0'.
    fn invert_lowest_one(n: i64) -> i64 {
        n & (n - 1)
    }

    if height < 2 {
        return 0;
    }

    // Determine which height to jump back to. Any number strictly lower than height is acceptable,
    // but the following expression seems to perform well in simulations (max 110 steps to go back
    // up to 2**18 blocks).
    if (height & 1) > 0 {
        invert_lowest_one(invert_lowest_one(height as i64 - 1)) as u64 + 1
    } else {
        invert_lowest_one(height as i64) as u64
    }
}

如果计算一下 get_skip_height(1048577) 这个值,你会很惊讶地发现,这个跳转点在 1 高度,如果内存里有该 header 存在,就只是一个 hash get 的消耗,但如果该值已经被清理掉了,依据 get_ancestor 的遍历逻辑,它需求访问 100w 多次 db 才能找到这个值,100w 次 io 的操作,在外部看来,这就是卡死了,而根据该算法的逻辑,每隔一段高度都会有这类跳转超长的数据存在,在没有把 header map dump 到数据库并会清理内存中的 header map的前提下,这种问题出现的概率是极高的。同样的问题也发生在读取上。

last common header 标记错误

相关修复 PR:

这个问题涉及的背景相对多一点,但理解该问题是很简单的事情。简单的说,在请求 block 消息发送的时候,需要从记录的与该节点的 last common header 开始,根据 header map 指引,找到没有请求的 block,然后发送。这里依赖 last common header 标记的正确性,以及及时性。

举个简单的例子,如果当前 tip 在 100000,对端 last common header 标记在 10,那么在找发送请求点的时候,就需要从 10 遍历到 100000,然后再开始真正的查找工作,一大段无效工作几乎每个节点的请求发出都需要重复一遍,就是因为该值的标记问题。

而真正导致这个问题严重的背景有三个:

  1. 0.31 及之前的版本,有一个冗余设计,即每个 block 可以向两个节点请求,
  2. 查找请求点与接受块是互斥的,没有并行,
  3. 因为整体模型设计的问题,没有很好处理 orphan block,导致已经接收的块还可以继续请求

尤其是第二条,导致了在查找的时候无法继续接受块信息,造成了卡死的现象。

0.31 的修复

在 0.31 的 release note 里,除了上诉问题,还有一些修复和优化,比如开启并行下载、修复 RocksDB 的 snapshot 使用问题,这些也与同步相关,但在这一篇就不展开讲解了。

小结

这一篇,讲的是同步问题所在,解决这些问题,卡顿的现象理论上就不存在了,但同步速度并没有达到最佳状态,下一篇讲讲优化问题。

评论区

加载更多

登录后评论