grpc のリバース プロキシとして nginx を使用する場合の落とし穴の概要

grpc のリバース プロキシとして nginx を使用する場合の落とし穴の概要

背景

ご存知のとおり、nginx は高性能な Web サーバーであり、負荷分散やリバース プロキシによく使用されます。いわゆるリバース プロキシは、フォワード プロキシに対応します。フォワード プロキシは、従来の意味での「プロキシ」です。たとえば、通常の状況では、中国では Google にアクセスできません。アクセスする必要がある場合は、プロキシのレイヤーを介して転送する必要があります。このフォワード プロキシはサーバー (つまり Google) を表し、リバース プロキシはクライアント (つまりユーザー) を表します。ユーザーのリクエストが nginx に到達すると、nginx はユーザーのリクエストを実際のバックエンド サービスにプロキシし、結果をユーザーに返します。

(画像はWikipediaより)

フォワード プロキシとリバース プロキシは、実際にはユーザーの観点から定義されます。フォワードは、ユーザーが要求するサービスをプロキシすることを意味し、リバースとは、ユーザーがサービスへの要求を開始するようにプロキシすることを意味します。両者の間には非常に重要な違いがあります。

フォワード プロキシ サーバーは要求元を認識せず、リバース プロキシ要求元はサーバーを認識しません。
上記の例について考えてみましょう。プロキシ経由で Google にアクセスすると、Google はリクエストがプロキシ サーバーから送信されたことしか感知できず、ユーザーを直接感知することはできません (もちろん、Cookie などを通じて追跡することはできます)。nginx リバース プロキシを使用する場合、リクエストがどのバックエンド サーバーに転送されるかはわかりません。

nginx をリバース プロキシとして使用する最も一般的なシナリオは、よく知られている http プロトコルです。nginx.conf ファイルを構成することで、リバース プロキシ ルールを簡単に定義できます。

ワーカープロセス 1;

イベント {
    ワーカー接続 1024;
}

http {
    mime.types を含めます。
    デフォルトタイプ アプリケーション/オクテットストリーム;

    サーバー{
        聞く 80;
        server_name ローカルホスト;

        
        位置 / {
            proxy_pass http://ドメイン;
        }
    }
}

Nginx は 1.13.10 以降で gRPC プロトコルのリバース プロキシをサポートしており、設定も同様です。

ワーカープロセス 1;

イベント {
    ワーカー接続 1024;
}

http {
    mime.types を含めます。
    デフォルトタイプ アプリケーション/オクテットストリーム;

    サーバー{
        81 http2 を聴く;
        server_name ローカルホスト;

        
        位置 / {
            grpc_pass http://ip;
        }
    }
}

しかし、需要シナリオがより複雑になると、nginx の gRPC モジュールには実際には多くの落とし穴があり、実装能力は http ほど完全ではないことがわかります。http のソリューションを適用すると、問題が発生します。

シナリオ

当初、私たちのシナリオは非常にシンプルでした。gRPC プロトコルを通じてシンプルな C/S アーキテクチャを実装しました。

ただし、この単純な直接接続は、一部のシナリオでは実現できません。たとえば、クライアントとサーバーが 2 つのネットワーク環境にあり、相互に接続されていない場合などです。この場合、単純な gRPC 接続を介してサービスにアクセスすることはできません。 1 つの解決策は、上記の nginx リバース プロキシ gRPC メソッドを使用して、中間プロキシ サーバーを介して転送することです。

nginx プロキシは、両方の環境にアクセス可能なクラスターにデプロイされるため、ネットワーク環境間での gRPC アクセスが可能になります。次の質問は、このルーティング ルールをどのように構成するかということです。初期の gRPC ターゲット ノードは明確であり、server1 と server2 の IP アドレスであることに注意してください。中間に nginx プロキシのレイヤーが追加されると、クライアントによって開始された gRPC 要求のオブジェクトはすべて nginx プロキシの IP アドレスになります。クライアントが nginx との接続を確立した後、nginx はリクエストを server1 に転送するか server2 に転送するかをどのように判断するのでしょうか? (ここで、server1 と server2 は、単に同じサービスの冗長展開ではありません。ユーザー ID などの要求の属性に基づいて誰が応答するかを決定する必要があるため、負荷分散を使用して応答要求をランダムに選択することはできません。)

解決

http プロトコルの場合、実装方法は多数あります。

パスによる差別化

リクエストはパスにサーバー情報を追加します (例: /server1/service/method)。その後、nginx はリクエストを転送するときに元のリクエストを復元します。

ワーカープロセス 1;

