助力位元組降本增效,大規模企業級 HTTP 框架 Hertz 設計實踐

語言: CN / TW / HK

日前,位元組跳動技術社群 ByteTech 舉辦的第七期位元組跳動技術沙龍圓滿落幕,本期沙龍以《位元組高效能開源微服務框架:CloudWeGo》為主題。在沙龍中,位元組跳動位元組跳動基礎架構服務框架資深研發工程師高文舉,跟大家分享了《大規模企業級 HTTP 框架的設計和實踐》,本文根據分享整理而成。

本文將從以下五個方面介紹 CloudWeGo 大規模企業級 HTTP 框架 Hertz:

  1. 位元組跳動內部 Go HTTP 框架的變遷;

  2. 企業級 HTTP 框架的設計考量和落地思路;

  3. Hertz 的核心特點;

  4. 未來規劃和挑戰;

  5. 總結。

位元組跳動內部 Go HTTP 框架的變遷

在正式開始介紹第一部分的內容之前,先給大家展示一組關鍵詞。2020 年初 Hertz 立項,2020 年 10 月,Hertz 釋出第一個可用版本2022 年 6 月,Hertz 正式開源。截至目前,Hertz 在位元組內部已經支撐超過 1.4 萬個業務服務日峰值 QPS 超過 5000 萬

Hertz 不僅支援業務服務,同時還會橫向支援位元組內部的各種基礎元件,包括但不限於位元組跳動服務網格控制面、公司級別壓測平臺以及 FaaS,還包括各種業務閘道器等等。Hertz 的高效能和極強的穩定性可以支撐業務複雜多變的場景。在公司內部 Hertz 接替了大量基於 Gin 框架開發的存量服務,大幅度降低了業務資源使用成本以及服務延時,助力公司層面的降本增效。

下面我們可以從 Hertz 出現的背景以及 Hertz 的設計目標和思路體會到,Hertz 的出現絕不是偶然。

基於 Gin 封裝

眾所周知,位元組內部使用 Golang 比較早,在大約 2014 年左右,公司就已經開始嘗試做一些 Golang 業務的轉型。2016 年,我們基於已開源的 Golang HTTP 框架 Gin 框架,封裝了 Ginex,這是 Ginex 剛開始出現的時期。

同時,2016 年還是一個開荒的時代,這個時期框架伴隨著業務快速野蠻地生長,我們的口號是“大力出奇跡”,把優先解決業務需求作為第一要務。Ginex 的迭代方式是業務側和框架側在同一個倉庫裡面共同維護和迭代。

問題顯現

2017 - 2019 年期間,也就是 Ginex 釋出之後,問題逐漸顯現。主要有以下幾點:

  • 迭代受開源專案限制

Ginex 是一個基於 Gin 的開源封裝,所以它本身在迭代方面是受到一些限制的。一旦有針對公司級的需求開發,以及 Bugfix 等等,我們都需要和開源框架 Gin 做聯合開發和維護,這個週期不能完全由我們自己控制。

  • 程式碼混亂膨脹、維護困難

由於我們和業務同學共同開發和維護 Ginex 框架,因此我們對於控制整個框架的走向沒有完全的自主權,從而導致了整體程式碼混亂膨脹,到後期我們發現越來越難維護。

  • 無法滿足效能敏感業務需求

另外,我們能用 Gin 做的效能優化非常少,因為 Gin 的底層是基於 Golang 的一個原生庫,所以如果我們要做優化,需要在原生庫的基礎上做很多改造,這個其實是非常困難的。

  • 無法滿足不同場景的功能需求

我們內部逐漸出現了一些新的場景,因此會有對 HTTP Client 的需求,支援 Websocket、支援 HTTP/2 以及支援 HTTP/3 等等需求,而在原生的 Ginex 上還是很難擴充套件的這些功能需求。

魔改開源框架

