别说现在世风日下,你自己做个好人,世上就多一个好人。控制你能控制的,接受你不能改变的。王阳明《传习录》
郑重声明: 最近有不少网友反馈,有读者直接抄袭本人的博客文章发布在自己的公众号以及知乎等其他平台,且未注明出处。在此严重警告抄袭者:本站博文如非注明转载则均属作者原创文章, 引用或转载请注明出处,如要商用请联系作者,谢谢。以前抄袭行为既往不咎,后期再发现此类行为,本人将采取行为严厉打击抄袭者,在此也感谢那些监督举报的网友。
写文不易,分享更不易,且抄且珍惜!
再次感恩大家 !!!

Lotus v1.10.0 版本已于北京时间今天上午 6 点更新上线了。本次更新的主要功能就是支持扇区消息上链的聚合提交。具体更新内容详情请移步这里 Lotus 网络升级 v1.10.0 - 将大大降低质押 Gas 成本

然而上线之后立马就有群友发现了一个 Bug,就是当你同时启用消息聚合(BatchPreCommits,AggregateCommits)和 FinalizeEarly 之后会出现大量扇区 FianlizeFailed。对此社区人员说 Master 分支已经修复了这个 Bug,只需改动一行代码(extern/storage-sealing/fsm.go):

CommitFinalizeFailed: planOne(
    // 这里把 CommitFinalizeFailed 改成 CommitFinalize
    // 避免死循环
	on(SectorRetryFinalize{}, CommitFinalize),
),

然而,我们测试发现并没有修复成功。使用 Master 分支编译后的程序还是会出现 FinalizeFailed 错误。

于是我们一边向官方开发人员提交 Bug,一边自己尝试修复这个 Bug,经过几次迭代,终于找到了比较完美的解决方案。下面记录一下我们这次解决这个问题的整个过程,如果我们解决问题的方法能给大家一些启发,那么本文的目的就达到了

修改方案 v1.0

首先查看日志不难发现,之所以失败的原因是:

  1. 当我们设置 FinalizeEarly=true 时,扇区在完成 C2 后进入 SubmitCommitAggregate 前就会进行一次 FinalizeSector 操作。
  2. 正常的扇区在进入 CommitWait 之后还会再执行一次 FinalizeSector 操作,但是此时扇区文件已经被拉走了,所以就进入 FinalizeFailed 状态了。

你仔细看了一下状态机的代码,就会发现扇区流转的逻辑有些问题(extern/storage-sealing/fsm.go 第 106 行):

// Commmiting 之前的状态都一样,从这里开始,聚合消息走的是 CommitFinalize
// 非聚合消息直接到 SubmitCommit 状态了
CommitFinalize: planOne(
	on(SectorFinalized{}, SubmitCommit),
	on(SectorFinalizeFailed{}, CommitFinalizeFailed),
),
SubmitCommit: planOne(
	on(SectorCommitSubmitted{}, CommitWait),
	on(SectorSubmitCommitAggregate{}, SubmitCommitAggregate),
	on(SectorCommitFailed{}, CommitFailed),
),
SubmitCommitAggregate: planOne(
    // 此处应该是到 CommitAggregateWait 状态,而不是到 CommitWait 状态
	on(SectorCommitAggregateSent{}, CommitWait),
	on(SectorCommitFailed{}, CommitFailed),
),
CommitWait: planOne(
	on(SectorProving{}, FinalizeSector),
	on(SectorCommitFailed{}, CommitFailed),
	on(SectorRetrySubmitCommit{}, SubmitCommit),
),
CommitAggregateWait: planOne(
    // 这里前面已经 Finalize 成功了,所以此处应该直接把扇区的状态改成 Proving
	on(SectorProving{}, FinalizeSector),
	on(SectorCommitFailed{}, CommitFailed),
	on(SectorRetrySubmitCommit{}, SubmitCommit),
),
  1. CommitAggregateWait 这个状态一直没有被使用。
  2. SubmitCommitAggregate 状态之后应该是走 CommitAggregateWait 状态,而不是 CommitWait
  3. CommitAggregateWait Plan 中,如果扇区已经 Proving 了(SectorProving{} State),则应该直接将该扇区的状态设置为 Proving

