gRPCとは何か?APISIXとの連携方法
September 28, 2022
gRPCとは何か
gRPCは、Googleがオープンソース化したRPCフレームワークで、サービス間の通信方法を統一することを目的としています。このフレームワークは、HTTP/2を転送プロトコルとして使用し、Protocol Buffersをインターフェース記述言語として使用します。これにより、サービス間の呼び出しのためのコードを自動生成することができます。
gRPCの優位性
gRPCは、Googleが開発者やクラウドネイティブ環境に与える卓越した影響力により、RPCフレームワークの標準となっています。
etcdの機能を呼び出したい?gRPC!
OpenCensusのデータを送信したい?gRPC!
Goで実装されたマイクロサービスでRPCを使用したい?gRPC!
gRPCの優位性は非常に強く、もしgRPCをRPCフレームワークとして選択しなかった場合、その理由をしっかりと説明する必要があります。そうでなければ、誰かが必ず「なぜ主流のgRPCを選ばないのか?」と尋ねてくるでしょう。Alibabaでさえ、自社のRPCフレームワークであるDubboを積極的に推進してきましたが、最新バージョンのDubbo 3では、プロトコル設計を大幅に改訂し、gRPCとDubbo 2の両方と互換性のあるgRPCのバリアントに変更しました。実際、Dubbo 3はDubbo 2からのアップグレードというよりも、gRPCの優位性を認めたものと言えます。
gRPCを提供する多くのサービスは、対応するHTTPインターフェースも提供していますが、そのようなインターフェースは互換性のためだけのものであることが多いです。gRPCバージョンの方がユーザーエクスペリエンスがはるかに優れています。gRPC経由でアクセスできる場合、対応するSDKを直接インポートできます。通常のHTTP APIしか使用できない場合、通常はドキュメントページに誘導され、対応するHTTP操作を自分で実装する必要があります。HTTPアクセスはOpenAPI仕様を通じて対応するSDKを生成できますが、HTTPは優先度が低いため、gRPCほどHTTPユーザーを真剣に扱うプロジェクトはほとんどありません。
gRPCを使用すべきか
APISIXは、etcdを設定センターとして使用しています。v3以降、etcdはそのインターフェースをgRPCに移行しました。しかし、OpenRestyエコシステムではgRPCをサポートするプロジェクトがないため、APISIXはetcdのHTTP APIを呼び出すしかありません。etcdのHTTP APIはgRPC-gatewayを通じて提供されています。本質的には、etcdはサーバー側でHTTPからgRPCへのプロキシを実行し、外部からのHTTPリクエストをgRPC-gatewayを通じてgRPCリクエストに変換します。この通信方法を数年間展開してきましたが、HTTP APIとgRPC APIの間の相互作用にいくつかの問題があることがわかりました。gRPC-gatewayがあるからといって、HTTPアクセスが完全にサポートされているわけではありません。まだ微妙な違いがあります。
以下は、過去数年間にetcdで遭遇した関連する問題のリストです:
- gRPC-gatewayがデフォルトで無効になっている。メンテナの怠慢により、一部のプロジェクトではetcdのデフォルト設定でgRPC-gatewayが有効になっていません。そのため、ドキュメントに現在のetcdがgRPC-gatewayを有効にしているかどうかを確認する指示を追加する必要がありました。詳細はhttps://github.com/apache/apisix/pull/2940を参照してください。
- デフォルトでは、gRPCはレスポンスを4MBに制限しています。etcdは提供するSDKでこの制限を解除していますが、gRPC-gatewayでは解除するのを忘れていました。その結果、公式のetcdctl(提供するSDKに基づいて構築されている)は正常に動作しますが、APISIXは動作しません。詳細はhttps://github.com/etcd-io/etcd/issues/12576を参照してください。
- 同じ問題 - 今回は同じ接続に対する最大リクエスト数です。GoのHTTP2実装には、単一のクライアントが同時に送信できるリクエスト数を制御する
MaxConcurrentStreams
設定があり、デフォルトで250に設定されています。通常、どのクライアントが同時に250以上のリクエストを送信するでしょうか?そのため、etcdは常にこの設定を使用してきました。しかし、すべてのHTTPリクエストをローカルのgRPCインターフェースにプロキシする「クライアント」であるgRPC-gatewayは、この制限を超える可能性があります。詳細はhttps://github.com/etcd-io/etcd/issues/14185を参照してください。 - etcdがmTLSを有効にした後、etcdはサーバー証明書とクライアント証明書として同じ証明書を使用します。gRPC-gatewayのサーバー証明書と、gRPC-gatewayがgRPCインターフェースにアクセスする際のクライアント証明書です。証明書にサーバー認証拡張が有効になっているが、クライアント認証拡張が有効になっていない場合、証明書の検証でエラーが発生します。再度、etcdctlで直接アクセスすると正常に動作します(この場合、証明書はクライアント証明書として使用されません)が、APISIXは動作しません。詳細はhttps://github.com/etcd-io/etcd/issues/9785を参照してください。
- mTLSを有効にした後、etcdは証明書のユーザー情報のセキュリティポリシー設定を許可します。前述のように、gRPC-gatewayはgRPCインターフェースにアクセスする際に固定のクライアント証明書を使用するため、最初にHTTPインターフェースにアクセスする際に使用された証明書情報は使用されません。したがって、クライアント証明書が固定されており変更されないため、この機能は自然に動作しません。詳細はhttps://github.com/apache/apisix/issues/5608を参照してください。
これらの問題を2点にまとめることができます:
- gRPC-gateway(およびおそらくHTTPをgRPCに変換する他の試み)は、すべての問題を解決する銀の弾丸ではありません。
- etcdの開発者はHTTPメソッドに十分な重点を置いていません。そして、彼らの最大のユーザーであるKubernetesはこの機能を使用していません。
ここでは特定のソフトウェアの問題について話しているのではありません。etcdはgRPCを使用する典型的な例に過ぎません。gRPCを主要なRPCフレームワークとして使用するすべてのサービスは、HTTPのサポートにおいて同様の制限があります。
APISIX 3.0はこの問題をどのように解決するか
「山がムハンマドのところに来ないなら、ムハンマドが山のところに行かなければならない」という言葉があります。OpenRestyの下でgRPCクライアントを実装すれば、gRPCサービスと直接通信できます。
作業量と安定性を考慮して、車輪の再発明をするのではなく、一般的に使用されるgRPCライブラリに基づいて開発することにしました。以下のgRPCライブラリを検討しました:
- NGINXのgRPCサービス。NGINXはgRPCを外部ユーザーに公開していません。高レベルのAPIさえありません。使用したい場合、いくつかの低レベルの関数をコピーし、それらを高レベルのインターフェースに統合するしかありません。統合すると追加の作業負荷が発生します。
- 公式のC++用gRPCライブラリ。私たちのシステムはNGINXに基づいているため、C++ライブラリを統合するのは少し複雑になる可能性があります。さらに、このライブラリの依存関係は約2GBに近く、APISIXの構築にとって大きな課題となります。
- 公式のGo実装のgRPC。Goには強力なツールチェーンがあり、プロジェクトを迅速に構築できます。しかし、残念ながらこの実装のパフォーマンスはC++バージョンには遠く及びません。そのため、別のGo実装であるhttps://github.com/bufbuild/connect-go/を検討しました。残念ながら、このプロジェクトのパフォーマンスも公式バージョンよりも優れていません。
- Rust実装のgRPCライブラリ。このライブラリは、依存関係管理とパフォーマンスを組み合わせるための自然な選択肢です。残念ながら、私たちはRustに不慣れであり、それに賭けることはできません。
gRPCクライアントの操作は基本的にすべてIOバウンドであるため、パフォーマンス要件は主要なものではありません。慎重に検討した結果、Go-gRPCに基づいて実装することにしました。
Luaのコルーチンスケジューラと連携するために、NGINX Cモジュールを書きました:https://github.com/api7/grpc-client-nginx-module。最初は、Goコードをcgoを通じて静的にリンクされたライブラリにコンパイルしてこのCモジュールに統合することを考えましたが、Goはマルチスレッドアプリケーションであるため、フォーク後に子プロセスが親プロセスのすべてのスレッドを継承しないことがわかりました。NGINXのマスターワーカーマルチプロセスアーキテクチャに適応する方法はありません。そのため、Goコードを動的リンクライブラリ(DLL)にコンパイルし、実行時にワーカープロセスにロードすることにしました。
GoのコルーチンとLuaのコルーチンを調整するために、タスクキュー機構を実装しました。LuaコードがgRPC IO操作を開始すると、Go側にタスクを送信し、自身を中断します。Goコルーチンがこのタスクを実行し、実行結果がキューに書き込まれます。NGINX側のバックグラウンドスレッドがタスク実行結果を消費し、対応するLuaコルーチンを再スケジュールし、Luaコードの実行を続けます。このようにして、gRPC IO操作はLuaコードにとって通常のソケット操作と変わりません。
これで、NGINX Cモジュールの作業のほとんどが完了しました。あとは、etcdの.proto
ファイル(gRPCインターフェースを定義している)を取り出し、それを修正し、Luaでファイルをロードして以下のetcdクライアントを取得するだけです:
local gcli = require("resty.grpc")
assert(gcli.load("t/testdata/rpc.proto"))
local conn = assert(gcli.connect("127.0.0.1:2379"))
local st, err = conn:new_server_stream("etcdserverpb.Watch", "Watch",
{create_request =
{key = ngx.var.arg_key}},
{timeout = 30000})
if not st then
ngx.status = 503
ngx.say(err)
return
end
for i = 1, (ngx.var.arg_count or 10) do
local res, err = st:recv()
ngx.log(ngx.WARN, "received ", cjson.encode(res))
if not res then
ngx.status = 503
ngx.say(err)
break
end
end
このgRPCベースの実装は、1600行のLuaコードだけで構成されるetcd-HTTPクライアントプロジェクトであるlua-resty-etcdよりも優れています。
もちろん、lua-resty-etcdを置き換えるにはまだ長い道のりがあります。etcdと完全に接続するために、grpc-client-nginx-moduleは以下の機能を完了する必要があります:
- mTLSサポート
- gRPCメタデータ設定のサポート
- パラメータ設定のサポート(例:
MaxConcurrentStreams
とMaxRecvMsgSize
) - L4からのリクエストのサポート
幸い、基盤は整っており、これらのサポートは当然のことです。
grpc-client-nginx-moduleはAPISIX 3.0に統合され、APISIXユーザーはAPISIXプラグインでこのモジュールのメソッドを使用して、gRPCサービスと直接通信できるようになります。
gRPCのネイティブサポートにより、APISIXはより良いetcdエクスペリエンスを得ることができ、gRPCヘルスチェックやgRPCベースのオープンテレメトリデータレポートなどの機能の可能性が広がります。
今後、APISIXのgRPCベースの機能がさらに増えることを楽しみにしています!