イベント {
    ワーカー接続 1024;
}

http {
    mime.types を含めます。
    デフォルトタイプ アプリケーション/オクテットストリーム;

    サーバー{
        聞く 80;
        server_name ローカルホスト;

        場所 ~ ^/server1/ {
            proxy_pass http://domain1/;
        }
        
        場所 ~ ^/server2/ {
            proxy_pass http://domain2/;
        }
    }
}

http://domain/ の末尾のスラッシュに注意してください。このスラッシュがない場合、要求されたパスは /server1/service/method となり、サーバーは /service/method の要求にのみ応答できるため、404 エラーが発生します。

リクエストパラメータによる差別化

リクエストパラメータに server1 の情報を入れることもできます:

ワーカープロセス 1;

イベント {
    ワーカー接続 1024;
}

http {
    mime.types を含めます。
    デフォルトタイプ アプリケーション/オクテットストリーム;

    サーバー{
        聞く 80;
        server_name ローカルホスト;

        場所 /サービス/メソッド {
            $query_string ~ x_server=(.*) の場合 {
                プロキシパス http://$1;
            }
        }
    }
}

しかし、gRPC の場合はそれほど単純ではありません。まず、gRPC は URI の書き込みをサポートしていません。nginx によって転送されたリクエストは元のパスを保持し、転送時にパスを変更することはできません。つまり、上記の最初の方法は実行できません。次に、gRPC は HTTP 2.0 プロトコルに基づいています。HTTP2 には queryString の概念がありません。リクエスト ヘッダーには、/service/method などのリクエスト パスを表す項目があります。このパスはリクエスト パラメータを運ぶことができません。つまり、パスを /service/method?server=server1 のように記述することはできません。つまり、上記の 2 番目の方法も実行不可能であるということです。

HTTP2:path のリクエスト ヘッダーはリクエストのパスを指定するので、:path を直接変更しないのはなぜでしょうか。

ワーカープロセス 1;

イベント {
    ワーカー接続 1024;
}

http {
    mime.types を含めます。
    デフォルトタイプ アプリケーション/オクテットストリーム;

    サーバー{
        80 http2 を聴く;
        server_name ローカルホスト;

        場所 ~ ^/(.*)/service/.* {
            grpc_set_header:パス/service/$2;
            grpc_pass http://$1;
        }
    }
}

しかし、実際に検証してみると、この方法は実行不可能であることがわかります。:path のリクエスト ヘッダーを直接変更すると、サーバーがエラーを報告します。考えられるエラーの 1 つは次のとおりです。

rpc エラー: コード = 利用不可、説明 = 不正なゲートウェイ: HTTP ステータス コード 502、トランスポート: 予期しないコンテンツ タイプ「text/html」を受信しました

パケットをキャプチャした後、grpc_set_header は :path の結果を上書きせず、新しいリクエスト ヘッダーを追加したことがわかりました。これは、リクエスト ヘッダーに 2 つの :path があるのと同じです。これが、サーバーが 502 エラーを報告した理由である可能性があります。

困ったときは、gRPC のメタデータ機能を思いつきます。クライアント側のメタデータにサーバー情報を保存し、nginx ルーティング時にメタデータ内のサーバー情報に従って対応するバックエンド サービスに転送することで、ニーズを満たすことができます。 Go 言語の場合、メタデータを設定するには、PerRPCCredentials インターフェースを実装し、接続を開始するときにこの実装クラスのインスタンスを渡す必要があります。

タイプ extraMetadata 構造体 {
    IP文字列
}

func (c extraMetadata) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    マップ[文字列]文字列を返す{
        「x-ip」:c.Ip、
    }, なし
}

func (c extraMetadata) RequireTransportSecurity() bool {
    偽を返す
}

関数main(){
    ...
    // nginxProxy は nginx プロキシの IP またはドメイン名アドレスです var nginxProxy string
    // serverIp はリクエスト属性に基づいて計算されたバックエンドサービスの IP アドレスです
    var serverIp 文字列
    con、err := grpc.Dial(nginxProxy、grpc.WithInsecure()、
        grpc.WithPerRPCCredentials(extraMetadata{Ip: serverIp}))
}

次に、nginx 構成のこのメタデータに従って、対応するサーバーに転送します。

ワーカープロセス 1;

イベント {
    ワーカー接続 1024;
}

http {
    mime.types を含めます。
    デフォルトタイプ アプリケーション/オクテットストリーム;

    サーバー{
        80 http2 を聴く;
        server_name ローカルホスト;

        場所 ~ ^/service/.* {
            grpc_pass grpc://$http_x_ip:8200;
        }
    }
}

