--- 摄于 2017 年 9 月 藏川线前段
上周,发现 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 广播的新状态
源码地址:https://github.com/cryptape/cita/blob/develop/cita-network/src/synchronizer.rs
Network 的同步逻辑完全写在 synchronizer.rs
这一个文件里面,主要函数是 update_current_status
和 update_global_status
,这两个函数接口是没有变动的,但是职能进行了严格划分:
update_global_status
负责从 非同步状态 进入 同步状态,即有权限在 非同步状态 发起请求update_current_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,这样,同步的速度应该会再次提高,至少停顿的时间将会节省掉。
细心的同学会在代码中看到这样一段:
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 对于同步块的处理。
请登录后评论
评论区
加载更多