はじめに
弊社は社名と同じ、法人カード「UPSIDER」と請求書カード払いサービス「支払い.com」等のサービスを提供し、「挑戦者を支える世界的な金融プラットフォームを創る」をミッションに掲げる会社です。 今回は、その根幹である決済領域のテックスタックを紹介しようと思います。
決済領域とはなんやねんという方はこちらの資料にわかりやすい説明が載っていますのでこちらもぜひチェケラしてください
ただ紹介しても面白味がないので、立ち上げフェーズにメインで進めていたRyoyaが導入時の経緯を、途中参画したTakuyaが参画後に使用してみた感想を、それぞれの視点で言い合う形式で進めていきます。
決済システムを取り巻くテックスタックとアーキテクチャ
各テックスタックの話に入る前に、決済領域のどこにどの技術やアーキテクチャを利用しているかを図とともに簡単に説明していこうと思います。 まず、決済システムを含めたUPSIDERのアプリケーションはほぼすべてKubernetesの上で動いています。決済システムは、そのインフラを活用して、多数のGoのマイクロサービスで構成されています。
そして、その一つ一つのGoのサービスのビルドとテストにはBazelを利用してきました。 また、メインのデータベースはSpannerとなっております。 それでは、各テックスタックを紹介していきます!
レッツ&ゴー
マイクロサービス
概要
僕らはProcessing領域のサービス群をマイクロサービスを使ってk8s上に構築しています。
また、それらサービス間の通信方法としてgRPCを採用しています。
導入時の経緯
決済のドメインにマイクロサービスを採用した理由として以下のようなものがありました。
- 決済という複雑かつ多様なドメインの責務を分離しつつもメンテしやすくするため
- 決済処理、カード発行、WebやアプリなどのUI向け機能などにわけることで、変更を加える際の影響範囲を限定的にする
- すべてのメンバーが決済領域の関わる全機能や領域に精通していなくてもある程度の開発ができるようにする
- 障害が起きた際の影響範囲を限定できる
- 例えば、Webでの画面表示向けの機能で障害が起きても、決済処理には影響が出ないようにする
- また、障害時の復旧作業の際に、作業が影響しうる他の機能を限定的にする
その一方で、マイクロサービスはドラスティックな変更には弱いというデメリットがあり初期フェーズでの採用はアンチパターンとされる場合も多いかと思います。
しかし、ユーザーが直接触る機能と違い、決済領域はカードの仕様や制約に縛られる部分が多いため、新規追加や中小規模の改変はあれど、既存のコアの機能にドラスティックな変更は入る頻度はあまり多くないことが予想されました。そこで、決済に関してはデメリットが大きくならないと判断して、導入を決断しました。
参画して使用してみた感想
参画当初は細かくサービスが分かれているので全体像の把握が困難でした。ただ、一つ一つはそこまで複雑なことはやっていないので割とすぐに慣れることができて、慣れたあとはメンテナンスが容易だと感じています。
また、gRPCを採用することによっていわゆるスキーマ駆動開発が強制されるため、開発順序の秩序が保たれるのもナイスです。
ただ、これはgRPC特有だと思いますが、Sliceのnilとemptyを区別することができないのは初心者に対する罠ポイントでした。(区別できる前提で設計をして実際に動かしてみたらどちらもnil扱いとなり、設計からやり直すというまぁまぁ大きな手戻りが発生してしまいました😭)
Go
概要
UPSIDERでは、大半のバックエンドの領域にGoを採用しています。決済領域もほぼすべてGoで書かれており、決済領域がUPSIDERのシステムの中で一番最初にGoで書かれた領域です。
導入時の経緯
初期のUPSIDERで言語を選定する中で、以下の基準で言語を選定した結果、決済領域にはGoを採用することになりました。
- 型があること
- 可読性が高いこと
- テストが書きやすいこと
- 並行処理が書きやすいこと
決済領域は、高い安定性とパフォーマンスが求められる一方で、複雑度・理不尽度の高いビジネスロジックを取り扱う上でメンテナンスしやすさも必要となります。そこで、上記のような強みを合わせ持つGoが最適な言語であると判断して導入を決めました。
参画して使用してみた感想
まずなんと言ってもマイクロサービス、gRPCとの圧倒的相性の良さが挙げられると思います。もともとGoは簡潔さを重んじるカルチャーの元作られている言語だと思っており、サービス単体の振る舞いがシンプルなこれらの技術と組み合わせることでGoならではのデメリットが打ち消されて、メリットが存分に発揮されていると感じます。
一例として、Goではいちいちエラーハンドリングを考慮しなければならないので、巨大なモジュールを作る場合などはerrorの扱いが複雑になりがちですが、マイクロサービスのような設計と併用することでデメリットが打ち消されているように思います。決済領域は例外ハンドリングを適切に行なった上で、異常系であってもその旨を伝えるレスポンスを返さないといけないのですが、Goのように常に例外を意識しながらコードを書いていくと、予想外のことが起こりづらいのです。
また、テストの実行がシンプルで素早いのはGoの明らかなメリットです。標準で提供されたテストの実行環境は他言語でテストフレームワークを使用した場合と比較して開発者体験が圧倒的に良いと感じています。決済領域のような、極端に品質を求められるシステムにおいて、テストコードの実装ハードルの低さはとても重要な要素です。
Google Cloud Spanner(Google Cloud)
概要
Google Cloud が提供するSpannerは、Google Cloud が提供するスケーラブルなフルマネージドデータベースです。大規模なトランザクション処理とSQLクエリのサポートが特徴です。
UPSIDERでは、どのデータストアを使うかサービス群の特徴によってそれぞれ判断おり、決済領域においてSpannerを採用しています。
導入時の経緯
決済領域のデータベースを選定する上で、まずドメインの性質から以下の基準を満たすデータベースを探しました。
- メンテナンスコストや整合性を大きく犠牲にせず、スケーラビリティを担保できる
- 決済領域はデータの取り扱い量が多いため
- 高い可用性・安定性
上記二点だけであれば、Amazon DynamoDBなども選択肢に入りましたが、最終的に下記の二点から、Spannerの採用に至りました。
- キャッチアップコストが低いこと
- インフラをGCP中心で構築していたこと
参画して使用してみた感想
私個人の話になりますが、まず参画時点ではSpannerの利用経験はなく、RDBは一通り使っていたものの、NoSQLはプロダクトで使ったことがないといったレベルでした。そのため、まずは概念の理解からスタートし、RDBとの考え方の差異を埋めていくところから始まりました。
とはいえ、そこまでハードルが高いものでもなく、また、SpannerはSQLによる問い合わせが可能なのも相まって、あまり苦戦はしなかった記憶があります。
ちゃんと運用し始めてからの感想としては、テーブル構成をしっかり考えておけば、ユーザー数がどんなに増えても安心なのは大変心強いと感じています。BtoBのサービスをホストしていると、しばしば障害が複数企業に影響してしまうなどのシチュエーションに遭遇しますが、ことデータストア周りにおいてはSpannerを利用することでそういったインシデントを未然に防ぐことができているように感じます。
一点苦言があるとすればGoogle Cloudのコンソールが少し使いづらいところです。クエリ結果をエクスポートする時とかなどはやむを得ずCLIを使っています。
Bazel
概要
Bazelは、Googleが開発したビルドおよびテストツールです。多言語サポートと実行環境の差異を排除したビルドを提供し、ビルドの再現性に優れているのが特徴です。
UPSIDERでは、決済領域のCI/CDにBazelを組み込み、ビルド・テストに利用しています。
導入時の経緯
Goの導入を決めた際には、ビルドの手段の準備が必要になってくるわけですが、その中でBazelを採用することになりました。
主な採用基準としては、以下のような点です。
- 実行環境への依存が少なく、再現性を持ったビルドができる
- 文法で比較的シンプルで書きやすい
- 当時 Google が推しているプロジェクト(だと思っていた)ので、今後のメンテナンスや普及についても期待が持てた。
ただし、Bazelは最初のセットアップが非常に大変で、悪戦苦闘しながらなんとかセットアップして運用してきました。
参画して使用してみた感想
そもそもbazelファイルの書き方だとか、コンフィグレーションの方法を意識しなくてはならず、Go最大の特徴でありメリットであるシンプルさから逸脱してしまっているように感じてしまいました。
また、これは最適化を怠っているからだとは思うのですが、ビルドにめちゃくちゃ時間がかかるようになってしまったのは残念ポイントです。そもそも純正のGoのビルドにそこまで時間がかかっている印象はなく、逐次最適化していかないと本来のビルドよりも遅くなってしまう状態はナンセンスだと感じています。
そのため、途中からBazelによるビルドはやめ、今はGO_PRIVATE
を設定して依存関係を成り立たせつつ、go build
を使ってデプロイ用のイメージを作成しています。
現在もBazelでビルドすることが前提になっているコードは残っていますが、それらもリプレイスしていく予定です。
Goのパッケージ: Testing(gomock & cmp)
概要
これまで紹介したセクションの中でも度々テストについては言及があったかと思いますが、僕らはテストに心臓を捧げた民族です。そのため、僕らのテストを支えてくれる技術に関して最後に紹介させてください。
導入時の経緯
gomock
最初は自前でinterfaceを実装したモックのコードを一つ一つ書いてきました。ただ、さすがにそのコードを書いてメンテすることに疲弊していきました 😥その中で、途中からgomockを利用することになりました。
cmp
Goの標準のテストパッケージでは、テスト結果と期待値を比較して差分をわかりやすく表示してくれるものはありません。最初はreflectパッケージを利用して、比較を行っていましたが、差分が生じたときの確認が一々手間になっていました。そこで、go-cmpパッケージを利用することになりました。
使用参画して運用してみた感想
gomock
参入した当初から使っていたので当たり前になっていましたが、これがない世界線には行きたくないなぁと感じています。高性能なテストダブルがコマンド一発で作られるのはめちゃくちゃ体験がよく、癖はありつつも慣れたらその利便性の虜になりました。
ただ、呼び出し前に期待値を設定することを強制されるので、そこは柔軟性にかけるように感じています。他言語のテストダブル系のツールだと呼び出した後にそれを検証するというのが一般的なように思っていて、気づかぬうちにテストケースが複雑化していることはしばしばあります。
cmp
参入した当初から使っていたので当たり前になっていましたが、これがない世界線には行きたくないなぁと感じています(n回目)
差分の見やすさに加え、値の比較時に利用される便利なオプションがたくさん提供されているのも満足度が高いですし、なんなら自分で作ることも容易にできるので、普段使いの利便性と拡張性どちらの特性も併せ持ったヒソカみたいなパッケージだなと思ってます。
まとめ
今回の記事を書くにあたって、導入・運用の二つの観点でそれぞれの技術についてのメリデメだったり、各フェーズでの課題や成功体験をシェアすることができました。意外とこういう機会は少なく、業務の合間合間にさらっと雑談として話されることが多い印象ですが、理解の整理や今後同様のシチュエーションにおける道標となり得る感触があったので、また開催しようと思っています。
同様のテクノロジースタックを検討しているエンジニアの皆さんにとって、この記事が有用な情報源となれたら嬉しいです!
今後も我々の技術選定は続く ——