Hango Rider:NetEaseShufanオープンソースEnvoyエンタープライズレベルのカスタム拡張フレームワーク

スケーラビリティは、ネットワークプロキシソフトウェアの最も重要な機能の1つです。柔軟で強力なスケーラビリティにより、ネットワークプロキシソフトウェアの機能の境界を大幅に拡大できます。新たなオープンソースの高性能ネットワークプロキシソフトウェアとして、Envoy自体は、C++に基づくネイティブ拡張やWASM/ Luaに基づく動的拡張など、比較的豊富なスケーラビリティを提供します。ただし、Envoyの既存のスケーラビリティ機能にはそれぞれ独自の制限があります。Envoyゲートウェイ/グリッドを大規模に実装する過程で、NetEase Shufanは、Luaベースのエンタープライズレベルのカスタム拡張フレームワークのセットを実装しました-Rider for Envoyは、Qingzhouマイクロサービスプラットフォームに適用され、ビジネス側。高性能、機能豊富、その他の要件。

現在、Rider(https://github.com/hango-io/rider)拡張フレームワークは完全にオープンソース化されており、オープンソースAPIゲートウェイHango(https://github.com/hango-io/hango )に統合されています。 -gateway)、Hangoゲートウェイに柔軟で強力で使いやすいカスタム拡張機能ます。

1.Envoyのスケーラビリティの現状

インターネットシステムでは、外界に公開する必要のあるほとんどすべてのシステムにネットワークプロキシが必要です。以前に登場したHAProxyとNginxは依然として人気があります。マイクロサービスの時代に入った後、より豊富な機能とより強力な管理を備えたAPIゲートウェイと制御機能はトラフィックポータルになりました必須コンポーネント。Envoyは、その優れたパフォーマンス、スケーラビリティ、および可観測性により、多数のAPIゲートウェイのデータプレーン選択になりました。トラフィックプロキシに必要な基本機能に加えて、Envoyはプロキシに必要な多くの高度な機能をネイティブに実装しています。高度な負荷分散、融合、電流制限など。したがって、Envoyに基づいて構築されたAPIゲートウェイは、すでに豊富な機能を備えており、ほとんどのアプリケーションプロキシのニーズを満たすことができます。ただし、APIゲートウェイの実際の使用シナリオでは、一部のアプリケーションまたはビジネスは、独自のニーズに応じて新しい機能を拡張します。これには、一部のHTTPヘッダーの単純な処理、独自のAPMとのドッキングなどがあります。したがって、APIゲートウェイアプリケーションやビジネスをサポートするために拡張して、必要に応じて対応する機能を拡張する機能が必要ですが、この機能はEnvoyでも実行できます。Envoyに基づくAPIゲートウェイのスケーラビリティは、スケーラビリティに依存していると言えます。使節の能力によって提供されます。次に、Envoyが現在提供している拡張メカニズムを見てみましょう。

1.1ネイティブC++拡張機能

Envoyは、プラグイン可能なフィルターメカニズムを介してネイティブC ++プラグインを拡張する機能を実装します。次の図に示すように、L4フィルターはプロトコルプロキシ機能とL4トラフィックガバナンス機能を拡張し、L7フィルターは豊富なトラフィックガバナンス機能を実装します。

image.png

この拡張メソッドはEnvoyによってネイティブに提供されるため、パフォーマンスは当然最高ですが、同時に2つの主要な問題に直面します。1つはプラグイン開発者がC ++言語の開発能力を持っている必要があること、もう1つはプラグインの開発後にプラグインを再開発する必要があるということです。Envoyバイナリファイルをコンパイルしてからデプロイメントをアップグレードすると、プラグイン機能の動的ロードを実現できません。この2つの問題を解決するために、Envoyコミュニティは、LuaとWASMに基づく拡張メカニズムを継続的に実装しています。まず、コミュニティLua拡張の原理を見てみましょう。

1.2コミュニティLua拡張

Lua言語を使用して元々C++言語で実装されたEnvoyプラグインを直感的に開発するには、次の2つの点を考慮する必要があります。1つはEnvoyプロセスでLuaスクリプトを実行する方法、もう1つはLuaスクリプトが内部を取得する方法です。 GetHeaderやBodyなどのEnvoyのデータと機能。これらの2つの観点から、EnvoyコミュニティのLua拡張の実装を明確に見ることができます(実際、WASMとRiderもこれらの2つの観点からのものです)。

下の図に示すように、上記で紹介したネイティブC ++拡張スキームとの違いは、Envoyの7層プラグインにLuaプラグインがあり、C++で開発されたこのLuaプラグインが上記の2つの質問に答えます。まず、EnvoyプロセスでLuaスクリプトを実行する方法。答えはLuaプラグインを使用することです。EnvoyのLuaプラグインはまだC ++で開発されているため、LuaスクリプトをLuaプラグインにロードして実行できます。 ; 2つ目は、LuaスクリプトがEnvoyの内部データを取得する方法です。関数は、LuaプラグインがEnvoyの内部データと関数をLuaCAPIの形式でLuaスクリプトに提供するというものです。

image.png

コミュニティLua拡張機能は、C ++よりもはるかに単純なLua言語に基づくプラグインを開発する機能をユーザーに提供します。一方、Envoyは、再コンパイルやアップグレードなしでLuaスクリプトを動的にロードすることをサポートします。しかし同時に、C ++とLua仮想マシン間の相互作用によって引き起こされるオーバーヘッドのため、Lua拡張機能のパフォーマンスはネイティブC ++拡張機能のパフォーマンスよりも当然悪くなり、Envoyコミュニティの現在のLuaCAPI相互作用方法はパフォーマンスの問題をさらに悪化させます。パフォーマンスの問題に加えて、コミュニティのLua拡張機能には大きな欠陥があります。プラグイン構成をサポートしていないため、コミュニティのLua拡張機能の実用性が大幅に低下します。対照的に、WASMとRiderはプラグインの構成可能性を実現し、RiderはLua拡張機能のパフォーマンスを最適化したため、RiderのLua拡張機能はパフォーマンスと機能の点でエンタープライズレベルの拡張機能のニーズを満たすことができます。

1.3コミュニティWASM拡張

WASMはフロントエンドから派生したテクノロジーであり、ますます複雑化するフロントエンドWebアプリケーションと制限されたJSスクリプト解釈パフォーマンスを解決するために生まれたテクノロジーです。WASMは言語ではなく、バイトコード標準です。理論的には、任意の言語をWASMバイトコードにコンパイルしてから、WASM仮想マシンで実行できます。

WASM拡張機能の実装原理は、Lua拡張機能の実装原理と似ています。WASMプラグインは、Envoy独自の4層または7層プラグインに実装され、WASM仮想マシンに組み込まれて動的にロードおよび実行されます。プラグ可能な拡張コード(WASMバイトコードにコンパイルされたもの)。また、WASM仮想マシンを介してEnvoyの内部データと機能にアクセスするためのインターフェイスを公開します。その原理を次の図に示します。

envoy-wasm-filters.png

WASMは、Envoyのスケーラビリティが直面するさまざまな問題を完全に解決しているようです。複数の言語をサポートし、プラグインの動的ロードをサポートし、プラグイン構成をサポートします。ただし、テストの結果、WASMはパフォーマンスの真髄に陥りました。 on C ++拡張機能のパフォーマンスは、他の言語で実装されたプラグインは言うまでもなく、Lua拡張機能のパフォーマンスよりもさらに劣っています(具体的なパフォーマンス比較の結果は第3部にあります)。

1.4まとめ

次の表に、現在のさまざまな拡張ソリューションの特性をまとめます。ネイティブC ++拡張機能は最高のパフォーマンスを発揮しますが、プラグインの動的読み込みをサポートしていません。コミュニティLua拡張機能は、プラグインの動的読み込みをサポートしていますが、サポートしていません。プラグイン構成。これはほとんど不可能です。使用;コミュニティWASM拡張機能は、プラグインの動的ロードとプラグイン構成の両方をサポートしますが、パフォーマンスは低下します。

拡張計画 プラグイン構成をサポートするかどうか 動的拡張をサポートするかどうか パフォーマンス サポートされている言語 開発の複雑さ
ネイティブC++ はい いいえ 最適な C ++ 繁雑
CommunityLua いいえ はい 貧しい ルア 単純
コミュニティWASM はい はい 違い C ++ / Rust/Go等 中くらい

上記のさまざまなスケーラビリティソリューションの長所と短所に応じて、NetEaseQingzhouマイクロサービスは独自のスケーラブルなフレームワークRiderを設計および実装しました。主な設計目標は次のとおりです。

  • Lua言語拡張のサポート
  • Luaプラグインを動的にロード、更新、および削除するためのEnvoyのサポート
  • Luaプラグイン構成の定義のサポート
  • カスタムLuaプラグインの有効スコープ、ゲートウェイレベル/プロジェクトレベル/ルートレベルをサポート
  • EnvoyコミュニティのLua拡張機能やWASM拡張機能よりも優れたパフォーマンス

次に、Rider拡張可能フレームワークの設計、最適化、および実践を見てみましょう。

2. Rider拡張可能フレームワークの設計、最適化、および実践

2.1初期の調査

コミュニティLua拡張機能でのパフォーマンスの低下とサポートされていないプラグイン構成の問題に対応して、Riderの初期のアーキテクチャは2つのモジュールを設計および実装しました。

  • Rider Filter:Rider FilterはEnvoyの7層プラグインであり、Luaコードを初期化して呼び出し、LuaCAPIまたはFFIインターフェイスを介して呼び出すためにEnvoy内のデータと関数をLuaSDKに提供するために使用されます。RiderはFFIを使用してほとんどのインターフェースを実装しており、その理論上のパフォーマンスは、CAPI実装に基づくコミュニティLua拡張機能よりも優れていることに注意してください。
  • Lua SDK:Lua SDKはLuaプラグインコードフレームワークであり、ユーザーはLuaSDKが提供するAPIを呼び出すことでリクエスト処理を実装できます。Lua SDKはグローバルおよびルートレベルのプラグイン構成を取得するためのAPIを提供するため、RiderのLua拡張機能はプラグイン構成の取得をサポートし、コミュニティのLua拡張機能の大きな問題を解決します。

次の図は、全体的なアーキテクチャ図です。

rider.drawio.png

私たちの初期のアーキテクチャは基本的にEnvoyの拡張性のニーズを満たしていましたが、多言語Luaのサポート、Luaプラグインの動的ロードのサポート、Luaプラグイン構成のサポートなどです。しかし、まだ次の問題があります。

  • Rider Filterには、FFIを使用しないインターフェイスがまだいくつかあり、パフォーマンスがわずかに不十分な場合があります。
  • より多くのプラグイン機能の開発をサポートするには、LuaSDKをさらに改善する必要があります。
  • 最初の問題の解決中に発見されたRiderの巨大なパフォーマンスの問題。

これらの問題に対応して、Riderのアーキテクチャをさらに改良し、Riderのパフォーマンスを詳細に分析して、新しいアーキテクチャを導き出しました。

2.2実用的な最適化

新しいRiderアーキテクチャによって解決される最初の問題は、FFIを最後まで実行しようとすることです。以前の調査によると、Luaは2つの方法でCを呼び出します。1つはネイティブCAPIを介したものです。各呼び出しは、スタックスペースを割り当てます。 (およびスタックスペース)フレームは異なり、ヒープに適用される連続したメモリの一部です)、スタックスペースを介してパラメーターと戻り値を渡します。もう1つの方法は、Luajitが提供するFFI呼び出しを使用することです。FFIの利点は、Cデータ構造を使用してC関数をLuaで直接呼び出すことができ、コードがJitに最適化されたバフを取得でき、ネイティブのLuaと比較してパフォーマンスが大幅に向上することです。したがって、RiderのCAPIによって実装されたインターフェイスをFFIに変換する必要があります。

