Sansan Builders Box

Sansanのものづくりを支える技術やデザイン、プロダクトマネジメントの情報を発信

レガシーシステムのおそうじ

年末ってことで。

Sansan事業部 プロダクト開発部 基盤チームの加畑です。
Sansan Advent Calendar 2018、11日目の記事です。

普段の本ブログは、いまをときめく機械学習領域のスーパーエースたちや、活気溢れる技術イベントや、圧倒的イケメンCTOが登場していますが、今回は地味で愚直な改善のお話です。 安心してください。

おかげさまでSansanは10余年もの間、特に大きな問題もなくサービスを提供し続けることができています。 このような歴史あるサービスの裏側では、ソフトウェアの本質的課題である「レガシー化」と向き合う必要があります。 今回は、レガシーシステムの改善という視点で、私が最近担当した3つの事例をご紹介します。

レガシーシステムとは

ソフトウェアの開発に携わる以上、レガシーという言葉は避けては通れません。 偶然ですが、Sansanでも最近立て続けに技術的負債を題材にした勉強会が開催されていました。

sansan.connpass.com

omotesandorb.connpass.com

ここで、レガシーという言葉について少し触れてみます。 一口にレガシーといっても、古いシステム=レガシーというわけではありません。 たとえ古かったとしてもきちんと動作し、メンテナンス性も高いシステムは、技術的負債の文脈でいうレガシーとは異なります。 名著『レガシーソフトウェア改善ガイド』ではレガシーを以下のように定義しています。

私の定義は非常に広いもので、保守または拡張が困難な既存のプロジェクトなら、なんでも「レガシー」(legacy)と呼ぶことにしている。

レガシーというとまずコードがイメージしやすいですが、同著ではコードに限らず、より広義にプロジェクト全体を対象としています*1。 その上で、レガシープロジェクト、つまり保守や拡張が困難になったプロジェクトに共通な性質として、「古い」「大きい」「引き継がれている」「ドキュメントが不十分」という4点を挙げています。

我々が日々開発を行っているSansanというサービス(以下、Sansan)は、長い歴史の中で、何世代もの開発者が流動的な仕様に対応してきた経緯により、レガシーなプロジェクトの性質をもっています。 実際、組織・サービスの成長に伴って、既存の仕様を踏襲しつつも保守や拡張を重ねてきています。

以降では、ソースコードとプロジェクトの中間をふわっと示す規模を指して「システム」と呼びます。レガシーなシステムは、レガシーなソースコードと比較すると以下のような特徴があり、改善のハードルが高いです。

  • サービスや事業の事情に依存しやすく、理想の状態がみえにくい
  • 影響範囲の把握に広い知識を要求される

レガシーシステム改善の戦略

レガシーシステムの改善、つまり既存システムの保守性・拡張性を高めるための戦略が大きく分けて2つあり、それぞれ「リライト」「リファクタ」と呼ばれています。

リライトは、既存システムを手放し、その仕様を踏襲あるいは改善しつつ、新たなシステムを構築する方法です。 既存のしがらみにとらわれず、理想とするシステムにより近いものを作り上げることができます。

リファクタは、既存システムを部分的に切り出し、改修を重ねていく方法です。 小さく始めやすく、イテレーティブなプロジェクトマネージが可能です。

レガシーシステムの改善に際し、このどちらを選択するかがプロジェクト戦略上の大きな分水嶺になります。 ここで、我々人間は、一般にリファクタよりもリライトを好みます。 レガシーシステムはその保守性の低さからレガシーと呼ばれているわけで、往々にして読み解くのがつらかったり、依存関係が複雑で修正箇所が広範囲にわたるためです。 レガシーな既存システムと相対したとき、それをがんばってなおすよりも、新たにシステムを構築したほうが楽そう、という気持ちになりがちです。

しかし、この直感に反して、リライトは基本的に悪手といわれています*2。 事業的にほとんどメリットがない、あったとしても他プロジェクトに対する優先度は上がりづらく、コストに見合わないと判断されやすいです。 まずはリファクタを進め、それでも立ち行かなくなった場合に、次の戦略としてリライトを検討するべきとされています。

Sansanにおける事例

