BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル RSocketでRESTに安息(Rest)を

RSocketでRESTに安息(Rest)を

原文(投稿日:2018/10/15)へのリンク

InfoQ Articles Add article Article queue List untranslated articles Navigation Main menu Logout Time is now Oct 22, 2018 01:44 on this server. Timezone: CST (GMT-6). GeneralMain contentTopics Body:Show plain editor

REST(Representational State Transfer)は、マイクロサービス間の通信におけるデファクトスタンダードになっています。これはよいことではありません — 実際、それは非常に残念なことです。なぜ、こんなことになったのでしょう?そう、RESTが登場した頃には、もっと悪い選択肢しかなかったのです。Roy Fielding氏が2000年にRESTを提案したとき、RESTはとても不味いサンドイッチの中にあった、ケールのサンドイッチだったのです。

当時使われていたのは、SOAPやRMI、CORBA、EJBでした。その中でJSONは、XMLの授けたもうた安息でした。URLを使ってテキストを書き出すのは簡単だったからです。加えて、JavaScriptがブラウザで本格的に使用されるようになっていたため、それまでのSOAPよりも、RESTを扱う方がはるかに簡単だったのです。昨今のマイクロサービスのトレンドとは違い、当時のアプリケーションは従来型のモノリシックな3層アプリケーションでした。それらが通信対象とする外部トラフィックのソースは大半がブラウザだったので、何かを生成する必要がある場合には、RESTは便利な選択だったのです。また、多くの人たちは、WebSphereのような大規模な商用製品から、JettyやTomcatへの移行を始めていました。これらはEJBを扱う機能さえ持っていなかったので、RESTは都合がよかったのです。

これがマイクロサービスとどう関わっているのでしょうか?初期のマイクロサービスのパイオニアたちは、今日の人たちとは違う理由からマイクロサービスに移行しました。彼らは大きな規模に対処しなければならなかったのです。非常に多くのユーザを獲得するようになったことで、単一のモノリスではすべてを処置できなくなりました。また、今日の多くの企業とは違い、大きな動機はコストではなく時間でした。サービスの提供が常にニーズから遅れていたのです。ユーザ数はますます増えて、モノリスでは処理できなくなったため、アプリを小さな部分に分割して、それらを何千という数のサーバに、さらに最終的には仮想マシンにデプロイできるようにしました。

それと同時に、アプリケーションを非常に早くデプロイできるようになりました。このモデルを採用した企業が、生き残ることができたのです。しかしながら、このレースの間は、自分たちの行なったことを考える時間はあまりありませんでした。彼ら初期のパイオニアたちは、指数関数的に増加するユーザと競争に対処しなければならなかったので、戦略的な解決策を選ぶことは理に叶っています。そのひとつが、サービス間の通信手段としてのRESTだったのです。

なぜRESTはマイクロサービスに向いていないか

アプリケーションをプログラムする場合、プログラム言語は最終的にマシンコードになります。これは明らかです。JavaやJavaScriptのような“インタープリタ”言語であっても同じことです。マシンコードに直接コンパイルされなくても、JITないしジャスト・イン・タイムコンパイラが使用されます。場合によっては、JITで処理されたコードが、エンジニアが記述してハンドチューンしたものより高速なこともあります — VMはまさに、現在コンピュータ科学の奇跡なのです。

ではなぜ、この奇跡を無駄にするのでしょう?マシン用に最適化されたバイナリメッセージを、サービスに最適化されたプロトコルで送信するのではなく、私たちは人間に最適化されたメッセージを使用しています。JSONやXMLといったものを、本を送信するために設計されたプロトコルを使って送信しているのです。これがどれほどばかげたことか、考えてみてください!バイナリのプログラムがあって、それがバイナリ構造をテキストに変換して、ネットワーク上をテキスト形式で送信して、受け取ったマシンが解釈し、バイナリ構造に戻して、アプリケーションで処理しているのです。

最近のCPUでは、キャッシュミスを回避することが重要です。残念なことに、大量のJSONや文字列の解析は、このキャッシュミスを発生させるのです!

