DUICUO

ByteDanceのオープンソースGo HTTPフレームワークHertzの設計手法

序文

Hertzは、ByteDanceのサービスフレームワークチームによって開発された、エンタープライズグレードの大規模マイクロサービスHTTPフレームワークです。高いユーザビリティ、スケーラビリティ、低レイテンシを特徴としています。ByteDance社内で1年以上の社内運用と反復開発を経て、CloudWeGoで正式にオープンソース化されました。現在、HertzはByteDance最大の社内HTTPフレームワークとなり、1万以上のサービスがオンライン統合され、ピークQPSは4,000万を超えています。様々な事業部門で利用されているだけでなく、Function Computeプラットフォーム(FaaS)、負荷テストプラットフォーム、各種ゲートウェイ、Service Meshコントロールプレーンなど、多くの社内基盤コンポーネントにも利用されており、高い評価を得ています。このような大規模なシナリオにおいて、Hertzは卓越した安定性とパフォーマンスを発揮します。社内での実践では、フレームワークの利用率が高いゲートウェイなど、特定の典型的なサービスにおいて、Hertzへの移行により、Ginフレームワークと比較してリソース使用量が大幅に削減されました。CPU使用率はトラフィック量に応じて30%~60%減少し、レイテンシも大幅に減少しました。

Hertzは社内外で単一のコードベースを維持し、オープンソースの利用を強力にサポートしています。オープンソース化を通じて、HertzはクラウドネイティブのGo言語ミドルウェアシステムの強化、CloudWeGoエコシステムの改善、そしてより多くの開発者や企業が大規模なクラウドネイティブ分散システムを構築するための、最新かつリソース効率の高い技術ソリューションを提供していきます。

この記事では、Hertz のアーキテクチャと機能に焦点を当てます。

プロジェクトの起源

当初、ByteDanceの社内HTTPフレームワークはGinフレームワークのラッパーであり、使いやすさや充実したエコシステムなどのメリットを誇っていました。しかし、社内業務の継続的な発展に伴い、高性能とマルチシナリオ機能への需要が高まりました。Go言語ネイティブのnet/httpの二次開発であるGinは、オンデマンド拡張とパフォーマンス最適化の点で大きな制限がありました。そのため、ビジネスニーズを満たし、さまざまな業務ラインにさらに良くサービスを提供するために、2020年初頭、ByteDanceのサービスフレームワークチームは、社内の使用シナリオとFasthttp、Gin、Echoなどの主流の社外オープンソースHTTPフレームワークを調査した後、自社開発のネットワークライブラリNetpollをベースにした社内フレームワークHertzの開発を開始しました。これにより、Hertzはエンタープライズレベルの要件に直面したときに、より優れたパフォーマンスと安定性を提供すると同時に、ビジネス開発のニーズを満たし、進化するテクノロジーに適応できるようになりました。

建築デザイン

Hertzは、初期設計段階において、業界における数多くの優れたHTTPフレームワークを徹底的に調査するとともに、近年の社内実践を通じて蓄積された経験も活用しました。フレームワーク全体が以下の要件を満たすようにするため、Hertzは4層アーキテクチャを採用し、各層における機能の一貫性を確保しつつ、層間インターフェースを通じて柔軟な拡張性を実現しています。図1に、全体的なアーキテクチャ図を示します。

図1: Hertzアーキテクチャ図

Hertzは、上から順にアプリケーション層、ルーティング層、プロトコル層、トランスポート層の4つの層に分かれています。各層はそれぞれ固有の機能を持ち、共通機能は共通層に抽象化されており、層をまたいだ再利用が可能です。さらに、メインリポジトリと同時にリリースされるサブモジュールであるHzスキャフォールディングは、ユーザーがプロジェクトのコアフレームワークを迅速に構築するのを支援し、実用的なビルドツールチェーンを提供します。

アプリケーション層

アプリケーション層は、ユーザーと直接やり取りする層であり、豊富で使いやすいAPIを提供します。主にサーバー、クライアント、その他の一般的な抽象化が含まれます。サーバーは、HandlerFuncsの登録、バインディング、レンダリングなどの機能を提供します。クライアントは、ダウンストリーム呼び出しやサービス検出などの機能を提供し、リクエスト、レスポンス、コンテキスト(RequestContext)、ミドルウェア、そしてHTTPリクエストに必要なその他の要素を抽象化します。Hertzのサーバーとクライアントはどちらも、ミドルウェアのような拡張機能を提供できます。