Sansanにおいても、レガシーシステムを改善するアプローチとして、日々地道なリファクタを重ねています。 前置きが長くなりましたが、以下では、リファクタに相当する3つの事例をご紹介します。

事例1. 名刺画像処理アーキテクチャの刷新

サーバ構成というレイヤで、名刺画像を処理するアーキテクチャをシンプルかつスケーラブルにした事例です。

課題

Sansanは名刺を扱うサービスであり、大量の名刺画像を保持しています。名刺画像のライフサイクルは概ね以下のとおりです。

  1. 名刺画像を取り込むクライアント*3は名刺画像をサーバに送信する
  2. サーバは受信した名刺画像のサイズを正規化し、AWSのS3に保存する
  3. 名刺画像を参照するクライアントは正規化済みの画像を取得し、ユーザに提供する

既存のアーキテクチャでは、2にあたる処理を、「画像サーバ」と呼ばれる単一のサーバが行っていました。画像サーバの実体はEC2上で動くnginxであり、その内部でLuaのコードが正規化処理(S/M/Lの3サイズ)を施していました。 また、3においても画像サーバを経由していました*4

f:id:kyabatalian:20181210171908p:plain
旧アーキテクチャ概要図

従来はこのアーキテクチャで十分に動作していたのですが、サービスの成長に伴って画像サーバのCPUリソースやパフォーマンスの面でボトルネックとなりつつありました。 スケールアップ、スケールアウトで凌ぐことも可能でしたが、将来の運用コスト増大が課題となっていました。

アプローチ

「画像サーバを使わないアーキテクチャ」へ刷新しました。具体的には以下を実現しました。

  • 各クライアントが直接S3にアクセスする
  • 画像の正規化処理を分散非同期化する

f:id:kyabatalian:20181210171931p:plain
新アーキテクチャ概要図

画像の正規化処理には、内製のメッセージングシステムを用いました。 名刺画像がS3にPUTされるとイベントが発火し、Cloud Watch Eventで生成されたテキストベースのメッセージがSQSのキューに投げ込まれます。 メッセージングシステムはキューをポーリングしており、新たな名刺画像の保存を検知するとS3からその名刺画像を取り出し、正規化処理を施してS3に書き戻します。

非同期処理としてAWS Lambdaを使う案もありましたが、名刺画像はSansanにとって根幹であり、不確実なリスクへの柔軟性を重視して、社内で実績のあるフルコントロール可能なシステムを採用しました。

また、修正は複数のアプリケーションに対して段階的に適用していきました。 「名刺画像の保存に失敗する」という事故は許容できないため、時間をかけてでもリスクを最小化したいという欲求があったためです。

結果

特に問題なく完了し、パフォーマンス・リソース・運用コストのボトルネックを解消することができました。 以下は、複数回のリリースを経て、画像サーバのCPU利用率が下がっていく様子です。

f:id:kyabatalian:20181206215246p:plain
画像サーバのCPU利用率

複数のアプリケーションを段階的にリリースしていったため、徐々に下がっていっていることがわかります。

考察

今回のように「問題なく動いているもの」に対し、将来の課題に備えて現時点で手を入れるか否かは難しい判断です。 それほど手間がかからない場合は通常のプロジェクトの合間の時間を使ったり、個人タスクとして片付けてしまってもいいかもしれませんが、ある程度の規模になると、無理が生じてきます。

本事例においては、画像サーバが近い将来にボトルネックになることはなんとなくわかっていた中で、実際に時間を使う価値があることを示すために、客観的な指標をつくったことがよかったと考えています。 まず、インフラチームが整備してくれている監視ツールを用いてCPU利用状況の推移や画面のパフォーマンスを把握しました。次に、画像サーバを経由しないアーキテクチャにするとどれくらいパフォーマンスの改善が見込めるかを、開発環境を使って実験しました。

余談

弊社のもうひとつのサービスであるEightも、6年以上の歴史があり、同じくレガシーシステムと向き合っています*5。Eightでの名刺画像周りのアーキテクチャ刷新については以下記事で紹介されています。

buildersbox.corp-sansan.com

似たようなサービスに見えても、求める要件、フェーズ、思想や文化が異なると、アーキテクチャ設計は変わってくるということがわかっておもしろいです。ご興味のある方はぜひ目を通してみてください。

事例2. 名刺データ量の削減

