2024.12.19
システムの穴を運用でカバーしようとしてミス多発… バグが大量発生、決算が合わない状態から業務効率化を実現するまで
リンクをコピー
記事をブックマーク
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にフォーカスして開発を続けることができるようになっている。
(次回につづく)
2024.12.20
日本の約10倍がん患者が殺到し、病院はキャパオーバー ジャパンハートが描く医療の未来と、カンボジアに新病院を作る理由
2024.12.19
12万通りの「資格の組み合わせ」の中で厳選された60の項目 532の資格を持つ林雄次氏の新刊『資格のかけ算』の見所
2024.12.16
32歳で成績最下位から1年でトップ営業になれた理由 売るテクニックよりも大事な「あり方」
2023.03.21
民間宇宙開発で高まる「飛行機とロケットの衝突」の危機...どうやって回避する?
PR | 2024.12.20
モンスター化したExcelが、ある日突然崩壊 昭和のガス工事会社を生まれ変わらせた、起死回生のノーコード活用術
2024.12.12
会議で発言しやすくなる「心理的安全性」を高めるには ファシリテーションがうまい人の3つの条件
2024.12.18
「社長以外みんな儲かる給与設計」にした理由 経営者たちが語る、優秀な人材集め・会社を発展させるためのヒント
2024.12.17
面接で「後輩を指導できなさそう」と思われる人の伝え方 歳を重ねるほど重視される経験の「ノウハウ化」
2024.12.13
ファシリテーターは「しゃべらないほうがいい」理由 入山章栄氏が語る、心理的安全性の高い場を作るポイント
2024.12.10
メールのラリー回数でわかる「評価されない人」の特徴 職場での評価を下げる行動5選
Climbers Startup JAPAN EXPO 2024 - 秋 -
2024.11.20 - 2024.11.21
『主体的なキャリア形成』を考える~資格のかけ算について〜
2024.12.07 - 2024.12.07
Startup CTO of the year 2024
2024.11.19 - 2024.11.19
社員の力を引き出す経営戦略〜ひとり一人が自ら成長する組織づくり〜
2024.11.20 - 2024.11.20
「確率思考」で未来を見通す 事業を成功に導く意思決定 ~エビデンス・ベースド・マーケティング思考の調査分析で事業に有効な予測手法とは~
2024.11.05 - 2024.11.05