3週間で48,000行のコードをこの世から抹消した話
qsona (twitter) です。以前、7,600行のコードを安全にこの世から抹消した話 という記事を投稿しましたが、今回はそれよりもずっと泥臭い話を書きたいと思います。あまりテクニカルな話はありますが、現場における取り組み・試行錯誤の経過を読んでいただければ幸いです。
背景
肥大化するRailsサービス
FiNCはマイクロサービスを指向しており、主にRuby on Railsで書かれたサービスが30個ほど存在します。しかし、FiNCアプリのメインとなるRailsのサービスは、テーブル数800を超える大きなサービスになっています。
FiNCのサービスは2014年から書きはじめており、かなり初期の段階(2015年)からマイクロサービス化を意識してきました。にもかかわらず1つのサービスが肥大化している理由はいくつかあります。
- 最初の1〜2年ですでに大量のコードが書かれていた。
- 開発初期にドメインが固まり切っていない機能は、いきなりマイクロサービスとして開発を始めることによるデメリットが大きいので、メインサービスに実装される傾向にいる。
このように、マイクロサービスを指向していても、メインのサービスはしばらく肥大化しつづけるという例は、FiNCに限らず他の会社のシステムでもよく見られるのではないでしょうか。
大量のデッドコード
一方で、FiNCアプリもメジャーバージョンアップを重ねており (現在は ver. 5)、最新版のアプリからは使われていない過去の機能やAPIも多く存在しています。
使われていないコードが多く残っていることは、様々な問題を引き起こします。たとえば、
- 問題が発生したときに、検索のノイズが増え、解決が遅くなる
- テストコードも消せていないため、CIに時間がかかる
- 起動時間が遅い
などがあります。特にCIについては、並列実行を取り入れても30分かかっていました。30分が速いか遅いかは組織によって感じ方が違うかもしれません(もっとかかっているところもあるでしょう)が、価値のデプロイのスピードを重視してマイクロサービスを取り入れているFiNCとしては本来許容できる長さではありません。他のサービスであればローカルで全部自動テストを回しても数十秒で終わるケースが多いので、なおさら負担を感じています。
したがって、使われていないコードは消えているに越したことはありません。にもかかわらず、使われていないコードが残ってしまう理由は、以下のようなものがあるのかなあと思っています。
- 機能の多くはAPIとして提供されており、クライアントの後方互換性を残すためにしばらくはAPIは残さなければならない。消すタイミングを逃してそのままになる。
- APIは内部通信で使われている場合もあり、使われている場合は他チームとのコミュニケーションや他チームが呼び出しを消す手間が必要になる。
消す瞬間の手間というよりは、消すための手続きや、いわゆる負債の管理が大変という話です。APIで呼ばれているのでDB直参照されているパターンよりはずっとマシではあるのですが、とはいえ消すまでに一苦労があります。
もちろん、コードを消すのは早いほど低コストでできるので、可能な限り早く消すべきだったのは間違いありません。
“やっていき”を阻みやすい構造
現存するほとんどのマイクロサービスは、テーブル数100以下の小規模サービスで、技術者やチームのオーナーシップも明確です。したがって、ライブラリのアップデートやリファクタリングは、自然にチームのライフサイクルに組み込まれて行われますし、規模が小さく影響範囲が限られているので少ない労力で進められます。
一方、問題となるメインサービスに関しては、ありとあらゆるチームが関係する(開発する可能性がある)、いわゆる神サービスでした。
基本的に活発に開発される機能に関してはどんどん追い出され、各チームが所有するサービスに実装されるようになっていっています。とはいえ、現在でもオーナーは不在で、なんとなく共同でメンテナンスされているという状態です。
各マイクロサービスでは、開発者がストレスを感じずに開発できていますが、たまにメインサービスに実装が必要になると、実装するのに体感で3倍くらい時間がかかります。とはいえ、たまにしか触れないので個人レベルで見ると改善する労力に対してメリットが少なく、結果として放置されやすい構造になっていました。
機運
あるサービスの完全終了
メインサービスのAPIを使ったあるクライアントアプリケーションがあり、このアプリケーションはしばらくアップデートを行わず休止状態にありましたが、この提供が3ヶ月ほど前に完全に打ち切られました。それにより、さらに多くのコードが消せるようになりました。
このとき特に高い問題意識を持っていたのは同僚の ota42y さん (twitter) で、これを機にコードを消したいと何度か提案していました。しかし、消したいということについては全員が同意するものの、前述のような理由で一向に進まないという状態が続いていました。
FiNC APP Server 定例での議論
なかなか進まない状況を打破する必要を感じていました。それを議論する場になったのが、FiNC APP Server 定例です。
現在FiNC APPを開発するチームはビジネスの粒度で4つに分割され、それぞれにサーバーサイドのエンジニアも散っている状態です。そのサーバーエンジニア同士がお互いに協力関係を作っていくために、週1回定例をしています。この定例には、マネージャーや各チームの状況等の共有や、各メンバーが自由にアジェンダを持ち込んで議論することができます。
このミーティングの約1ヶ月前のある回にて、これに関する議論をqsonaが持ち込みました。要約すると
- こういった改善の活動は、やるやると言いながら進んだためしがない。ちゃんとチームとして進められるようにしたい。
というものです。そしてこの中でさまざまな熱い議論が交わされ、最終的には Yoshiyuki Hirano さん(twitter)の鶴の一声(?)で、今回は qsona が旗振り役をやることになりました。
準備と実行
戦略の立案
戦略というほど仰々しいものではないが、旗振り役としてどうやって進めるかを検討しました。ゴールをどこに置くか、消していく手順や方法、メンバーの時間のとり方、テストやリリースの方法(一気に出すのか小出しにするのか、など)、リリースの時期などを計画しました。
- QAチームに依頼し、リグレッションテストを行う。
- テスト開始日はQAチームの都合に合わせる。リリース日はテスト完了後すぐに。
- 特に定量目標を設けず、「QA開始までに消せるだけ消してCIを通す」のをゴールにする。
- 参加の強制はせず、やりたい人がやるというスタンス。
- なるべく時間を決めて、人が集まってやるスタイルで進める。
など、最初にいくつか方針を決めて、このプロジェクトを開始しました。以下でこのプロジェクトの実行の内容について説明する中で、その判断についても触れていきます。
消せる箇所の洗い出し
開発の最初期から実装を進めていて一番歴史を良く知っている Fumiya Shinozuka (shinofumijp, twitter) さんが積極的に協力してくれたことで、消せるAPIやモデルがかなりリストアップされました。また、 ota42y さんがAPIのアクセスログを調べ、直近1ヶ月でアクセスがないAPIをリストアップしてくれました。これで十分スタートできる状況になりました。
もっと細かい粒度で使われていないコードを調べていくための先人の知恵も色々ありますが、今回は上記の粗い粒度で十分でした。
QAの予定の確保
影響範囲を考えたときに、少なくとも、FiNCアプリと法人向けWebアプリの2つについて、リグレッションテストが必須であるという結論になりました。QAチームと相談したところ、9月の2週目なら実施可能という返事をもらえました。
リグレッションテストが必要かどうかは状況により変わり、前回の7,600行の際は不必要と判断(Stagingでしばらく放置することで代替)しています。もちろん手動でのテストの工数は多くかかり、やらないで済むに越したことはありませんが、このサービスに関してはそうそう綺麗に影響範囲が限定されていない状況なので、やむなしの判断です。
リグレッションテストを行うと決まったからには、あとはテストの前日までにどれだけ多くのコード削減をねじ込みつつCIを通せるか、が勝敗の鍵です。目標の日付とゴールが明確に決まったことはこのプロジェクトに良い影響をもたらしました。
工数の確保
工数という言葉はなんだかあまり好きになれず、工数工数言われると萎えてしまうタイプですが、とはいえ工数を確保するには、それに対する価値を説明できる必要はあります。この価値を説明しにくいというのが、この種の活動を進みにくくする原因の一つです。「コードを消す」のような改善活動の価値を定量的に説明するのは、特にやり始めはなかなか難しいのです。もちろん出来ないことはないし、説明すればビジネスサイドの理解も得られるのですが、今回は一旦、「勝手にやろう!」ということにしました。どれくらいかはわからないけれど、やって成果がでることはほぼ100%の自信があったし、一度やって成果を得てしまえば後から説明しやすくなるという打算もありました。
ということで、根回し等はせず、とりあえずやりたいひとが集まってやる!というスタイルにしました。
集まってやる
チームで戦う感を出していくために、物理的でも論理的(テレビ会議)でもいいのですが、とにかく最初は極力みんなで集まってやることにしました。これは良い施策でした。それぞれの箇所の詳しい人にすぐに聞けるので、各人が手を止めずに作業を進めることができました。
また、コードを消す際にはとにかく作業のコンフリクトが起きやすいだろう、という予見がありました。それを完全に避けるには、誰も同じ時間に作業しないという方法がありますが、それはなかなか難しいです。全員が集まってやる方法の場合、同時に行われる点ではリスクが大きいですが、同期的にコミュニケーションを取れるので、実際にはコンフリクトをうまく避けながら進められました。
(失敗) Visual Studio Code の Live Share 機能を利用する
コンフリクトを起こさないために、VSCodeのLive Share機能を利用して完全に同期しながら進めるというのをトライしましたが、30分くらいやってみてうまくいかないことに気づきました。コードを消すのは作業がメインなので、使い慣れたエディタ上でないと難しく、VSCodeをメインで利用していない勢にとっては効率が落ちてしまったためです。ファイルシステムレベルで同期する方法があればよかったかもしれません。
一方、このトライアルで、Live Share機能自体は使い方によってはかなり良いものだと実感できたので、一つの知見にはなりました。
こまめにpushする
前回の7,600行のときは、テストが通らないコミットを避けながらコードを消していきましたが、今回はその考えを完全に捨てました。ある程度のビッグバン的リリースは避けられず、リグレッションテストを行う以上、とにかく消せそうなコードを期間内にたくさん消すのが成果であり、コミットの綺麗さにこだわっても旨味があまりなかったためです。かわりに、 cleaning ブランチに全員が細かい単位でどんどんpushしていき (他の人が先にpushした場合は git fetch origin cleaning => git rebase origin/cleaningしてからpush)、コンフリクトを最小限にしました。
自動テストが最後まで進む状況をなるべく維持する
上記の結果、初日から最終日の夜までCIが通らない(テストでfailする)状況が続きました。それは許容していたのですが、たまにそもそもテストが最後まで回る前に終了することが起きました。例えば、APIの実装を消したのにそれをマウントしている部分が残っていて、起動時にすでに落ちている、などです。CIは落ちてもよいが、落ちているテストの数などは気にかけたいため、なるべく上記のようなことに気づいたら直すようにしました。
該当ユーザの退会処理を行う
今回終了したアプリケーションfooのユーザは一切アクセスしなくなるので、fooのユーザであるかどうかを判定するメソッド User#foo_user? も削除対象にしたいのですが、そのためにはユーザのデータを消すか隔離する必要がありました。アプリケーションには退会という機能があったため、退会の処理を行い、他の処理からこれらのユーザに関するデータが引っかからないようにしました。
法人向けwebアプリのライブラリアップデート
今回の影響範囲の一つ、法人向けwebアプリは、ライブラリのバージョンが古い問題がありました。リグレッションテストを行うことにあやかって、 Nobuhiko Sawai さんが気合を入れて、時間の許す限りライブラリのバージョンを上げていきました。特に、Reactのversionをoutdatedでないバージョンまで上げることができました。Reactのバージョンが古いとReactの開発者には(ReactのDeveloper Toolにて)一瞬でバレるので、これにて一安心です。
結果
かかった時間
ランチでわいわい消す会にはインターン生も参加してくれるなど、多くの人が手伝ってくれましたが、最終的に主にコミットしたのは ota42y , Fumiya Shinozuka , Nobuhiko Sawai , qsona の4人で、この期間でそれぞれ1〜1.5日程度コミットしたので、大体5人日くらいを要しました。
リグレッションについてはアプリ側2日, 法人向けwebアプリに1日を要しました。アプリ側については、別の理由(アプリの基盤部分の実装変更)によりいずれにしてもリグレッションテストを行う予定があり、それに合わせたため、追加の工数はかからずにすみました。
成果
冒頭に紹介した通り、約 48,000 行の削減が行われました。
内訳を見ると、最も多いのはシードデータで、約 22,000 行が削減されました。以前はシードデータをコードで管理していましたが、数が増えるにつれてデプロイに時間がかかるなどの問題がおこり、新しい機能ではコードとは別に管理しています。これについての詳細は今回は省き、また別途ブログで紹介したいと思います。
APIはかなり消した割には行数はさほどではなく、約1,300行程度。model/servicesが約6,000行。テストが 約6,000 行。ViewとCSSをあわせて約6,000行。残りはRake Taskなどです。
リグレッションテストを終えて、無事にデプロイすることができました。この結果、CIの速さだけでも平均約30分=>約20分に短縮し、目に見える成果になりました。
学んだこと
そもそも48,000行も消せる行があること (まだまだあります) 自体がホメられたことではないし、明示的に時間を確保せずにやりたいひとが集まってやる!というのもホメられた戦略ではないので、この内容から学びを語るのはいささか軽率な気がします。が、この短い期間の活動を通して、自分なりに感じたことを書いてみようと思います。
旗振りは得意な人がやればよい
まず大前提として、自分はこのFiNC APP Serverチームが大好きなのですが、その理由を考えてみるといくつかあります。
まず、技術面でお互いに信頼感があることです。お互いに、相手が得意なことを任せれば自分がやるよりも良い結果になるだろう、と思えること。また、技術的な議論をする際にベースの技術レベルが揃っていて、大筋からそれずに有意義に補完しあえることが挙げられます。
また、お互いにサポートし合うような関係もあります。基本的に普段はそれぞれが別のサービスを持っていて干渉することは少ないのですが、今回のような共通の問題があるときに、その問題に対し(仮に定量化されていなくても)だいたい全員が共通の認識を持っていて、一緒に解決していこうという協力的な姿勢があります。
その中で、今回 qsona は最初にプロジェクト進行のお膳立てをして、あとはそこそこ平均的なコミットをしました。一方で特に問題意識の高かった ota42y さんは、コードを消すための前提の準備だったり、ユーザの退会処理など、プロジェクトに最も価値の高い貢献をしています。
はじめ自分は、最も問題意識の強い人が旗を振るのが良いのではないかと思っていました(言い出しっぺの法則)。これだけでなく他にも様々な横断的な問題があり、一人ですべての問題をリードしていくのはスケールしないので、分散的組織にしていきたいという思いがあるからです。
しかし実際には、今回のように協力を得られる前提であれば、旗を振るだけなら自分はそこまで負担ではなく、そういう役割は個人的にも好きだったりもするので、旗振りも単に一つの役割として、好きor得意な人がやればよい場合もあると感じました。このように考えると、分散的(ボトムアップ)な組織づくりの考えとは相反せずに進めることができます。
プロジェクトが良い状態でないと、一般的なベストプラクティスを適用できない
ライブラリの更新やコードの削除など、技術的負債の返却は、できる限り小さく行って小さくリリースするのがベストだと思っています。しかし、今回はいろいろ検討して、小さくても大きくてもリグレッションテストをしなければならないのは変わらないため、大きく変更してテストすることにしました。また、リリースを細かく分けることも考えましたが、リリース可能な単位で細かく分割してコミットをしていくコストが高すぎたため、これも諦めました。
これを安直に「ベストプラクティスは状況によって変わる」と言いたくないというのが個人的な心情ですが、とはいえ、ある程度(というか、かなり)サービスが良い状態でないと、細かい単位ですばやくリリースするのは難しいことに気づきました。
今回の大掃除ではまだ「かなり良い状態」までは行きませんが、これを3,4回繰り返して、かついくつかのマイクロサービスへの移行を完了させれば、かなりメンテナブルな状態にすることができそうです。
適切な命名と名前空間は、後々のコード削除を容易にする
今回削除を進める中では、素朴なgrep (agを利用していますが) が良く利きました。また、ビジネスドメインによって名前空間が分かれていたため、それごとゴソッと消すことができる、という箇所もいくつかありました。こういった先人たちの良い心がけが、今回の作業を楽にしてくれた実感がありました。
今後の展望
取り組みのための時間を明示的に取るようにする
今回はメンバーの自助努力に頼った取り組みになりました。もちろん努力してくれるメンバーに恵まれているのは良いことですが、それだけに頼り続けることは難しいです。個人的にも、実際に最後CIが通るまでテストを修正する作業など (最終日 Fumiya Shinozuka さんと隣の席で一緒に潰していった)、高揚感があって楽しいのですが、ちょっと疲れたなーという感想もあります。
したがって、次からはもう少し明示的に、各チームと調整をして時間を取って進めるのが良いと思っています。
効果を明確に示せると、それがやりやすくなります。減らせた行数やCIの時間短縮など、わかりやすい数値がいくつかとれました。この機会にRake Statsを整備したので、定量的に観測しやすくもなりました。それを元に、効果を示して時間を確保していくのが、旗振り役として次にやるべきことだと考えています。
コードを消すための事前情報の取得
このコードが消せるか消せないのかを判定するために、事前にある程度情報を集める必要がありますが、今回はかなり粗い粒度で問題ありませんでした。しかし、進めるにつれて、これは本当に消せるのか自信がない・・というようなコードも出てくる可能性が高いです。
これについてはいくつか先例があり、一例をあげると cookpadのshiaさんによる Ruby の lazy loading の仕組みを利用して未使用の gem を探す の記事は非常に参考になります。プロファイラのgemも利用できるかもしれません。(FiNCのメインサービスはcookpadさんのcookpad_allほどには大規模ではありませんが、) 他社さんの取り組みを参考にしながら、FiNCとして効果的な進め方を模索してやっていきたいと思います。
Developer Experience を開発チームの第一級の概念としてサポートする
最近、 Fuji, Goro (gfx) さんが DX という言葉を(再)定義し、このDXという概念が盛り上がりを見せています。 (参考: DX: Developer Experience (開発体験)は重要だ — Islands in the byte stream)
この言葉が広まるのは必然なことだと感じていて、開発において非常に大事にすべきだが今まで一言で当てはまる言葉がなかったところに、わかりやすい言葉を定義したことで共感が広まったものと思います。FiNCにおいてもこのDeveloper Experienceは非常に大事だと考えています。
このDXを、単に生産性を高めるためのサブ的な概念ではなく、第一級の概念として定義し、チームがこれをしっかり追っていくべきだと考えています。今後、コードを消すことに限らずDXを高める様々な取り組みを活性化していき、それをユーザーへの価値にまで繋げていきたいと思っています。
エンジニア積極募集中!
株式会社FiNCは、社員・フリーランス問わず、エンジニアを積極募集しています。
- 48,000行なんてぬるい、480,000行くらい消してやるという方
- マイクロサービスへ機能を切り出していくことに興味がある方
- コードの品質や設計にこだわりのある方
- プロジェクトを推し進めるのが得意な方、フォロワーとしてプロジェクトを支える貢献をしてくれる方
- etc…
ヘルスケアのプラットフォームとして、開発にアクセルを踏んでおり、とても面白いフェーズだと思います。ぜひ以下のリンクからカジュアルにお声がけください(もしくはもっとカジュアルにtwitterのDMも承ります)。お待ちしております!!