RESTを使用する理由としてよく引き合いに出されるのが、“可読性”があるためデバッグが容易である、という点です。読みにくいのはツーリングの問題です。JSONテキストに可読性があるのは、それを読むことのできるツールがある場合に限られます。それがなければバイトの羅列に過ぎません。さらに、データが送信される時間の半分は、圧縮ないし暗号化されています。どちらも可読性はありません。そもそも、人がこれを読んで、どの程度“デバッグ”できるのでしょうか?1キロバイトのJSONを毎秒わずか10個、要求するサービスがあるとします。これは1日で860メガバイト、“戦争と平和”の250倍に相当します。これほどの量は誰にも読めません。単なるお金の無駄です。

一方で、バイナリデータを送信する必要がある場合や、JSONではなくバイナリフォーマットを使用したい場合もあります。この時は、データをBase64エンコードしなければなりません。これはつまり、データを2度シリアライズしていることに他なりません。これもまた、現代のハードウェアを使用する効率的な方法ではないのです。

最終的にRESTは、HTTP上のハックとして実装されました。HTTPは現在、サービス間でトランスポートデータを送信するハックとして使用されています。HTTPは元々、インターネット内で本を持ち歩くために設計されたものです。サービスが相互通信するために使用するものではありません。そうではなく、すべてのデータを処理するアプリケーションに最適化されたフォーマットを使用するべきです。

よいマイクロサービス通信とは何か?

RESTがサービス間通信のベストチョイスではないとするならば、何がベストなのでしょうか?マイクロサービス通信用に設計されたプロトコルに私たちは何を望むのか、いくつか考えてみましょう。

まず最初に、双方向であることが必要です。クライアントがサーバを呼び出すことしかできなければ、RESTでは大きな問題になります。双方が互いに同じ能力を持つことによって、自然な方法でアプリケーション間のインタラクションを生成できるのです。そうでなければ、サーバ呼び出しをシミュレートするための長期ポーリングなど、不格好な回避策を考えなくてはなりません。HTTP/2であれば部分的に回避可能ですが、やはりクライアント側から起動する必要があります。必要なのは、クライアントとサーバが相互に、自由にコールできることなのです。

もうひとつの要件は、サービス間の接続は同じコネクションで、同時に複数のリクエストをサポートしなければならない、ということです。これは多重化(multiplexing)と呼ばれるものです。この場合、ひとつの接続でリクエストを個々に区別するための、何らかの方法が必要になります。これは、ひとつのリクエストが終わってから次の要求が始まるHTTPとは違います。多重化では、さまざまなリクエストをトラッキングしなければなりません。これを行うよい方法のひとつは、それぞれのリクエストをバイナリフレームで表現することです。各フレームにはリクエストと合わせて、関連するメタデータを保持することができます。それを使用して、フレームを適切な場所に配信することが可能になるのです。

単一コネクション上でデータを送信する場合には、リクエストを断片化する機能が必要です。ひとつのコネクション上の大きなリクエストは、他のすべてのリクエストをブロックします。いわゆる非割り込み型(head-of-the-line)ブロッキングです。それを回避するためには、リクエストを小さなサイズに断片化して、ネットワーク上を送信することが必要になります。送信されるデータはフレーム化されているので、より小さなフレームのフラグメントに分割されて、反対側で再組み立てされる可能性があります。この方法で、リクエストを互いにインターリーブすることができるのです。大きなリクエストが小さなリクエストをブロックすることがなくなり、より応答性のよいシステムを構築できます。

接続に関するメタデータを交換する機能も有効です。場合によっては、全体的なトレースレベルの設定や、辞書ベースの圧縮に関する情報交換など、ビジネストランザクションの一部ではない送信データがあります。これらはビジネスロジックとは無関係ですが、接続レベルでコントロールされるものです。メタデータを交換することで、これが実現できます。

アプリケーションのコードには、リストを取得したり、リストを返したり、あるいはその両方を行う関数やメソッドが少なくありません。マイクロサービスでも、これはよくある話です。RESTはこのような状況にうまく対処できません。その結果、あらゆる種類のハックや複雑な処理が発生します。

