2019の20日目の記事です。devblog.thebase.inPAY株式会社でテックリードを務める東と申します。バックエンド全般に広く携わっています。最近はサーバーアプリばかり書いていますがインフラもわりとやります。当ブログの読者の方には弊社のことをご存じない方もたくさんいらっしゃるかと思いますので、簡単に社の紹介をさせていただきます。ID」などの決済サービスを開発・運営している会社です。「支払いのすべてをシンプルに」をミッションに掲げ、お金を扱うすべての事業者・個人がもっと豊かな生活ができることを目指しています。決済というミッションクリティカルなテーマを扱うにあたって、品質保証は最も重要な課題です。弊社のメインプロダクトたるオンライン決済サービス「PAY.JP」にも5000ケース程度の自動テストが記述されており、常時CIやローカルで実行され続けています。自動テストはその実行時間がネックとなることが多々あります。今年の9月頃まで、PAYのテストはCircleCIで15分程度かかっていました。テストが遅いと開発のテンポが落ち、CIが詰まりはじめ、そのストレスが限界を超えればお金で解決することを余儀なくされます。お金を積んだところでcpu1コアあたりのクロックは頭打ちであり、1テストあたりの実行時間はそう簡単には短くはなりません。世界が到来することでしょう。幸いにして昨今は個人の開発マシンですら8core16coreの時代です。これを生かさない手はありません。今回は、並列化を軸にしたPAYのテスト高速化の取り組みについてお話させていただきます。docker-compose今回の内容にはpyramidフレームワーク固有の事情はほぼ出てこないため、python3+pytest全般のtipsとして利用できるかと思います。テストランナーを並列実行に対応させるまずはpytestの並列化拡張を探すところから始めました。いう拡張が存在していましたが、並列タスクを実行する場所を色々選べる反面制約が多く、弊社の用途では使い物になりませんでした。pytestの実行をpythonのthreading/multiprocessingで分散するシンプルなもので、まさに我々の求めているものでした。pytest-parallelの導入pytest-parallel自体はインストールして--workers=NUMのようなオプションをつけるだけで並列化できるのですが、とりあえずテスト実行をしてみたところ実行が10倍以上遅くなりました。scope扱いになってしまっているようでした。scopeで用意されています。scope判定が壊れているせいでこれがテストケース実行のたびに再生成されているようです。これが直らないことには話がはじまらないということで、直したものがこちらになります。https://github.com/feiz/pytest-parallel/tree/pass-nextitem-to-runtest_protocolこれを導入することでテストランナーの並列化対応は解決しました。multiprocesspytest-parallelはマルチスレッド/マルチプロセスともに対応していますが、SQLAlchemyがスレッドセーフでない、そもそも後付けでスレッドセーフにするのは難易度が高いなどの問題により、マルチプロセスのみを使うことにしました。割り切ることはたいせつです。データベース次にデータベースです。fixtureとして実装されていましたが、マルチプロセス化するとこれらが各プロセスでそれぞれ実行されてしまい、同じ名前のデータベースを生成しようとしてクラッシュします。同一のdbに複数のプロセスからテストを実行する場合、トランザクションで詰まってパフォーマンスに悪影響があるということも考えられます。そこで、db名末尾にpidを付与してプロセスごとに独立したデータベースを参照するように手を加えました。f"postgres://db:5432/test_payjp_{os.getpid()}"これは簡略化した擬似コードですが、PAYの設定ファイルはpythonファイルになっているため、割と簡単に実現できました。テスト環境のPostgresイメージをpostgres-ramに変更することで、すこしばかりのパフォーマンス改善も行いました。SQLiteに差し替え可能にできれればよかったのですが、PAYはPostgresに依存しているところが多く、残念ながら実行できていません。ソケット枯渇問題テストのために64coreマシンで128並列実行などをして遊んでいたところ、テスト実行中に突然DBコネクションが張れなくなる問題が発生しました。察しのいい方ならピンときそうなトラブルですが、TIME_WAITなコネクションが増殖してアプリケーションコンテナ側のポートが食いつぶされてしまったのが原因でした。これはテスト中だけSQLAlchemy側のコネクションプールを有効にすることで解消できました。並列化の実験にはGCPのプリエンプティブルインスタンスが安くて手軽で非常に便利でした。個人のアカウントで64coreぐらいのマシンを立ち上げて実験してすぐ落とすぐらいの使い方であれば月300円ぐらいで十分に遊べます。並列化後にボトルネックがどういうところに現れるのかの肌感を掴んでおくためにも、一度はやっておくとよいでしょう。Redisストレージ整備の一環としてRedisのテスト環境も見直しました。置き換えられるように改修しました。cpuコアと同様、昨今の環境ではメモリも大量に余っていることが多いため、やれるものをinmemoryにしてしまうのは手っ取り早いテスト高速化の手法です。ファイルシステムを触るテストコードを高速化する手法はお気に入りでした。テスタビリティ全般に通じる話ですが、重要なのはミドルウェアや外部環境との接続部分をラップして差し替えが自由にできる構造を最初から組んでおくことです。テストコードの順番依存テスト環境の改善により実行はできるようになりましたが、不可解なFAILがランダムかつ大量に発生するようになりました。これまでのテストは直列かつ同一の実行順で実行されていましたが、マルチプロセス分散されたことで実行順が不安定になり、前提条件が満たされていない実行が発生してしまうようになってしまったようです。PAYではテストケースごとにDBをrollbackするようになっていないため、このような問題が起こりえます。実行の前提条件構築がテストケース間で正しく分離されていない悪いテストを書いてしまっているということです。2ヶ月程度をかけて、これをしらみ潰しに改善しました。残念ながらここについては一般的な解法はなさそうに思います。参考までに、原因を特定する手順として社内ドキュメントに書いた内容をかいつまんでご紹介します。エラーの直接の原因を把握する大抵、常識的に考えて当然(ある|ない)はずのデータが(ない|ある)といった不可解なエラーとして表出します。混乱せずにまずは何が起こったのかを正しく読み解きます。できるだけ狭い範囲で再現させるテストランナーの個別実行機能を使ってできるだけ狭い範囲で100%再現できるように発生条件を絞り込みます。対処する多くの場合、前提データの前処理や後処理が悪いことが原因のため、テストデータ生成基盤に手を加えて改善することになります。setUp/tearDownメソッドのような手法よりもfixture個別の前後処理の記述がやりやすいため、積極的に活用するとよいでしょう。idを添えて記載していました。以上のような改善を経て、pytest-parallelの導入を決意してからおよそ7ヶ月、並列化を志したときから数えれば実に1年と1ヶ月をかけてちゃんと動く並列化を実現できました。2分53秒に改善しました。(5.3倍!)before↓afterテスト基盤を整えよう。私の経験上、テストはアプリケーション本体以上にコードが腐敗しやすい場所に思えます。あなたがこれからテストコードを書き始めるか、プロジェクトのテストコードがまだ育っていないなら、今すぐにでもお使いのテストランナーのrandomizeオプションをオンにしてみるべきです。それだけで今回一番つらかった問題(順番依存)が起きてしまうリスクを大幅に低減できます。必要に駆られる前から並列化を行っておくこともおすすめできます。本来テストケースは分離されていて当然のものであり、きれいなテストを書いていれば簡単に並列化できるものだからです。これらは不健全なテストをふるいにかけ、テストコードを健全に保つ大きな助けになります。ミドルウェアへの直接的な依存もよく吟味すべきです。昨今はdockerの台頭でテスト環境にミドルウェアそのものを用意することのハードルが非常に低くなっているため、手を抜きがちな部分ではあります。テストダブルで依存を自由に管理できないとそもそもテストを書くのが面倒極まりないですし、書くのが面倒なテストは書かれなくなるか、コピペが横行して加速度的に腐敗します。残念ながらすでに問題が起きているのなら、諦めて粛々と改善しましょう。それが一番の近道です。おわりにテストに実行速度の問題が表出するようなタイミングでは、対処しようにも他の問題と複合していて手が出せない状態になっている可能性が高いです。このような問題に拘って本来やるべき開発に支障がでたり、CircleCIに無限に予算を吸われてしまうようになる前に、ぜひテスト実行基盤を見直してみることをおすすめします。この記事が、みなさんの快適な開発の助けになれば幸いです。セキュリティエンジニアのクリスです。使用には耐えるものですが、魔改造の粋を超えられていない(スレッド実行機能が死んでいる)こともあり、本家にPRは送れていません。;(↩PAYの本番環境にはPgBouncerが居るため、アプリケーション側ではコネクションプーリングは行わない設定になっていました。手っ取り早いのは確かなのですが、「本来使うものとは違うもの」でテストしているのもまた確かなので、場合によっては速度を度外視して本来のものに近い環境でテストするステージを設けることも検討するべきです。)の開発チームによるブログです。Groupの大木です。Developerをしている東口です。Groupのマネージャーをしている山田です。12/1から始まっ...Archiveスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。