変換の最初のステップで問題が発生しました。初期のRiderアーキテクチャではFFIを使用してEnvoyBody関連のインターフェイスを公開できないようです。まず、初期のRiderがネイティブCAPIを使用してEnvoyBody関連のインターフェイスを公開する必要があった理由を見てみましょう。初期のRiderアーキテクチャ図では、Lua Codeにはon_requestとon_responseの2つの主要な関数があります。これらの2つの関数は、RiderFilterがLuaCodeを実行するときに、RiderFilterがLuaCodeのみを実行するため、Riderアーキテクチャによって指定されたLuaコードに実装する必要がある関数です。 Lua仮想マシンからこれら2つの関数を取得し、decodeHeadersステージとencodeHeadersステージでそれぞれ実行してみてください。その後、on_request関数またはon_response関数にBody関連のインターフェイス呼び出しがある場合、ライダーフィルターはまだ実行されていません。データはまだ取得できないため、最初にLuaのコルーチンを一時停止し、Rider FilterがdecodeDataまたはencodeDataステージで実行されるまで待ってから再開するだけで、このメソッドをFFIで実現することはできません。 、およびLua仮想マシンと対話することによってのみ実現できます。

rider_next.jpg

上記の問題に基づいて、Riderの新しいアーキテクチャは、以前のアーキテクチャに基づいていくつかの改良を加えました。上の図に示すように、アーキテクチャ全体のモジュールは変更されていません。変更されたのは、で実装する必要のある機能です。 Riderフレームワークによって指定されたLuaプラグインと、のRiderFilter実行時間内のこれらの関数。上の図に示すように、元のon_request関数とon_response関数はさらにHeaderステージ関数とBodyステージ関数に分割され、BodyのLuaコルーチンを一時停止する必要がないように、RiderFilter処理のHeaderステージとBodyステージでそれぞれ呼び出されます。処理(後でWASMの実装が同様の細分化であることがわかりました)。したがって、新しいアーキテクチャの要求処理フローは次のようになります。

  • ライダープラグイン構成(Luaプラグイン構成を含む)は、LDSおよびRDSの一部としてパイロットを通じて提供されます。
  • Envoyは、RiderFilterを含むHTTPリクエストごとに7層のフィルターチェーンを構築します。Rider Filterが初期化されると、LuaSDKモジュールと対応するプラグインがファイルシステムからLuaVMにロードされます。
  • リクエスト処理段階では、ライダーフィルターはDecode段階とEncode段階でそれぞれLuaコードのon_request_header、on_request_body、on_response_header、on_response_bodyメソッドを呼び出します。
  • ユーザーのLuaコードの実行中に、RiderFilterによってカプセル化された対応するインターフェイスがLuaSDKを介して呼び出されます。たとえば、要求と応答の情報の取得、変更、外部サービスの呼び出しなど、ログの印刷が行われます。

