RoadRunner: PHP nie jest stworzone, by umierać, ani Golang na ratunek

RoadRunner: PHP nie jest stworzone, by umierać, ani Golang na ratunek

Hej Habra! Jesteśmy aktywni na Badoo praca nad wydajnością PHP, ponieważ mamy dość duży system w tym języku, a problem z wydajnością jest kwestią oszczędności pieniędzy. Ponad dziesięć lat temu stworzyliśmy do tego PHP-FPM, który początkowo był zbiorem łat dla PHP, a później wszedł do oficjalnej dystrybucji.

W ostatnich latach PHP poczyniło ogromne postępy: udoskonalono wyrzucanie elementów bezużytecznych, podniósł się poziom stabilności - dziś można bez problemu pisać demony i długowieczne skrypty w PHP. To pozwoliło Spiral Scout pójść dalej: RoadRunner, w przeciwieństwie do PHP-FPM, nie czyści pamięci pomiędzy żądaniami, co daje dodatkowy wzrost wydajności (chociaż takie podejście komplikuje proces programowania). Obecnie eksperymentujemy z tym narzędziem, ale nie mamy jeszcze żadnych wyników do udostępnienia. Aby czekanie na nie było przyjemniejsze, publikujemy tłumaczenie ogłoszenia RoadRunner ze Spiral Scout.

Podejście z artykułu jest nam bliskie: rozwiązując nasze problemy, również najczęściej korzystamy z pęczka PHP i Go, czerpiąc korzyści z obu języków i nie rezygnując z jednego na rzecz drugiego.

Enjoy!

W ciągu ostatnich dziesięciu lat stworzyliśmy aplikacje dla firm z listy Fortune 500, a także dla firm, które mają nie więcej niż 500 użytkowników. Przez cały ten czas nasi inżynierowie rozwijali backend głównie w PHP. Ale dwa lata temu coś miało duży wpływ nie tylko na wydajność naszych produktów, ale także na ich skalowalność - wprowadziliśmy Golang (Go) do naszego stosu technologicznego.

Niemal natychmiast odkryliśmy, że Go pozwala nam tworzyć większe aplikacje z 40-krotną poprawą wydajności. Dzięki niemu mogliśmy rozszerzyć nasze istniejące produkty PHP, ulepszając je, łącząc zalety obu języków.

Powiemy Ci, jak połączenie Go i PHP pomaga rozwiązywać realne problemy programistyczne i jak stało się dla nas narzędziem, które może pozbyć się niektórych problemów związanych z Umierający model PHP.

Twoje codzienne środowisko programistyczne PHP

Zanim porozmawiamy o tym, jak możesz użyć Go do ożywienia umierającego modelu PHP, przyjrzyjmy się Twojemu domyślnemu środowisku programistycznemu PHP.

W większości przypadków uruchamiasz swoją aplikację przy użyciu kombinacji serwera WWW nginx i serwera PHP-FPM. Ten pierwszy obsługuje pliki statyczne i przekierowuje określone żądania do PHP-FPM, podczas gdy sam PHP-FPM wykonuje kod PHP. Być może używasz mniej popularnej kombinacji Apache i mod_php. Ale chociaż działa to trochę inaczej, zasady są takie same.

Przyjrzyjmy się, jak PHP-FPM wykonuje kod aplikacji. Gdy przychodzi żądanie, PHP-FPM inicjuje proces potomny PHP i przekazuje szczegóły żądania jako część jego stanu (_GET, _POST, _SERVER itp.).

Stan nie może się zmienić podczas wykonywania skryptu PHP, więc jedynym sposobem na uzyskanie nowego zestawu danych wejściowych jest wyczyszczenie pamięci procesu i ponowne jej zainicjowanie.

Ten model wykonania ma wiele zalet. Nie musisz się zbytnio martwić o zużycie pamięci, wszystkie procesy są całkowicie odizolowane, a jeśli jeden z nich "zginie" zostanie automatycznie odtworzony i nie wpłynie to na pozostałe procesy. Ale to podejście ma też wady, które pojawiają się przy próbie skalowania aplikacji.