アプリケーション層における重要な抽象化は、`ServerHandlerFunc` の抽象化です。当初、Hertz ルートハンドラー (`HandlerFunc`) は標準の `context.Context` を受け入れませんでした。広範な実践を通して、ビジネスアプリケーションでは通常、RPC クライアント間やログ記録やトレースなどのコンポーネント間の受け渡しに標準のコンテキストが必要であることがわかりました。しかし、`RequestContext` のライフサイクルは単一の HTTP リクエストに限定されており、前述のシナリオでは非同期の送信と処理が頻繁に発生するため、`RequestContext` を直接渡すとデータの不整合が発生する可能性があります。この問題に対処するために何度も試みましたが、根本的な問題は、`RequestContext` のライフサイクルを必要に応じて適切に延長できないことでした。最終的に、様々な設計上のトレードオフを経て、ルートハンドラーのシグネチャに標準のコンテキストパラメータを追加しました。異なるライフサイクルを持つ 2 つのコンテキストを分離することで、コンテキストライフサイクルの不整合によって引き起こされるさまざまな例外を根本的に解決しました。

型HandlerFunc func(c context.Context, ctx *app.RequestContext)

ルーティング層

ルーティング層は、URI に基づいて対応する処理機能を一致させる役割を担います。

当初、Hertzのルーティングはhttprouterをベースに開発されていましたが、ユーザー数が増加するにつれて、httprouterは徐々にニーズを満たせなくなってきました。これは主に、httprouterが静的ルートとパラメータルートを同時に登録できないことに起因していました。つまり、/a/bと/:c/dのルートを同時に登録することはできませんでした。/a/bと/:c/bのようなより特殊な要件であっても、/a/bルートにマッチさせると、両方のルートがマッチしてしまう可能性があります。

Hertz はこれらのニーズを満たすためにルートツリーを再構築し、ルート登録時に高い自由度を実現しました。静的ルートとパラメータ化ルートの登録、上記の例で静的ルート /a/b を優先するといった優先度マッチング、/a/b と /:c/d を登録するといったルートバックトラッキング(/a/d に一致する)や、/a/b を登録するといった末尾のスラッシュによるリダイレクト(/a/b/ に一致する場合は /a/b にリダイレクト)など、Hertz はユーザーのニーズを満たす豊富なルーティング機能を提供しています。その他の機能については、Hertz の設定ドキュメントをご覧ください。

プロトコル層

プロトコル層は、さまざまなプロトコルの実装と拡張を担当します。

Hertzはプロトコル拡張をサポートしています。ユーザーは、以下のインターフェースを実装するだけで、ニーズに合わせてエンジン上でプロトコルを拡張できます。ALPNプロトコルネゴシエーションによる登録もサポートされています。Hertzは当初HTTP/1実装のみをオープンソース化していましたが、今後はHTTP/2やQUICなどの実装も段階的にオープンソース化していく予定です。プロトコル層拡張によって得られる柔軟性は、HTTPプロトコルの範疇を超えることさえあります。ユーザーは、ニーズを満たす任意のプロトコル層実装を登録し、Hertzエンジンに追加することで、トランスポート層がもたらす究極のパフォーマンスをシームレスに享受できます。

型 ServerFactory インターフェース {
新規(コア コア) (サーバー プロトコル.サーバー、エラー エラー)
}

タイプサーバーインターフェース{
Serve(c context.Context, conn network.Conn) エラー
}

トランスポート層

トランスポート層は、基盤となるネットワーク ライブラリの抽象化と実装を担当します。

Hertzは、基盤となるネットワークライブラリの拡張をサポートしています。HertzはNetpollとネイティブかつ完全に互換性があり、大幅なレイテンシ最適化を提供するため、レイテンシに敏感なアプリケーションに最適です。NetpollのTLS機能サポートはまだ開発中であり、TLSはHTTPフレームワークの必須機能ですが、Hertzは標準のGolangネットワークライブラリに基づく実装もサポートしており、ライブラリ間のワンクリック切り替えが可能です。ユーザーは適切なライブラリを選択して既存のライブラリを置き換えることができます。さらに、より効率的なネットワークライブラリやその他の要件に合わせて、必要に応じてライブラリを拡張することもできます。

Hz足場

Hertzと並んで、Hzと呼ばれるユーザーフレンドリーなコマンドラインツールもオープンソース化されました。ユーザーはIDL(独立定義言語)を入力するだけで、Hzは定義済みのインターフェース情報に基づいてワンクリックでプロジェクトのスキャフォールディングを生成し、Hertzをすぐに使用できるようになります。HzはIDLベースのアップデートもサポートしており、IDLの変更に基づいてプロジェクトコードをインテリジェントに更新します。現在、HzはThriftとProtobufの両方のIDL定義をサポートしています。このコマンドラインツールには、個々のニーズに合わせて使用​​できる豊富な組み込みオプションがあります。また、公式のProtobufコンパイラと独自のThriftgoコンパイラも利用しており、どちらもカス​​タムコード生成プラグインをサポートしています。デフォルトのテンプレートが要件を満たさない場合は、必要に応じて定義できます。