ここで、渡した x-ip メタデータ情報を参照するために、構文 $http_x_ip が使用されていることに注意してください。この方法は効果的であることが証明されており、クライアントは nginx プロキシを介してサーバーの gRPC サービスに正常にアクセスできます。

要約する

nginx の gRPC モジュールに関するドキュメントが少なすぎます。公式ドキュメントでは、いくつかの指示の目的のみが記載されており、メタデータ メソッドについては説明されていません。また、このトピックについて触れているオンライン ドキュメントもほとんどないため、トラブルシューティングに 2、3 日かかりました。同じ問題に遭遇した人々の役に立つことを願って、ここでプロセス全体を要約します。

grpc のリバース プロキシとして nginx を使用する際の落とし穴に関するこの記事はこれで終わりです。より関連性の高い nginx grpc リバース プロキシ コンテンツについては、123WORDPRESS.COM で以前の記事を検索するか、以下の関連記事を引き続き参照してください。今後も 123WORDPRESS.COM を応援していただければ幸いです。

以下もご興味があるかもしれません:
  • プレフィックスケースを削除する Nginx リバース プロキシ構成のチュートリアル
  • Nginx リバース プロキシ構成の完全なプロセス記録
  • 複数のサーバーにNginxリバースプロキシを実装する方法
  • nginx を介してローカルでリバースプロキシを構成するプロセス全体
  • nginx リバース プロキシでの proxy_pass の実装
  • nginxリバースプロキシを使用するときに長時間接続を維持する方法
  • Nginx リバースプロキシの例の詳細な説明
  • Nginx リバース プロキシから go-fastdfs へのケースの説明

<<:  MySql の null 関数の使用の共有

>>:  フォーム OnSubmit と input type=image の使用の概要

推薦する

Vue のデータ応答性に関する詳細な理解

目次1. ES 構文のゲッターとセッター2. ES構文でのdefineProperty 3. Vue...

VUE 3 テレポート コンポーネントと使用構文をすぐに使い始める

目次1. テレポートの紹介1.1. 複数のテレポートを使用する2. テレポートを使用する理由3. テ...

Vuex のコアコンセプトと基本的な使用法の詳細な説明

目次導入始めるインストール①直接ダウンロードする方法②CND法③NPM方式④糸法NPMインストールの...

HTMLを圧縮しない理由はいくつかある

理由は簡単です。 HTML ドキュメントでは、複数の空白文字は 1 つの空白文字と同等です。つまり、...

Vueはシンプルな虫眼鏡効果を実装します

この記事では、参考までに、簡単な虫眼鏡効果を実現するためのVueの具体的なコードを紹介します。具体的...

Linux ディスクデバイスと LVM 管理コマンドの詳細な例

序文Linux オペレーティング システムでは、デバイス ファイルは特別なタイプのファイルです。これ...

Vueはツリー構造の追加、削除、変更、チェックのサンプルコードを実装します

実は多くの会社がユーザー権限ツリーに似た機能を持っています。最近、追加、削除、修正のツリー構造を書き...

HTML テーブル マークアップ チュートリアル (14): テーブル ヘッダー

<br />HTML 言語では、タグを使用してテーブルにタイトルを自動的に追加できます。...

Vueページの画像が表示されない問題の解決方法

新しいバージョンの設定インターフェースを作る際に、vueフレームワークを使用して実装しました。ページ...

LINUX での IPTABLES ファイアウォールの基本的な使用方法のチュートリアル

序文パブリック IP を持つ本番 VPS の場合、必要なポートのみが開かれ、IP とポートを制御する...

Linux のハードリンクとソフトリンクの原理と使用法の分析

Linux システムには、ファイル共有を解決するために使用できるリンク ファイルと呼ばれる種類のファ...

JavaScript における型の必須および暗黙的な変換の詳細な説明

目次1. 暗黙的な変換二重等号での変換ブール型変換「+」と「-」 2. 強制型変換' ...

Webフロントエンド開発経験の概要

XMLファイルは、可能な限りutf-8でエンコードする必要があります。gb2312には、?など、保存...

Centos7 で yum を使用して Ceph 分散ストレージをインストールするチュートリアル

目次序文yumソース、epelソースを設定するCephソースの設定Cephとそのコンポーネントをイン...

MySQL 5.7.20 のインストールと設定方法のグラフィック チュートリアル (win10)

この記事では、MySQL 5.7.20のインストールと設定方法を参考までに紹介します。具体的な内容は...