Wady i nieefektywność zwykłego środowiska PHP

Jeśli jesteś profesjonalnym programistą PHP, to wiesz od czego zacząć nowy projekt - od wyboru frameworka. Składa się z bibliotek wstrzykiwania zależności, ORM, tłumaczeń i szablonów. I oczywiście wszystkie dane wprowadzone przez użytkownika można wygodnie umieścić w jednym obiekcie (Symfony/HttpFoundation lub PSR-7). Frameworki są fajne!

Ale wszystko ma swoją cenę. W dowolnym frameworku na poziomie przedsiębiorstwa, aby przetworzyć proste żądanie użytkownika lub uzyskać dostęp do bazy danych, będziesz musiał załadować co najmniej dziesiątki plików, utworzyć wiele klas i przeanalizować kilka konfiguracji. Ale najgorsze jest to, że po wykonaniu każdego zadania będziesz musiał wszystko zresetować i zacząć od nowa: cały kod, który właśnie zainicjowałeś, staje się bezużyteczny, z jego pomocą nie będziesz już przetwarzał kolejnego żądania. Powiedz to każdemu programiście, który pisze w jakimś innym języku, a zobaczysz zdumienie na jego twarzy.

Inżynierowie PHP od lat szukali sposobów rozwiązania tego problemu, stosując sprytne techniki leniwego ładowania, mikroframeworki, zoptymalizowane biblioteki, pamięć podręczną itp. Ale w końcu i tak trzeba zresetować całą aplikację i zacząć od nowa, raz za razem. (Uwaga tłumacza: ten problem zostanie częściowo rozwiązany wraz z pojawieniem się preload w PHP 7.4)

Czy PHP z Go może przetrwać więcej niż jedno żądanie?

Możliwe jest pisanie skryptów PHP, które żyją dłużej niż kilka minut (do godzin lub dni): na przykład zadania cron, parsery CSV, wyłączniki kolejek. Wszyscy działają według tego samego scenariusza: pobierają zadanie, wykonują je i czekają na następne. Kod cały czas znajduje się w pamięci, oszczędzając cenne milisekundy, ponieważ ładowanie frameworka i aplikacji wymaga wykonania wielu dodatkowych czynności.

Ale tworzenie długowiecznych skryptów nie jest łatwe. Każdy błąd całkowicie zabija proces, diagnozowanie wycieków pamięci jest irytujące, a debugowanie F5 nie jest już możliwe.

Sytuacja poprawiła się wraz z wydaniem PHP 7: pojawił się niezawodny Garbage Collector, łatwiejsza stała się obsługa błędów, a rozszerzenia jądra są teraz szczelne. To prawda, że ​​inżynierowie nadal muszą uważać na pamięć i być świadomi problemów ze stanem w kodzie (czy istnieje język, który może ignorować te rzeczy?). Jednak PHP 7 ma dla nas mniej niespodzianek.

Czy można przyjąć model pracy z długowiecznymi skryptami PHP, dostosować go do bardziej trywialnych zadań, takich jak przetwarzanie żądań HTTP, a tym samym pozbyć się konieczności ładowania wszystkiego od nowa przy każdym żądaniu?

Aby rozwiązać ten problem, najpierw musieliśmy zaimplementować aplikację serwerową, która mogłaby akceptować żądania HTTP i przekierowywać je jeden po drugim do procesu roboczego PHP bez zabijania go za każdym razem.

Wiedzieliśmy, że możemy napisać serwer WWW w czystym PHP (PHP-PM) lub używając rozszerzenia C (Swoole). I choć każda metoda ma swoje zalety, obie opcje nam nie odpowiadały – chcieliśmy czegoś więcej. Potrzebowaliśmy czegoś więcej niż tylko serwera WWW - oczekiwaliśmy rozwiązania, które uchroni nas przed problemami związanymi z „twardym startem” w PHP, a jednocześnie będzie można je łatwo dostosować i rozszerzyć pod konkretne aplikacje. Oznacza to, że potrzebowaliśmy serwera aplikacji.