改完之后本地测试一下发现能否正常 Finalize 了。于是顺便给官方提交了一个 PR:

解决方案 1.0 PR

修改方案 v2.0

正当我刚提交完 PR,准备打完收工下班回家的时候,Lion 大神(同事) 提醒我说:”老铁,你刚刚改的方案还有 Bug。因为当 FinalizeEarly=false 的时候,你后面会漏掉 FinalizeSector 这个环节, 直接把扇区状态设置为 Proving 了”

回顾了一下代码,发现还真是个大 Bug,只不过我们刚刚测试的时候是设置 FinalizeEarly=true,所以测试并没有发现问题:

CommitAggregateWait: planOne(
    // 这里直接把扇区的状态改成 Proving 是有问题的
	on(SectorProving{}, Proving),
	on(SectorCommitFailed{}, CommitFailed),
	on(SectorRetrySubmitCommit{}, SubmitCommit),
),

于是我们 Pass 掉这个方案,提出了一个新的方案:

在扇区 CommitFinalize 状态时调用 FinalizeSector 失败并重试的时候,强制把这个扇区设置成 Finalize 成功就好了。

代码修改也很简单,只需要修改两处:

  1. extern/storage-sealing/fsm.go 第 390 行,增加 CommitFinalize 状态处理 handler。

     case CommitFinalize:
     // 这里不直接 fallthrough 了,而是交给 handleCommitFinalizeSector() 函数处理
     return m.handleCommitFinalizeSector, processed, nil
    
  2. extern/storage-sealing/states_sealing.go 第 693 行,添加 handleCommitFinalizeSector() 函数实现

     func (m *Sealing) handleCommitFinalizeSector(ctx statemachine.Context, sector SectorInfo) error {
     	return m.doFinalizeSector(ctx, sector, true)
     }
    
     func (m *Sealing) handleFinalizeSector(ctx statemachine.Context, sector SectorInfo) error {
     	return m.doFinalizeSector(ctx, sector, false)
     }
    
     func (m *Sealing) doFinalizeSector(ctx statemachine.Context, sector SectorInfo, earlyFailedRetry bool) error {
     	// TODO: Maybe wait for some finality
    
     	cfg, err := m.getConfig()
     	if err != nil {
     		return xerrors.Errorf("getting sealing config: %w", err)
     	}
    
     	if err := m.sealer.FinalizeSector(sector.sealingCtx(ctx.Context()), m.minerSector(sector.SectorType, sector.SectorNumber), sector.keepUnsealedRanges(false, cfg.AlwaysKeepUnsealedCopy)); err != nil {
     	    // 主要修改逻辑:如果是 FinalizeEarly 失败重试的话,直接返回 Finalize 成功
     	    // 因为此时扇区已经被拉回存储了
     		if cfg.FinalizeEarly && earlyFailedRetry == false {
     			return ctx.Send(SectorFinalized{})
     		}
    
     		return ctx.Send(SectorFinalizeFailed{xerrors.Errorf("finalize sector: %w", err)})
     	}
    
     	return ctx.Send(SectorFinalized{})
     }
    

改完测试发现,无论 FinalizeEarly 设置成 true 还是 false,扇区都能被成功 Finalize。我们认为这次已经完美解决这个问题了,于是把之前的 PR 关闭,重新提交了一个 PR。

修改方案 V2.0 PR

修改方案 v3.0

晚上回去之后我们收到官方开发人员回复,大概意思是他们意识到这个 Bug 了,但是他们有其他的修复方案,并且已经提交了 PR。

于是立马看了一下他们的实现方式,提交 PR 的是 Lotus 项目核心开发者 magik6k 同学,他改的是调度管理器,修复的思路也很简单,就是在执行 FinalizeSector 调度的时候先找一下扇区文件在哪里:

  1. 如果扇区还在密封机器上,那么说明在 CommitFinalize 节点的 FinalizeSector 并没有执行成功,则此时按照正常的 FinalizeSector 调度。
  2. 如果扇区已经在存储机器上了,那么说明已经 Finalize 成功了,此时只需要做一次空的 FinalizeSector 调度就行了。

