位元組微服務HTTP框架Hertz使用與原始碼分析|擁抱開源

2022-09-02 09:09:21

一、前言

Hertz[həːts] 是一個 Golang 微服務 HTTP 框架,在設計之初參考了其他開源框架 fasthttpginecho 的優勢, 並結合位元組跳動內部的需求,使其具有高易用性、高效能、高擴充套件性等特點,目前在位元組跳動內部已廣泛使用。 如今越來越多的微服務選擇使用 Golang,如果對微服務效能有要求,又希望框架能夠充分滿足內部的可客製化化需求,Hertz 會是一個不錯的選擇。

對於原始碼該如何閱讀,本身就值得思考。這篇文章我將以第一次閱讀Hertz原始碼的視角,分享自己的思考過程,也藉此梳理一下自己閱讀原始碼的方法論。

接下來需要你對應開啟Hertz的官方檔案,以及在本地克隆Hertz的程式碼倉庫,我們開始吧。

Hertz倉庫地址:https://github.com/cloudwego/hertz

Hertz檔案地址:https://www.cloudwego.io/zh/docs/hertz/getting-started/

二、架構設計

這是一張Hertz官方檔案的架構設計圖,圖中的一個個元件對應hertz原始碼包內的一個個package資料夾,實現了對應的功能,如下:

三、快速開始

接下來按照檔案的指示,通過hertz的命令列工具初始化一個最簡單的hertz專案,先觀其形,再會其意。

對應檔案地址:https://www.cloudwego.io/zh/docs/hertz/getting-started/

# 安裝hertz的命令列工具,用於生成hertz初始程式碼
go install github.com/cloudwego/hertz/cmd/hz@latest
# 通過hz工具生成程式碼,如果建立的專案不在GOPATH/src路徑下,則需要額外宣告-module引數
hz new -module hertz-study

此時按照檔案指示,對專案進行編譯執行可以存取這個HTTP服務了,它預設實現了一個/ping介面。

curl http://127.0.0.1:8888/ping
# 響應
{"message":"pong"}% 

四、原始碼解析

server概覽

首先看一下main.go函數,這是hertz服務的啟動入口,大概可以猜測內容是:1. 初始化了一個預設的hz服務;2. 完成了一些註冊工作;3. 啟動hz服務(HTTP服務)。

func main() {
   h := server.Default()
​
   register(h)
   h.Spin()
}

回想剛剛這個 http://127.0.0.1:8888/ping 的介面服務,它所宣告的IP和Port並未由你手動指定,並且/ping介面也不是你編寫的,或許是這個server.Default()的作用。

反之我如果需要指定HTTP服務啟動的各種客製化化的設定,是否是給這個server.Default()傳引數?又或者是換一個建立h的方法?

Default()

// Default creates a hertz instance with default middlewares.
func Default(opts ...config.Option) *Hertz {
   h := New(opts...)
   h.Use(recovery.Recovery())
​
   return h
}

檢視Default()方法,發現確實可以傳入引數(猜測就是可以自定義設定的內容),然後我們進一步分析New方法的內容,它接受了一個不定長度的Option陣列為參。

// Option is the only struct that can be used to set Options.
type Option struct {
  F func(o *Options)
}
​
// New creates a hertz instance without any default config.
func New(opts ...config.Option) *Hertz {
  options := config.NewOptions(opts)
  h := &Hertz{
    Engine: route.NewEngine(options),
  }
  return h
}

接著我們再進入config.NewOptions方法觀察這個Option切片將如何把我們自定義的內容應用到Hertz服務的初始化上去。

func NewOptions(opts []Option) *Options {
   options := &Options{
      KeepAliveTimeout: defaultKeepAliveTimeout,
      ReadTimeout: defaultReadTimeout,
      IdleTimeout: defaultReadTimeout,
      RedirectTrailingSlash: true,
      RedirectFixedPath: false,
      HandleMethodNotAllowed: false,
      UseRawPath: false,
      RemoveExtraSlash: false,
      UnescapePathValues: true,
      DisablePreParseMultipartForm: false,
      Network: defaultNetwork,
      Addr: defaultAddr,
      MaxRequestBodySize: defaultMaxRequestBodySize,
      MaxKeepBodySize: defaultMaxRequestBodySize,
      GetOnly: false,
      DisableKeepalive: false,
      StreamRequestBody: false,
      NoDefaultServerHeader: false,
      ExitWaitTimeout: defaultWaitExitTimeout,
      TLS: nil,
      ReadBufferSize: defaultReadBufferSize,
      ALPN: false,
      H2C: false,
      Tracers: []interface{}{},
      TraceLevel: new(interface{}),
      Registry: registry.NoopRegistry,
   }
   // 將自定義設定應用上去的方法
   options.Apply(opts)
   return options
}
​
func (o *Options) Apply(opts []Option) {
  for _, op := range opts {
    op.F(o)
  }
}

通過觀察config.NewOptions原始碼,它首先初始化了一個Options結構,這個結構存放了Hertz服務的各種初始化資訊,此時的Options的各個屬性都是預設固定的,直到呼叫了options.Apply(opts)方法,將自定義的設定應用上去。