Czy Go może w tym pomóc? Wiedzieliśmy, że może, ponieważ język kompiluje aplikacje w pojedyncze pliki binarne; jest wieloplatformowy; wykorzystuje własny, bardzo elegancki model przetwarzania równoległego (współbieżność) oraz bibliotekę do pracy z HTTP; i wreszcie tysiące bibliotek i integracji typu open source będą dla nas dostępne.

Trudności w łączeniu dwóch języków programowania

Przede wszystkim należało ustalić, w jaki sposób dwie lub więcej aplikacji będą się ze sobą komunikować.

Na przykład za pomocą doskonała biblioteka Alex Palaestras, możliwe było współdzielenie pamięci między procesami PHP i Go (podobnie jak mod_php w Apache). Ale ta biblioteka ma funkcje, które ograniczają jej użycie do rozwiązania naszego problemu.

Postanowiliśmy zastosować inne, bardziej powszechne podejście: zbudować interakcję między procesami poprzez gniazda/potoki. Podejście to okazało się niezawodne w ciągu ostatnich dziesięcioleci i zostało dobrze zoptymalizowane na poziomie systemu operacyjnego.

Na początek stworzyliśmy prosty protokół binarny do wymiany danych pomiędzy procesami i obsługi błędów transmisji. W najprostszej formie ten typ protokołu jest podobny do siatka с nagłówek pakietu o stałym rozmiarze (w naszym przypadku 17 bajtów), który zawiera informacje o typie pakietu, jego rozmiarze oraz binarną maskę do sprawdzania integralności danych.

Po stronie PHP, której użyliśmy funkcja pakowania, a po stronie Go biblioteka kodowanie/binarne.

Wydawało nam się, że jeden protokół to za mało - i dodaliśmy możliwość dzwonienia usługi net/rpc go bezpośrednio z PHP. Później bardzo pomogło nam to w rozwoju, ponieważ mogliśmy łatwo zintegrować biblioteki Go z aplikacjami PHP. Efekt tej pracy można zobaczyć na przykład w naszym innym produkcie open-source Goridge.

Rozdzielanie zadań na wielu pracowników PHP

Po wdrożeniu mechanizmu interakcji zaczęliśmy myśleć o najefektywniejszym sposobie przekazywania zadań do procesów PHP. Gdy zadanie nadejdzie, serwer aplikacji musi wybrać wolnego pracownika do jego wykonania. Jeśli pracownik/proces zakończy się z błędem lub „zginie”, pozbywamy się go i tworzymy nowy, aby go zastąpić. A jeśli pracownik/proces zakończył się pomyślnie, zwracamy go do puli pracowników dostępnych do wykonania zadań.

RoadRunner: PHP nie jest stworzone, by umierać, ani Golang na ratunek

Do przechowywania puli aktywnych pracowników użyliśmy buforowany kanał, aby usunąć nieoczekiwanie „martwych” pracowników z puli, dodaliśmy mechanizm śledzenia błędów i stanów pracowników.

W rezultacie otrzymaliśmy działający serwer PHP, który jest w stanie obsłużyć dowolne żądania przedstawione w postaci binarnej.

Aby nasza aplikacja zaczęła działać jako serwer WWW, musieliśmy wybrać niezawodny standard PHP do reprezentacji przychodzących żądań HTTP. W naszym przypadku po prostu przekształcać żądanie net/http z Przejdź do formatu PSR-7dzięki czemu jest kompatybilny z większością dostępnych obecnie frameworków PHP.

Ponieważ PSR-7 jest uważany za niezmienny (niektórzy twierdzą, że technicznie tak nie jest), programiści muszą pisać aplikacje, które zasadniczo nie traktują żądania jako jednostki globalnej. To dobrze pasuje do koncepcji długotrwałych procesów PHP. Nasza ostateczna implementacja, która nie ma jeszcze nazwy, wyglądała tak:

