藏川线前段

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

CITA 系列之微服务联动 —— 同步(一)

上周,发现 Network 同步功能的优化可能性,我把同步的逻辑重写了一遍,Network 是同步的发起点,也是同步的结束点,要控制好节奏和流量,原有逻辑处理得过于复杂,已经不太好改了,现在的处理方案也不是最好的,后续还可以继续优化。

同步功能是什么

分布式应用的一个很大的便利是可以水平扩展,就是非常方便地堆机器用以提高吞吐量、性能等,区块链虽然单纯堆机器并不能提高性能,但是它确实是一个分布式应用,水平拓展是很方便的一件事,那么水平拓展的一个问题就是,数据需要在各个节点都有一份相同的备份,新加入节点需要首先向原有节点请求同步数据,达到最新数据后,开始正常工作。

在 CITA 的应用中,分为只读节点和共识节点两种类型,只读节点除了不参与共识,其他行为与共识节点一致,它需要时刻与共识节点进行同步数据,共识节点在出现网络异常,下线等情况下也需要向其他共识节点发出同步请求。综合来看,一般触发同步请求的情况大致分为以下几类:

涉及的模块

同步主要涉及模块是:

这一篇,我们先从起点开始,讲讲 Network 的处理思路,以及后续可以优化的地方。

流程图

整体流程

 请求同步节点                                  | 被请求节点
                                             |
 ------                --------              |           --------             -------
| Chain |  <--------> | Network | <------->  | <------> | Network | <------> | Chain |
 ------                --------              |           --------             -------
                          |                  |
                          |                  |
                          V                  |
                      ----------             |
                     | Executer |            |
                      ----------             |

network 同步状态判断

update_global_status:

current_height global_height other condition action
20 19 no action
20 21 !is_sync && old_global < new_global start sync
20 > 21 !is_sync || timeout start sync

update_current_status:

new_height old_height sync_end_height global_height other condition action
20 50 sync
20 19 30 in sync
20 19 19 20 is_sync stop sync
20 19 19 50 is_sync continue sync

is_sync:是否在同步状态

new_height :接到 chain 广播的新状态

Network 实现

源码地址:https://github.com/cryptape/cita/blob/develop/cita-network/src/synchronizer.rs

Network 的同步逻辑完全写在 synchronizer.rs 这一个文件里面,主要函数是 update_current_statusupdate_global_status ,这两个函数接口是没有变动的,但是职能进行了严格划分:

同步状态:从 Network 发起同步请求开始,到完全同步到最高高度为止,这段时间定义为同步状态

/// Get messages and determine if need to synchronize or broadcast the current node status
pub struct Synchronizer {
    tx_pub: mpsc::Sender<(String, Vec<u8>)>,
    con: Arc<Connection>,
    current_status: Status,
    global_status: Status,
    // current_status <= sync_end_status
    sync_end_height: u64, 
    // Is it in sync?
    is_synchronizing: bool,
    latest_status_lists: BTreeMap<u64, VecDeque<u32>>,
    block_lists: BTreeMap<u64, Block>,
    rand: ThreadRng,
    // Timer for each height processing
    remote_sync_time_out: Instant,
    /// local sync error
    local_sync_count: u8,
}

这个结构体里面,记录了几个比较重要的信息:

进入和退出同步状态

update_global_status 通过 global_status 与 current_status 的对比,可以很容易地判断出,是否需要进入同步状态,要注意的是,只有在 非同步状态 的时候,才需要考虑是否进入同步状态(超时除外)。

update_current_status 判断是否继续同步、是否需要退出同步。

同步步长及优化点

目前同步步长调整到了 20 个块一轮,在处理完前 19 个块之前,network 不会再次向外发起同步,当执行完之后,如果还没有达到链上高度,那么就会再次向外请求下一轮 20 个块的同步,在压测状态下,9000 个块大概需要 1 个多小时就能完成同步(正常出块是 3 秒一个块,9000 个块需要 7 个多小时才能完成)。

相对来说,同步速度还是可以接受的,但是,一轮一停顿这种方式是可以继续优化的,思路也很简单,做成流水线(滑动窗口)模式,发起同步和提交同步块给 Chain 这两个行为拆开,一个线程去做同步,保持缓存长度一直为 20,另一个线程根据 Chain 的广播消息,以两个块一次的方式将缓存中的块发送给 Executor 和 Chain,这样,同步的速度应该会再次提高,至少停顿的时间将会节省掉。

为什么 sync_end_height 赋值为同步最高高度减一

细心的同学会在代码中看到这样一段:

match blocks.last() {
    Some(block) => {
        if let Some(header) = block.header.as_ref() {
            let height = header.get_height() - 1;

            if height > self.sync_end_height {
                self.sync_end_height = height;
            }
        }
    }
    None => {}
}

这个地方,给 sync_end_height 赋值之前,将同步的最高高度减一了,这是为什么?

这个问题要从 bft 和 proof 的处理策略上说起,细心的同学在用 getBlockbynumber 查询高度为 0 或者 1 的块的时候,会发现,这两个块的 proof 为 null,而从 2 开始的块,查询出来的块的 proof 都有值,并且 proof 内的 height 都等于当前块高减一。

CITA 在共识 proof 的存储上做了一些手脚,将当前块的 proof 存储在了下一个块里面,共识完成的时候,proof 和 block 是同时出来的,但是存储的时候,会将 proof 放在下一个块里面,延时确认。

这样的处理在同步的时候就会有一个特殊的地方,必须有两个块同时到达,才能从下一个块上拿到上一个块的 proof,然后确定上一个块的有效性,如果是链上的最高高度,就会将 proof 放在 u64::Max 这个位置。这样的话,同步的时候,虽然拿到了 20 个块,但是实际上,只有前 19 个是可以被验证的,最后那个,除非是最高高度,否则都无法验证处理。

所以,sync_end_height 的赋值,必须为同步高度减一,同时也解释了为什么在滑动处理的时候需要 2 个块一组向 Chain 发送同步块。

最后

这篇讲解了目前 network 同步功能目前的方案和实现,下一篇讲解 Executor 和 Chain 对于同步块的处理。

评论区

加载更多

登录后评论