必要なのは、アプリケーションで行っているように、反復的なデータを簡単かつ自然な方法で処理できるプロトコルです。データのリスト全体を読み込んで処理し、すべてを処理した後にデータのリストを返す、という方法は合理的ではありません。到着したデータを順次処理できることが望まれます。データをストリーミングできるようにしたいのです。長いデータリストのある場合は、データが処理されるまで待ちたくはありません。データが使用可能になったときに送信して、その都度、応答を返してもらうのが望ましい方法です。

これによって、さらに応答性のよいシステムが実現します。この方法は、ファイルからバイトデータを読み込んでネットワーク越しにストリーミングするような場合から、データベースクエリの結果の返送、ブラウザのクリックストリームデータのバックエンドへの送信まで、あらゆる場面で利用できます。プロトコルにファーストクラスのストリーミングサポートがあれば、Sparkなどの別システムを用意してストリーム処理を行なう必要はなくなります。データを保存する要求がなければ、Kafkaのようなものも必要ありません。

ストリーム経由で送信されるデータに対して、次に必要なのはアプリケーションフロー制御です。バイトレベルのフロー制御は、すべてが同じサイズで、一般的にネットワークカードの観点から処理に要するコストが同じである、TCPのようなものならばうまく機能します。しかしながら、アプリケーションでは、すべてが同じコストではありません。10ミリ秒で処理可能な10キロバイトのメッセージもあれば、処理に10秒を要する10バイトのデータもあります。

マイクロサービスに見られるもうひとつのシナリオは、処理可能なデータ速度よりも、ダウンロードプロセスの処理レートが遅いことです。この場合、TCPバッファがフルになることはありません。ダウンストリームサービスの応答性を維持するために、負荷をかけ過ぎないようにトラフィックのフローを制御する、何らかの方法が必要になります。

基盤となるネットワークに関わらずメッセージが流れることができるように、アプリケーションは、速度制御ができなくてはなりません。アプリケーション開発者にとって、メッセージのサイズが何バイトであるかを判断するのは、特に言語間においては困難です。その一方で、送信するメッセージの数を判断するのは難しくはありません。これによってサービスは、ネットワークフロー制御とアプリケーションフロー制御を裁定することが可能になるのです。アプリケーションの処理がネットワークより早い場合もあれば、ネットワークの速度がアプリケーションよりも速い場合もあります。アプリケーションフロー制御を行うことで、テールレイテンシ(tail latency)も安定したものになります。これもまた、アプリケーションの応答に寄与します。他のアプリケーションに見られるような危険なハックである、無制限のキュー(unbounded queues)を使用する必要もなくなります。

これまで述べたように、RESTfulなWebサービスの大きな欠点は、(事実上)テキストベースとして実装されていることです。バイナリデータの送信には、データをBase64エンコードする必要があります。結果的に、すべてが2回シリアライズされることになります。本当に必要なのはバイナリです。なぜならそれは、すべてのもの — テキストも含みます — を表現できるからです。また、アプリケーションにとって、特に数値に関しては、テキストよりもバイナリを処理する方がはるかに効率的です。さらに、当然ながらコンパクトです — 余分な括弧や波括弧、山括弧がないからです。そして最後に、データがバイナリならば、フォーマット次第では、シリアライズやデシリアライズをゼロにすることも可能です。この記事の範囲から外れますが、Simple Binary Encoding (SBE)Flatbuffesなどを参考にしてください。これらはJSONを使うよりも、はるかに高速です。

そして最後に、さまざまなトランスポート上でリクエストを送信できることが望まれます。RESTfulなWebサービスで通常使用されるHTTPはTCP上でのみ動作しますが、本当に望ましいのはネットワークの抽象化です。仕様に従ってプログラムするだけで、トランスポートを心配しなくてすむようにしたいのです。それと同時に、ブラウザと通信するアプリケーションは、WebSocket上で動作可能であるべきです。アプリケーションをデプロイする場所を変えるたびに、新しいネットワークツールキットにスイッチしなければならないのではたまりません。アプリケーションを変更しなくても、トランスポートを簡単に交換できるべきなのです。

どのプロトコルが条件にかなうのか?