RoadRunner: PHP nie jest stworzone, by umierać, ani Golang na ratunek

Przedstawiamy RoadRunnera — wydajny serwer aplikacji PHP

Naszym pierwszym zadaniem testowym był backend API, który okresowo pęka w nieprzewidywalny sposób (znacznie częściej niż zwykle). Chociaż nginx był wystarczający w większości przypadków, regularnie napotykaliśmy błędy 502, ponieważ nie mogliśmy wystarczająco szybko zbalansować systemu dla oczekiwanego wzrostu obciążenia.

Aby zastąpić to rozwiązanie, na początku 2018 roku wdrożyliśmy nasz pierwszy serwer aplikacji PHP/Go. I od razu uzyskałam niesamowity efekt! Nie tylko całkowicie pozbyliśmy się błędu 502, ale byliśmy w stanie zredukować liczbę serwerów o dwie trzecie, oszczędzając dużo pieniędzy i pigułek na ból głowy dla inżynierów i menedżerów produktu.

Do połowy roku udoskonaliliśmy nasze rozwiązanie, opublikowaliśmy je na GitHubie na licencji MIT i nadaliśmy mu nazwę RoadRunner, podkreślając w ten sposób jego niesamowitą szybkość i skuteczność.

Jak RoadRunner może ulepszyć Twój stos programistyczny

Stosowanie RoadRunner umożliwiło nam użycie Middleware net/http po stronie Go do przeprowadzenia weryfikacji JWT, zanim żądanie dotrze do PHP, a także do globalnej obsługi WebSockets i agregacji stanu w Prometheus.

Dzięki wbudowanemu RPC możesz otworzyć API dowolnej biblioteki Go dla PHP bez pisania opakowań rozszerzeń. Co ważniejsze, dzięki RoadRunner możesz wdrażać nowe serwery inne niż HTTP. Przykłady obejmują uruchamianie programów obsługi w PHP AWS Lambda, tworzenie niezawodnych wyłączników kolejek, a nawet dodawanie gRPC do naszych aplikacji.

Z pomocą społeczności PHP i Go poprawiliśmy stabilność rozwiązania, zwiększyliśmy wydajność aplikacji nawet 40-krotnie w niektórych testach, udoskonaliliśmy narzędzia do debugowania, zaimplementowaliśmy integrację z frameworkiem Symfony oraz dodaliśmy obsługę HTTPS, HTTP/2, wtyczki i PSR-17.

wniosek

Niektórzy ludzie wciąż są złapani w przestarzałe pojęcie PHP jako powolnego, nieporęcznego języka, który nadaje się tylko do pisania wtyczek do WordPress. Ci ludzie mogliby nawet powiedzieć, że PHP ma takie ograniczenie: kiedy aplikacja jest wystarczająco duża, trzeba wybrać bardziej „dojrzały” język i przepisać bazę kodu gromadzoną przez wiele lat.

Na to wszystko chcę odpowiedzieć: pomyśl jeszcze raz. Wierzymy, że tylko Ty ustalasz jakiekolwiek ograniczenia dla PHP. Możesz spędzić całe życie na przechodzeniu z jednego języka na drugi, próbując znaleźć idealne dopasowanie do swoich potrzeb, lub możesz zacząć myśleć o językach jak o narzędziach. Domniemane wady języka takiego jak PHP mogą w rzeczywistości być przyczyną jego sukcesu. A jeśli połączysz go z innym językiem, takim jak Go, stworzysz znacznie potężniejsze produkty, niż gdybyś był ograniczony do używania jednego języka.

Pracując z wieloma Go i PHP, możemy powiedzieć, że je kochamy. Nie planujemy poświęcać jednego na rzecz drugiego – wręcz przeciwnie, będziemy szukać sposobów na uzyskanie jeszcze większej wartości z tego podwójnego stosu.

UPD: witamy twórcę RoadRunner i współautora oryginalnego artykułu - Lachesis

Źródło: www.habr.com

Dodaj komentarz