マイクロサービス化するならリビルドで!ビジネスロジックをGoで書き直してわかったこと

この記事では モノタロウがGoとprotobufで進める爆速マイクロサービス開発とそれを支えるプロセス - MonotaRO Tech Blog のうち、主にアーキテクチャにおける詳細について紹介します。

自己紹介

藤本 洋一 プラットフォームエンジニアリング部門 CTO-Officeグループ AVLチーム

楽天、SaaSベンチャーを経て、モノタロウに入社してマイクロサービス化にとりくむエンジニアの話

2019年5月入社。商品検索基盤のマイクロサービスとデータ生成に携わり、その後モダナイゼーションのファクトとなる横断的なシステムの分析、技術決定プロセスの策定を行う。AVLでは主に実装におけるアーキテクチャを担当する。

マイクロサービス化について

一般に、モダナイゼーションはガートナーの「7つのR」を軸に議論され、クラウド移行やマイクロサービス化が期待されることが多いと思います。その中からアプリケーションのマイクロサービス化と関係しそうなところを抜き出してみると、内容にかなり幅があることがわかると思います。

キーワード 概要
Refactor(リファクタ) 既存コードを再構成して技術的負債の解消や非機能要件の改善を行う
Rearchitect(リアーキテクト) 大幅にコードを変更して新しいアーキテクチャに移行し、新たな能力を獲得する
Rebuild(リビルド) 外部仕様を維持したまま新たなコードベースで再構築する

特に、アプリケーションにおいて内部ロジックは既存コードのままにするのか、それともロジックも一から再実装するのか、というのは悩ましいポイントかと思います。

分割だけであれば既存のコードベースを維持する選択肢もありそうですが、並行稼働期間の二重メンテナンスのような別の課題も出てきます。

モノタロウでは、AVLというプロジェクトでGo言語でのマイクロサービス化を行いました。ここでのリビルドにあたりますが、その中でどのようなプラクティスが役立ち、最終的にどんなメリットがあったのか、紹介してみたいと思います。

課題を認識する

こちらの記事でも触れているように、モノタロウではサプライチェーンの高度化を進めています。

しかし、既存コードはクリーンアーキテクチャ化を進めてきたものの、機能の多さから内部依存が複雑でメンテナンス性の悪化や、パフォーマンスの制約がありました。

スコープと技術選定

そこで、AVLでは出荷目安・出荷ステータスを計算するエンドポイントを対象に、「モノリスからマイクロサービスへ」を参考にしつつスキーマファーストの考え方で分割することとしました。

このエンドポイントは、

  • 1リクエストで最大100品番(=SKU)
  • リクエストパラメータによって条件が変わる
  • 高速なレスポンスタイム

を満たさなくてはならず、パフォーマンスとスケーラビリティが求められることが分かっていました。 そこで、社内で検討が進んでいた最新の開発プラクティスで実践することとしました。 主なところでは、

  • protobufでスキーマ定義する
  • Goで戦術的DDDを使う
  • GitHub Copilotを活用する
  • ストラングラーパターンで移行する

を前提として検討を進めました。

あえてスコープから外したことにDBスキーマの分離があります。今回は参照系のみということでDBは既存のものを使うこととしました。

ゴールイメージを共有する

まず、クラス図を作って登場するクラスと呼び出し関係を把握しました。既存コードを分析した上で、Mermaidで作成したToBeが下図です。

Mermaidのクラス図

これによって、チーム内でプロジェクトの工数感や現状からどれくらいの飛躍を目指すのか共通認識が生まれます。また、見積もりの精度も改善されます。

ゴールの意識を合わせる

同時に、設計に登場するキーワードも洗い出すことができます。AVLでは用語集も作成して、今後のビジネスにおける概念との結びつきを確認しながら進めました。

また、パラメータや処理のパターンがある程度明らかになったことで、リクエストの品番数が多い場合にパフォーマンスの問題が起こりそうなことも分かりました。

既存コードから分かった問題点

既存ソースコードの分析を通じて、以前にクリーンアーキテクチャ化されたものの、その後の改修の積み重ねによって次のような問題点があることがわかりました。

曖昧なデータ構造

元のコードには都度for文で配列からIDを探している箇所が複数ありました。

古くからのコードがリストでレコードを取得していた名残りかもしれませんが、データがあるはずなのかそうでないのか、重複レコードを許容するのかどうか、コードに表現されないという問題があります。

for stat in stockStat:  # 在庫状況をループ
    if stat["dcCode"] == dcCode:  # 倉庫コードが一致なら
        currNum = stat["currNum"]  # 現在在庫数を取得
        orderNum = stat["orderNum"]  # 受注数を取得
        break  # ループを抜ける

処理フローの混在

処理がカスケードされたif文で構成されており、それぞれ見ているフィールドが異なるため、どの条件に当たるのか自明ではありませんでした。機能追加した時に条件を付け足していったものと思われます。