RESTとHTTP/2がより適している、という人もいるでしょう。HTTP/2はHTTP/1より優れていますが、仕様を読む限り、その唯一の目的はよりよいWebブラウザプロトコルを作ることであって、マイクロサービスでの使用を目的として設計されたものではありません。その目的で、すなわち、サーバHTMLからWebブラウザへの通信手段として使われるべきです。繰り返しますが、マイクロサービスの通信を目的としたものではないのです。しかも、URLを扱わなくてはなりませんし、さまざまなHTTPメソッドをアプリケーションに対応させたりしなくてはなりません — そもそも、これらのメソッドは、サーバ間通信を目的としたものではないのです。

HTTP/2はストリーミングを提供しますが、サーバプッシュのみに限られます。ですから、RESTをHTTP/2上で使用するには、クライアントからリクエストを開始して、その上でデータをサーバにプッシュしなくてはなりません。HTTP/2のフロー制御はバイトベースのフロー制御です。これはWebブラウザには好都合ですが、アプリケーションには向いていません。アプリケーションで実行されている処理を対象としてアプリケーションフローを制御する手段については、依然として存在しないのです。

最近はgRPCの利用に関して、多くのノイズがあります。gRPCは概念的にはSOAPによく似ていますが、サービス定義にXMLを使う代わりに、Protobufを使用します。SOAPと同じように、URLとヘッダマジックの寄せ集めなのですが、こちらではHTTP/2を使用します。これはつまり、gRPCが、ブラウザ用に設計されたHTTP/2と強く結び付いているという意味です。さらに悪いことに、Webブラウザではサポートされていません。

それどころか、gRPCコールをRESTコールに変換するためのプロキシが必要であるため、それを使う目的さえも失われます。ここで明らかになるのは、gRPCの設計の貧弱さです。なぜHTTP/2をプロトコルとして使用するのか、なぜブラウザで動作しないのか?その本来の目的によって永遠に制限を受ける上に、意図したところでも使用できないのです。ここから次のポイントである、RESTの最大の制限はHTTPと結び付いているという事実にある、という点につながります。

必要なのは、サービス間通信用に設計されたプロトコルです。サービス間通信を目的として特に設計されたプロトコルを使用することで、極めてシンプルで、より信頼性の高いアプリケーションを開発することができます。ハックや回避策、インピーダンスのミスマッチは存在しません。

建設資材に例えてみればよく分かります。小さな橋を造るのには木材が最適です。小さな川を越えるためであれば、それで何の問題もありません。

それを使ってもっと長い距離を渡ろうとすると、事態は複雑になってきます。

木製の橋でも成功したものはあります。ですが、もっとよい資材で作られた近代的な橋と比較すると、失敗する確率が非常に高かったのです。しかも非常に複雑で、建築にもずっと長い時間が必要でした。現在、鉄筋とコンクリートが使われている理由はここにあります。メンテナンスが用意で、建築費が安く、より長い距離を渡すことができるのです。

最新のサービスを開発するために、HTTPの代替となる最新の素材が必要です。オープンソースのRSocketはサービス用に設計され、アプリケーションレベルのフロー制御を内蔵した、コネクション指向のメッセージ駆動プロトコルで、ブラウザ内でもサーバ上でも同じように機能します。実際にWebブラウザで、マイクロサービスのバックエンドへのトラフィックを処理することも可能です。しかもバイナリです。テキストでもバイナリデータでも同じように機能し、ペイロードの断片化が可能です。アプリケーションがネットワークプリミティブとして実行する、すべてのインタラクションをモデル化します。つまり、アプリケーションキューをセットアップしなくても、データのストリームやPub/Subを実行できるのです。

RESTは理にかなった、まっとうなソリューションです。問題があるのはマイクロサービスです。分散システムは、それ自体でも十分に難しいのですから、そのために設計されたものでないものを使うことで、さらに複雑にするようなまねはしたくありません。

著者について

Robert Roeser氏はNetifiの共同創設者でCEOです。NetflixとNikeで大規模な技術プロジェクトを率いてきた、分散リアルタイムで10年のキャリアを持つベテランです。

Article Resources/DownloadsArticle Resources/Downloads File Display name

この記事に星をつける

おすすめ度
スタイル

BT