RoadRunner: PHP nie jest stworzony, aby umierać, a Golang spieszy na pomoc

RoadRunner: PHP nie jest stworzony, aby umierać, a Golang spieszy na pomoc

Cześć, Habr! W Badoo intensywnie pracujemy nad wydajnością PHP, ponieważ mamy dość dużą systemę opartą na tym języku, a wydajność to kwestia oszczędności pieniędzy. Ponad dziesięć lat temu stworzyliśmy PHP-FPM, który początkowo był zestawem poprawek dla PHP, a później stał się częścią oficjalnej dystrybucji.

W ciągu ostatnich lat PHP znacznie się rozwinęło: poprawiło się zarządzanie pamięcią, zwiększyła się stabilność - teraz na PHP można bez większych problemów pisać demony i długoterminowe skrypty. To pozwoliło Spiral Scout pójść dalej: RoadRunner, w przeciwieństwie do PHP-FPM, nie oczyszcza pamięci między żądaniami, co daje dodatkowy zysk w wydajności (choć to podejście komplikuje proces rozwoju). Obecnie eksperymentujemy z tym narzędziem, ale na razie nie mamy wyników, którymi moglibyśmy się podzielić. Aby czekanie na nie było przyjemniejsze, publikujemy tłumaczenie zapowiedzi RoadRunnera od Spiral Scout.

Podejście przedstawione w artykule jest nam bliskie: przy rozwiązywaniu naszych zadań również najczęściej używamy połączenia PHP i Go, czerpiąc korzyści z obu języków i nie rezygnując z jednego na rzecz drugiego.

Miłej lektury!

W ciągu ostatnich dziesięciu lat tworzyliśmy aplikacje zarówno dla firm z listy Fortune 500, jak i dla biznesu z audytorium liczącym nie więcej niż 500 użytkowników. Przez cały ten czas nasi inżynierowie rozwijali backend głównie w PHP. Jednak dwa lata temu coś mocno wpłynęło nie tylko na wydajność naszych produktów, ale także na ich skalowalność - wprowadziliśmy Golang (Go) do naszego staku technologii.

Prawie od razu odkryliśmy, że Go pozwala nam tworzyć większe aplikacje z wzrostem wydajności sięgającym 40 razy. Dzięki niemu mogliśmy rozwijać istniejące produkty napisane w PHP, poprawiając je dzięki połączeniu zalet obu języków.

Opowiemy, jak połączenie Go i PHP pomaga rozwiązywać rzeczywiste problemy w programowaniu i jak stało się dla nas narzędziem, które może pomóc w części problemów związanych z modelem 'umierającego' PHP.

Twoje codzienne środowisko PHP-dewelopera

Zanim opowiemy, jak za pomocą Go można ożywić model 'umierającego' PHP, przyjrzyjmy się twojemu standardowemu środowisku PHP-dewelopera.

W większości przypadków uruchamiasz aplikację za pomocą kombinacji serwera WWW nginx i serwera PHP-FPM. Pierwszy obsługuje pliki statyczne i przekazuje specyficzne zapytania do PHP-FPM, a sam PHP-FPM wykonuje kod PHP. Możliwe, że używasz mniej popularnego połączenia Apache i mod_php. Choć działa to nieco inaczej, zasady pozostają te same.

Przyjrzyjmy się, jak PHP-FPM wykonuje kod aplikacji. Gdy przychodzi zapytanie, PHP-FPM inicjuje podrzędny proces PHP i przekazuje szczegóły zapytania jako część jego stanu (_GET, _POST, _SERVER itd.).

Stan nie może się zmieniać w trakcie wykonywania skryptu PHP, zatem nowy zestaw danych wejściowych można uzyskać tylko w jeden sposób: czyszcząc pamięć procesu i inicjując go od nowa.

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

Wady i nieefektywność standardowego środowiska PHP