データベースに存在する不要なカラムをDROPした事例です。

課題

Sansanでは名刺の情報をはじめとする大量のデータを、RDBMSに保持しています。 その中で、最も中心的で大きなテーブルに、不要とみられるカラムがありました。 これははるか昔に作成された全文検索用のテキストを保持するカラムなのですが、現在その機能は使っておらず、データベースの容量を圧迫するだけの存在になっていました。

このカラムの存在と、どうやら使われていなさそうだという認識は数年前からあったのですが、DROPされずに残っていました。 このカラムが本当に不要なのか、DROPすることでどういう影響があるのか、確信をもてないためです。これは典型的なレガシーシステムの課題です。 例えば、アプリケーションのコードを管理するGitリポジトリ内の全ファイルをgrepしたとしても、ソース管理されてない古(いにしえ)のバッチが存在しないとも言い切れません。

アプローチ

そこで、この不確定さを前提とした上で、「参照箇所の抽出・修正漏れ」と「漏れが発生した場合の被害」を最小化する方法を模索し、様々な計画を比較検討した結果、最終的には以下の手順をとりました。

  1. アプリケーションコードの洗い出し
  2. 暗黙的に参照している箇所があれば修正
  3. アプリケーション全体の動作確認
  4. 開発環境の対象カラムをこっそりRENAMEしてしばらく放置
  5. 本番環境の対象カラムをRENAMEしてしばらく放置
  6. 本番環境の対象カラムをDROP

もちろんこれが必ずしも最適な手順というわけではなく、改善によって得られる価値及び速度と、リスク・コストとのバランスを勘案した結果です。

Sansanというサービスを提供するために、裏側では複数のアプリケーションが動いており、概ね各チームが担当機能を持っています。 それらすべてを私が把握し、テストすることは不可能です。そこで、1と3では、部内メンバーの手を借ります。 部内メンバーは当然それぞれのプロジェクトをやっているわけで、このリファクタのために時間を割いてもらうためには、適切な説明と無理のない期限を提示するなど、適切な配慮が必要です。

また、4のRENAMEを挟んでいるのは、もし失敗した場合、つまり万が一参照している箇所が残っていてアプリケーションが動作しない、などの問題が発生したときの、ロールバックを想定しています。 テーブルのサイズが極めて大きく、DROPしてしまった場合にもとに戻すのに膨大な時間がかかるのに対し、RENAMEだと一瞬で終わります。 一方、RENAMEでもひろえないケースもあります。アプリケーションから SELECT * していて、かつアプリケーションで row[3] みたいに配列のインデックスでアクセスしている場合、テーブルのカラム名が変わってもアプリケーションは(意図しない状態で)機能してしまいます。Sansanの場合は設計上このようなケースは限りなく0に近く、DROPに伴うロールバックコストより優位です。このような判断を積み重ね、上記のような手順にしました。

結果

問題なくDROPまで完了し、データベースサイズを削減するとともに、不要なアプリケーションコードを根本から削除することができました。 以下は、複数のインスタンスで、ディスクサイズが削減した様子です。

f:id:kyabatalian:20181207155126p:plain
データベースサイズの削減

また、DROPしたカラムの値は、テーブルへのレコードの挿入時に、トリガを使って作成していました。 カラムをDROPしたついでにトリガもDROPすることができ、挿入のパフォーマンス向上にも寄与しました。

考察

データベースの寿命はアプリケーションよりも長く、一度構築すると手を入れづらい箇所です。 一方で、データベースのスキーマ設計はある意味ではアプリケーションの根幹であり、本来的にはサービスの成長に伴って修正されていくべきともいえます。 このようなジレンマによりデータベースのリファクタは大変ですが、そのぶん長期にわたる効果が期待できます。

今回のプロジェクトを進めるにあたりいろいろと調査したのですが、データベースのリファクタに関する情報はインターネット上にはあまり見当たりません。 理由としては、センシティブな情報であるということと、サービスごとの事情が大きく影響することが考えられます。 私の場合は、部内で有識者へ相談した以外に、同事例であるメルカリさんの記事や、書籍PostgreSQLユーザのSlackコミュニティを参考にして、とりうる選択肢をあげ、検証しながら計画を進めました。

事例3. サンプル名刺の撤廃

