← Blog
Laravel4 min czytania

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:reload

Ten 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ć schemat http.
  • max_execution_time w config/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-requests chroni przed wyciekami pamięci, ale nie zastąpi szukania przyczyny.
  • Po każdym deployu: octane:reload, queue:restart i 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ć.