今後もHzのイテレーションを継続し、様々な一般的に使用されるミドルウェアを継続的に統合し、より高度なモジュール構築機能を提供していきます。これにより、Hertzユーザーはオンデマンドでカスタマイズできるようになり、柔軟なカスタム構成を通じて、独自の開発ニーズを満たすスキャフォールドを構築できるようになります。

共通コンポーネント

Commonコンポーネントは、主にエラー処理、ユニットテスト、可観測性関連機能(ログ、トレース、メトリクスなど)といった共通機能を格納します。サービス可観測性機能については、Hertzはデフォルトの実装を提供しており、ユーザーは必要に応じて設定できます。また、特定の要件を持つユーザーは、Hertzが提供するインターフェースを介してこれらの機能を注入することもできます。例えば、Trace機能については、Hertzはデフォルトの実装に加え、HertzとKitexを連携させる例も提供しています。カスタム実装を注入するには、次のインターフェースを実装します。

 // トレーサーは HTTP の開始時と終了時に実行されます。
型Tracerインターフェース{
Start(ctx context.Context, c *app.RequestContext) コンテキスト.Context
Finish(ctx context.Context, c *app.RequestContext)
}

特徴

ミドルウェア

Hertzは、サーバー向けのミドルウェア機能に加え、クライアント向けのミドルウェア機能も提供しています。ミドルウェアを使用することで、ログ記録、パフォーマンス統計、例外処理、認証ロジックなどの一般的なロジックをビジネスロジックから分離し、ビジネスコードに集中することができます。サーバーとクライアントのミドルウェアは、`Use`メソッドを使用して登録することで、同じように使用できます。ミドルウェアの実行順序は登録順序と同じで、前処理ロジックと後処理ロジックの両方がサポートされています。

サーバーとクライアントのミドルウェア実装は異なります。サーバーでは、スタックの深さを削減し、ミドルウェアがデフォルトで次のミドルウェアを実行するようにし、ユーザーがミドルウェアの実行を手動で終了できるようにしたいと考えています。そのため、サーバーのミドルウェアを2種類に分割します。1つは、同じ関数呼び出しスタック上にないミドルウェア(図2のBとCに示すように、ミドルウェアは呼び出し後に制御を戻し、前のミドルウェアが次のミドルウェアを呼び出す)で、もう1つは、同じ関数呼び出しスタック上にあるミドルウェア(図2のCとビジネスハンドラーに示すように、ミドルウェアは呼び出し後に次のミドルウェアを呼び出し続けます)です。

図2: ミドルウェアリンク

基本的な要件は、現在の呼び出し位置のインデックスを保存し、それをインクリメントし続けるための場所を持つことです。RequestContext は、インデックスを保存するのに適した場所です。しかし、クライアント側ではインデックスを保存するのに適した場所がないため、インデックスの実装を放棄し、すべてのミドルウェアを同じ呼び出しチェーン上に構築するしかありません。その場合、ユーザーは次のミドルウェアを手動で呼び出す必要があります。

ストリーミング

Hertzは、サーバーとクライアントの両方にストリーミング機能を提供します。HTTPファイル転送は非常に一般的なシナリオであり、サーバー側でのアップロードとクライアント側でのダウンロードが同程度に普及しています。Hertzはサーバーとクライアントの両方でストリーミングをサポートしています。内部ゲートウェイのシナリオでは、GinからHertzに移行すると、トラフィック量に応じてCPU使用率を30%~60%削減できます。サービスの負荷が高いほど、メリットは大きくなります。Hertzでストリーミング機能を有効にするのは簡単で、サーバーまたはクライアントのいずれかに設定を追加するだけです。CloudWeGoウェブサイトにあるHertzドキュメントのストリーミングセクションを参照してください。

Netpollは最小時間(LT)トリガーモードを使用するため、ネットワークライブラリはTCPバッファからユーザー空間にデータを能動的に読み取り、バッファに保存します。そうでない場合、epollイベントは継続的にトリガーされます。そのため、リクエスト量が非常に多いシナリオでは、Netpollによるユーザー空間メモリへのデータの継続的な読み取りにより、OutOfMemoryError(OOM)が発生する可能性があります。HTTPファイルアップロードは典型的な例ですが、HTTPアップロードサービスは非常に一般的であるため、標準ネットワークライブラリ「go net」をサポートし、Hertz向けに特別な最適化を施して「Read()」インターフェースを公開することでOOMを回避しています。