逐漸地,某些業務線開始做初步的嘗試,他們會對另外的一些開源框架進行魔改。比較典型的例子是有一些業務線嘗試基於 Fasthttp 進行魔改,Fasthttp 是一款主打高效能的開源框架,基於它進行魔改可以短期內幫助業務解決問題。這種魔改現象帶來的問題是,框架魔改是一些業務線自發的行為,各個業務線可能會基於自身業務特性進行各自維護,從而導致維護成本上升非常嚴重。

到這裡我們彷彿陷入了 Ginex 的怪圈。如前段時間爆火的電視劇《開端》一樣,我們彷彿是從一輛開往學院南路的 45 路公交車上醒來,發現自己要前往公司進行下一代 Ginex 框架的維護工作。

大家也可以思考一下,如果是你來應對這樣的場景,你會怎麼做呢?

小結

第一章節的內容總結如下:

  • 早期基於開源框架封裝

基於早期開源的 Golang HTTP 框架,實現了 Ginex 的封裝。

  • 隨著實踐發展,問題逐漸出現

框架混亂膨脹,框架的維護越來越困難,業務的新需求無法得到很好地滿足。

  • 為了解決問題出現基於另外的開源框架魔改的萌芽

我們需要思考如何跳出魔改的怪圈,把位元組內部的企業級框架做得更好。

另外,還有一個遺留問題,就是應該如何跳出這個魔改的怪圈呢?這個問題第二章節會為大家進行解答。

企業級 HTTP 框架的設計考量和落地思路

跳出怪圈

為了跳出魔改的怪圈,我們決定從以下三個方面開始著手。

  • 自主研發

既然 Ginex 是因為基於開源框架 Gin,沒法做一些靈活的控制,那我們就改為完全自主研發框架。自主研發框架的程式碼全鏈路自主可控,也可以避免引入任何三方不可控因素,這樣我們能夠對自己的框架有一個比較完備的掌控力。

  • 質量控制

下圖列舉了一些常規的質量控制手段。我要著重強調的是模糊測試,模糊測試在位元組內部是廣泛應用於 Hertz 框架的穩定性測試中。它的核心點在於通過一系列的模擬服務,嘗試模擬出線上使用者在使用我們的框架時,實際遇到的一些場景和使用方式。然後通過一些隨機的演算法,生成儘可能複雜、覆蓋各種 Case 的場景,這可以讓我們檢測出一些潛在的問題。這套測試也在 Hertz 早期的質量建設中,幫助我們將一些問題防患於未然。

  • 嚴格准入

既然 Ginex 的問題是大家都在向裡面寫入內容,那麼我們可以控制入口,建立一套完備的需求開發以及 Review 的閉環,控制迭代的整體流程,從而控制程式碼准入。同時我們配備統一的需求管理以及嚴格的發版准入規範,做一個標準的公司級別的框架。

舉一個比較形象的例子,如果我們把下一代框架比作一個人——“框架人”,自主研發表示這個“框架人”首先會擁有對自己身體的主導權,他不會受到來自於環境或者他人的影響;質量控制表示“框架人”能夠定期體檢,提早發現一些潛在的疾病,將其扼殺於搖籃;嚴格准入表示“框架人”有科學的飲食攝入和自律的生活習慣。可想而知,如果我們能夠做到以上三點,我們的“框架人”就能夠擁有一個健康的體魄。

痛點梳理

明確了應該如何跳出怪圈之後,我們還應該明確知道這個框架要具備哪些功能和特性,也就是首先應該聚焦到框架的核心痛點上。“框架人”不能只有健康的體魄,還應該擁有有趣的思想和靈魂。一個成熟的框架不僅僅要應對來自業務側的需求,如功能需求、效能需求和易用穩定等,還要考慮框架自身的發展,而這一點恰恰是我們在 Ginex 的迭代過程中忽略的。

如下圖右側金字塔所示,最上層是高效支撐,毋庸置疑框架的存在肯定是為了支撐我們的業務需求。中間層是一個質量保證的紅線框架,框架需要保證它自身的質量,只有以高質量完成的框架才能有自信承擔位元組內部的 5000 萬 QPS,以及各種各樣的使用場景。金字塔的最底層是長期、可持續性發展,這也是作為未來想要保持持續迭代的框架最重要的一點。

