字節(jié)跳動開源 Go HTTP 框架 Hertz 設(shè)計實踐
前言
Hertz 是字節(jié)跳動服務(wù)框架團隊研發(fā)的超大規(guī)模的企業(yè)級微服務(wù) HTTP 框架,具有高易用性、易擴展、低時延等特點。在經(jīng)過了字節(jié)跳動內(nèi)部一年多的使用和迭代,如今已在 CloudWeGo 正式開源。目前,Hertz 已經(jīng)成為了字節(jié)跳動內(nèi)部最大的 HTTP 框架,線上接入的服務(wù)數(shù)量超過 1 萬,峰值 QPS 超過 4 千萬。除了各個業(yè)務(wù)線的同學(xué)使用外,也服務(wù)于內(nèi)部很多基礎(chǔ)組件,如:函數(shù)計算平臺 FaaS、壓測平臺、各類網(wǎng)關(guān)、Service Mesh 控制面等,均收到不錯的使用反饋。在如此大規(guī)模的場景下,Hertz 擁有極強的穩(wěn)定性和性能,在內(nèi)部實踐中某些典型服務(wù),如框架占比較高的服務(wù)、網(wǎng)關(guān)等服務(wù),遷移 Hertz 后相比 Gin 框架,資源使用顯著減少,CPU 使用率隨流量大小降低 30%-60%,時延也有明顯降低。
Hertz 堅持內(nèi)外維護一套代碼,為開源使用提供了強有力的保障。通過開源, Hertz 也將豐富云原生的 Golang 中間件體系,完善 CloudWeGo 生態(tài)矩陣,為更多開發(fā)者和企業(yè)搭建云原生化的大規(guī)模分布式系統(tǒng),提供一種現(xiàn)代的、資源高效的的技術(shù)方案。
本文將重點關(guān)注 Hertz 的架構(gòu)設(shè)計與功能特性。
項目緣起
最初,字節(jié)跳動內(nèi)部的 HTTP 框架是對 Gin 框架的封裝,具備不錯的易用性、生態(tài)完善等優(yōu)點。隨著內(nèi)部業(yè)務(wù)的不斷發(fā)展,高性能、多場景的需求日漸強烈。而 Gin 是對 Golang 原生 net/http 進行的二次開發(fā),在按需擴展和性能優(yōu)化上受到很大局限。因此,為了滿足業(yè)務(wù)需求,更好的服務(wù)各大業(yè)務(wù)線,2020 年初,字節(jié)跳動服務(wù)框架團隊經(jīng)過內(nèi)部使用場景和外部主流開源 HTTP 框架 Fasthttp、Gin、Echo 的調(diào)研后,開始基于自研網(wǎng)絡(luò)庫 Netpoll 開發(fā)內(nèi)部框架 Hertz,讓 Hertz 在面對企業(yè)級需求時,有更好的性能及穩(wěn)定性表現(xiàn),也能夠滿足業(yè)務(wù)發(fā)展和應(yīng)對不斷演進的技術(shù)需求。
架構(gòu)設(shè)計
Hertz 設(shè)計之初調(diào)研了大量業(yè)界優(yōu)秀的 HTTP 框架,同時參考了近年來內(nèi)部實踐中積累的經(jīng)驗。為了保證框架整體上滿足:1. 極致性能優(yōu)化的可能性;2. 面對未來不可控需求的擴展能力, Hertz 采用了 4 層分層設(shè)計,保證各個層級功能內(nèi)聚,同時通過層級之間的接口達到靈活擴展的目標。整體架構(gòu)圖如圖 1 所示。
圖 1:Hertz 架構(gòu)圖
Hertz 從上到下分為:應(yīng)用層、路由層、協(xié)議層和傳輸層,每一層各司其職,同時公共能力被統(tǒng)一抽象到公共層(common),做到跨層級復(fù)用。另外,同主庫一同發(fā)布的還有作為子模塊的 Hz 腳手架,它能夠協(xié)助使用者快速搭建出項目核心骨架以及提供實用的構(gòu)建工具鏈。
應(yīng)用層
應(yīng)用層是和用戶直接交互的一層,提供豐富易用的 API,主要包括 Server、Client 和一些其他通用抽象。Server 提供了注冊 HandlerFunc、Binding、Rendering 等能力;Client 提供了調(diào)用下游和服務(wù)發(fā)現(xiàn)等能力;以及抽象一個 HTTP 請求所必須涉及到的請求(request)、響應(yīng)(Response)、上下文(RequestContext)、中間件(Middleware)等等。Hertz 的 Server 和 Client 都能夠提供中間件這樣的擴展能力。
應(yīng)用層中一個非常重要的抽象就是對 Server HandlerFunc 的抽象。早期,Hertz 路由的處理函數(shù) (HandlerFunc)中并沒有接收標準的 context.Context,我們在大量的實踐過程中發(fā)現(xiàn),業(yè)務(wù)方通常需要一個標準的上下文在 RPC Client 或者日志、Tracing 等組件間傳遞,但由于請求上下文(RequestContext)生命周期局限于一次 HTTP 請求之內(nèi),而以上提到的場景往往存在異步的傳遞和處理,導(dǎo)致如果直接傳遞請求上下文,會導(dǎo)致出現(xiàn)一些數(shù)據(jù)不一致的問題。為此我們做了諸多嘗試,但是因為核心原因在于請求上下文(RequestContext)的生命周期無法優(yōu)雅的按需延長,最終在各種設(shè)計權(quán)衡下,我們在路由的處理函數(shù)簽名中增加一個標準的上下文入?yún)?,通過分離出生命周期長短各異的兩個上下文的方式,從根本上解決各種因為上下文生命周期不一致導(dǎo)致的異常問題,即:
type HandlerFunc func(c context.Context, ctx *app.RequestContext)
路由層
路由層負責根據(jù) URI 匹配對應(yīng)的處理函數(shù)。
起初,Hertz 的路由基于 httprouter 開發(fā),但隨著使用的用戶越來越多,httprouter 漸漸不能夠滿足需求,主要體現(xiàn)在 httprouter 不能夠同時注冊靜態(tài)路由和參數(shù)路由,即 /a/b,/:c/d 這兩個路由不能夠同時注冊;甚至有一些更特殊的需求,如/a/b、/:c/b ,當匹配 /a/b 路由時,兩個路由都能夠匹配上。
Hertz 為滿足這些需求重新構(gòu)造了路由樹,用戶在注冊路由時擁有很高的自由度:支持靜態(tài)路由、參數(shù)路由的注冊;支持按優(yōu)先級匹配,如上述例子會優(yōu)先匹配靜態(tài)路由 /a/b ;支持路由回溯,如注冊 /a/b、/:c/d,當匹配 /a/d 時仍然能夠匹配上;支持尾斜線重定向,如注冊 /a/b,當匹配 /a/b/ 時能夠重定向到 /a/b 上。Hertz 提供了豐富的路由能力來滿足用戶的需求,更多的功能可以參考 Hertz 配置文檔。
協(xié)議層
協(xié)議層負責不同協(xié)議的實現(xiàn)和擴展。
Hertz 支持協(xié)議的擴展,用戶只需要實現(xiàn)下面的接口便可以按照自己的需求在引擎(Engine) 上擴展協(xié)議,同時也支持通過 ALPN 協(xié)議協(xié)商的方式注冊。Hertz 首批只開源了 HTTP1 實現(xiàn),未來會陸續(xù)開源 HTTP2、QUIC 等實現(xiàn)。協(xié)議層擴展提供的靈活性甚至可以超越 HTTP 協(xié)議的范疇,用戶完全可以按需注冊任意符合自身需求的協(xié)議層實現(xiàn),并且加入到 Hertz 的引擎中來,同時,也能夠無縫享受到傳輸層帶來的極致性能。
type ServerFactory interface { New(core Core) (server protocol.Server, err error)}type Server interface { Serve(c context.Context, conn network.Conn) error}
傳輸層
傳輸層負責底層的網(wǎng)絡(luò)庫的抽象和實現(xiàn)。
Hertz 支持底層網(wǎng)絡(luò)庫的擴展。Hertz 原生完美適配 Netpoll,在時延方面有很多深度的優(yōu)化,非常適合時延敏感的業(yè)務(wù)接入。Netpoll 對 TLS 能力的支持有待完善,而 TLS 能力又是 HTTP 框架必備能力,為此 Hertz 底層同時支持基于 Golang 標準網(wǎng)絡(luò)庫的實現(xiàn)適配,支持網(wǎng)絡(luò)庫的一鍵切換,用戶可根據(jù)自己的需求選擇合適的網(wǎng)絡(luò)庫進行替換。如果用戶有更加高效的網(wǎng)絡(luò)庫或其他網(wǎng)絡(luò)庫需求,也完全可以根據(jù)需求自行擴展。
Hz 腳手架
與 Hertz 一并開源的還有一個易用的命令行工具 Hz,用戶只需提供一個 IDL,根據(jù)定義好的接口信息,Hz 便可以一鍵生成項目腳手架,讓 Hertz 達到開箱即用的狀態(tài);Hz 也支持基于 IDL 的更新能力,能夠基于 IDL 變動智能地更新項目代碼。目前 Hz 支持了 Thrift 和 Protobuf 兩種 IDL 定義。命令行工具內(nèi)置豐富的選項,可以根據(jù)自己的需求使用。同時它底層依賴 Protobuf 官方的編譯器和自研的 Thriftgo 的編譯器,兩者都支持自定義的生成代碼插件。如果默認模板不能夠滿足需求,完全能夠按需定義。
未來,我們將繼續(xù)迭代 Hz,持續(xù)集成各種常用的中間件,提供更高層面的模塊化構(gòu)建能力。給 Hertz 的用戶提供按需調(diào)整的能力,通過靈活的自定義配置打造一套滿足自身開發(fā)需求的腳手架。
Common 組件
Common 組件主要存放一些公共的能力,比如錯誤處理、單元測試能力、可觀測性相關(guān)能力(Log、Trace、Metrics 等)。對于服務(wù)可觀測性的能力,Hertz 提供了默認的實現(xiàn),用戶可以按需裝配;如果用戶有特殊的需求,也可以通過 Hertz 提供的接口注入。比如對于 Trace 能力,Hertz 提供了默認的實現(xiàn),也提供了將 Hertz 和 Kitex 串起來的 Example。如果想注入自己的實現(xiàn),也可以實現(xiàn)下面的接口:
// Tracer is executed at the start and finish of an HTTP.type Tracer interface { Start(ctx context.Context, c *app.RequestContext) context.Context Finish(ctx context.Context, c *app.RequestContext)}
功能特性
中間件
Hertz 除了提供 Server 的中間件能力,還提供了 Client 中間件能力。用戶可以使用中間件能力將通用邏輯(如:日志記錄、性能統(tǒng)計、異常處理、鑒權(quán)邏輯等等)和業(yè)務(wù)邏輯區(qū)分開,讓用戶更加專注于業(yè)務(wù)代碼。Server 和 Client 中間件使用方式相同,使用 Use 方法注冊中間件,中間件執(zhí)行順序和注冊順序相同,同時支持預(yù)處理和后處理邏輯。
Server 和 Client 的中間件實現(xiàn)方式并不相同。對于 Server 來說,我們希望減少棧的深度,同時也希望中間件能夠默認的執(zhí)行下一個,用戶需要手動終止中間件的執(zhí)行。因此,我們將 Server 的中間件分成了兩種類型,即不在同一個函數(shù)調(diào)用棧(該中間件調(diào)用完后返回,由上一個中間件調(diào)用下一個中間件,如圖 2 中 B 和 C)和在同一個函數(shù)調(diào)用棧的中間件(該中間件調(diào)用完后由該中間件繼續(xù)調(diào)用下一個中間件,如圖 2 中 C 和 Business Handler)。
圖 2: 中間件鏈路
其核心是需要一個地方存下當前的調(diào)用位置 index,并始終保持其遞增。恰好 RequestContext 就是一個存儲 index 合適的位置。但是對于 Client,由于沒有合適的地方存儲 index,我們只能退而求其次,拋棄 index 的實現(xiàn),將所有的中間件構(gòu)造在同一調(diào)用鏈上,需要用戶手動調(diào)用下一個中間件。
流式處理
Hertz 提供 Server 和 Client 的流式處理能力。HTTP 的文件場景是十分常見的場景,除了 Server 側(cè)的上傳場景之外,Client 的下載場景也十分常見。為此,Hertz 支持了 Server 和 Client 的流式處理。在內(nèi)部網(wǎng)關(guān)場景中,從 Gin 遷移到 Hertz 后,cpu 使用量隨流量大小不同可節(jié)省 30%-60% 不等,服務(wù)壓力越大,收益越大。Hertz 開啟流式功能的方式也很容易,只需要在 Server 上或 Client 上添加一個配置即可,可參考 CloudWeGo 官網(wǎng) Hertz 文檔的流式處理部分。
由于 Netpoll 采用 LT 的觸發(fā)模式,由網(wǎng)絡(luò)庫主動將將數(shù)據(jù)從 TCP 緩沖區(qū)讀到用戶態(tài),并存儲到 buffer 中,否則 epoll 事件會持續(xù)觸發(fā)。因此 Server 在超大請求的場景下,由于 Netpoll 持續(xù)將數(shù)據(jù)讀到用戶態(tài)內(nèi)存中,可能會有 OOM 的風險。HTTP 文件上傳場景就是一個典型的場景,但 HTTP 上傳服務(wù)又是很常見的場景,因此我們支持標準網(wǎng)絡(luò)庫 go net,并針對 Hertz 做了特殊優(yōu)化,暴露出 Read() 接口,防止 OOM 發(fā)生。
對于 Client,情況并不相同。流式場景下會將連接封裝成 Reader 暴露給用戶,而 Client 有連接池管理,那這樣連接就多了一種狀態(tài),何時關(guān)連接,何時復(fù)用連接成了一個問題。由于框架側(cè)并不知道該連接何時會用完,框架側(cè)復(fù)用該連接不現(xiàn)實,會導(dǎo)致串包問題。由于 GC 會關(guān)閉連接,因此我們起初設(shè)想流式場景下的連接交由用戶后,由 GC 負責關(guān)閉,這樣也不會導(dǎo)致資源泄漏。但是在測試后發(fā)現(xiàn),由于 GC 存在一定時間間隔,另外 TCP 中主動關(guān)閉連接的一方需要等待 2RTT,在高并發(fā)場景下會導(dǎo)致 fd 被打滿的情況。最終我們提供了復(fù)用連接的接口,對于性能有場要求用戶,在使用完連接后可以將連接重新放入連接池中復(fù)用。
性能表現(xiàn)
Hertz 使用字節(jié)跳動自研高性能網(wǎng)絡(luò)庫 Netpoll,在提高網(wǎng)絡(luò)庫效率方面有諸多實踐,參考已發(fā)布文章字節(jié)跳動在 Go 網(wǎng)絡(luò)庫上的實踐。除此之外,Netpoll 還針對 HTTP 場景進行優(yōu)化,通過減少拷貝和系統(tǒng)調(diào)用次數(shù)提高吞吐以及降低時延。為了衡量 Hertz 性能指標,我們選取了社區(qū)中有代表性的框架 Gin(net/http)和 Fasthttp 作為對比,如圖 3 所示??梢钥吹剑琀ertz 的極限吞吐、TP99 等指標均處于業(yè)界領(lǐng)先水平。未來,Hertz 還將繼續(xù)和 Netpoll 深度配合,探索 HTTP 框架性能的極限。
圖 3:Hertz 和其他框架性能對比
一個 Demo
下面簡單演示一下 Hertz 是如何開發(fā)一個服務(wù)的。
- 首先,定義 IDL,這里使用 Thrift 作為 IDL 的定義(也支持使用 Protobuf 定義的 IDL),編寫一個名為 Demo 的 service。這個服務(wù)有一個 API: Hello,它的請求參數(shù)是一個 query,響應(yīng)是一個包含一個 RespBody 字段的 Json。
// idl/hello.thriftnamespace go hello.examplestruct HelloReq { 1: string Name (api.query="name");}struct HelloResp { 1: string RespBody;}service HelloService { HelloResp Hello(1: HelloReq request) (api.get="/hello");}
- 接下來我們使用 hz 生成代碼,并整理和拉取依賴
$ hz new -idl idl/hello.thrift -mod Demo$ go mod tidy && go mod verify
- 填充業(yè)務(wù)邏輯,比如我們返回 hello, ${Name},那我們在biz/handler/example/hello_service.go 中添加以下代碼即可
// Hello .// @router /hello [GET]func Hello(ctx context.Context, c *app.RequestContext) { var err error var req example.HelloReq err = c.BindAndValidate(&req) if err != nil { c.String(400, err.Error()) return } resp := new(example.HelloResp) resp.RespBody = "hello, " req.Name c.JSON(200, resp)}
- 編譯并運行項目
$ go build$ ./Demo
到現(xiàn)在一個簡單的 Hertz 項目已經(jīng)生成,下面我們來測試一下
$ curl http://localhost:8888/hello?name=Xiaoming// 如果看到以下返回說明服務(wù)已經(jīng)正常啟動起來啦$ {"RespBody":"hello, Xiaoming"}
(以上 demo 可以在 hertz-examples 中查看) 之后就可以愉快的構(gòu)建自己的項目了。
后記
希望以上的分享能夠讓大家對 Hertz 有一個整體上的認識。同時,我們也在不斷地迭代 Hertz、完善 CloudWeGo 整體生態(tài)。歡迎各位感興趣的同學(xué)們加入我們,共同建設(shè) CloudWeGo。
參考資料
- Hertz: https://github.com/cloudwego/hertz
- Hertz Doc: https://www.cloudwego.io/zh/docs/hertz/
- 字節(jié)跳動在 Go 網(wǎng)絡(luò)庫上的實踐: https://www.cloudwego.io/zh/blog/2021/10/09/字節(jié)跳動在-go-網(wǎng)絡(luò)庫上的實踐/