2.2.1パフォーマンスの最適化

新しいアーキテクチャ設計の本来の目的はパフォーマンスを向上させることであるため、新しいアーキテクチャが開発されたらすぐにパフォーマンステストを実施しました。テストシナリオは次のとおりです。

  • 環境:ローカルコンテナ環境
  • バックエンド:Nginx4コア
  • 送料:4
  • クライアント:Wrk 4t 32c
  • Luaプラグイン:get_bodyインターフェースを100回呼び出す

実装の比較:

  • CAPI:RiderのCAPI実装、元のLuaとCの相互作用、スタックスペースを介したパラメーターと戻り値の受け渡し;
  • FFIOld:Riderの初期のFFI実装。

テスト結果を次の図に示します。従来のネガティブ最適化、および多くのネガティブ最適化(もちろん、100回の呼び出しのため)、FFIOldではCAPIと比較してQPSが30%低下しています。最初に考えたのは、コードの記述に問題があり、新しいアーキテクチャでは多くのオーバーヘッドが発生するということです。そこで、FFIOldに基づいてRiderによって実装されたHeader APIをテストしました。パフォーマンスは、FFIOldに基づいたBody APIと似ています。つまり、FFIOldに問題があります。RiderFFIによって実装された初期のAPIのパフォーマンスは、それほど良くない可能性があります。 CAPIとして!

