こんにちは!株式会社VOYAGE MARKETINGで働くエンジニアの yopidax です。
約20年ほど続くサービス、ECナビの技術的負債の返済に取り組んでいます。
今回は直近で、レガシーコードを大量に削除したので、そのアプローチをご紹介したいと思います。
目次
解析の対象と抱える課題
ECナビを長年支える、Perlで書かれたバッチが対象です。コードはGitLabのリポジトリで管理されていて、規模をまとめるとこんな感じです。
- ファイルの数
- バッチ関連全体 : 3,315
- うち、Perlファイル(.pm, .pl) : 1,111
- Perlバッチの数(crontab調べ)
- 255
ECナビを長年支えるバッチですが、レガシー故に多くの課題を抱えています。
- テストコードが書かれてない、あっても継続的なテストが行われないため、壊れている
- crontabの設定のみ削除され、放置されたコードが大量に残っており、生きているコードを把握できない
- 20年前から継ぎ足しの開発、近年あまりメンテナンスされておらず、知っている人が少ない
- 現役で事業貢献している機能も多く、全てリライトするよりもうまく活かし続けたい
放置されたコードが多く、何をするにも足枷となるため、まずは「実行されないコードを削除し、生きているコードだけにする」ことで、メンテナンス性の向上と、現状を把握できるようにすることから始めました。
アプローチ
対象ファイル数が多いので、効率よく対処するために、まずは母集団を機械的に一括して減らすことを初手としました。その後で、必要に応じて手作業で個別に対応する想定です。
PerlはLL(lightweight language)であり静的解析が苦手なため、動的解析によるアプローチを選択しました。しかし、動的解析を行うにも、都合の良いPerlモジュールを見つける事が出来なかったので、自作する事にしました。
自作にあたっては、汎用的にはせず、今回の制約条件を活用しました。
- Perlの利用箇所がcrontabキックに集約されていること、キック方式が統一されていること
- crontabの実行サイクルが1ヶ月以内に1度でも実行されるのが、3件除いて全てであること
元々、バッチ以外でもPerlを利用していたので、管理画面での操作をトリガーに実行されるケースもあったのですが、過去の取り組みで大半が削除されていたことから、crontabキックへ集約されていることを把握していました。方式に関しては、crontabの設定一覧を調査し、統一されている事を知りました。
解析は、本番環境で実施しました。開発環境だと、データの過不足によりバッチが動作せず、意図するログを収集できない可能性があります。
以下の手順で行いました。
- 実行されるファイルを洗い出す
- Perlの特殊変数 %INC をログとして吐き出す仕組みを追加する
- 実行されないファイルを知る
- GitLabで管理されるPerlファイルを
git ls-files ‘*.pm’
等で出力し、ログと突き合わせる事で、簡単に実行されないファイルを洗い出すことができます
- GitLabで管理されるPerlファイルを
1. 実行されるファイルを洗い出す
を詳しく説明します。
実行されるファイルを洗い出す
Perlの特殊変数 %INC
を用いて、実行されるファイルを洗い出すことにしました。
%INC
は、モジュール名とモジュールのファイルパスの組み合わせを保持します。
以下のようなモジュールを作成し、実行時に読み込まれた全てのモジュールを出力できるようにしました。
ログを出力するモジュール
package IncModuleLogger; use constant LOG_PATH => "/hoge/load_module_logger/logs/"; END { my $file = LOG_PATH . $ENV{MODULE_LOGGER_OUTPUT_FILE_NUMBER}; open my $fh, '>>', $file or die; print $fh "$0 -> $0\n"; while (my ($key, $value) = each(%INC)) { ## perlのコアモジュール、ライブラリは除外。perl5ディレクトリ以下にある。 if ($value !~ 'perl5') { print $fh "$key -> $value\n"; } } close $fh; } 1;
バッチの実行タイミングや処理するデータの有無により、読み込むモジュールが変わるケースがあります。1バッチ辺りが依存するモジュールを網羅的に収集できるよう、ログは同一のファイルに上書き保存しています。
%INC
には、Perlコアモジュールやサードパーティのライブラリも含まれます。Cartonによる依存管理をしていたため除外しました。しかし、実行の起点となるファイル名は含まれないので、ログとして残すために $0
を利用しています。
実行
コマンド
before common.sh perl ecnavi.pl after common.sh 55 perl ecnavi.pl
common.sh に追記
export PERL5OPT="-I$/path/to/ecnavi/logger -MIncModuleLogger" export MODULE_LOGGER_OUTPUT_FILE_NUMBER="$1"
既存のバッチは全て、共通のシェルスクリプトで環境変数設定などの事前処理を行っていました。こちらに PERL5OPT
を追加し、作成したモジュールを読み込ませることで、既存のPerlコードを変更すること無く追加できました。他にも、バッチとログファイルを後から紐付けて集計しやすいように、バッチ毎にユニークな番号を環境変数として渡し、ログファイル名としました。
ログのサンプル
/path/to/ecnavi/batch/ecnavi.pl -> /path/to/ecnavi/batch/ecnavi.pl Error.pm -> /path/to/ecnavi/lib/error/Error.pm Mysql.pm -> /path/to/ecnavi/lib/db/Mysql.pm Ecnavi/Model.pm -> /path/to/ecnavi/common/ecnavi/Model.pm IncModuleLogger.pm -> /path/to/ecnavi/logger/IncModuleLogger.pm
モジュール名 -> モジュールパス
で出力しています。
各モジュール内部でのモジュール検索パス指定によっては、出力されるモジュールパスが一部相対パスになるため、注意が必要です。/hoge/fuga/piyo/../dump.pm
など。今回はモジュール名がほぼユニークだったため、こちらを解析に利用しました。
いざ、大量削除
1ヶ月間のログ収集で、252/255バッチ分のログを収集できたので、これを元に実行されないファイルを洗い出し、削除を行いました。削除の際に気をつけたことをいくつか挙げます。
Perlファイルをgrepする
削除対象のパッケージ名やモジュール名からgrepし、関連ファイルの洗い出しを行いました。ymlやiniなど、動的解析では検知できないファイルがあるからです。
結局、grepするなら動的解析を行った意味が無いのでは?と思いますが、動的解析なしですと、1111ファイル全てをgrepする必要があります。解析を行うことで対象を絞り込む事ができるため、作業量は大幅に削減されます。また、今回のケースでは、削除対象が同じパッケージにまとまっている事が多かったため、全体で50回ほどのgrepで済みました。
リリース単位を細かくする
例えば、1MRの対象を、特定のディレクトリ/同系のライブラリ/クラス単位にするなどです。
テストが全て通ったからヨシッ!と言いたいところですが、冒頭で述べた通り、大半はテストが無いか壊れている状況でした。 幸い、冪等性が担保されたバッチが多くリカバリが容易であること、デプロイ環境が整備されており、何かミスがあった場合にすぐに戻せることから、リリースの単位を小さくすることで影響範囲を最小に留めるという判断をしました。
結果
手を動かした日数と削除量、ログ収集が1ヶ月で済んだことを踏まえると、かなりコストパフォーマンスの良いアプローチでした。
工数
- ログ収集の仕込み
- 2日
- 収集
- 1ヶ月
- 解析と削除
- 4日
実績
Perlファイルだけですと、1/3ほど削除する事ができました。また、リリース単位を細かく(MRで12くらい)したことが効いたのか、無事故で作業を終えることが出来ました。今回行った動的解析が信頼できることもわかりました。
- バッチ関連全体
- 807(3315 - 2508)ファイル
- Perlファイル(.pm, .pl)
- 408(1111 - 703)ファイル
まとめ
レガシーシステムを改善する前段階として、現状の機能を必要十分に絞り込み把握するために、今回の作業を行いました。削除を行うことで、問題の分母を大きく、手間を掛けず減らすことができます。感想ですが、動的解析により、コードが実行されないことが保証されているため、安心感を持って削除できました。また、一括で消せる爽快感もあります!!
とはいっても、上記の課題を抱える中で、コード量を削減していく手段に銀の弾丸はありません。様々な手段を組み合わせながら、段階的に減らしていくと進みやすいです。実際我々も、他にもアレコレしました。
また、このアプローチは言語問わず実現できる方法です。例えばPHPですと、OPcacheを元に解析、なんてこともしました。
今回の作戦があなたのプロジェクトのレガシー改善の一助になれば幸いです。他の方法もあれば、教えて下さい!
[PR]
株式会社VOYAGE MARKETINGでは、一緒に働く仲間を募集しています!
https://hrmos.co/pages/voyagegroup/jobs/vm-e02hrmos.co https://hrmos.co/pages/voyagegroup/jobs/vm-e03hrmos.co