Jeśli zajmujesz się profesjonalnym programowaniem w PHP, wiesz, od czego zacząć nowy projekt — od wyboru frameworka. To biblioteki do wstrzykiwania zależności, ORM-y, tłumaczenia i szablony. Oczywiście wszystkie wejściowe dane od użytkowników można wygodnie umieścić w jednym obiekcie (Symfony/HttpFoundation lub PSR-7). Frameworki są świetne!

Ale wszystko ma swoją cenę. W każdym frameworku na poziomie enterprise do obsługi prostego zapytania użytkownika lub dostępu do bazy danych trzeba załadować co najmniej dziesiątki plików, stworzyć liczne klasy i przeanalizować kilka konfiguracji. Najgorsze jest to, że po wykonaniu każdej czynności musisz wszystko zresetować i zacząć od nowa: cały właśnie zainicjowany przez ciebie kod staje się bezużyteczny, nie możesz za jego pomocą obsłużyć kolejnego zapytania. Powiedz o tym któremuś programiście, który pisze w innym języku — a zobaczysz zdziwienie na jego twarzy.

Programiści PHP przez lata szukali sposobów na rozwiązanie tego problemu, wykorzystując przemyślane metody »leniwego« ładowania, mikroforki, zoptymalizowane biblioteki, pamięć podręczną itd. Jednak w końcu nadal trzeba zresetować całą aplikację i zaczynać od nowa, raz po raz. (Nota tłumacza: częściowo problem ten zostanie rozwiązany wraz z pojawieniem się preload w PHP 7.4)

Czy PHP z pomocą Go może obsłużyć więcej niż jedno żądanie?

Można napisać skrypty PHP, które przetrwają dłużej niż kilka minut (aż do godzin lub dni): na przykład zadania cron, parsowanie CSV, przetwarzanie kolejek. Wszystkie one działają zgodnie z tym samym schematem: pobierają zadanie, wykonują je, czekają na następne. Kod pozostaje w pamięci, oszczędzając cenne milisekundy, ponieważ załadowanie frameworka i aplikacji wymaga wykonania wielu dodatkowych czynności.

Jednak opracowanie skryptów długożyjących nie jest takie proste. Każdy błąd całkowicie zabija proces, diagnostyka wycieków pamięci doprowadza do szału, a korzystanie z debugowania F5 jest już niemożliwe.

Sytuacja poprawiła się wraz z wydaniem PHP 7: pojawił się niezawodny zbieracz śmieci, łatwiej jest obsługiwać błędy, a rozszerzenia rdzenia są teraz chronione przed wyciekami. Prawda, inżynierowie nadal muszą ostrożnie obchodzić się z pamięcią i pamiętać o problemach z stanem w kodzie (czy istnieje język, w którym można by nie zwracać na to uwagi?). Mimo wszystko w PHP 7 czeka nas mniej niespodzianek.

Czy można wziąć model pracy z długożyjącymi skryptami PHP, dostosować go do bardziej trywialnych zadań, takich jak obsługa żądań HTTP i tym samym pozbyć się potrzeby ładowania wszystkiego od nowa przy każdym żądaniu?

Aby rozwiązać ten problem, najpierw trzeba było zaimplementować aplikację serwerową, która mogłaby przyjmować żądania HTTP i przekazywać je jeden po drugim do pracownika PHP, nie zabijając go za każdym razem.

Wiedzieliśmy, że możemy stworzyć serwer internetowy w czystym PHP (PHP-PM) lub przy użyciu rozszerzenia C (Swoole). I choć każda metoda ma swoje zalety, żadna z nich nas nie satysfakcjonowała – oczekiwaliśmy czegoś więcej. Potrzebowaliśmy nie tylko serwera internetowego – planowaliśmy uzyskać rozwiązanie, które uwolniłoby nas od problemów związanych z "ciężkim startem" w PHP, a jednocześnie byłoby łatwe do dostosowania i rozbudowy według konkretnych aplikacji. To znaczy, potrzebny był nam serwer aplikacji.