框架科學發展觀

基於上一部分,我們可以進一步梳理出框架的需求痛點。痛點主要有兩個方面:

  • 多樣的需求:支撐支撐各個業務線及基礎設施 (橫向擴充套件性)。

  • 靈活的結構:貫穿HTTP生命週期的掌控力 (縱向模組化)。

在此基礎上進一步抽象出框架的科學發展觀

  • 聚類需求:面向通用能力展開設計。

  • 跳出區域性:針對一些複雜問題,在更大範圍內尋求最優解。

後續我會針對這個科學發展觀進一步闡述 Hertz 究竟是如何實現的。

小結

第二章節的內容總結如下:

  • 跳出怪圈

引入“框架人”的概念,幫助大家理解框架的自研、質量控制和嚴格准入。

  • 痛點梳理

為“框架人”注入有趣的靈魂,框架需要應對來自業務側的多樣化需求,還要保證自己的可持續性發展。

  • 框架科學發展觀

需求聚類,跳出區域性。

Hertz 的核心特點

Hertz 框架是如何實現第二章節中提到的框架痛點和科學發展觀的呢?本章節將具體進行介紹。

分層抽象

首先介紹 Hertz 框架的架構設計。下圖是一個請求從建立、連線到完成的全過程。左側是客戶端,右側是服務端,在我們發起連結建立請求之後,連結建立完成;之後客戶端發起請求到服務端,服務端進行路由處理,然後將路由導向業務邏輯處理;業務邏輯處理完畢後,服務端返回這個請求,完成一次 HTTP 請求的呼叫。

那麼在這個過程中我們的框架到底做了哪些事情呢?從圖中不難發現,首先框架進行了連結處理,其次是協議處理,之後基於路由做了邏輯分發,即路由處理,最後做了業務邏輯處理。我們把框架做成一個結構之後會發現,這個結構包含的就是這四部分。

基於這個邏輯,我們可以看一下 Hertz 的整體架構圖。如下圖所示,從下往上看紅線框圈住的部分,可以發現這就是上文提到的請求建立的全過程。各層的能力及作用如下:

  • 傳輸層 Transport:抽象網路介面;

  • 協議層 Protocol:解析請求,渲染響應編碼;

  • 路由層 Route:基於URL進行邏輯分發;

  • 應用層 Application:業務直接互動,出現大量 API。

我們可以看到圖中除了中間部分包含的四層,左右兩側各有兩列。右側是通用層 Common,主要負責提供通用能力、常用的日誌介面、鏈路追蹤以及一些配置處理相關的能力等。左側是 Hertz 的程式碼生成工具 Hz,又稱腳手架工具,它可以幫助我們在內部基於 IDL 快速地生成專案骨架,以加速業務迭代。

Hertz 的分層設計是能夠和程式碼組織結構一一對映的。下圖是 Hertz 倉庫裡面的程式碼組織結構,可以看到根目錄下的 cmd 包裡面存放著 Hz 工具,在 pkg 包下存放著上述主要四層以及通用層 Common。因此同學們看到架構設計圖之後,可以直接在 Github 學習 Hertz 的程式碼。

Hertz: https://github.com/cloudwego/hertz

總體來說,Hertz 的架構設計理念就是 “簡潔有序,保證讓所有開發者輕鬆理解,在開發的過程中持續貫徹”

易用可擴充套件

那麼基於 Hertz 的架構設計,應該如何展開易用性和可擴充套件性呢?下圖是 Hertz 架構主要四個層級的抽象。

  • 應用層

應用層提供了一些通用能力,包括繫結請求、響應渲染、服務發現/註冊/負載均衡以及服務治理等等。其中,洋蔥模型中介軟體的核心目的是讓業務開發同學基於這個中介軟體快速地給業務邏輯進行擴充套件,擴充套件方式是可以在業務邏輯處理前和處理後分別插樁埋點做相應處理。一些比較有代表性的應用,包括日誌打點、前置的安全檢測,都是通過洋蔥模型中介軟體進行處理的。

  • 路由層

