BEAR.Sunday Advent Calendar 2020 21日目の記事です!(18日目から始まったので正確には4日目?)

Symfonyしか使ってないと思われがちな弊社ですが、主にマイクロサービス系のコマンドラインアプリケーションではDIにRay.Diを活用しています。 Ray.Diのダウンロード数を日々上げ続けているのは弊社かもしれません(主にCIで…汗)。

さて、導入から今まで、普通のInjectorしか使っていなかったのですが、初めて コンパイル 機能を使ってみようと思い立ちました。 以前は、実装を変更したときに本番サーバー上でキャッシュクリアする実装ないしオペレーションをしないといけないのは面倒だと思い、実行効率より開発効率優先で敢えてコンパイル機能を使わないという方針でやっていました。 しかし、インフラのECS化が進んできた中で、毎回新規にコンテナが起動してフレッシュなソースコードが使われるようになり、キャッシュクリアの手間を考えなくて良くなったため、ようやく実行効率を考えることができるようになりました。

早速使ってみることにします。Ray.Diの コンパイル 機能のドキュメントはこちらにあります。 https://github.com/ray-di/Ray.Di#performance-boost

実際のソースコード上の変更

今回の対象はECSのFargate上で動くコマンドラインアプリケーションです。 アプリケーションはキューからコマンドの引数を取ってコマンドを実行し続けるランナースクリプトと、そのランナーから起動される実際のコマンドを実行するスクリプトで構成されています。 ランナーのほうはそのコンテナ内で毎回1回だけ起動し、ランナーが死んだらコンテナも死ぬ仕組みなので、コンパイルしても旨味がないと思われます。そこで、「実際のコマンドを実行するスクリプト」のみを対象にInjector部分を変更してみることにしました。

BEFORE

<?php

// 略

$injector = new Injector(new FooModule());
$command = $injector->getInstance(FooCommand::class);

// run
$app = new Application();
$app->add($command);
$app->setDefaultCommand($command->getName(), true);

$app->run();

AFTER

<?php

// 略

$injector = new ScriptInjector(__DIR__.'/cache/', function(){ return new FooModule(); });
$command = $injector->getInstance(FooCommand::class);

// run
$app = new Application();
$app->add($command);
$app->setDefaultCommand($command->getName(), true);

$app->run();

ドキュメント通りではないのですが、ScriptInjectorのソースコード読んだ限りちゃんと「まだコンパイルされたものがなかったらコンパイル」という挙動があるようだったので、こうしてみました。

動作確認

ScriptInjectorにした状態で1回目の実行をすると、指定したキャッシュフォルダにキャッシュファイル(コンパイルされたDI設定のファイル)ができます。 DI内で $this->bind() したクラスやinterfaceの数だけファイルができていました。
※ 今回の対象が小さなスクリプトなので数は少ないです。

スクリーンショット_2020-12-21_13_14_58

キャッシュファイルの1つの中身を見てみると、こんな感じでした。

<?php

namespace Ray\Di\Compiler;

$instance = new \FooApp\DependencyInjection\LoggerProvider('/Users/h-hishida/projects/foo-app/src/DependencyInjection/../../var/foo.log');
$is_singleton = false;
return $instance->get();

ScriptInjectorに切り替えても、元のコマンドラインアプリケーションのスクリプト自体は正常に実行完了し、生のInjectorを使ったと時と変わらない成果物も生成できることが確認できました。

なお、私は疑い深いので、「こんなんでホントにコンパイルされたものが使われているのかな?」と不安になったので、キャッシュファイルの1つに echo 'Hello ScriptInjector'; と書き込んだ状態で実行してみました。 正常実行のログとともに Hello ScriptInjector が出力されたので、ちゃんと使われているようです :smile:

計測結果

time をつけてコマンドを実行する簡易計測です。

通常のInjector利用

7.46s user 0.08s system 99% cpu 7.559 total

ScriptInjector利用(1回目…コンパイル処理あり)

7.65s user 0.08s system 99% cpu 7.769 total

ScriptInjector利用(2回目…コンパイルされたものを利用できる)

7.33s user 0.06s system 99% cpu 7.403 total

スクリプトが小さすぎるせいか、あまり有意な差が出たようには見えませんね…。 しかし、通常、1つのコンテナが起動してから死ぬまで、数十〜数百回実行されるスクリプトなので、1秒未満の差でもチリツモで効果が出ることが予想されます。 (ECSで実行する前提なので、1個のコンテナの処理可能件数が増えたり、同じ物量のキューを流したとき、より短い時間で処理が終わることを期待しています)

まとめ

次々に新しいアプリケーションを開発する受託開発と異なり、自社サービス開発では1つのアプリケーションを数年に渡って保守し続けることになるため、意外と地味な作業が多くなります。 同じアプリケーションの機能改善やバグ修正を続ける中でも、新しい技術やライブラリの新しい機能を使ってみることで、技術的好奇心も満たしながら開発を進めることができます。

完全に技術的興味だけによる提案はNGなのですが、アプリケーションを正しい方向に改善する視点をもって提案するのがポイントだと思います!