Czy Go może w tym pomóc? Wiedzieliśmy, że może, ponieważ ten język kompiluje aplikacje do pojedynczych plików binarnych; jest wieloplatformowy; wykorzystuje własny, bardzo elegancki model współbieżności (concurrency) oraz bibliotekę do obsługi HTTP; i w końcu będziemy mieli dostęp do tysięcy bibliotek open-source i integracji.

Trudności w integracji dwóch języków programowania

Przede wszystkim trzeba było określić, jak dwa lub więcej aplikacji będą komunikować się ze sobą.

Na przykład, za pomocą wspaniałej biblioteki Aleksa Palaeustrasa można było zrealizować współdzielenie pamięci między procesami PHP i Go (analogicznie do mod_php w Apache). Jednak ta biblioteka ma swoje cechy, które ograniczają jej zastosowanie w rozwiązaniu naszego problemu.

Postanowiliśmy zastosować inne, bardziej rozpowszechnione podejście: zbudować interakcję między procesami przez gniazda/pipeline. To podejście w ciągu ostatnich kilku dziesięcioleci udowodniło swoją niezawodność i zostało dobrze zoptymalizowane na poziomie systemu operacyjnego.

Na początek stworzyliśmy prosty protokół binarny do wymiany danych między procesami i obsługi błędów transmisji. W najprostszej formie protokół tego rodzaju przypomina netstring z z nagłówkiem o stałej wielkości (w naszym przypadku 17 bajtów), który zawiera informacje o typie pakietu, jego rozmiarze oraz dwójkowej masce do sprawdzania integralności danych.

Po stronie PHP użyliśmy funkcji pack, a po stronie Go – biblioteki encoding/binary.

Jednego protokołu uznaliśmy za niewystarczające – dodaliśmy możliwość wywoływania usług Go net/rpc bezpośrednio z PHP. Później bardzo nam to pomogło w rozwoju, ponieważ mogliśmy łatwo integrować biblioteki Go w aplikacjach PHP. Wynik tej pracy można zobaczyć, na przykład, w naszym innym produkcie open-source Goridge.

Rozdzielanie zadań pomiędzy kilka pracowników PHP

Po wdrożeniu mechanizmu interakcji zaczęliśmy myśleć, jak najbardziej efektywnie przekazywać zadania procesom PHP. Kiedy przychodzi zadanie, serwer aplikacji musi wybrać wolnego pracownika do jego wykonania. Jeśli pracownik/proces zakończył pracę z błędem lub 'umarł', pozbywamy się go i tworzymy nowego w zamian. A jeśli pracownik/proces zakończył zadanie pomyślnie, zwracamy go do puli dostępnych pracowników.

RoadRunner: PHP nie jest stworzony, aby umierać, a Golang spieszy na pomoc

Do przechowywania puli aktywnych pracowników użyliśmy buforowanego kanału, aby usunąć z puli niespodziewanie 'umarłych' pracowników, dodaliśmy mechanizm śledzenia błędów i stanów pracowników.

W efekcie uzyskaliśmy działający serwer PHP, zdolny do przetwarzania wszelkich zapytań, przedstawionych w formacie binarnym.

Aby nasza aplikacja zaczęła działać jako serwer webowy, musieliśmy wybrać niezawodny standard PHP do reprezentacji wszelkich przychodzących zapytań HTTP. W naszym przypadku po prostu przekształcamy żądanie net/http z Go w format PSR-7, aby był zgodny z większością dostępnych dzisiaj frameworków PHP.

Ponieważ PSR-7 uważany jest za niemodyfikowalny (niektórzy powiedzą, że technicznie nie jest), deweloperzy muszą pisać aplikacje, które zasadniczo nie traktują żądań jako globalnej encji. To doskonale pasuje do koncepcji długoterminowych procesów PHP. Nasza finalna implementacja, która jeszcze nie otrzymała nazwy, wyglądała tak:

