Профилирование Сервис Контейнера Laravel
Чтобы дать представление о размере рассматриваемого приложения, существует около 2500 классов, и подавляющее большинство этих классов получают свои зависимости в качестве параметров конструктора. Нередко в этих классах можно увидеть более 10 зависимостей, поэтому вы можете себе представить, что отношения между этими классами могут быть очень сложными.
Когда сервис контейнеру предлагается создать экземпляр класса, он намеревается создать экземпляр каждой из его зависимостей, которые имеют собственные зависимости, у которых есть собственные зависимости и так далее. Вы можете себе представить, что это может создать довольно запутанный граф зависимостей.
Это факт, о котором разработчики приложений, многие из наиболее часто внедряемых классов были настроены как синглтоны в AppServiceProvider
, чтобы уменьшить ненужные повторяющиеся экземпляры классов, но нам нужно было измерить, чтобы узнать, нужно ли нам идти дальше.
Я пытался использовать функцию профилирования xdebug
, но мне было трудно рассуждать, что я увидел. Вместо этого я разработал более целенаправленный способ, помогающий идентифицировать классы, которые будут сконфигурированы как синглтоны.
Я добавил приведённый ниже вызов afterResolving
в AppServiceProvider
:
class AppServiceProvider extends ServiceProvider
{
public function register()
{
// Не делайте это продакшене
$this->app->afterResolving(function($resolved) {
if (!is_string($resolved)) {
$resolved = get_class($resolved);
}
file_put_contents(storage_path('container.resolve.log'), $resolved . "\n", FILE_APPEND);
});
}
}
Каждый раз, когда Сервис Контейнер Laravel создаёт объект, создаваемый класс регистрируется в storage/container.resolve.log
. Если это приводит к тому, что ваше приложение взрывается, попробуйте создать файл вручную и установить для него разрешения.
touch storage/container.resolve.log
chmod 777 storage/container.resolve.log
В моём случае приложение будет работать, но ведение логов замедлит работу, контроллеры иногда будут работать дольше, чем 30 секунд, что приведёт к тайм-ауту. Оказывается, это хороший признак того, что данная оптимизация будет вам полезна. Нам просто нужны некоторые данные, чтобы начать, поскольку по мере уменьшения количества создаваемых объектов производительность будет увеличиваться, как это произошло в моём случае.
Количество строк в файле журнала/лога соответствует количество объектов, созданных сервис контейнером. Я использовал приведённую ниже строку, чтобы получить список классов, отсортированный по количеству созданных экземпляров.
$ sort storage/container.resolve.log | uniq -c | sort -k1 -n
5 App\Foo
123 App\Bar
54321 App\Baz
Затем я посмотрел на худших нарушителей, узнал, сохраняют ли они состояние, а если нет, то настроил их как синглтоны в \App\Providers\AppServiceProvider::register
.
public function register()
{
$this->app->singleton(Foo::class);
}
После добавления нескольких классов, я обнулил журнал и повторил эксперимент, сделав несколько запросов и отыскав классы с большим количеством экземпляров, которые не сохраняют состояние. Повторять по мере необходимости.
echo "" > storage/container.resolve.log
Обратите внимание, что если в приложение есть постоянные процессы, такие как задачи или Octane
с Swoole/RoadRunner
, вам лучше использовать scoped
, вместо синглтона.
public function register()
{
$this->app->scoped(Baz::class);
}
Чтобы узнать, какое влияние на производительность оказывают изменения, удалите или закомментируйте вызов afterResolving
, добавленный в AppServiceProvider
, и сравните время выполнения приложения с новыми вызовами singleton
/scoped
и без них.
До этого моё приложение создавало >300 тысяч объектов, а после — <10 тысяч. Это дало значительный прирост производительности с точки зрения используемой памяти, процессорного времени, и что наиболее важно, времени ответа на запросы.
С точки зрения недостатков, добавление большого количества синглтонов сломало многие модульные тесты. Изначально они не ожидали общих экземпляров, и мне пришлось явно указать контейнеру, чтобы в некоторых случаях он забывал об экземплярах (forgetInstance()
), чтобы я мог получить новый.
app()->singleton(Foo::class);
app()->instance(DependencyOfFoo::class, $dependencyOfFooMock);
// Синглтон устанавливается с зависимостью $dependencyOfFooMock
$foo = app()->make(Foo::class);
app()->instance(DependencyOfFoo::class, $dependencyOfFooMock2);
// Не использует $dependencyOfFooMock2, мы получаем исходный
// синглтон с ранним mock'ом.
$foo = app()->make(Foo::class);
app()->forgetInstance(Foo::class);
// Использует $dependencyOfFooMock2
$foo = app()->make(Foo::class);
Неиспользование синглтонов имеет преимущество, что не нужно явно забывать экземпляры (forgetInstance()
). Вы можете условно сделать классы синглтонами, когда не находитесь в тестовой среде, но это будет означать, что вы получите другое поведение в тестах по сравнению с продакшеном, что может быть нежелательно.