例えば、以下のようなコードではif文の順序によって結果が変わってしまいますが、なぜその順番でなくてはならないのかコードから把握するのは困難です。

# 受取商品の場合
if is_pickup:
    return ...
if shortStockDef["productType"] != WProductType.STOCK:  # 在庫でないなら
    return ...
if reserveNum >= quantity:  # 引当可能なら
    return ...

アドホックなデータ取得

主要なデータは一括で取得していましたが、比較的最近追加されたと思われる一部の処理では品番ごとにその処理に必要なデータを追加で取得していました。

リクエストの品番数が多い場合に大量のクエリを発行してしまう、いわゆるN+1問題のリスクがあります。

効果的な改善を行う

処理フローを分割する

長年改修されてきたこともあり、元のコードはデータ参照と条件分岐が複雑でしたが、既存コードの分析からある程度処理フローを分離できそうだと分かりました。

AVLでは戦術的DDDでモデルを作成して、ロジックをメンバ関数にして共通のインターフェースを作り、コードの見通しを良くしてユニットテストを増やしました。

リファクタリング

一般的なリファクタリングではありますが、基本的にコード上で新旧比較できる範囲で行いました。 例えば、関数の呼び出し順は変えない、メソッド名を大きく変えない、といった方針で進めました。

そんな進め方で良いコードになるのか?と思われるかもしれませんが、最低限のリファクタリングができていればGoのメリットを享受することができます。

一旦Goの型システムに沿ったコードになれば、言語サーバーであるgoplsやgofmtのようなツールや、GitHub Copilotを積極的に活用できるようになります。上図の左側のコードではCopilotが生成するコードも中途半端になりがちですが、右側であればそれなりに使えるコードを生成してくるので、変更やテスト追加が格段に容易になります。

いきなり完璧を目指すのではなく、経済的合理性を優先して早くメリットを得ることを優先しました。

N+1問題とロジックの独立性を考慮した設計

既存のデータの流れを分析して、品番をまたがってデータ取得した方が良い部分と、品番ごとにロジックを実行した方が良い部分があることがわかりました。

そこで、データ取得とロジックを分離して、一度中間モデルに分解した上で、品番ごとにロジックを適用するようにしました。

データ取得とロジックの分離

データは実践ハイパフォーマンスMySQL 第3版にも書かれているように一括でSELECT … INで取得して、sqlcで取得したデータを中間モデルに変換します。mysqlのsqlcはやや制約が多いのですが、比較的シンプルなクエリでは問題ありませんでした。

ロジックは品番ごとに実行することで、実装がシンプルでユニットテストも書きやすくなります。 また、並列処理で問題になりやすいロックも最小限で済みます。

実際には、この変換を2段階行っています。そうすることでN+1問題を解消しつつ不要なデータ取得を削減する効率化と、更新頻度の低いテーブルをキャッシュでラップする抽象化を同時に実現することができました。

安全に移行する

実行時のデータを取る

以上の分割を行って、想定通り並列処理されているか、きちんとデータで判断したいところです。一般に、分散トレーシングかプロファイラで見ることが多いと思います。

今回の場合、並列実行を見るためトレースを中心に活用しました。

Datadogでのトレースの例

このように、Datadog上で想定通りに並列処理されており、処理時間も十分短いことがわかりました。

Datadogの活用など、モニタリングの詳細については別記事で紹介予定です。

新旧比較による検証

ストラングラーパターンでのリリースに向けて、新旧のエンドポイントに同じリクエストを投げて結果の差分を毎日チェックするスナップショットテストを実施して、同等のロジックになっていることを検証しました。

こちらの詳細も別記事で紹介予定です。

まとめ

モノタロウにとってはビジネスロジックをGoで実装する初めての試みでしたが、ここまでの成果を通じて、マイクロサービスを通じたリアルタイムな出荷目安をより広くフロントエンドに展開できるようになりました。

レガシーなロジックだから触らないのが一番、と考えてしまいがちですが、十分な現状分析をした上で妥当な改修範囲を設定してリビルドすることで、再実装のメリットを最大限に生かしたコードの書き換えと開発プラクティスの改善が実現できます。

その一方で、マイクロサービス化に適したモノリスの分割や、スキーマファーストな開発、Goの開発手法の展開はようやく手がついたばかりというところです。

モノタロウではAVLを皮切りに 「全ては会社の競争力を生み出すために」アーキテクチャを刷新し、ドメインモデリングも組織再編もエンジニア教育も一つ一つ丁寧に積み上げてモダナイズを進めた話|CTOロングインタビュー - MonotaRO Tech Blog のチャレンジが進みます。

モノタロウでは随時エンジニアの採用募集をしております。この記事に興味を持っていただけた方や、モノタロウのエンジニアと話してみたい!という方はカジュアル面談 登録フォームからご応募お待ちしております。