RoadRunner: PHP nie jest stworzony, aby umierać, a Golang spieszy na pomoc

Przedstawiamy RoadRunner — wysokowydajny serwer aplikacji PHP

Naszym pierwszym testowym zadaniem był backend API, na którym okresowo występowały nieprzewidywalne skoki zapytań (znacznie częściej niż zwykle). Mimo że w większości przypadków możliwości nginx były wystarczające, regularnie napotykaliśmy błąd 502, ponieważ nie mogliśmy wystarczająco szybko zbalansować systemu do 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 uzyskaliśmy niesamowity efekt! Nie tylko całkowicie pozbyliśmy się błędu 502, ale również udało nam się zmniejszyć liczbę serwerów o dwie trzecie, oszczędzając mnóstwo pieniędzy i tabletek przeciwbólowych dla inżynierów i menedżerów produktów.

W połowie roku udoskonaliliśmy nasze rozwiązanie, opublikowaliśmy je na GitHubie na licencji MIT i nazwaliśmy RoadRunner, podkreślając tym samym jego niesamowitą prędkość i wydajność.

Jak RoadRunner może poprawić Twój stos rozwoju

Zastosowanie RoadRunner pozwoliło nam wykorzystać middleware net/http po stronie Go, aby przeprowadzać weryfikację JWT jeszcze przed dotarciem zapytania do PHP, a także do obsługi WebSockets i globalnego agregowania stanów w Prometheus.

Dzięki wbudowanemu RPC można otworzyć API dowolnych bibliotek Go dla PHP bez konieczności pisania wrapperów w postaci rozszerzeń. Co ważniejsze, z pomocą RoadRunnera można wdrażać nowe serwery, które różnią się od HTTP. Przykłady obejmują uruchamianie w PHP procesorów AWS Lambda, tworzenie niezawodnych parserów kolejek, a nawet dodawanie gRPC do naszych aplikacji.

Przy wsparciu społeczności PHP i Go poprawiliśmy stabilność rozwiązania, w niektórych testach zwiększyliśmy wydajność aplikacji do 40 razy, udoskonaliliśmy narzędzia do debugowania, zaimplementowaliśmy integrację z frameworkiem Symfony oraz dodaliśmy wsparcie dla HTTPS, HTTP/2, wtyczek i PSR-17.

Podsumowanie

Niektórzy wciąż tkwią w przestarzałym wyobrażeniu o PHP jako o wolnym, nieporęcznym języku, nadającym się jedynie do pisania wtyczek pod WordPress. Ci ludzie mogą nawet powiedzieć, że PHP ma takie ograniczenie: gdy aplikacja staje się wystarczająco duża, trzeba wybierać bardziej «dojrzały» język i przepisywać nagromadzony przez lata kod.

Na to wszystko chcemy odpowiedzieć: pomyśl jeszcze raz. Uważamy, że tylko Ty sam nakładasz jakiekolwiek ograniczenia na PHP. Możesz spędzić całe życie przechodząc z jednego języka na drugi, próbując znaleźć idealne dopasowanie do swoich potrzeb, lub możesz zacząć postrzegać języki jako narzędzia. Rzekome niedociągnięcia języka takiego jak PHP mogą w rzeczywistości być powodem jego sukcesu. A jeśli połączysz go z innym językiem, takim jak Go, stworzysz znacznie potężniejsze produkty, niż gdybyś ograniczył się do korzystania z jednego języka.

Po pracy z zestawem Go i PHP możemy stwierdzić, że pokochaliśmy je. Nie planujemy poświęcać jednego na rzecz drugiego — wręcz przeciwnie, będziemy szukać sposobów, aby wydobyć jeszcze więcej korzyści z tego podwójnego stosu.

UPD: witamy twórcę RoadRunnera i współautora oryginalnego artykułu — Lachezis

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster