
2025.02.12
職員一人あたり52時間の残業削減に成功 kintone導入がもたらした富士吉田市の自治体DX“変革”ハウツー
リンクをコピー
記事をブックマーク
Taro L. Saito氏:もともと何をしたかったかというと、「Scalaでクライアントもサーバーも実装できるんだったら、Scalaでそのまま通信できないのか?」というところが出発点になって、今新しいフレームワークを整えているところです。
ScalaはFunctional(関数型)で、かつstatically typed(静的型付き)、というobject orientedな言語になっているのがいいところです。
これよく考えてみると、Scalaの関数をそのままRPCのメソッドにできるよね。Scalaのオブジェクトには、ちゃんと型がついているし、パラメータ名もついている。これもそのままRPCのリクエストやレスポンスタイプに応用できるよね。そうすると、ScalaはRPCを実装するのに実は非常に適した言語なんじゃ? なにもRESTとかProtocol Bufferを持ち出さなくても、Scalaをそのまま使えばいいんじゃ? というところで始まったアプローチが「Airframe RPC」、Scalaファーストな実装です。
まず、RESTとかProtocol Bufferのことはすべて忘れてください。Scalaのトレイトでメソッドを定義する。モデルクラス・ケースクラスでネットワーク中に渡すデータを定義する。
このRPCのインターフェイスをScalaで書くことによって、例えばサーバーの実装をそのままScalaで書けるし、クライアントの実装のためのコードを生成してあげて、Scala.jsだったりWebアプリケーションで動くようなサービスが簡単に作れるんじゃないかということです。
さらに、Airframe RPCはなにもScala.jsだけに限ったわけではなくて、例えばScala同士の通信も使える、応用することができます。
バックエンドサーバーもpluggableになっていて、今のところ実装はHTTP/1ベースのFinagleとHTTP/2ベースのgRPCが使えるようになっていますし、クライアント側もFinagleのクライアントだけじゃなくて、gRPCクライアント、さらによく使われているWebクライアントであるOkHttpだったり、もしpure JavaでいきたいんだったらJavaのURLConnection、標準ライブラリに入っているような簡易なURLConnection clientを使うこともできます。
こういうRPCのフレームワークを実装するときに何かが必要になってくるかというのを噛み砕いてみると、だいたい4つあります。
ネットワーク中にデータを流さなくてはいけないので、まず必要なのは、メッセージのシリアライザ・デシリアライザ。
さらに、どんなバイナリフォーマットでデータを表現しなければならないのかというところが肝になってきます。RESTの場合はJSONを使いますし、gRPCの場合はProtocol Buffer。Airframe RPCの場合は、次で紹介しますが、MessagePackを使っていきます。
そしてRPCのインターフェイスの言語としては、ここではもうScalaを使っていこうと。
4番目のクライアント・サーバーの実装については、コード生成したりとかライブラリのほうで自動的にサーバーを立ち上げられるような工夫がいくつかなされます。
では、シリアライゼーションについて見ていきます。基本的にはScalaのオブジェクトをMessagePackに変換したいわけですけれども、じゃあそもそもMessagePackって何かというと、JSONと比較してもらうとすごくわかりやすくて、MessagePackはいわばコンパクトなJSONのバイナリフォーマットだと思ってください。
例えばここに例があります。JSONにおいて27バイトで表されるこういうデータです。パラメータを2つもっているデータをMessagePackにすると、18バイトの小さなバイナリで表すことができます。
MessagePackは、基本的にprefix、データの型と長さを表したフォーマット。ここで「82」というのはエレメントを2つ持ったMap型です。次の「A7」というのは7バイトのStringを表すprefixで、次にデータが続きます。Booleanは1バイトで表せますし、Integerも1バイトで表す。そういうよく使うデータを短く表す工夫が組み込まれているデータフォーマットです。
Treasure DataではMessagePackをすごくよく使っていて、これを使ってScalaのオブジェクトのシリアライゼーションをやるとすごく効率がいいんじゃないかというところで、airframe-codecというインターフェイスを導入しました。
基本的には、入力オブジェクト、すなわちインプットからMessagePackに変換する方向を「Pack」、その逆方向、すなわちMessagePackからデータに戻すほうを「Unpack」というインターフェイスを定義して、Scalaに適用しています。
Scalaはいろいろなデータタイプがあるので、そのあらゆるデータタイプに対して、predefined、すなわちあらかじめコーデックを定義してあげます。
例えばPrimitiveタイプ、すなわちByteやChar、Integer、それからLong、Double、String。そういう基本的なデータ型に対してのコーデックを定義してあげたり、Collection、Array、Sequence、List、そういうScalaのコレクションに対応したコーデックを定義して、複雑なデータ構造でもMessagePackに変換できるような工夫がなされています。
その他、Java特有のデータに対するコーデックなども定義されているので、ほぼ基本的にケースクラスを定義したらほとんどMessagePackに変換していけます。
さらに複雑なデータ構造の場合、例えばケースクラス。ここではケースクラスのAというものがありますけれども、これにはport、name、timeoutという3つのパラメータがあります。その各々のパラメータに対応したコーデックを組み合わせることで、ケースクラスのような複雑にネストしたデータ構造(data structure)でもMessagePackに変換できるようになります。
ここの例ではMessageCodec.of[A]というAirframeコーデックが定義されているメソッドを呼び出すと、オブジェクトAをMessagePack経由で読み書きするコーデックが生成されます。
ここではわかりやすさのためにJSONを紹介していますけれども、基本的にJSONはMessagePackにダイレクトに変換できるデータで、ここではJSONをMessagePackに変換してコーデックを通して元のオブジェクトに戻すという、そういう操作がなされています。逆方向も当然できる。なので、MessagePack経由でScalaの複雑なデータタイプを変換することが可能になっています。
シリアライザができたところで、RPC用のインターフェイスをどうすればいいかというところを見ていきます。
Airframe RCPではScalaのトレイトに対して「@RPC」というアノテーションを付けてあげます。「@RPC」というこのRPCのアノテーションがついたトレイトに定義されているパブリックなfunctionは、すべてRPCのエンドポイントになります。
関数に複雑なケースクラスのデータが入っていた場合は、それがそのままデータ構造を定義するスキーマ言語(schema language)になる。このScalaで書かれたインターフェイスをバックエンド側・クライアント側で共有して実装を進めていくという流れになる。
関数呼び出しを実際にネットワーク上で送るにはどうすればいいかというと、例えばこのpersonとmessageという2つの引数があるhelloという関数をRPCで実行するときには、この関数呼び出しの引数に対応するMap型、すなわちScalaのMapのデータ構造を作ってあげます。
personというargument、すなわち引数にはPersonのケースクラスのオブジェクト、messageには例えば”Hello RPC!”というメッセージを入れたMapを作ります。このMapをAirframeコーデックを使ってMessagePackに変換してあげる。そしてHTTPのPOSTメソッドを使ってサーバー側に送ってあげるようにします。
基本的にこの関数に対応するエンドポイントのマッピングは、Scalaで定義されたパッケージ名。例えば「hello.api.v1」というパッケージでこの関数が定義されていたら、そのあとに「/」をつけて「hello」というマッピングをしています。contentのbodyには、先ほどシリアライズしたMapのfunctionのargumentのMapデータが入る。こういうかたちでリモートの関数を呼び出すデータを伝えられます。
サーバーサイドの実装はRPCのアノテーションがついたトレイトを単純に自然に実装するだけで済みます。MyServiceというトレイトがあったら、それを実装したMyServiceImplというのを普通のScalaのコードを書くように実装するだけでサーバーの実装が可能になります。
airframe-httpではこの今実装したRPCのimplementationをWebサーバーにする仕組みが備わっていて、FinagleをバックエンドにしたWebサーバーに立ち上げるのがこれぐらいのコード量で実現できます。Finagleというのは、Twitterが作っている高性能なHTTPサーバーのライブラリです。
例えばRouterというのはRPCのエンドポイントをルーティングするための機能なんですけれども、そこに先ほどのRPCの実装を追加する。ここには複数のRPCの実装を追加できます。Finagle.server.withRouterで今ここで定義したrouterを登録してあげて、任意のポートナンバー、ここでは8080番を定義して、まずサーバーをスタート。これだけでWebサーバーが立ち上がる。RPC用のWebサーバーが立ち上がる。
また、別の実装の仕方として、gRPCを使ったバックエンドも同様に実装できます。先ほどと同様に、例えばRPCの「sayHello」というトレイトを定義して、これを素直に継承して実装する。
これも先ほどと同様にRouterに登録するんですけれども、違いは、先ほどFinagleだったところをgRPCにするだけです。そうすると、今度はHTTP2ベースの高速なgRPCベースのバックエンドのWebサーバーが立ち上がる。
gRPCはHTTP2ベースで非常に高速で、その高速になっている理由というのは、1つのHTTPコネクションの中に複数のリクエストをmultiplex(多重化)して詰め込むことができるからです。よりローレイテンシ(低遅延)なリクエストハンドリングが可能になっています。
ここで見てほしいのは、Protocol Bufferがどこにも出てこないところです。gRPCは基本的にProtocol Bufferで使うのが主流なんですけれども、ここではなにもProtocol BufferなしでもScalaのインターフェイスだけあればgRPCのサービスが使えてしまうというところがおもしろいところです。
より少しアドバンスドなトピックとして、実際にどうやってgRPCを拡張しているのか。
Airframe RPCはgRPC-JavaというJava版のgRPCの実装をベースに実装されています。でも、gRPCそのものは実はProtocol Bufferを使わなくてもいい。データフォーマットagnostic、すなわちデータフォーマットに依存しないフレームワークになっていて、例えばScalaのインターフェイスをエンドポイントにしたり、MessagePackをProtocol Bufferの代わりに使いたいという拡張をしたいときには、2か所拡張してあげれば実現できるようになっています。
RPCのエンドポイントを拡張するのに必要なのがMethodDescriptor。MethodDescriptorに対してScalaのインターフェイスにマッピングするエンドポイントを定義してあげる。
さらに、どのようにデータをシリアライズするか。それをgRPCではMarshallerといっていますけれども、そのMarshallerのカスタマイズもできるようになっている。ここでMarshallerの実装として、MessagePackをやりとりするMarshallerをここに埋め込んでいます。
おもしろいところなんですけれども、じゃあHTTP1とHTTP2ベースのサーバーを使ってどれぐらい性能差があるかというのを見てみました。
ここで比較しているのはAirframe RPCのFinagleバックエンドとgRPCバックエンド。それからScalaPBというProtocol BufferのデータをScalaケースクラスにマッピングするライブラリですね。これもgRPC-Javaベースの実装になっている。
あとはgRPC-Javaをそのまま使った場合です。これはProtocol BufferのデータをJavaのクラスにマッピングする実装になっています。gRPC-JavaはgRPC界隈で最も速い部類の実装になっていて、これと同じぐらいの性能が出ていたら非常に優秀ということなんですけれども、FinagleとgRPCを比べてみて非常に性能差がある。倍以上の開きが出ている。
この青い部分がsynchronized、RPCのリクエストを1個1個処理した場合の性能です。Finagleだと1秒間にだいたい5,000ぐらいのRPCが処理できる。gRPCベースになると、8,000とか9,000とかそれぐらいのRPCを1秒間に処理できるようになっています。この性能自体は、僕が持っているMac Bookでローカルに実行したものです。
赤い部分が非同期に実行した場合です。非同期(asynchronous)というのはどういうことかというと、RPCのリクエストを同時にたくさん投げて、でも結果は、戻ってきた順に処理する。結果を1個1個待ったりはしない実装になっているので、asynchronousにするとすごく速く処理できます。gRPCベースのものは、ScalaPBでも1秒間に4万以上のRPC、Airframe gRPCだと4万6,000、gRPC-Javaだと5万2,000、非常に高いスループットを出してくれます。
ちょっとおもしろかったので、なぜScalaPBがほかの実装より遅いのかというのを気になって調べてみたんですけれども、ScalaのFutureを使うところで10パーセントぐらいオーバーヘッドがあったり、Protocol BufferをScalaのケースクラスにマッピングするところでオーバーヘッドがあるようでした。
でも、ScalaPBの作者に確認したところ、それほどに最適化やパフォーマンスはがんばっていないらしいので、まだ最適化の余地があるところのようです。でも、基本的にgRPC-Javaと比べても10パーセントか15パーセントぐらいのオーバーヘッドしかないので、非常に実用的なパフォーマンスだと思います。
では今度は、RPCのクライアントをどう作っているかというところを紹介します。
基本的にはこのRPC、Scalaで書かれたインターフェイスの定義を読み取って、テンプレートベースのコード生成をしています。その生成するためにsbt-airframeというプラグインを作成しました。これを使うとgRPCのクライアントを生成したり、Scala.jsクライアントを生成したりなどもできますし、OpenAPIのスキーマも生成できます。
例えばOpenAPIを生成すると何がいいかというと、YAMLのAPIのspecを生成するとき、例えばSwagger Editorなどを使ってAPIのドキュメントをきれいに表示できたり、まだ試していないんですけれども、例えばクロスランゲージなRPCクライアント、つまりScala以外の言語からAirframe Rpcのサーバーにアクセスするためのクライアントも、もしかしたら生成できるかもしれない。
それでは実際に今まで話してきたことを全部つなげてみます。このMyServiceというRPCがあって、それに対して生成されたクライアントがServiceSyncClient。これにairframe-httpで定義されている標準的なWebクライアントを渡してあげる。これでクライアントができる。
クライアントからmyServiceのインターフェイスを呼び出して、helloというメソッドに対してPersonというケースクラスを渡してあげる。これで実際裏側では、プログラムからクライアントへ渡って、裏側でMessagePackに変換されてサーバーに渡って、サーバーでこのmyServiceの実装を実行して、結果をMessagePackに変換して、またMessagePackからオブジェクトに戻して変換してプログラムに渡す。この一連の動作が全部この短い行数の中に収まっているというところですね。
ここでも裏側ではMessagePackだったりgRPCだったりFinagleだったりいろいろな技術が使われているんですけれども、全部忘れてScalaにフォーカスして開発を続けることができるようになっている。
(次回につづく)
2025.02.06
すかいらーく創業者が、社長を辞めて75歳で再起業したわけ “あえて長居させるコーヒー店”の経営に込めるこだわり
2025.02.13
“最近の新人は報連相をしない”という、管理職の他責思考 部下に対する「NG指示」から見る、認識のズレを防ぐコツ
2025.02.13
AIを使いこなせない人が直面する本当の課題 元マッキンゼー・赤羽雄二氏が“英語の情報”を追い続ける理由
2025.02.12
マネージャーは「プレイング3割」が適切 チームの業績を上げるためのマネジメントと業務の比率
PR | 2025.02.07
プロジェクトマネージャーは「無理ゲーを攻略するプレイヤー」 仕事を任せられない管理職のためのマネジメントの秘訣
2025.01.07
1月から始めたい「日記」を書く習慣 ビジネスパーソンにおすすめな3つの理由
2025.02.06
落合陽一氏や松尾豊氏の研究は社会に届いているか? ひろゆき氏が語るアカデミアの課題と展望
2025.02.10
32歳で「すかいらーく」を創業、75歳で「高倉町珈琲」で再起業 「失敗したからすかいらーくができた」横川竟氏流の経営哲学
2025.02.12
何度言っても変わらない人への指示のポイント 相手が主体的に動き出す“お願い”の仕方
2025.02.13
「みんなで決めたから」を言い訳にして仲良しクラブで終わる組織 インパクトも多様性も両立させるソース原理
2025.02.06
すかいらーく創業者が、社長を辞めて75歳で再起業したわけ “あえて長居させるコーヒー店”の経営に込めるこだわり
2025.02.13
“最近の新人は報連相をしない”という、管理職の他責思考 部下に対する「NG指示」から見る、認識のズレを防ぐコツ
2025.02.13
AIを使いこなせない人が直面する本当の課題 元マッキンゼー・赤羽雄二氏が“英語の情報”を追い続ける理由
2025.02.12
マネージャーは「プレイング3割」が適切 チームの業績を上げるためのマネジメントと業務の比率
PR | 2025.02.07
プロジェクトマネージャーは「無理ゲーを攻略するプレイヤー」 仕事を任せられない管理職のためのマネジメントの秘訣
2025.01.07
1月から始めたい「日記」を書く習慣 ビジネスパーソンにおすすめな3つの理由
2025.02.06
落合陽一氏や松尾豊氏の研究は社会に届いているか? ひろゆき氏が語るアカデミアの課題と展望
2025.02.10
32歳で「すかいらーく」を創業、75歳で「高倉町珈琲」で再起業 「失敗したからすかいらーくができた」横川竟氏流の経営哲学
2025.02.12
何度言っても変わらない人への指示のポイント 相手が主体的に動き出す“お願い”の仕方
2025.02.13
「みんなで決めたから」を言い訳にして仲良しクラブで終わる組織 インパクトも多様性も両立させるソース原理
着想から2か月でローンチ!爆速で新規事業を立ち上げる方法
2025.01.21 - 2025.01.21
新人の報連相スキルはマネージメントで引きあげろ!~管理職の「他責思考」を排除~
2025.01.29 - 2025.01.29
【手放すTALK LIVE#45】人と組織のポテンシャルが継承されるソース原理 ~人と組織のポテンシャルが花開く「ソース原理」とは~
2024.12.09 - 2024.12.09
『これで採用はうまくいく』著者が語る、今こそ採用担当に届けたい「口説く」力のすべて
2024.11.29 - 2024.11.29
【著者来館】『成果を上げるプレイングマネジャーは「これ」をやらない』出版記念イベント!
2025.01.10 - 2025.01.10