はじめに
Platform Group の久保田( @ykbt13 )です!
BASEではリアーキテクチャとしてバックエンドの既存機能を旧リポジトリから新リポジトリへ移行する作業を日々行っています。詳しく知りたい方はぜひこちらを参照してください。
そんななか、BASEにおけるコア機能の1つである商品の発送機能の移行が行われました。しかしながら、コア機能であるがゆえに様々な改修が繰り返されて複雑化してしまった発送機能では移行前の動作を保証する術がテストのみでは不安があります。
そこで、リアーキテクチャを円滑に進めるべく、本番環境上で移行前後の処理を同時実行しデータベースの結果を比較することで動作の保証を行うツールを開発しました。
この記事では、同様にリアーキテクチャを進めている方々を対象に、そのツール(BASE内では通称DryRunと呼んでいますので以降DryRunと記載させていただきます)について、記載していきます。
TL;DR
- リアーキテクチャを手助けするツール DryRunを開発しました
- 実運用では部分的に本番環境を使ったDryRunを行いました
- DryRunを作っていくにあたって、いかに外部影響が少ないサンドボックスを作り上げることが重要なのかということに気づきました
リファクタとリライト
少し本記事の内容から反れますが、背景につながりますので、リファクタとリライトについて触れていきます。
「レガシーソフトウェア改善ガイド」において、レガシーなソフトウェアを改善していく方法としてリファクタとリライトについて触れられています。
リファクタリングは外部から見た際に内部の挙動を変えずにソフトウェアを改善していく作業であり、リライトは同一機能を一から作り直す作業に当たるものです。そのうえでリライトはリスクとコストのかかるものであり、できるだけリファクタリングのみで改善ができないのかを検討したうえでリライトを選択すべきであるとしています。ただ逆説的に言うと、そのリスクの保証やコスト面の解決ができれば、リファクタリングでは得られなかったメリットを享受したうえで改善ができるのではないかとも言えるではないかと思っています。
リアーキテクチャをお手伝いするツール DryRun
さてBASEにおいては、レガシーソフトウェアの改善としてリアーキテクチャが行われています。直近ではBASEにおけるコア機能である商品の発送機能のリライトによるリアーキテクチャが行われました。
なぜリファクタではなくリライトによる移行なのかというと、旧リポジトリでは MVC フレームワークでの密結合を前提としたモノリスですが、新リポジトリではDDDとクリーンアーキテクチャをベースとした疎結合なモジュラーモノリスであるため、アーキテクチャのパラダイムシフトが起きておりチームとしてリライトによる移行を選択したからです。
リライトによる大きなリスクの1つとして、移行前後でのリグレッションリスクというものがあります。これはもちろんテストを通して保証することになりますが、移行前の挙動と同義であるというのを保証するのにテストのみでは限界があると思っています。そこで、移行前と移行後の更新処理に着目して、データベースに対する永続化が同じものであるかどうかというものを確認することでその保証が行えるのではないかと考えました。これを実現するツールを作成しています。
永続化を行っている箇所をある種のサンドボックスとして提供して、移行前の旧機能と同時実行し各処理で更新されたテーブルを比較することで、データベースに対する永続化が同じものであるかどうかを比較するものとなっています。移行後のリライトされたコード視点ではサンドボックスであるかの区別はできず、あたかも本来行うべき永続化処理を行っているかのように振る舞っているため、DryRunという命名をしました。
かなり単純化されたものにはなりますが、擬似的なPHPのコードでDryRunの挙動を示しますと
移行元の擬似コード
// 移行先の機能へのリクエスト function requestNewFeature(): NewResult { } // 更新された各データベースの比較 function compareDatabase(NewResult $newResult, OriginResult $originResult): CompareResult { } // 本来実行される移行元の処理 function executeOrigin(): void { if ($isDryRun === true) { $newResult = requestNewFeature(); } // 移行元の処理の実行 // DryRunであってもなくても必ず実行されます if ($isDryRun === true) { compareDatabase($newResult, $originResult) } }
移行先の擬似コード
// データベースにアクセスする根本のinterface interface DatabaseAccess { } // DryRun中のデータベースアクセス class DryRunDatabaseAccess implements DatabaseAccess { } // 通常のデータベースアクセス class OriginDatabaseAccess implements DatabaseAccess { } function getDatabaseAccess(): DatabaseAccess { return $isDryRun == true ? new DryRunDatabaseAccess() : new OriginDatabaseAccess(); } // DryRun向けの処理結果を作成する function createDryRunResult(): DryRunResult { } // 本来実行される移行先の処理 function executeNewFeature(): void { $databaseAccess = getDatabaseAccess(); // 移行先の処理 // DryRun中だとしてもそうじゃないとしても挙動は変わりません。 return $isDryRun == true ? createDryRunResult() : $result }
となります。実際には移行先環境ではDoctrineというORMを利用しており、DoctrineのDBコネクタをオーバライドしているのと、Ray.DiというDIフレームワークを利用して、擬似コードのような形でデータベースの向き先を変更しております。
仕組み上ある程度部品化されたコードが準備されているものの、移行対象の機能ごとにサンドボックスを一つ一つ作り上げる必要があるため、頻繁に使えるツールではありません。しかしながら、準備できさえすれば各種環境で動作させることが可能であり、移行元のコードが必ず実行されるようになるため、本番環境でさえも動作させることができます。
発送処理におけるDryRunの実運用
発送処理のリアーキテクチャは実働部隊が別途おりまして、DryRunはPlatformチームにて開発をいたしました。DryRunの開発が終わったタイミングで、リアーキテクチャされた発送処理に対して細かい単位(具体的には、支払い方法別)に分割して順次DryRunを適応していき、本番環境へのDryRunを実行していきました。
本番環境で動作させるということで安全に倒すように様々なガード処理を加えていたのですが、意外にもスムーズに動いてしまい、安心するとともにすごいものを作り出してしまったなあと開発当時に思っていた記憶があります。
また特に苦労した点なんですが、BASEのプロダクトの制限で個人情報を保存できる環境に制限があり、比較結果のログの出し方やサンドボックスに保存する際のマスク処理など、細かい単位での対応が必要なところに苦労いたしました。振り返ると1つの共通化したパーツを作り込んでいくというよりかは、泥臭く細かいサンドボックスを作り込んでいくという作業がメインだったなと思います。
そうして順次DryRunを稼働していく中で、実際に開発していただいていたエンジニアと協力して、見つかっていったバグなどの修正を加えてもらいました。
DryRunを利用する目的として、データベースに対する永続化が同じものであるかどうかというものがありましたが、実運用していくうえで副次的にこのようなメリットも見つかっています。
- 実際に本番で運用されているデータという膨大なパターンのあるデータを使ってリリース前にコードを走らせることができた
- リライト中に旧機能への新規開発が行われていてその追従がきちんと行われているのかを確認できた
これによって、細かいバグなどがリリースまでに解消でき、また本来の目的であるリグレッションへの対応も行えました。BASE内部の話になってしまいますが実際に開発を行ったエンジニアの方々お疲れ様でした。
反省点や課題点
実際に運用してみたうえでメリットだけはなく、デメリットもいくつか出てきてしまったのでそれらも振り返っておきます。
少し細かい点となってしまいますが、こういった課題がありました。
- 移行前後で同時に実行することによるパフォーマンスへの影響があった
- データベースへの書込は実際のRDBMSに書き込んでいたことにより、他のDryRun処理への影響が完全には分離できていなかった
いかに他への影響が少ないサンドボックスを作り込んでいくべきかという点が重要だったんだなと作ってみて改めて感じました。
特に今回、仕組みづくりに使える人員コストを加味して、PHPコード上(つまりはアプリケーションレイヤー)でサンドボックスを作り込んでいたのですが、そもそもデータベースから分けることやモック自体もHTTPサーバ化してPHPコード外で行うなど、各サンドボックスが得意な部分で分割して作り込んでいくというのが良かったのかもしれないと思っています。また、他のDryRun処理への影響という意味ではバージョニングがなされた形でデータベースへの書込みを行うことで回避できたなという反省点もあります。
このあたりはリアーキテクチャが続いていく中で、またDryRun機能を使う機会はあると思っているので、改善していければなと思っています。
おわりに
本記事では、リアーキテクチャをお手伝いするDryRunというツールについて紹介させてもらいました。
いざ作って動かしてみたときに想像していたよりもきちんと動作してしまい、本番環境でデバッグをするというとんでもないものを生み出してしまったなという感覚がありますが、個人的には作っていった中でいろいろと学びがありました。
BASEという環境において作られたものになるので、どこまで外部の方々に参考になるのかはわかりませんが、こういったリアーキテクチャの方法もあるんだなという一例として掲示させていただきます。
どこかでこんな方法で移行作業を行っているのだなと参考になるときが来れば幸いです。