路由層也是非常通用的,主要提供靜態路由、引數路由、為路由配置優先順序以及路由修復的能力,如果我們的路由層沒辦法滿足使用者需求,它還能支撐使用者做自定義路由的擴充套件。但實際應用中這些路由能力完全能夠滿足絕大多數使用者的需求。

  • 協議層

Hertz 同時提供HTTP/1.1HTTP/2HTTP/3也是我們在建設中的能力,我們還會提供Websocket 等 HTTP 相關的多協議支援,以及支援完全由業務決定的自定義協議層擴充套件

  • 傳輸層

目前我們已經內建了兩個高效能的傳輸層實現。一個是基於 CloudWeGo 開源的高效能網路庫 Netpoll的傳輸層擴充套件,另一個是支援基於標準庫的傳輸層擴充套件。此外,我們也同樣能支援在傳輸層上進行自定義傳輸層協議擴充套件

下圖每一層中標紅的能力都能夠體現出,我們能夠在框架的任何一個分層上支撐使用者做最大程度的自由定製,這樣可以最大程度地滿足企業級內部使用者和潛在使用者的業務需求。如果同學們想要深入瞭解 Hertz,可以參考 CloudWeGo 官網的 Hertz 部分,上述所有內容均有具體描述。

官網:https://www.cloudwego.io/zh/docs/hertz/

效能探索

在效能方面,Hertz 又是如何在自主可控的範圍內做高效能探索的呢?

場景描述

熟悉 Hertz 程式碼的同學會發現,我們的HTTP/1.1協議借鑑了一些 Fasthttp 的優化思路和手段。HTTP/1.1 協議中的 Header 為不定長資料段,往往需要解析到最後一行,才能夠確定是否完成解析。同時,為了減少系統呼叫次數,提升整體解析效率,涉及 IO 操作時,我們通常引入帶 buffer 的 IO 資料結構。如下圖所示,它的核心點是最下層的 buffer,buffer 是一個類似於一塊完整的記憶體空間,我們可以將 IO 讀到的資料放進這個空間做暫存。

bufio.Reader 的問題

這樣做出現的問題是,原生的 bufio.Reader 長度是固定的,請求的 Header 大小超出 buffer 長度後,.Peek() 方法直接報錯 (ErrBufferFul),無法完成既定語義功能。

一些可能的解

對於上述問題,其實有一些可能的解決方法:

  • 直接利用 bufio.Reader 的侷限當做 Feature,通過 buffer 大小作為 Header 大小的限制。如果超出這個大小,Header 直接解析報錯,這也是 Fasthttp 的做法。但實際上超出 buffer 長度後報錯會導致我們沒辦法處理這部分請求,從而導致框架功能受限

  • header 解析帶狀態,暫存中間資料,通過在上層堆疊額外複雜度的方式突破 bufio 本身的限制。但是暫存中間態會涉及到一些記憶體的拷貝,必然會導致效能受限

真實使用環境複雜多變

位元組內部的使用場景非常多,我們不僅要支援各種業務線的開發,還要支援一些橫向的基礎元件。不同的業務,不同的場景,資料規模各異。如何成為通用且高效的地解決 bufio.Reader 的問題成為 Hertz 面臨的內部重要挑戰。我們既然已經站在 Fasthttp 這個“巨人”的肩膀上了,能否往前再走一步呢?

答案是肯定的。基於內部的使用場景,同時結合 Netpoll 的優勢,我們設計出了自適應 linked buffer,並且用它替代掉了原生的 bufio.Reader。從下圖可以看到,我們的 buffer 不再是一個固定長度的 buffer,而是一條鏈,這條鏈上的每一個 buffer 大小能夠根據線上真實請求進行動態擴縮容調整,同時搭配 Netpoll 中基於 LT 觸發的模型做資料預拷貝。從實施效果上來看,這個自適應調整能夠讓我們的業務方完全無感地支撐任何他們的業務特性。也是因為我們能夠將 buffer 進行動態擴縮容調整,從而能夠保證在協議層最大程度做到零拷貝協議解析,這能夠帶來整體解析上的效能提升,時延也會更低。