レガシーシステムのリファクタを起点としつつ、結果としてUI改善にも貢献した事例です。

課題

Sansanにログインすると表示されるホーム画面には、自身の所有する名刺の一覧が並んでいます*6。 ここで、まだ名刺を一枚も取り込んでいないユーザに対しては、「名刺を取り込むと、このように表示されますよ」ということを伝えるため、サンプル名刺を表示していました。 このサンプル名刺は新規ユーザの登録と同時にシステム側で自動的に作成・保存しており、以下の課題がありました。

  • ユーザ数のぶんだけ同内容の名刺画像が複製されるため、ストレージを無駄に消費する
  • ユーザ自身が取り込んでいないのに総名刺数に加算されてしまうため、利用促進の運用時に考慮が必要になる

もともとの発端は、事例1において修正箇所のひとつとしてサンプル名刺が話題にあがりました、 チーム内で議論を進めるうちに「そもそもサンプル名刺いらなくない?」となりました。 ただ、これまでの事例と違ってUIに影響する以上、レガシーシステム改善だけの判断で進めることはできませんでした。

アプローチ

基盤チームの担当領域は、基本的にUIよりも下のレイヤにあたるため、プロダクトマネージャ(以下、PM)やデザイナが所属していません。 そこで、当該機能を担当するチームのPMと直接会話し、UIの改善と合わせてチーム横断的にプロジェクトを進めることになりました。 また、UIが変わることに加え、前述のユーザの利用実績にも影響するため、プロダクト開発部の枠を超え、ユーザと直接やりとりをする部署にも調整をお願いしました。

このとき、ユーザへのメリット、社内の運用チームへのメリットを見出す必要がありました。 単に「レガシーシステムだからなくしたい」というのはエンジニア側の都合でしかなく、他のメンバーも業務として時間を割く以上、それに見合う理由が必要だからです。 数回のヒアリングと議論を重ね、合意したところで各担当者のタスクと期限を決め、あとは各チームで責任を持って進めました。

結果

サンプル名刺の登録を廃止し、代わりに名刺のスキャンを促進するUIをリリースしました。

f:id:kyabatalian:20181207171041p:plain
名刺のスキャンを促進するUI

これにより、システム観点、ユーザ観点、運用観点の課題が解決しました。さらにUI改善自体の効果もあり利用率向上に寄与しました。

f:id:kyabatalian:20181207172033p:plain

考察

結果としてうまくいった要因のひとつは、影響する各チームに対しサンプル名刺の撤廃に対するメリットを導出したことだと考えています。 課題にあげていた利用促進への影響などは、最初から把握していたわけではなく、各方面へのヒアリングの結果でてきた課題です。 これがプロジェクト後半に発覚していたら、他のタスクにも影響して全体の進行が遅れているところでした。

「レガシーシステムの改善」はエンジニア起点の欲求のため、そこを入り口にすると、その他のチームや上層部からみるとプロジェクト自体がコストに見えがちです。 トレードオフだと思っていたことが実はそうでもなかったり、他のメリットを提供することでデメリットを上回る解が見つかってトレードオフ自体を解消できる場合もあります。 全員の幸せを諦めずに考えきるということが重要かもしれません。

事例からの学び

レガシーシステムの改善という文脈で、最近かつ私が担当した3つの事例を紹介してみました。 もちろんここに書いている以上に細かな部分では、うまくいかなかったところもあるのですが、概ね成功したと思っています。 これらを通じて、レガシーシステムのリファクタを成功させるために重要だと感じたのが、「計測」「分解」「合意」です。

計測する

リファクタに限らず、何かを改善するときにまずやるべきなのは現状の把握であって、レガシーシステムの場合は、現状の課題を計測すべきです*7。 何かを改善するための第一歩として必要なのは、現状を客観的に把握することです。現状を知って初めて、問題定義が可能になり、改善のプロジェクトがはじまります。

事例1を例に取ると、画像サーバは、メンテナンス性は低いものの安定に稼働していましたし、スケーラビリティの観点では、サーバのスケールアップ・アウトでまだしばらくは十分に使える状態でした。 このように「現状問題がないもの」にいま手を入れるかどうかの判断に最も有効なのは、客観的な計測値を使うことです。 開発環境を使って改善前後のパフォーマンスを計測し、大幅に改善する見込みがあること、及び、そのトラフィックがサービス全体において有意に支配的であることを概算しました。