image.png

そこで、FFIの基本原理を理解しました。FFIはLuajitが提供する機能です。LuajitはLuaを実行する仮想マシンです。Java仮想マシンと同様に、Luajitにはコンパイルモードと解釈モードの2つの動作モードがあります。コンパイルモードは解釈モードよりも高いです。パターンは良好です。興味があれば理由を見つけることができます)。Luajitは、デフォルトで解釈モードで実行されます。実行プロセス中に、コンパイル可能なホットコードを記録します。後続の実行では、ホットコードを直接マシンコードに変換して実行しようとし、パフォーマンスが向上します。

Riderに戻ると、RiderのLuaプラグインもLuajit仮想マシンで実行され、何万ものQPSリクエストによってLuaプラグインコードが確実にホットコードになり、LuajitはLuaプラグインを次のように変換しようとします。マシンコード、およびFFI定義C関数もマシンコードに変換されて実行されます。パフォーマンスは確かに向上するようですが、期待に応えられません。理由は、Luajitホットコードをマシンコードに変換しようとするためです。 。試行が失敗する可能性があり、失敗は解釈モードに縮退し、パフォーマンスが大幅に低下します。Luajitは、プログラムがコンパイル済みモードで実行されているかどうかを確認する方法を提供します。Luaコードの前に次のコードを追加します。

