私のチームは、マイクロサービスアーキテクチャを備えたスケーラブルなソリューションを設計しており、レイヤー間のトランスポート通信としてgRPCを使用することを計画しています。そして、非同期grpcモデルを使用することにしました。 example( greeter_async_server.cc )が提供する設計は、RPCメソッドの数をスケーリングすると実行可能ではないようです。これは、RPCメソッドごとに新しいクラスを作成し、それらを作成する必要があるためです。 HandleRpcs()
内のオブジェクトはこのようになります。 ペーストビン (短いサンプルコード)。
void HandleRpcs() {
new CallDataForRPC1(&service_, cq_.get());
new CallDataForRPC2(&service_, cq_.get());
new CallDataForRPC3(&service, cq_.get());
// so on...
}
ハードコーディングされるため、すべての柔軟性が失われます。
実装する300-400RPCメソッドがあり、300-400クラスを持つことは面倒であり、10万以上のRPCリクエスト/秒とこのソリューションを処理する必要がある場合は非効率的です非常に悪いデザインです。すべてのリクエストで、この方法でオブジェクトを作成するオーバーヘッドに耐えることはできません。誰かが親切にこれの回避策を教えてもらえますか? async grpc c++
は、同期コンパニオンのように単純ではありませんか?
編集:状況をより明確にするために、そしてこの非同期の例の流れを把握するのに苦労しているかもしれない人のために、私は何を書いていますこれまで理解してきましたが、どこか間違っていたら訂正させてください。
Async grpcでは、一意のタグをcompletion-queueにバインドする必要があるたびに、ポーリング時に、特定のRPCがクライアントにヒットしたときにサーバーがそれを返し、返されたものから推測できるようにします。通話の種類に関するunique-tag。
service_->RequestRPC2(&ctx_, &request_, &responder_, cq_, cq_,this);
ここでは、現在のオブジェクトのアドレスを一意のタグとして使用しています。これは、完了キューでRPC呼び出しに登録するようなものです。次に、HandleRPCs()
をポーリングして、クライアントがRPCにヒットしたかどうかを確認します。ヒットした場合は、cq_->Next(&tag, &OK)
がタグを埋めます。ポーリングコードスニペット:
while (true) {
GPR_ASSERT(cq_->Next(&tag, &ok));
GPR_ASSERT(ok);
static_cast<CallData*>(tag)->Proceed();
}
キューに登録したunique-tagはCallDataオブジェクトのアドレスだったので、Proceed()
を呼び出すことができます。これは、ロジックがProceed()
内にある1つのRPCでは問題ありませんでした。ただし、すべてのRPCをCallData内に配置するたびに、RPCが増えるため、ポーリング時に、RPC1(postgres呼び出し)、RPC2へのロジックを含む唯一のProceed()
を呼び出します。 (mongodb呼び出し)、..など。これは、すべてのプログラムを1つの関数内に記述するようなものです。したがって、これを回避するために、virtual void Proceed()
でGenericCallData
クラスを使用し、それから派生クラスを作成しました。RPCごとに1つのクラスで、独自のProceed()
内に独自のロジックがあります。これは実用的な解決策ですが、多くのクラスを作成することは避けたいと思います。
私が試したもう1つの解決策は、すべてのRPC-function-logicをproceed()
から独自の関数に入れ、グローバルstd::map<long, std::function</*some params*/>>
を維持することでした。したがって、一意のタグを使用してRPCをキューに登録するたびに、対応するロジック関数(ステートメントにハードコードし、必要なすべてのパラメーターをバインドします)を格納してから、一意のタグをキーとして格納します。ポーリング時に、&tag
を取得したら、マップでこのキーを検索し、対応する保存された関数を呼び出します。ここで、もう1つのハードルがあります。これは、関数ロジック内で実行する必要があります。
// pseudo code
void function(reply, responder, context, service)
{
// register this RPC with another unique tag so to serve new incoming request of the same type on the completion queue
service_->RequestRPC1(/*params*/, new_unique_id);
// now again save this new_unique_id and current function into the map, so when tag will be returned we can do lookup
map.emplace(new_unique_id, function);
// now you're free to do your logic
// do your logic
}
これがわかります。コードは別のモジュールに拡散されており、RPCベースです。それが状況をクリアすることを願っています。誰かがこのタイプのサーバーをもっと簡単な方法で実装できたらと思いました。
スレッドは古いですが、現在実装しているソリューションを共有したいと思いました。これは主に、CallDataを継承してスケーラブルにするテンプレートクラスで構成されています。このように、新しい各rpcは、必要なCallDataメソッドのテンプレートを特殊化するだけで済みます。
Calldata
ヘッダー:class CallData {
protected:
enum Status { CREATE, PROCESS, FINISH };
Status status;
virtual void treat_create() = 0;
virtual void treat_process() = 0;
public:
void Proceed();
};
CallData
実装を続行します:void CallData::Proceed() {
switch (status) {
case CREATE:
status = PROCESS;
treat_create();
break;
case PROCESS:
status = FINISH;
treat_process();
break;
case FINISH:
delete this;
}
}
CallData
ヘッダーからの継承(簡略化):template <typename Request, typename Reply>
class CallDataTemplated : CallData {
static_assert(std::is_base_of<google::protobuf::Message, Request>::value,
"Request and reply must be protobuf messages");
static_assert(std::is_base_of<google::protobuf::Message, Reply>::value,
"Request and reply must be protobuf messages");
private:
Service,Cq,Context,ResponseWriter,...
Request request;
Reply reply;
protected:
void treat_create() override;
void treat_process() override;
public:
...
};
次に、理論的には特定のrpcについて、次のようなことができるはずです。
template<>
void CallDataTemplated<HelloRequest, HelloReply>::treat_process() {
...
}
これは多くのテンプレート化されたメソッドですが、私の観点からは、rpcごとにクラスを作成するよりも望ましいです。