見出し画像

Now in REALITY Tech #95 ローカル上の自動テストを高速化した話

こんにちは!REALITYのサーバチームでエンジニアをやっているまつだです。最近はマリオカート配信を見るのにハマっていまして、配信者さんたちの神プレイや6vs6の熱さに夢中になって暮らしています。

サーバチームの自動テストの運用について

バグの少ないコードを書くために、現在ではほとんどのソフトウェア組織がなんらかの自動テストを書いていると思います。REALITYでも自動テストをもちろん運用しており、主に開発途中のコードの正しさをローカルで検証するため、およびPull Requestに問題がないかをGitHub Actionsで検証するためにテストを動かしています。

サーバチームでは特にビジネスロジックを実装するレイヤー(以降Service層と呼びます)のコードを実装する際は、重点的にテストを書いています。
Service層ではMySQLやRedisへのアクセスをする場合があるのですが、テスト実行時に毎回DockerでMySQLやRedisのコンテナを起動し、それらを参照することでDBの書き込みなどを行う処理のテストを行えるようにしています。また、テスト終了時にはこれらのコンテナを削除しています。

自動テストは大事!でもだんだん遅くなってきて…

開発中のコードを検証するために、テストは開発環境で何度も何度も実行することになります。ところがREALITYのサーバのコードベースが大きくなるにつれ、このテストの速度が無視できないほど低下してきており、テストが実行されるまでの待ち時間が大きなストレスとなるようになってきました。

REALITYのサーバはマイクロサービスアーキテクチャを採用しているのですが、コードベースが一番大きなサーバのService層についてテストを実行すると僕の環境では完了までに35秒ほどかかるようになっていました。つまり、コードを書いたら、その正しさを確かめるために35秒間他のことをして、そしてそのテスト結果が返ってきたらまた元に戻ってコードを書き始める、と言うサイクルを何度も繰り返すことになります。待てないほど遅いと言うわけではないですが、集中を阻害するには十分な時間です。

35秒で何ができる?カップラーメンは(ry

自動テストの実行時間がストレスになってしまうと、開発をする際にテストを書くのが億劫になりそもそもテストを書かなくなる、というよくない方向に進んでしまうことはあるあるだと思います。このままコードが大きくなると実行時間もどんどん長くなることが予想されます。よくない傾向です。

自動テスト高速化への道

実行時間のボトルネックの特定

というわけで自動テストが早くなると(僕が)一番嬉しい、配信周りのサーバにおけるService層の自動テストの高速化に取り組むことにしました。

何かを高速化させる際に最初にやるべきことは、ボトルネックの特定です。
まずボトルネックの特定をするために、テスト起動時に実行される前処理なども含めて処理ごとにかかっている時間の計測を行いました。

実際に計測してみた結果、主に実行に時間がかかっていたのは次の2つの処理でした。

1. MySQLとRedisのDockerコンテナの起動
2. MySQL上でテーブルを作成する処理

前述した通り、Service層では自動テストの実行時に毎回MySQLとRedisのコンテナを新規に起動しています。また、これらのコンテナの起動は直列に行われており、MySQLコンテナの起動が完了し、MySQLサーバとの接続を確認できてからRedisコンテナの起動を開始…という処理になっていました。

また、新規に起動したMySQLコンテナのDBは当然空の状態であり、このDBに対してレコードの挿入や検索などを行うためには、操作対象となるテーブルを事前に作成してやる必要があります。このテーブル数が多ければ多いほど作成処理全体の実行時間が伸びていくのですが、REALITYではテーブルをシャーディングして保持する場合が頻繁にあり、実行時間が伸びやすい環境になっていました。

以上2つの前処理が実行時間の大部分を占めており、テストコード本体の実行についてはほとんど時間がかかっていませんでした。ボトルネックが特定できたので、あとはこれら2つの処理を高速化できればテスト全体の高速化が達成できることになります。

覚悟しろボトルネック

DBのテーブル作成処理の高速化

まず最初にすぐ改善できそうなテーブル作成処理の高速化に取り掛かることにしました。
というのも、テーブルの作成処理は直列に行われていたので、これを並列化するだけでかなり速度が改善できそうだなと考えたからです。

実際、並列化をするだけでかなりの高速化に成功し、僕の手元の環境では自動テスト全体の実行時間は16秒ほどになりました。

コンテナ起動処理の高速化

次にコンテナ起動処理の高速化です。

Service層においては自動テストをするたびに毎回コンテナを新規に起動するしています。これにより、前回のテストでDBに保存されていたレコードが残っているせいでテストが失敗したりと言うことが絶対に起こらないようになっています。この性質自体は重要です。しかし、テストコードを実行する際に真に要求されているのはDB上の全てのテーブルについて空であること、そしてRedisにキーが残っていないことの保証です。それらを保証するために毎回Dockerコンテナを作り直すのはどう考えてもやりすぎです。

ということで、MySQLコンテナ、Redisコンテナを毎回起動するのをやめ、前回実行時に起動していたコンテナが存在していた場合はそれらを使うようにしました。
しかし、そのままでは前回の自動テスト実行時に挿入されたDBレコードや、Redisのキーが残ってしまっている可能性があります。
これはコンテナを使い回す際にはMySQLであればDROP DATABASE、Redisであればflushallを事前に実行するようにするだけで解決できました。

また、MySQLのコンテナ起動処理とRedisのコンテナ起動処理が直列に動いていたので、ついでに並列化しておきました。

このような改善をしたところ、僕の手元の環境ではテスト全体の実行時間は8秒ほどになりました。

最終結果

以上の改善により、元の実行速度の4倍以上となる8秒程度の時間でテストの実行が終わるようになりました!(あくまで僕の環境での改善なので他の方の環境では必ずしもこの比率で改善されるとは限りませんが)。
一度起動したコンテナをわざわざ削除するようなことはそうそうないので、開発中は実質常にこの速度向上の恩恵を受けることができるようになりました。

実際開発をしていてもテストの実行によって集中力が削がれることが減り、またテストの辛さがなくなったのでもりもりテストを書くようになり、生産性が上がっているのを感じます。まだまだ早くする余地はありそうですが、これだけでもかなり効果がありました。
こんなに簡単に直せるのならもっと早くやっておけばよかった… 🥺

まとめ

今回は実行時間が遅くなっているサーバの自動テストを高速化する取り組みを紹介させていただきました。
自動テストのように何度もサイクルを回すものは、少しの高速化でも積み重なると時間と集中力の大きな節約になるということを改めて実感しました。今後はGitHub Actionsで回している自動テストの高速化に取り組みたいです。