【Lotus項目源代碼導讀】Lotus Daemon過程啓動框架

lotus是Lotus項目中的全節點命令行二進制。啓動全節點的命令是:

lotus daemon

daemon進程的功能是全節點的完整功能,概述一下主要包括如下

  • 通過P2P網絡發現其他節點,並進行通信
  • 從其它全節點獲取區塊數據,並對每一次區塊的消息及區塊打包信息進行校驗並保存到本地存儲
  • 爲其它全節點提供區塊同步的服務
  • 錢包管理服務,包括轉帳、消息簽名等
  • RPC API服務

本文就daemon進程的啓動過程進行分析

Command Action

lotus的命令行採用https://gopkg.in/urfave/cli.v2,以下是daemon命令的Action代碼邏輯:

var DaemonCmd = &cli.Command{
 Name:  "daemon",
 Usage: "Start a lotus daemon process",
 Flags: []cli.Flag{
 ...
 },
 Action: func(cctx *cli.Context) error {
   ...
  ...
  // 創建FsRepo,即lotus的Repo
  r, err := repo.NewFS(cctx.String("repo"))// 初始化Repo,例如創建初始文件等
  if err := r.Init(repo.FullNode); err != nil && err != repo.ErrRepoExists {
   return xerrors.Errorf("repo init error: %w", err)
  }// 獲取參數文件
  if err := paramfetch.GetParams(build.ParametersJson(), 0); err != nil {
   return xerrors.Errorf("fetching proof parameters: %w", err)
  }
​
​
  // 如果是Genesis啓動方式,則加載Genesis文件
  genesis := node.Options()
  if len(genBytes) > 0 {
   genesis = node.Override(new(modules.Genesis), modules.LoadGenesis(genBytes))
  }...
  var api api.FullNode
  // 創建全節點, 從第二個參數起都是Option
  stop, err := node.New(ctx,
   node.FullAPI(&api),
   // 上線, 多數初始化邏輯在此實現
   node.Online(),
   // Repo相關的初始化
   node.Repo(r),
​
   genesis,
​
   node.ApplyIf(func(s *node.Settings) bool { return cctx.IsSet("api") },
    node.Override(node.SetApiEndpointKey, func(lr repo.LockedRepo) error {
     apima, err := multiaddr.NewMultiaddr("/ip4/127.0.0.1/tcp/" +
      cctx.String("api"))
     if err != nil {
      return err
     }
     return lr.SetAPIEndpoint(apima)
    })),
   node.ApplyIf(func(s *node.Settings) bool { return !cctx.Bool("bootstrap") },
    node.Unset(node.RunPeerMgrKey),
    node.Unset(new(*peermgr.PeerMgr)),
   ),
  )
  ...
  // 根據repo內的配置創建APIEndpoint, 即網絡監聽對象
  endpoint, err := r.APIEndpoint()
  ...
  // 啓動RPC服務
  return serveRPC(api, stop, endpoint)
 },

啓動框架邏輯

上述代碼雖省略了一些關鍵代碼,但粗略一看代碼邏輯很少啊,肯定是有大量的邏輯藏在哪些語句中!是的,就是node.Online這句!在進入node.Online之前,我們先看一下node.New是什麼東西:

func New(ctx context.Context, opts ...Option) (StopFunc, error) {
    settings := Settings{
        modules:  map[interface{}]fx.Option{},
        invokes:  make([]fx.Option, _nInvokes),
        nodeType: repo.FullNode,
    }// apply module options in the right order
    if err := Options(Options(defaults()...), Options(opts...))(&settings); err != nil {
        return nil, xerrors.Errorf("applying node options failed: %w", err)
    }// gather constructors for fx.Options
    ctors := make([]fx.Option, 0, len(settings.modules))
    for _, opt := range settings.modules {
        ctors = append(ctors, opt)
    }// fill holes in invokes for use in fx.Options
    for i, opt := range settings.invokes {
        if opt == nil {
            settings.invokes[i] = fx.Options()
        }
    }
​
    app := fx.New(
        fx.Options(ctors...),
        fx.Options(settings.invokes...),
​
        fx.NopLogger,
    )// TODO: we probably should have a 'firewall' for Closing signal
    //  on this context, and implement closing logic through lifecycles
    //  correctly
    if err := app.Start(ctx); err != nil {
        // comment fx.NopLogger few lines above for easier debugging
        return nil, xerrors.Errorf("starting node: %w", err)
    }return app.Stop, nil
}

估計多數人看到以上代碼有點蒙了,又是Settings,又是Options(複數)和Option(單數),而且更費解的是有的Option/Options帶了fx包名,有的則沒有帶fx包名。好的,我們先看一眼Settings的完整代碼:

type Settings struct {
    // modules is a map of constructors for DI
    //
    // In most cases the index will be a reflect. Type of element returned by
    // the constructor, but for some 'constructors' it's hard to specify what's
    // the return type should be (or the constructor returns fx group)
    modules map[interface{}]fx.Option
​
    // invokes are separate from modules as they can't be referenced by return
    // type, and must be applied in correct order
    invokes []fx.Option
​
    nodeType repo.RepoType
​
    Online bool // Online option applied
    Config bool // Config option applied
}

裏面又是Option! 看來一定得了解這個Option是什麼了,注意這是帶fx包名的:

// An Option configures an App using the functional options paradigm
// popularized by Rob Pike. If you're unfamiliar with this style, see
// https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html.
type Option interface {
    apply(*App)
}
type optionFunc func(*App)
​
func (f optionFunc) apply(app *App) { f(app) }

它是在第三方包中定義,代碼文件爲"go.uber.org/[email protected]/app.go"。 Option定義是很超級簡單,但仍是不容易理解。原作者已經估計到多數人的感受,在註釋中加了一條鏈接:https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html,有興趣者自行去詳細瞭解一下。

但Option只是[go.uber.org/fx]這個包中很小一部分,fx包是uber公司開源的Golang的依賴注入框架。lotus的代碼也是採用該框架,所以對這個框架的理解是非常有必要的。

依賴注入(Dependency Injection)是一種設計模式,最出名的應用是Java的Spring框架,不瞭解的讀取自行搜索一下。關於這個包的使用方法可以參https://pkg.go.dev/go.uber.org/fx這裏回頭繼續介紹Option, 這裏先做一個簡要的解釋:Option可以理解成是影響App行爲的一個選項,如果想讓選項生效就執行apply方法,而App則可理解爲整個程序進程,而Options也就是多個Option的組合,可以想象到Options裏所有Option應該是按順序執行的,否則可能會導致結果不是我們所預期的。

fx.Option是fx包內定義的Option,是一個接口(見以上代碼),而Option(不帶fx.前綴)是lotus內部定義的:

type Option func(*Settings) error

繼續看一下Option相關的幾個函數:

func ApplyIf(check func(s *Settings) bool, opts ...Option) Option {
    return func(s *Settings) error {
        if check(s) {
            return Options(opts...)(s)
        }
        return nil
    }
}
​
func If(b bool, opts ...Option) Option {
    return ApplyIf(func(s *Settings) bool {
        return b
    }, opts...)
}

以上是根據判斷條件應用Option,回頭看一下前面daemon中的node.New語句的代碼,多個option可組合成Options(實際是一個函數)的參數,返回類型又是Option,而且某些Option還可以用ApplyIf指定條件應用。感覺象是。。。另一種語言?這個大膽的猜想是正確的,這就是領域描述語言(DSL)的至簡版本,有興趣的讀者可以上網搜索瞭解一下概念。

到此,爲什麼lotus在fx.Option基礎上又定義了一個Option,原因就比較清楚了:fx.Option的功能比較簡單,缺少類似象ApplyIf這樣的邏輯,所以Option是lotus裏對fx.Option的功能增強。另外fx.Option對應是的App(即fx.App),但在lotus中使用的是Settings結構。

接着,再看一下方法:

// Override option changes constructor for a given type
func Override(typ, constructor interface{}) Option {
    return func(s *Settings) error {
        if i, ok := typ.(invoke); ok {
            s.invokes[i] = fx.Invoke(constructor)
            return nil
        }if c, ok := typ.(special); ok {
            s.modules[c] = fx.Provide(constructor)
            return nil
        }
        ctor := as(constructor, typ)
        rt := reflect.TypeOf(typ).Elem()
​
        s.modules[rt] = fx.Provide(ctor)
        return nil
    }
}

以上也有些費解,這裏解釋一下用途即可:Override函數輸入參數是兩個參數:typ, constructor,typ就是類型,constructor就是構造函數了。輸出是一個Option,本質就是一個func。func裏面做的事情是將typ和constructor的對應關係保存起來。那爲什麼叫Override呢,原因是typ類型的對象本身默認用New即可構造,現在默認的構造行爲需要被重載。

至此,大家可以感覺到這些與DI有關,這裏我們大致這樣理解就可以了:如果需要定義一個新的類型對象(例如一個接口),我們就定義好構造函數(第一個輸出參數必須是該類型),然後將類型和構造函數作爲輸入參數去調用Override即可,後續至於什麼時候去生成這個類型的對象,就是DI容器去操心的事情。

現在回過頭再去理解前面的node.New的代碼邏輯,應該就比較好理解一些了,這裏我就僅大致介紹一下:開始是將所有的Option進行apply,然後將settings.modules裏保存的構造方法傳遞給fx的DI框架,再調用fx裏的app.Start。

後續

前面講了lotus啓動過程的框架邏輯,後續將繼續講解lotus啓動過程中與業務邏輯相關的部分。

End
非常感謝您對 IPFS&Filecoin 項目的持續支持。我們很高興繼續與您一起,爲人類信息建立一個強大的,去中心化和高效的基礎。
FilCloud 幫你迅速瞭解 IPFS 領域的熱點技術和應用
公衆號:filcloud

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章