Ostatnim razem analizowałem wydajność kontenera Laravela. Spotkałem się, że aplikacja spędza dużo czasu na budowaniu zależności, szczególnie w przypadku ciężkich punktów końcowych. To było dziwne, ponieważ wolałbym oczekiwać, że odpowiednia logika będzie najcięższą częścią wniosku.
Problem
Okazuje się, że domyślnie każda zależność w Laravel jest niewspółdzielona. Gdy więc aplikacja wymaga określonej zależności, kontener utworzy i wstrzyknie nowe wystąpienie tej zależności. Na przykład, w Symfony domyślnie każda zależność jest współdzielona:
W kontenerze usług wszystkie usługi są domyślnie współużytkowane. Oznacza to, że za każdym razem, gdy pobierzesz usługę, otrzymasz to samo wystąpienie.
https://symfony.com/doc/current/service_container/shared.htmlTak więc implementacja kontenera w Laravel jest dziwna, ponieważ oznacza to, że każda duża aplikacja korzystająca z Laravel będzie miała problem z wstrzyknięciem wielu instancji zależności. Marnuje to dużo zasobów, ponieważ aplikacja musi rozpoznać parametry określonej klasy za pomocą odbicia (magia automatycznego okablowania), a następnie zbudować wszystkie zależności i zrobić te same rzeczy podczas budowania zależności tej konkretnej klasy i tak dalej. 😄 🤯
\Illuminate\Container\Container::resolve
// If an instance of the type is currently being managed as a singleton we'll
// just return an existing instance instead of instantiating new instances
// so the developer can keep using the same objects instance every time.
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
Jak to rozwiązać?
Użyj zakresu lub pojedynczego tonu zamiast powiązania u dostawców:
$this->app->scoped(ServiceInterface::class, Service::class);
$this->app->singleton(ServiceInterface::class, Service::class);
Istnieje jedna różnica między zakresem a singletonem.
Metoda z zakresem wiąże klasę lub interfejs z kontenerem, który powinien zostać rozwiązany tylko raz w ramach danego żądania Laravel / cyklu życia zadania. Chociaż ta metoda jest podobna do metody singleton, wystąpienia zarejestrowane przy użyciu metody z zakresem będą opróżniane za każdym razem, gdy aplikacja Laravel rozpoczyna nowy "cykl życia", na przykład gdy proces roboczy Laravel Octane przetwarza nowe żądanie lub gdy proces roboczy kolejki Laravel przetwarza nowe zadanie:Można również ręcznie wyczyścić zależności w zakresie:
Można również ręcznie wyczyścić zależności w zakresie:
$this->container->forgetScopedInstances();
W większości przypadków ta zmiana powinna być bezpieczna, ale należy zachować ostrożność i sprawdzić, czy usługi są bezstanowe.
Usługi
okablowane (Auto-wired Services) Niestety, domyślnie używane są niewspółużytkowane zależności. Oznacza to, że gdy mamy usługi automatycznie okablowane bez żadnego interfejsu, nie musimy ich deklarować u żadnego dostawcy, a następnie nie możemy ich udostępnić.
Aby rozwiązać ten problem, wymyśliłem tylko następujące paskudne rozwiązanie:
$this->app->scoped(Service:class, Service:class)
Możemy więc po prostu utworzyć dostawcę i zdefiniować usługę za pomocą metody zakresu, aby udostępnić tę usługę.
Porównanie
przed
Po
Te wyniki są po pewnych zmianach z bind na scoped, ale nadal nie wszystkie, więc nadal jest miejsce na poprawę. Czas w Blackfire nie ma znaczenia, ale nadal widzimy wzrost wydajności o ~60% podczas zależności budowania.
Podsumowanie
Ciekawi mnie, co było pierwotną przyczyną zaimplementowania kontenera w ten sposób. Oznacza to, że Laravel może nie być najlepszą opcją dla dużych monolitycznych zastosowań. Kilka lat temu pracowałem z ogromnym monolitem opartym na frameworku Symfony rozwijanym przez około 50 programistów backendu i nawet nie wyobrażam sobie wykorzystania Laravela w takim projekcie z taką konstrukcją kontenera. To byłby chyba koszmar.