アルパカ証券の裏側 - はじめに
こんにちは。shirou(@r_rudi) と申します。アーキテクトという名の雑用係をしています。
Alpaca Japanでは、2021年8月に「アルパカ証券」という証券サービスをはじめました。
この一連の文章は、アルパカ証券の裏側のシステムやその開発体制などについて述べたものです。なるべく証券分野に限らず説明していく予定ですので、証券サービスを立ち上げようとしている人たちにはもちろん、それ以外の方にも参考にしていただけるような文章を目指したいと思っています。
アルパカ証券とは
アルパカ証券の詳細はホームページをご覧ください。また、第一種金融商品取引業者登録完了時のプレスリリースにも、「アルパカ証券」サービスの特徴が記載されています。
全体設計方針
まず最初に、アルパカ証券を構成するシステムの全体設計方針について説明します。
マイクロサービス vs モノリシック
設計は2018年中頃ぐらいから徐々に始まりました。2018年当時にはすでにマイクロサービスで設計しているという事例はありました。しかし、なんせ証券システムを作ったことがない。というか、業務仕様もまだ固まっていない。たくさんの開発者がいるわけでもない。こういう状況ではどう分割するのが正しいかも分からないため、 core という大きなものに全部入れ込んでしまえ、という方針にしました。従ってここはモノリシックとなっています。業務仕様が固まっていないため、トランザクションをどうするのかが後々問題になりそうなため、DBは1つとし原則としてcoreからしかアクセスしないようにしました。
Auth0などの外部サービスとの接続部分については External Interface(以下exif) として分割しました。これにより我々のシステムと外部サービスとを切り離すことができました。この設計は後々、とある外部サービスがサービス終了してしまって代替サービスに乗り換える、という事件が起きた時に役に立ちました。
コンポーネント間の接続にgRPCを使用する
業務ロジックを一手に引き受ける core と、外部サービスとの接続を担当する exif に分割するとして、ではその間をなにでつなぐのか、というところでgRPCを採用することにしました。 2018年時点でもすでにgRPCは多くのサービスで使われており、各種言語での実装がそろっていました。特に、gRPC-Gatewayが存在していたのがgRPC採用の決め手となりました。gRPC-Gatewayを利用することで、ユーザーからはHTTP APIで受け付けられるようになります。
この構成を図にすると以下のようになります。なお、図で示すようにEKSで動いていますが、それについては後述します。
全体設計図
ユーザーからはHTTP APIで受け、webapiというgRPC-Gatewayを実装したサーバーがgRPCに変換し、coreに送ります。coreは外部サービスを使用する場合、対応するexifのAPIを呼びます。exifは対応する外部サービスに対して外部サービスに応じたプロトコルで通信します。
以上のように、通信は必ずcoreを通るようにしています。これにはユーザーの権限がcoreで一括管理されているからです。将来的には権限管理を担うマイクロサービスとして切り離すことも考えています。
coreはモノリシックな実装となっていますが、中身はgRPCサービスとして個々に構築されモジュール化されています。それぞれのgRPCのサービスは分離できるように実装しているため、やろうと思えば今後gRPCサービスの単位で分割し、マイクロサービス化していくこともできます。これは実際に実装していって判明した利点でした。
同期 vs 非同期
当初は全部非同期にし、イベント駆動型アーキテクチャやEvent Sourcingの考えを取り入れた設計にしようと考えていました。イベントを基盤とし、そのイベントを全部記録することで証券システムに求められる説明責任を果たせると考えました。
しかし、実際に非同期にするとやはりいろいろな考慮が必要になり、同期の箇所も必要になる。そうなると同期非同期が混在して混乱する、という事態が見えてきました。そのため、わりと早い段階で全部同期とする、という決定をしました。これについては、もっとイベントドリブンにしたほうが良かったのではないかな、という気がしていますので、将来的には変更する可能性は高いです。
なお、ログについてはwebapiで受け付けたJSONと、変換したgRPCメッセージをすべて保存しています。
自前認証 vs 外部認証サービス利用
ログインパスワードを持つとそれだけで脆弱性に繋がります。そのため、当初から認証は外部サービスを使用することにしました。各種サービスを検討しましたが、最終的にはAuth0を採用しました。
Auth0を使うことでログイン(認証)自体はAuth0で行うようになります。そのため、仮に我々のシステムが乗っ取られたり内部犯行者がいたとしても顧客のパスワードは保護されています。また、パスワードの再発行などのフローもAuth0へ任せられるようになりました。
Auth0にログイン後のシステムとの連携方法ですが、JWTを利用しています。このJWTを使って、ブラウザからシステムのJSON APIを叩く、という形式にしました。これにより認証済みかどうかがシステム側で判別できます。
一方、JWTを使うことでセッションの即時無効化ができなくなる、という問題が出てきました。このシステムではJWTのリフレッシュ間隔を短時間とし、さらに、API呼び出しごとにcoreで権限チェックをしています。また、権限チェックで弾かれた場合、フロント側で自動的にセッションを破棄しています。これによりJWTのセッション即時無効化の問題を解消しています。
キャッシュ vs キャッシュしない
何度かredis等を入れようという話が持ち上がりましたが、「まだ早い」と言い続け、結局今でもほぼキャッシュを入れていません。キャッシュを入れることで応答速度は上がるものの、キャッシュの不整合に伴う不具合および、その不具合が起きたときの問題追求の困難さが分かっているのでキャッシュを入れないようにしています。Rules of Optimizationに従っています。
より正確に言うと、OpenCensusとDataDogを入れており、常にAPIの応答速度を計測しています。この応答速度がある程度遅く、かつ、ユーザーに影響が出るようなAPIについては、DBにはインデックスを入れたりと少しの最適化をしています。継続して計測しており、必要であればキャッシュを入れようと考えています。
なお、OpenCensusからOpenTelemetryへの移行をしたいと考えていますが、まだ実現できていません。ここはまたの機会に掘り下げる予定です。
モノレポ vs マルチレポ
1つのレポジトリに全部のソースコードを入れるモノレポというものがありますが、弊社ではアクセス権限をGitHubで行いたいために、以下の4つのレポジトリに分割するマルチレポとしています。
- backend (coreとexif)
- web (webapiとJavaScript)
- infrastructure
- grpc
backendではcoreとexifのバックエンドシステムに関するコード、webにはwebapiとフロントエンドのコードを持ちます。infrastructureにはAWS上の各種リソースを管理するTerraformとk8sのmanifest、grpcはgrpc定義のprotoファイルを格納しています。
backendの人はbackendレポジトリのみ、webの人はwebレポジトリのみ見られるようにしています。また、infrastructureのレポジトリはごく限られた人しかアクセスできません。
grpcレポジトリは例外的にすべての開発者が見られるようになっています。このgrpcレポジトリ上で議論し、出来上がったインタフェースに基づいて各レポジトリで実装を進めています。これは開発フローに深く関連しているため、後日解説します。
実装言語
webapiについてはgRPC-Gatewayの関係上、Go言語しかありません。それ以外についてはどんな言語でも良いのですが、以下の2つの理由によりJava 11を選択しました。
- JVMの安定性
- Java人材の豊富さ
JVMの安定性については疑う余地がありません。後者については、開発者の人数が絶対足りないことは分かっていたので重要視しました。個人的にはGoで進めたかったのですが、2018年当時にGoの人材を求めるのは時期尚早でした。また、外部との通信でSOAPを使う必要があり、JavaのSOAPライブラリのほうが枯れているなどの事情もありました。なお、開発開始直後はJava 11がまだリリースされていませんでしたので、Java 10で開発を進め11がリリースされた段階で切り替えました。11はLTSであり、しばらくは使い続ける予定です。
コード生成
gRPCを選定した理由の1つとして、幅広い言語に対するコード生成が可能ということがあります。
gRPCからのコード生成
gRPCからはコードを3種類生成しています。
- Go
- Java
- TypeScript
GoとJavaのコードはgRPCから直接生成できます。生成したコードはCIを通すためにレポジトリの中に入れてコミットしています。
TypeScriptコードは直接生成できないため、少し複雑です。gRPCからgRPC-Gatewayの定義を利用して OpenAPI に変換し、OpenAPIからOpenAPI generatorを利用してTypeScript-fetchのコードを生成しています。 OpenAPIからの生成にはTypeScriptの中でもいろいろな種類があるのですが、一番基本的かつ生成されたコードが読みやすいTypeScript-fetchを採用しました。
DB定義からのコード生成
コード生成はメッセージだけではありません。DB定義も生成していますし、DB定義コードも生成しています。
- Google Spread Sheetで各テーブルのスキーマを管理
- Google Spread SheetからDBスキーマを生成、DBを更新
- 更新したDBから、 pg2any を用いてHibernate用Javaコードを生成
- 加えて、pg2anyはDB SchemeからgRPCのメッセージ定義も生成
特に4のDB定義からgRPCメッセージを生成することは重要です。弊社ではDBにPostgreSQLを採用しており、enumを多数定義しています。このenumの値がDBとgRPCとで異なると、そのままバックエンドとフロントエンドの不整合につながります。そのため、固定値についてはGoogle Spread Sheetの一箇所で管理し、gRPCのEnum定義としてどこからも同じ値を使えるようにしています。
コード生成
gRPCの定義とコード生成は開発スタイルにも深く影響しています。開発フローについてはまた後日説明します。
使用している技術およびフレームワーク
ここまでアルパカ証券システムの設計方針や全体的な概要を示してきました。 使用している技術や外部サービスを並べると以下のようになります。これらの技術を具体的にどう使用しているかは今後説明していきます。
- gRPC
- フロントエンド
- TypeScript
- React/Redux
- Jest/Testing Library
- Storybook
- Webサーバ
- Go
- gRPC-Gateway
- バックエンド
- Java 11
- Gradle 6
- Spring Boot
- Hibernate
- PostgreSQL
- JUnit5
- MarketStore
- 外部サービス
- Auth0
- SendGrid
- インフラ/モニタリング
- Kubernetes (AWS EKS)
- Terraform
- Envoy
- OpenCensus
- DataDog
- CircleCI
まとめ
「アルパカ証券の裏側」として、まずはシステムの設計理念について説明しました。だいぶ保守的な設計にしつつ、攻めるところは攻めるというバランスを狙っています。
これから数回に分けて、いろいろな観点でシステムについて述べていく予定ですので、お付き合いください。
おまけ
アルパカ証券では一緒に新しい証券システムを開発してくれる仲間を募集しています。ご興味がありましたら、こちらを御覧ください!