Laravel Octane w produkcji — co zmienia długo żyjący proces i jak nie dać się zaskoczyć
Octane uruchamia aplikację raz i obsługuje nią tysiące żądań. To daje wydajność, ale zmienia zasady gry: stan w singletonach, wycieki pamięci, przeładowanie po deployu. Opisujemy, jak wdrażamy Octane na produkcji i na co uważać.
W klasycznym PHP każde żądanie zaczyna się od zera: framework się ładuje, obsługuje żądanie i znika. To proste i bezpieczne, ale kosztowne — przy każdym żądaniu powtarzamy tę samą pracę. Laravel Octane zmienia ten model: aplikacja startuje raz, a potem obsługuje wiele żądań w tym samym procesie. Używamy go m.in. w CRM dla brokerów finansowych, który obsługuje duży wolumen danych i użytkowników. Poniżej to, czego nauczyliśmy się po drodze.
Jak działa Octane
Octane (laravel/octane, licencja MIT, wspiera Laravel 10–13) uruchamia aplikację na serwerze aplikacji, który trzyma proces PHP przy życiu. Do wyboru są cztery serwery (dokumentacja):
- FrankenPHP — serwer napisany w Go na bazie Caddy, z oficjalnym obrazem Dockera.
- Swoole i Open Swoole — rozszerzenia PHP; tylko one dają dodatkowe funkcje Octane opisane niżej.
- RoadRunner — serwer aplikacji w Go.
Każdy worker to osobny proces z własną kopią aplikacji. Domyślnie Octane uruchamia jeden worker na rdzeń procesora.
Pułapka nr 1: stan, który przeżywa żądanie
Service providery wykonują register() i boot() raz na worker, a nie raz na żądanie. Wszystko, co trafi do singletona albo zmiennej statycznej, zostaje na kolejne żądania. Typowe błędy:
- Wstrzyknięcie requestu, kontenera albo konfiguracji do singletona. Singleton zapamięta obiekt z pierwszego żądania i będzie go używał w kolejnych — np. z danymi innego użytkownika.
- Dopisywanie do tablic statycznych. Każde żądanie dokłada element, pamięć rośnie bez końca.
- Zapamiętywanie danych bieżącego użytkownika w polach klasy, która jest rozwiązywana z kontenera jako singleton.
Rozwiązania z dokumentacji: rejestrować takie klasy przez bind zamiast singleton, wstrzykiwać closure zwracający aktualną instancję (fn () => Container::getInstance()) albo przekazywać request i konfigurację w wywołaniu metody. Helpery request(), config() i app() zawsze zwracają bieżące wartości.
W praktyce przed włączeniem Octane przeglądamy wszystkie singletony i pola statyczne w aplikacji i w pakietach. To najważniejszy krok całego wdrożenia.
Pułapka nr 2: pamięć
Nawet w dobrze napisanej aplikacji pamięć potrafi powoli rosnąć przez bibliotekę, której autor nie przewidział długo żyjącego procesu. Octane ma na to bezpiecznik: opcja --max-requests (domyślnie 500) restartuje worker po określonej liczbie żądań. Ustawiamy ją świadomie i obserwujemy zużycie pamięci workerów w monitoringu — jeśli rośnie szybko, szukamy przyczyny zamiast zmniejszać limit w nieskończoność.
Pułapka nr 3: deploy bez przeładowania
Workery trzymają kod w pamięci, więc po wdrożeniu nowej wersji dalej wykonują starą, dopóki ich nie przeładujesz. Po każdym deployu trzeba uruchomić:
php artisan octane:reloadTen sam problem dotyczy innych długo żyjących procesów: workerów kolejki (queue:restart) i serwera SSR przy Inertii (inertia:stop-ssr). Wszystkie trzy komendy powinny być w skrypcie wdrożeniowym — przekonaliśmy się o tym na własnej skórze, gdy po deployu serwer SSR przez chwilę renderował strony starym kodem.
Konfiguracja produkcyjna
- Supervisor pilnuje procesu Octane i podnosi go po awarii. W konfiguracji warto ustawić długi
stopwaitsecs, żeby workery zdążyły dokończyć żądania przy zatrzymaniu. - Nginx przed Octane — serwuje pliki statyczne i terminuje TLS, a do aplikacji przekazuje tylko żądania dynamiczne.
OCTANE_HTTPS=true, gdy aplikacja działa za HTTPS — inaczej wygenerowane linki mogą mieć schemathttp.max_execution_timewconfig/octane.php(domyślnie 30 sekund) — długie operacje i tak przenosimy do kolejki.
Funkcje tylko dla Swoole
Przy Swoole lub Open Swoole Octane daje dodatkowe narzędzia:
Octane::concurrently()— równoległe wykonanie kilku zadań (np. zapytań do zewnętrznych API) w task workerach.Octane::tick()— callback uruchamiany cyklicznie.- Cache Octane — pamięć współdzielona między workerami, bardzo szybka, ale czyszczona przy restarcie serwera. Dobra na dane pomocnicze, nie na coś, co musi przetrwać.
- Tabele Swoole (
Octane::table()) — współdzielone struktury danych o stałym schemacie.
Kiedy Octane ma sens
Octane daje najwięcej tam, gdzie start frameworka jest znaczącą częścią czasu obsługi żądania: w API z dużą liczbą krótkich żądań i w aplikacjach z intensywnym ruchem. Nie naprawi wolnych zapytań do bazy ani braku indeksów — te trzeba rozwiązać osobno. Dlatego zanim włączymy Octane, zawsze najpierw mierzymy, gdzie aplikacja traci czas.
Jak podchodzimy do przyspieszania aplikacji — od pomiaru, przez bazę danych i cache, po Octane i usługi w Rust — opisujemy na stronie optymalizacji wydajności aplikacji.
Podsumowanie
- Octane trzyma aplikację w pamięci i obsługuje nią wiele żądań — na FrankenPHP, Swoole, Open Swoole albo RoadRunner.
- Największe ryzyko to stan w singletonach i polach statycznych — przejrzyj je przed wdrożeniem.
--max-requestschroni przed wyciekami pamięci, ale nie zastąpi szukania przyczyny.- Po każdym deployu:
octane:reload,queue:restarti restart SSR. - Octane nie naprawi wolnej bazy danych — zacznij od pomiaru.
Rozważasz Octane w swojej aplikacji w Laravelu? Napisz do nas — sprawdzimy, czy się opłaci i co trzeba przygotować.