現在計測されていないものを計測するのは往々にして大変ですが、初期にやっておくとプロジェクトを進める上で最後まで効いてきます。 「なぜやるのか」を客観的に定義しておくことの重要性は、リファクタでも通常のプロジェクトでも変わりません。

分解する

なるべく刻みましょう、ということです。 レガシーシステムのリファクタでは、得られる効果が見えにくいぶん、リスクが悪目立ちしがちです。 いま動いているものに手を入れる以上、障害を起こさないことを要件としますが、完全なプロジェクトは存在しません。 そこで、なにかあったときのダメージを最小化する計画をたてるべきです。

事例1では、なるべくダメージの少ないアプリケーションから順番にリリースしていきました。 事例2では、DROPの前に動作確認やRENAMEなどを挟みました。 事例3では、まず利用状況をレポートするバッチと社内ツールを先にリリースし、運用チーム側で問題が起きないことを確認した上で、UIをリリースしました。

リリースの速度とのトレードオフで刻んでいるひまがないケースもありますが、まずプロジェクトマネジメントとしてリスクの見積もりは基本であり、レガシーシステムのリファクタにおいても同様です。

合意する

ステークホルダー、他チームメンバー、自チームメンバー、そして自分自身がそのプロジェクトのもたらす価値について納得するべきです。 特にレガシーシステムの改善は、通常の開発プロジェクトと比してその価値がわかりづらいという問題があります。 プロジェクトを始めたはいいが、周りや自分自身に「なんでやってるんだっけ?」という問いが生まれることがよくあります。 これを説明するために重要なのが、前述の計測値です。

レガシーシステムの改善に関してよく聞く話で、「経営層がその価値をわかってくれない」というのがありますが、多くの場合は説明不足か、その方法が良くないケースがほとんどだと思っています。 経営層にとってもそれが事業にとってリスク、コストであるならば排除したいはずです。 一方で、技術的な都合を並べて「レガシーなのでなくしたい」と言われたところで判断のしようがありません。

だれかに判断を迫るなら判断に足る情報を提供する必要がありますし、レガシーシステムの改善に関しては特にそれが重要です。 そして、「判断に足る情報」として有効なのが、前述のとおり計測した結果です。 その点、Sansanではかなり理解があり、その面で苦労した記憶はありません。 強いていえば、事例3ではPMやデザイナを含めた他の開発チーム、また運用チームにも実際の業務が発生するため、事業部長に対して説明が必要でした。 画像サーバの維持コストを示したら瞬時に承認されました。

まとめ

レガシーシステムの改善に関する3つの事例と、それらのプロジェクトで得た個人的な学びを書いてみました。

会社のブログなのでかっこつけて書いていますが、うまくいかなかったこともたくさんありますし、私個人の功績ではもちろんなく、周囲に助けられまくって進めることができました。 今回の改善はほんの一部で、まだまだレガシーシステムなので、引き続きおそうじをがんばっていきたいです。 こういう地道な改善も楽しくがんばっていますよ、ということが伝われば幸いです。

最後までお読みいただきありがとうございました。 年末ですし、あわよくば2019年にプロジェクト化するべく、レガシーシステムに想いを馳せてみてはいかがでしょうか。

宣伝

LT大会をやります。

sansan.connpass.com

部署、サービス、技術領域をまたがって11名ほど登壇し、カレンダーに書いたことや書かなかったことをお話します。 楽しいはずなので、ぜひお気軽にご参加ください。

*1:コードを対象とした書籍『レガシーコード改善ガイド』もあり、こちらも名著として知られています

*2:少なくともレガシーシステム改善、技術的負債の返済という文脈においては

*3:スキャナやスマホアプリなど複数の経路があります

*4:2でネットワーク障害などによりS3への保存が失敗した場合に、画像サーバのローカルに一時保存するためです

*5:たまに聞かれるのですが、裏側の仕組みは基本的に独立しています

*6:画面はプロダクトサイト https://jp.sansan.com/ を参照ください

*7:通常のサービスのリリースでKPIを計測するのと同じです

© Sansan, Inc.