針對 HTTP/1.1 進行中的優化

因為目前在位元組內部 HTTP/1.1 還是一個比較主流的協議,所以我們基於 HTTP/1.1 做了很多嘗試。

首先是協議層探索。我們正在嘗試基於Header Passer 的重構,把解析 Header 的流程做得更高效。我們還嘗試了做一些傳輸層預解析,將一些比較固化的邏輯下沉到傳輸層做加速。

其次是傳輸層探索。這包括使用 writev 整合傳送 Header & Body達到減少系統呼叫次數的目的,以及通過新增介面整合 .Peek() + .Skip() 語義,在內部提供一個更高效的實現。

Hertz Benchmark

下圖是 Benchmark 的開源資料。左側第一張圖是在同等的機器環境上,Hertz 和橫向的框架 Gin、Fasthttp 極限 QPS 比較情況,藍線是 Hertz 處於較高極限 QPS 的狀態。第二張圖是 TP99 時延狀態,第三張圖是 TP999 時延狀態,可以看到 Hertz 的整體時延是處於一個更低的水平上。

位元組跳動服務網格控制面從 Gin 遷移至 Hertz

CloudWeGo 公眾號曾釋出關於位元組跳動服務網格控制面的文章,講述位元組跳動服務網格從 Gin 框架遷移到 Hertz 的落地實踐。下圖是他們程式碼展示的真實收益,從 Gin 框架替換成為 Hertz 框架後,CPU 流量從大概快到 4K 降到大約只有 2.5K,Goroutine 數量從 6w 降到不足 100 個,Goroutine 穩定性得到極大地提升。同時替換成 Hertz 後,框架相關的開銷已經基本消失,服務網格在線上穩定承載了超過 13M QPS 的流量

位元組跳動服務網格基於 Hertz 框架的實踐: https://mp.weixin.qq.com/s/koi9q_57Vk59YYtO9cyAFA

小結

第三章節的內容總結如下:

  • 分層抽象

解構 HTTP 框架,分層解耦。

  • 易用可擴充套件

提供了更豐富 API 和足夠靈活的拓展能力,在每一層抽象中都提供了一個足夠靈活的擴充套件能力應對可能的需求。

  • 自主可控的高效能探索

自適應 buffer,零拷貝解析,未來將會進行更多的高效能探索。

未來規劃和挑戰

我認為 Hertz 未來的發展規劃主要圍繞以下幾個方面:首先,打造泛 HTTP 框架。我們的最終目標是希望 Hertz 能夠解決在 HTTP 領域內的所有問題;其次,助力 CloudWeGo,希望 Hertz 能夠助力 CloudWeGo 打造一個企業級雲原生微服務矩陣;最後希望 Hertz 能夠持續服務更多的使用者

總結

本次分享的主要內容總結如下:

  • 位元組跳動內部 Go HTTP 框架的變遷:從基於開源封裝,到開啟自研之路;

  • 企業級 HTTP 框架的設計考量和落地思路:破圈、需求提煉、框架科學發展觀;

  • Hertz 核心特點:分層抽象、易用可擴充套件、自主可控的效能探索;

  • Hertz 未來的規劃和挑戰:框架持續打磨、助力 CloudWeGo、服務更多使用者。

最後歡迎對 Hertz 感興趣的同學積極參與到 CloudWeGo 社群中,我們一起完善 Hertz,共同建設 CloudWeGo!

以上內容整理自第七期位元組跳動技術沙龍《位元組高效能開源微服務框架:CloudWeGo》,獲取講師 PPT 和回放影片,請關注 CloudWeGo 公眾號,並在後臺回覆關鍵詞 “一週年”

專案地址

GitHub:https://github.com/cloudwego

官網:http://www.cloudwego.io