クライアントの場合は状況が異なります。ストリーミングシナリオでは、接続はリーダーとしてカプセル化され、ユーザーに公開されます。しかし、クライアントは接続プールを持つため、接続に余分な状態が追加され、いつ接続を閉じて再利用するかという問題が生じます。フレームワークは接続がいつ使い果たされるかを把握できないため、接続の再利用は現実的ではなく、パケットスニッフィングにつながる可能性があります。ガベージコレクター(GC)が接続を閉じるため、当初の計画では、ストリーミングシナリオで接続がユーザーに引き渡された後にGCが接続を閉じ、リソースリークを回避する予定でした。しかし、テストの結果、GCの時間間隔と、TCPで接続をアクティブに閉じるために必要な2 RTTの待機時間により、高並列シナリオではファイル記述子(fds)がいっぱいになる可能性があることが判明しました。最終的に、接続再利用インターフェースを提供しました。パフォーマンス要件が厳しいユーザー向けに、使用済みの接続を接続プールに戻し、再利用できるようにしました。

パフォーマンス

Hertzは、ByteDanceが自社開発した高性能ネットワークライブラリであるNetpollを利用しています。Netpollは、弊社の公開記事「ByteDanceのGoネットワークライブラリの実践」で詳述されているように、ネットワークライブラリの効率向上において豊富な経験を有しています。さらに、NetpollはHTTPシナリオに最適化されており、コピーとシステムコールを最小限に抑えることでスループットを向上させ、レイテンシを削減します。Hertzのパフォーマンス指標を測定するために、図3に示すように、コミュニティの代表的なフレームワークであるGin(net/http)およびFasthttpと比較しました。Hertzの最大スループットとTP99は、業界をリードする指標の一つであることがわかります。Hertzは今後もNetpollと緊密に連携し、HTTPフレームワークのパフォーマンス限界を探っていきます。

図3: Hertzと他のフレームワークのパフォーマンス比較

デモ

以下は、Hertz がサービスを開発する方法の簡単なデモンストレーションです。

  1. まず、IDLを定義します。ここでは、IDL定義としてThriftを使用します(Protobufで定義されたIDLもサポートされています)。Demoという名前のサービスを作成します。このサービスのAPIはHelloです。リクエストパラメータはクエリで、レスポンスはRespBodyフィールドを含むJSONファイルです。
 // idl/hello.thrift
名前空間 go hello.example

構造体HelloReq{
1: 文字列名 (api.query="name");
}

構造体HelloResp{
1: 文字列 RespBody;
}

サービス HelloService {
HelloResp Hello(1: ​​HelloReq リクエスト) (api.get="/hello");
}
  1. 次に、hz を使用してコードを生成し、依存関係を整理して取得します。
 $ hz new -idl idl/hello.thrift -mod デモ
$ go mod tidy && go mod verify
  1. ビジネス ロジックを入力するには、たとえば、「hello」と `${Name}` を返す場合は、次のコードを `biz/handler/example/hello_service.go` に追加します。
 // こんにちは。
// @router /hello [GET]
func Hello(ctx context.Context, c *app.RequestContext) {
var err エラー
var req の例.HelloReq
エラー = c.BindAndValidate(&req)
err != nil の場合 {
c.String(400, err.Error())
戻る
}

resp := new(example.HelloResp)
resp.RespBody = "こんにちは、" + req.Name
c.JSON(200、それぞれ)
}
  1. プロジェクトをコンパイルして実行する
 $ ビルドを実行
$ ./デモ

これでシンプルなHertzプロジェクトが生成されました。テストしてみましょう。

 $ curl http://localhost:8888/hello\?name\=Xiaoming
// 次の応答が表示された場合、サービスは正常に開始されたことを意味します。
$ {"RespBody":"こんにちは、シャオミン"}

(上記のデモは hertz-examples で閲覧できます) その後は、自由に独自のプロジェクトを構築できます。

追記

このシェアでHertzの概要を理解していただけたことを願っています。Hertzは継続的に改良を重ね、CloudWeGoエコシステムの改善に取り組んでいます。興味のある学生の皆様、ぜひ一緒にCloudWeGoを構築してみませんか?

参考文献

  • ヘルツ: https://github.com/cloudwego/hertz
  • Hertz ドキュメント: https://www.cloudwego.io/zh/docs/hertz/
  • ByteDance の Go ネットワークライブラリの実践: https://www.cloudwego.io/zh/blog/2021/10/09/%E5%AD%97%E8%8A%82%E8%B7%B3%E5%8A%A8%E5%9C%A8-go-%E7%BD%91%E7%BB%9C%E5%BA%93%E4%B8%8A%E7%9A%84%E5%AE%9E%E8%B7%B5/