並且應用上去的方式很特別,它將這個預設建立的Options結構的指標作為引數傳遞給每一個你宣告的Option的F方法,通過F方法的呼叫去為Options結構賦值,因為是指標,自然能將所有的賦值應用到同一個Options上去。

而具體的Option的F方法如何定義,則可以靈活實現,這也是Hertz擁有良好擴充套件性的原因之一。

// Default creates a hertz instance with default middlewares.
func Default(opts ...config.Option) *Hertz {
  // h是*Hertz型別,是框架的核心結構
   h := New(opts...)
   h.Use(recovery.Recovery())
​
   return h
}

此時注意到還有一個h.Use(recovery.Recovery())方法,寫法很像是gin框架的中介軟體使用方式。

// Recovery returns a middleware that recovers from any panic and writes a 500 if there was one.
func Recovery() app.HandlerFunc {
   return func(c context.Context, ctx *app.RequestContext) {
      defer func() {
         if err := recover(); err != nil {
            stack := stack(3)
​
            hlog.CtxErrorf(c, "[Recovery] %s panic recovered:\n%s\n%s\n",
               timeFormat(time.Now()), err, stack)
            ctx.AbortWithStatus(consts.StatusInternalServerError)
         }
      }()
      ctx.Next(c)
   }
}

通過閱讀註釋確實發現這是個中介軟體,用於從panic中recover。

register()

func main() {
   h := server.Default()
​
   register(h)
   h.Spin()
}

回到最初的main方法中,經過分析我們知道了Default方法大致完成了預設(自定義)Hertz結構的宣告,下面看一下register函數的內容

// register registers all routers.
func register(r *server.Hertz) {
​
   router.GeneratedRegister(r)
​
   customizedRegister(r)
}
​
// GeneratedRegister registers routers generated by IDL.
func GeneratedRegister(r *server.Hertz) {
  //INSERT_POINT: DO NOT DELETE THIS LINE!
}
​
// customizeRegister registers customize routers.
func customizedRegister(r *server.Hertz) {
  r.GET("/ping", handler.Ping)
​
  // your code ...
}

register(h)的工作是路由註冊(也就是介面的宣告),內部完成了兩種型別的註冊,GeneratedRegister()的註釋指出這部分路由是由IDL生成的,關於IDL先賣個關子,你只要知道IDL描述了介面互動的結構。

customizedRegister()則是用於註冊自定義的路由介面,並且初始化了一個你熟悉的/ping,當然也你可以在這裡註冊自己需要的路由,使用的方式也與gin很相似。

Spin()

最後分析一下main方法中的的第三部分,Spin方法。

// Spin runs the server until catching os.Signal or error returned by h.Run().
func (h *Hertz) Spin() {
   errCh := make(chan error)
   h.initOnRunHooks(errCh)
   go func() {
      // 核心方法
      errCh <- h.Run()
   }()
​
   signalWaiter := waitSignal
   if h.signalWaiter != nil {
      signalWaiter = h.signalWaiter
   }
​
   if err := signalWaiter(errCh); err != nil {
      hlog.Errorf("HERTZ: Receive close signal: error=%v", err)
      if err := h.Engine.Close(); err != nil {
         hlog.Errorf("HERTZ: Close error=%v", err)
      }
      return
   }
​
   hlog.Infof("HERTZ: Begin graceful shutdown, wait at most num=%d seconds...", h.GetOptions().ExitWaitTimeout/time.Second)
​
   ctx, cancel := context.WithTimeout(context.Background(), h.GetOptions().ExitWaitTimeout)
   defer cancel()
​
   if err := h.Shutdown(ctx); err != nil {
      hlog.Errorf("HERTZ: Shutdown error=%v", err)
   }
}

完成了一系列的初始化和宣告操作之後,Spin()負責觸發Hertz的執行,並且處理執行過程中的各種異常。其核心是errCh <- h.Run()

func (engine *Engine) Run() (err error) {
   if err = engine.Init(); err != nil {
      return err
   }
​
   if !atomic.CompareAndSwapUint32(&engine.status, statusInitialized, statusRunning) {
      return errAlreadyRunning
   }
   defer atomic.StoreUint32(&engine.status, statusClosed)
​
   // trigger hooks if any
   ctx := context.Background()
   for i := range engine.OnRun {
      if err = engine.OnRun[i](ctx); err != nil {
         return err
      }
   }
​
   return engine.listenAndServe()
}

再看到末尾的engine.listenAndServe()方法,這是一個介面,檢視其實現類,發現可以追溯到standard和netpoll兩個包。

作為一個HTTP服務,最重要的就是提供網路通訊互動能力,Hertz使用了可插拔的自研網路庫netpoll負責網路通訊,進一步優化了效能,這部分也將在後續的文章著重分析。

至此Hertz服務開始執行,你可以通過控制檯請求:

curl http://127.0.0.1:8888/ping
{"message":"pong"}% 

五、小結

使用hz工具生成最簡易的Hertz程式碼後,本文粗淺地分析了main方法的內容,將其分為三個部分,服務設定宣告Default()、路由註冊register()、HTTP服務啟動Spin()

雖然沒有提及Hertz框架架構圖當中的各種型別的package,但是其實處處有它們的身影,後續文章將以此文為基礎,深入分析框架的各個功能元件,揭開Hertz的神祕面紗。