type
status
date
slug
summary
tags
category
icon
password
Sub-item
Last edited time
Oct 15, 2023 04:08 AM
Parent item
领域
背景
Flow节点
Flow节点由各种数组组件组成,这些组件从节点内部和外部源接收信息,处理后输出。如果以数据流图的形式描述,那么:
- 数据处理组件就是顶点
- 交换消息的两个组件被表示为一条边
- 每个组件有一个专用道协程池来处理,一个有 k 个 goroutine 作为工作线程的顶点最多可以让 k 个核心保持忙碌。这种设计的好处是,即使节点的一个(或几个)数据处理顶点完全被淹没,节点也可以保持响应
- 节点内的顶点通常相互信任;而外部消息是不可信的。根据其消耗的输入,顶点必须仔细区分可信输入和不可信输入。
组件的定义在:
例如,
VoteAggregator
是共识节点内的数据流顶点,其工作是收集来自其他共识节点的传入投票。因此,相应的函数 AddVote(vote *model.Vote)
将所有输入视为不可信。此外,VoteAggregator
的函数AddBlock(block *model.Proposal)
、InvalidBlock(block *model.Proposal)
、PruneUpToView(view uint64)
处理来自节点内主要共识逻辑,这些函数但参数是可信的。外部拜占庭消息导致良性哨兵错误
注: 哨兵错误就是使用哨兵模式处理错误:
if errors.Is(err, XFailedErr)...
来自另一个节点的任何输入都是不可信的,顶点处理应该妥善处理任何任意恶意输入。在任何情况下,拜占庭输入都不应导致顶点的内部状态被破坏。根据约定,由于拜占庭输入导致的失败案例由特定类型的哨兵错误表示。
拜占庭输入是一种特殊情况,其中函数返回“良性错误”。错误良性的关键属性是,尽管遇到错误情况,返回错误的组件仍能完全正常工作。
所有良性错误都应在顶点内处理。作为实现错误处理的一部分,开发人员必须考虑哪些错误条件在当前组件的上下文中是良性的。
无效的内部消息导致异常
默认情况下,来自其他内部组件的任何输入都是受信任的。如果内部消息格式错误,则应将其视为严重异常。
【例外】:某些内部组件未经验证就转发外部消息。例如,同步引擎将块消息转发到合规引擎,而不验证该块是否有效。在这种情况下,转发块的有效性不是同步引擎和合规引擎之间合同的一部分,因此无效的转发块不应被视为无效的内部消息。
错误处理
区块链系统要保证安全第一,如果存在如下情况,那么就应该被当做错误来处理
- 协议违规(来自不同节点),其本身已明确定义
- 协议未覆盖的情况(在最坏的情况下可能会被利用来危害系统)
- 节点内部状态损坏
最佳实践
- 错误是 API 的一部分:如果函数返回错误,则该错误的含义会清楚地记录在该函数的 GoDoc 中。我们从概念上区分以下两类错误:
- 良性错误:尽管出现错误,但返回错误的组件仍然完全正常工作。 良性故障案例表示为类型化哨兵错误(基本错误和高级错误),因此我们可以进行类型检查。
- 异常:该错误是内部状态损坏的潜在症状。 例如,健全性检查失败。在这种情况下,该错误很可能是致命的。
- 错误返回的文档应该是每个接口的一部分。我们还鼓励将更高级别的接口文档复制到每个实现中,并可能使用特定于实现的注释来扩展它。特别是,当 API 级合约直接记录在实现之上时,它简化了实现的维护工作。
- 添加新的哨兵通常意味着更高级别的逻辑必须妥善处理额外的错误情况,可能会触发削减等。因此,更改指定哨兵错误集通常被认为是破坏性 API 更改。
- 超出指定良性哨兵错误的所有错误都被视为意外故障,即潜在状态损坏的症状。
- 我们采用高保证软件工程的基本原则,将已知良性错误之外的所有内容视为严重故障。在意外故障情况下,我们假设顶点的内存状态已被破坏,并且不再保证正常运行。唯一安全的恢复途径是从先前持久的安全状态重新启动顶点。根据约定,顶点应使用相关的不可恢复上下文抛出任何意外异常。
- 我们的 BFT 系统中的许多组件都可以返回良性错误(类型 (a)和不可恢复的异常(类型 (b))
- 特定错误是良性错误还是异常取决于调用者的上下文。不能仅根据错误类型将错误分为良性错误或异常错误。
- 例如,当按 ID 查询区块时(方法
Headers.ByBlockID(flow.Identifier)
),存储池可能返回storage.ErrNotFound
。在许多情况下,storage.ErrNotFound
是预期的,例如,如果节点正在接收新的区块提案并检查父节点是否已被摄取或需要从其他节点请求。相反,如果我们正在查询一个我们知道已经最终确定的区块,并且存储返回一个storage.ErrNotFound
,则某些内容已严重损坏。 - 使用特殊的
irrecoverable.exception
错误类型来表示意外错误(并从错误堆栈中删除任何哨兵信息)。 - 他们将从第三方模块返回的任何错误解释为意外错误
- 他们对其堆栈帧内部的意外情况做出反应并返回一般错误
- 需要把模块返回的任何记录的哨兵错误解释为意外
这适用于较高级别函数将从较低级别函数返回的哨兵解释为异常的任何场景。为了构建一个示例,让我们看一下我们的
storage.Blocks
API,它有一个 ByHeight
方法来按高度检索最终的区块。以下可能是一个假设的实现:在以下情况下,函数可以使用
irrecoverable.NewExceptionf
:在以下情况下,函数必须使用
irrecoverable.NewExceptionf
:为了简单说明,让我们考虑一些函数体,其中有对其他较低级别函数的多个后续调用。在大多数情况下,在正常操作期间总是期望或从不期望特定的哨兵类型。
如果是预期的话,那么应该记录哨兵类型。
如果始终不符合预期,则不应在函数的 godoc 中提及该错误。
如果没有积极肯定错误是预期的良性哨兵,则该错误将被视为不可恢复的异常。
因此,如果在整个函数体中始终不期望使用哨兵类型 T,请确保函数的 godoc 中未提及哨兵 T。后者完全足以将 T 归类为不可恢复的异常。
- 对仅返回良性错误的组件进行可选简化。
- 在这种情况下,您可以使用未定义类型的错误来表示良性错误情况(例如使用
fmt.Errorf
)。 - 使用未定义类型的错误,代码将违反我们的最佳实践指南,即良性错误应表示为类型化哨兵错误。因此,只要所有返回的错误都是良性的,请分别为每个公共函数清楚地记录这一点。例如,像下面这样的语句就足够了:
- 避免一般性错误,例如
- 使用哨兵错误
- 如果某些操作可能失败,请创建特定的哨兵错误。
- 在 goDoc 中记录正常操作期间预期的所有错误类型。这使得调用代码能够专门检查这些错误,并决定如何处理它们。
- 错误在调用堆栈中冒泡并包装以创建有意义的跟踪.。注:通过%w嵌套包装,传递给上一层
- 在有足够的上下文来决定在正常操作期间是否会出现错误的级别上来处理错误。
- 意外类型的错误表明节点的内部状态可能已损坏。
- 我们预计在正常操作期间该组件(网络层)会出现暂时性错误
- 网络层使用第 3 方库,该库不会针对预期的故障情况公开特定且详尽的哨兵错误
- 消息是否成功发送并不重要,或者稍后会重试
- 该代码已经区分了核心业务逻辑中的预期错误和意外错误,并且我们相应地记录了它们。
- 该代码在适当的级别处理预期的错误,仅传播意外的错误。
实践建议
反模式
继续尽力而为不是一种选择,即以下是 Flow 上下文中的反模式:
在极少数情况下,这是可以接受的。例如,当尝试通过网络向另一个节点发送消息并且网络层出错时。在这种情况下,请添加注释并解释为什么即使其他组件返回错误也可以继续继续。对于本例,通过记录日志来处理错误是可以接受的,因为:
安全优先于活力
理想情况下,当遇到意外错误时,顶点应该重新启动(从已知的良好状态)。根据约定,顶点应使用相关的不可恢复上下文抛出任何意外异常。
如果这超出了范围,我们会优先考虑安全性而不是活跃性。这意味着我们宁愿让节点崩溃,也不愿尽力继续。如有疑问,请使用以下方法作为后备:
错误处理准则的设置可以最大限度地减少技术债务,即使我们不支持立即重新启动组件。
如果您遵循了错误处理准则,那么将组件的重新启动逻辑作为技术债务留待以后使用,希望不会增加太多开销。
相反,如果您记录错误并尽最大努力继续,您还会将用于区分错误的整个逻辑保留为技术债务。后面添加这个逻辑的时候,就需要重新审视整个业务逻辑了。