位元組微服務HTTP框架Hertz使用與源碼分析|擁抱開源
一、前言
Hertz[həːts] 是一個 Golang 微服務 HTTP 框架,在設計之初參考了其他開源框架 fasthttp、gin、echo 的優勢, 並結合位元組跳動內部的需求,使其具有高易用性、高性能、高擴展性等特點,目前在位元組跳動內部已廣泛使用。 如今越來越多的微服務選擇使用 Golang,如果對微服務性能有要求,又希望框架能夠充分滿足內部的可定製化需求,Hertz 會是一個不錯的選擇。
對於源碼該如何閱讀,本身就值得思考。這篇文章我將以第一次閱讀Hertz源碼的視角,分享自己的思考過程,也藉此梳理一下自己閱讀源碼的方法論。
接下來需要你對應打開Hertz的官方文檔,以及在本地克隆Hertz的代碼倉庫,我們開始吧。
Hertz倉庫地址://github.com/cloudwego/hertz
Hertz文檔地址://www.cloudwego.io/zh/docs/hertz/getting-started/
二、架構設計
這是一張Hertz官方文檔的架構設計圖,圖中的一個個組件對應hertz
源碼包內的一個個package
文件夾,實現了對應的功能,如下:
三、快速開始
接下來按照文檔的指示,通過hertz
的命令行工具初始化一個最簡單的hertz
項目,先觀其形,再會其意。
對應文檔地址://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 //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()
}
回想剛剛這個 //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 //127.0.0.1:8888/ping
{"message":"pong"}%
五、小結
使用hz工具生成最簡易的Hertz代碼後,本文粗淺地分析了main方法的內容,將其分為三個部分,服務配置聲明Default()
、路由註冊register()
、HTTP服務啟動Spin()
。
雖然沒有提及Hertz框架架構圖當中的各種類型的package,但是其實處處有它們的身影,後續文章將以此文為基礎,深入分析框架的各個功能組件,揭開Hertz的神秘面紗。