主要更改的逻辑在 extern/sector-storage/manager.go 第 530 行:

修改方案 V3.0 PR

不得不说,这个实现方式更加稳妥一些,干的漂亮。而且我们的方案二其实还是有点小 Bug 的,因为如果凑巧 2 次 FinalizeSector 都调用失败的话,那么这个扇区在做时空证明的时候是会有问题的, 因为我们在 CommitFinalizeFailed Retry 的时候强制把状态设置成 SectorFinalized 了,但是实际跑的过程中可能由于各种网络原因,扇区是真的没拉回来,那么这个扇区在做时空证明的时候是找不到的。

不过其实你认真看一下 magik6k 的逻辑会发现还是有 2 个问题:

  1. 他在判断扇区是否 Finalize 成功时候是判断 sealed 文件是否存在,而如果你熟悉 FinalizeSector 的下载流程的话,它是先下载 sealed 文件,然后再下载 cache 文件。假如在下载完 sealed 后由于一些原因(如网络中断)导致 Finalize 操作中断了,那么这个判断就是有问题的,会导致 cache 文件一直没法下载过来。

     // 此处的 storiface.FTSealed 应该改成 storiface.FTCache 
     sealedStores, err := m.index.StorageFindSector(ctx, sector.ID, storiface.FTSealed, 0, false)
    

    其实准确的来说应该改成这个数组的最后一项才对

     // extern/sector-storage/storiface/filetype.go 第 19 行
     var PathTypes = []SectorFileType{FTUnsealed, FTSealed, FTCache}
    
  2. 在判断是算力 worker 还是存储 worker 的时候,逻辑也有些问题,如果 Miner 开启了 sealing 功能的话,则 Finalize 也可能出错:

     // 此处如果 Miner 开启了 sealing 功能,那么这里的 store.CanSeal 也是 true
     // 那么就可能进行第二次 FinalizeSector, 把 Miner 当做 sealing worker, 从而将扇区从 Miner 再上拉走。
     // 所以这里还需要排除 Miner 的这种情况
     if store.CanSeal {
         pathType = storiface.PathSealing
         break
     }
    

总结

今天这篇文章我们讲述了一个小 Bug 的修复过程,从中我们可以得到以下信息:

  1. Lotus 其实是一个非常复杂的区块链项目,对于这种大型的项目,即使修一个很小的 Bug 也要考虑非常周到,否则可能修复一个 Bug 后又新增了好几个
  2. 对于大型项目,在动手改动之前你需要对整个项目架构,业务流程有个通盘的了解,否则你很难下手去改任何一个功能。
  3. 对于任何问题,一直想是想不出完美解决方案的,你要在动脑的同时动起手来,也许开始的解决方案并不完美,但是在你动手写代码的过程中会不断有新的想法蹦出来,即使没有也没关系,也许别人能从你的代码中受到启发,从而想出更好的解决方案。在我们刚开始提交第一个 PR 的时候,官方开发人员并没有看出他们的代码逻辑有任何问题,但是后面他们意识到了。
  4. 不要老是吐槽”官方代码写的很烂”,其实官方的开发者水平还是挺高的,之所以你会觉得烂,首先是因为它没有满足你的个人需求(或者说小部分人的需求),因为他们要考虑大部分人的需求,而且不能提高使用门槛;其次是因为这个项目确实很复杂,工作量很大,但是项目周期有比较紧,所以只能走 先实现,后优化 的道路。

最后是一点广告时间:我们的新公司 深圳原语科技有限公司 营业已经快一个月了,新产品 【原语云】 即将上线。原语云专注智能运维的云服务平台。提供海量服务器的可视化智能运维、Filecoin的集群方案/代码优化/可视化管理/应用生态等一站式云解决方案!

还有,以后我们关于 Filecoin 相关的技术文章都会首发于我们的 【原语云】 微信公众号。想要第一时间阅读的同学可以关注一下,谢谢。