她那个时候还太年轻,不知道所有命运的赠送的礼物,早已在暗中标好了价格。 -- 断头皇后

本文首发于 原语云 微信服务号,文末有二维码,欢迎关注我们。

Lotus 1.10.0 版本发布的时候,当 miner 的 FinalizeEarly 设置为 true 的时候会触发 FinalizeFailed 的 bug,那会为了让客户用上合并提交的功能而又不影响质押增速的情况下, 紧急了给了个修复方案,后面官方也给了一个修复提交,具体请参考上篇文章:FinalizeFailed的紧急修复方案

无论是上述哪种方案对于默认的通过miner来下载数据的集群都是可以正常工作,但是对于不是单纯的通过 miner 来下载数据的大集群还有一些额外的问题需要解决。 本篇文章给出分析的过程并讲述了原语云的一种通用性的解决方案供学习参考。

一、多点数据下载的需求

当一天的算力增速比较大,例如 200TiB,500TiB,甚至 PiB 级别的时候,即使给 miner 机器增加一个 160GiB 的聚合网卡,并让其将密封好的数据下载到最终存储, 在实际落地过程中会出现很多问。最直接的影响就是爆快证明和时空证明的计算,因为持续的网络IO会导致整个硬件进入一种IO等待的假死状态而无法响应快速的计算需求, 结果就是你会发现爆块时间超过 30 秒,或者时空证明做了半个小时也没完成。也就是默认的 miner 下载数据的方式成为了一个瓶颈,这个问题的解决思路大致有如下两种:

  1. 单独的存储 worker: 1 到 n 台存储 worker 接入密封 miner,miner 调度存储 worker 去下载数据存储到 worker 本地磁盘,然后通过 nfs 挂载到时空证明 miner 或者直接使用存储 worker 进行时空证明计算,这是很多大矿工的架构。
  2. 单独的下载 worker: 1 到 n 台下载 worker 接入密封 miner,miner 调度下载 worker 去下载数据存储到存储机器或者类似于 ceph 的分布式存储系统中,然后 miner 通过 nfs 挂载存储机器或者使用 POSIX 协议挂载分布式存储来读取扇区。

原语云用的是第二种解决方案,我们引入一个叫做网关机器的角色专门用于下载数据到存储机器或者分布式存储系统。 数量从 1 台到 N 台不等,我们都是建议使用至少两台,因为如果有一台坏了,另外一台可以自动切换继续下载数据不至于让密封计算机器上面的缓冲磁盘(SSD)堆满而导致一大堆问题。 大体的网络架构如下:

网关机器本质上就是一台运行着修改版本的 lotus-worker 进程,打开 CanStore 然后关闭 CanSeal,使用 8 核 16GiB 内存的低配置的 intel 机器即可。 质押管理 miner 会自动按照设定的算法调度网关机器去下载数据到存储网络然后声明扇区到主miner对应的 sectorstore 上,再结合网络分离这样一天可以完成 PiB 级别数据的下载。

无论是多存储 worker 还是多下载 worker 的架构,之前提供的修复方案或者现在官方的解决方案在 SubmitCommitAggregate 后的 FinalizeSector 的扇区下载部分存在如下问题: 如果是多存储worker的架构或者存储worker有多个 sectorstore,第二次 FinalizeSector 的时候如果执行下载 worker 不是数据所在的 worker 会导致扇区重新下载一次, 而这个操作很没有必要,隐约增加网络的负载。

如果是 多下载 worker + 分布式存储架构,也就是时空证明 miner 和下载 worker 共享一个或者多个分布式存储,第二次 FinalizeSector 时候如果执行下载的 worker 和数据最终落盘的 worker 共享同一个分布式存储会直接导致扇区被删除,即使不共享也会如上导致扇区再被下载一次。

二、通用性的解决方案

以下给出我们最终的解决方案,也是在 2k 网络环境反复测试验证过的方案,无论是官方默认方案、基于多存储worker的实现还是类似于我们这样的网关设计都适用, 整个思路归结为一句话:

只要FinalizeSector的扇区存在 不在永久存储 的 type就去调用 FinalizeSector 和对应类型的数据的下载。

具体思路如下:

  1. 查找扇区的 unsealed,sealed 和 cache 的存储信息,如果任何一种已经存在,并且不在最终数据落盘的存储上面,就标记该扇区需要进行 FinalizeSector 调用。
  2. 凡是不在最终存储的扇区类型,都需标记该扇区需要被下载到最终存储设备。

核心代码如下:

  fileType := storiface.FTNone
  for _, ft := range storiface.PathTypes {
    stores, err := m.index.StorageFindSector(ctx, sector.ID, ft, 0, false)
    if err != nil {
      return xerrors.Errorf("find %s sector: %w", ft.String(), err)
    }

    // analysis the storage and track the current
    // type for later fetch if all of them are not in final storage
    inFinal := false
    for _, st := range stores {
      if st.CanFinal {  // In the final storage ?
        inFinal = true
        break
      }
    }
    if len(stores) == 0 {
      // found nothing, and that could be a case
    } else if inFinal == false {
      fileType |= ft
    }
  }
  selector := newExistingSelector(m.index, sector.ID, storiface.FTCache|storiface.FTSealed, false)
  err := m.sched.Schedule(ctx, sector, sealtasks.TTFinalize, selector,
    m.schedFetch(sector, fileType, storiface.PathSealing, storiface.AcquireMove),
    func(ctx context.Context, w Worker) error {
      _, err := m.waitSimpleCall(ctx)(w.FinalizeSector(ctx, sector, keepUnsealed))
      return err
    })
  if err != nil {
    return err
  }

  // sector download to final storage
  fetchSel := newAllocSelector(m.index, storiface.FTCache|storiface.FTSealed, storiface.PathStorage)
  if len(keepUnsealed) == 0 {
    fileType &= ^storiface.FTUnsealed
  }
  // Fetch the sector ONLY for the above fileTypes
  err = m.sched.Schedule(ctx, sector, sealtasks.TTFetch, fetchSel,
    m.schedFetch(sector, fileType, storiface.PathStorage, storiface.AcquireMove),
    func(ctx context.Context, w Worker) error {
      _, err := m.waitSimpleCall(ctx)(w.MoveStorage(ctx, sector, storiface.FTCache|storiface.FTSealed|moveUnsealed))
      return err
    })
  if err != nil {
    return xerrors.Errorf("moving sector to storage: %w", err)
  }

这里解释一下上述代码如何判断是否最终存储?

  1. 对于单读的 miner 做下载数据或者使用存储 worker 集群的方案这个判断直接使用 st.CanStore 即可,也就是设置可以存储的存储设备都可以理解为最终存储,这个你可以直接去看官方的修。
  2. 而对于我们的网关 worker 的方案,网关 worker 的存储本身就需要设置 CanStore=true 以便于 miner 调度其去下载数据, 所以我们给 StorageInfoLocalStorageMeta 增加了一个 CanFinal 的字段用于标记对应的存储为最终存储。

具体思路如下:

  1. 扇区判断使用全部的三种类型(虽然unsealed大体上不需要),任何一种类型都不放过,同时兼容参数keepUnsealed的设置。
  2. 数据下载前因为进行了类型的查询,避免了运行时的 SectorNotFound 的错误,也可以通过重试解决下载失败的情况,这个修改可以让 FinalizeSector 重复安全的调用

最后:欢迎关注我们,一起探讨学习。