local verbo = require("jit.v")
verbo.start()

次に、圧力テストを続行し、Luajitが以下を出力することを確認します。

image.png

この図は、2つの重要な情報を示しています。1つはTRACEが出力した場合、Luajitがコンパイルモードを終了したことを意味します。もう1つは、コンパイルモードを終了する理由がCのパラメーターの型変換であるということです。 FFIで定義された関数はサポートされていません。次に、このパラメータに移動します。

local function get_header_map_value(source, key)
    local ctx = get_context_handle()
    if not ctx then
        error("no context")
    end

    if type(key) ~= "string" then
        error("header name must be a string", 2)
    end

    local buffer = ffi_new("envoy_lua_ffi_str_t[1]")
    local rc = C.envoy_http_lua_ffi_get_header_map_value(ctx, source, key, #key, buffer)
    if rc ~= FFI_OK then
        return nil
    end
    return ffi_str(buffer[0].data, buffer[0].len)
end

C. envoy_http_lua_ffi_get_header_map_value(ctx、source、key、#key、buffer)で渡されるctxはLuaのライトユーザーデータタイプであり、envoy_http_lua_ffi_get_header_map_value関数はクラスのポインタータイプとして宣言されています。Luajitは変換中に変換を完了できません。コンパイルモードが終了しました。真実は明らかです。次のステップはこの問題を解決することです。具体的な設計はここで説明するには詳細すぎます。興味がある場合は、オープンソースコミュニティに移動できます。最適化後の効果を見てみましょう。

まず、上記のget_bodyパフォーマンステストに続いて、FFINew(最適化されたFFI実装)データのセットが追加されます。次の図に示すように、FFINewのパフォーマンスはFFIOldのパフォーマンスより66%高く、 16 %高くなっています。 CAPIのそれよりも。、FFIの利点が最終的​​に反映されます。

image.png

上記のパフォーマンスの向上は、参照としてのみ使用できます。結局、get_bodyインターフェイスは100回呼び出されるため、さまざまな複雑さのプラグインに対して簡単なパフォーマンステストを実行しました。

  • 単純なフィルター:get_headerを10回呼び出します。
  • 通常のフィルター:set_headerを20回呼び出し、get_headerを10回呼び出し、最後に20個のヘッダーを削除します。
  • 複雑なフィルター:通常のフィルター+get_bodyへの30回の呼び出し。

結果を下の図に示します。FFINewのパフォーマンスはFFIOldのパフォーマンスよりも優れており、それぞれ15%、22%、および29%のパフォーマンスが向上しています。

image.png

最後に、RiderとコミュニティWASMおよびLuaのパフォーマンスをさらに比較しました。

  • RiderOld:Riderの初期アーキテクチャの実装。
  • RiderNew:現在のRiderの実装。
  • WASMC ++:バージョン1.17のコミュニティWASM実装。
  • RawLua:コミュニティバージョン1.17のLua拡張機能の実装。
  • RawC ++:EnvoyネイティブC++拡張機能の実装。

image.png

上の図に示すように、RiderのパフォーマンスはコミュニティのWASMおよびLuaよりも優れており、EnvoyのネイティブC ++プラグインと比較してパフォーマンスが約10%向上し、パフォーマンスが約10%低下します。ここでのWASMプラグインはC++SDKで実装されており、内部テストによると、他のWASM言語SDKで実装されたプラグインのパフォーマンスは低下します。また、RiderのパフォーマンスはコミュニティLuaと比較して10%未満しか改善されていませんが、個人的には、パフォーマンステストプラグインのLuaとC ++の間のデータの相互作用は比較的単純であり、基本的には伝送であると感じていますFFIの利点を反映していない単純な文字列の。

2.2.2機能強化

パフォーマンスの問題が解決したら、次のステップは機能の拡張、つまりLua SDKの強化です。これは、Riderが現在サポートしているすべてのLuaSDKの概要です。

  • envoy.req.get_header(name)
  • envoy.req.get_header_size(name)
  • envoy.req.get_header_index(name、index)
  • envoy.req.get_headers()
  • envoy.req.get_body()
  • envoy.req.get_metadata(key、filter_name)
  • envoy.req.get_dynamic_metadata(key、filter_name)
  • envoy.req.get_query_parameters(max_args)
  • envoy.req.set_header(name、value)
  • envoy.req.set_headers(headers)
  • envoy.req.clear_header(name)
  • envoy.resp.get_header(name)
  • envoy.resp.get_header_size(name)
  • envoy.resp.get_header_index(name、index)
  • envoy.resp.get_headers()
  • envoy.resp.get_body()
  • envoy.resp.set_header(name、value)
  • envoy.resp.set_headers(headers)
  • envoy.resp.clear_header(name)
  • envoy.streaminfo.start_time()
  • envoy.streaminfo.current_time_milliseconds()
  • envoy.streaminfo.downstream_local_address()
  • envoy.streaminfo.downstream_remote_address()
  • envoy.streaminfo.upstream_cluster()
  • envoy.streaminfo.upstream_host()
  • envoy.logTrace(メッセージ)
  • envoy.logDebug(メッセージ)
  • envoy.logInfo(メッセージ)
  • envoy.logWarn(メッセージ)
  • envoy.logErr(メッセージ)
  • envoy.filelog(msg)
  • envoy.get_base_config()
  • envoy.get_route_config()
  • envoy.httpCall(cluster、headers、body、timeout)
  • envoy.respond(headers、body)

2.3ライダーの練習

NetEaseの社内メディア事業は、Riderをベースにした複数のLuaプラグインを使用して開発・立ち上げられました。その中でも、フルリンクトレースログを印刷するTraceプラグインは、2020年第1四半期に立ち上げられました。現在、すべてのゲートウェイに接続されています。数十万のQPSを処理し、安定して実行されます。

3.ライダー拡張可能フレームワークの将来計画

将来的には、安定性、パフォーマンス、機能の観点からRiderの保守と最適化を継続していきます。

  • 安定性:現在、RiderはNetEaseの内外の複数のビジネスパーティに大規模に実装されており、今後さらに改善してRiderの安定性を確保していきます。
  • パフォーマンス:RiderのパフォーマンスはコミュニティのLuaやWASMよりも優れていますが、ネイティブC ++拡張機能とのパフォーマンスのギャップをさらに狭めるために、今後もパフォーマンスを最適化していきます。
  • 機能:Rider APIの観点からコミュニティLuaおよびWASMと連携し、最も包括的なAPI機能を提供します。

もっと詳しく知る

著者について: NetEaseShufanのシニアエンジニアであるWangKaiは、主にQingzhouマイクロサービス、Qingzhou APIゲートウェイ、およびその他の関連製品のデータプレーンの研究開発、拡張、拡張を担当しています。彼は、データプレーンのEnvoy拡張機能の拡張と実際の実装に豊富な経験を持っています。

{{o.name}}
{{m.name}}

おすすめ

転載: my.oschina.net/u/4565392/blog/5440026