Przyspieszamy PHP-konektory do Tarantoola za pomocą Async, Swoole i Parallel

Przyspieszamy PHP-konektory do Tarantoola za pomocą Async, Swoole i Parallel

W ekosystemie PHP obecnie istnieją dwa konektory do pracy z serwerem Tarantool — to oficjalne rozszerzenie PECL tarantool/tarantool-php, napisane w C, oraz tarantool-php/client, napisane w PHP. Jestem autorem tego drugiego.

W tym artykule chciałbym podzielić się wynikami testowania wydajności obu bibliotek i pokazać, jak przy minimalnych zmianach w kodzie można osiągnąć wzrost wydajności o 3-5 (na syntetycznych testach!).

Co będziemy testować?

Będziemy testować wspomniane wyżej synchronizowane konektory, uruchomione asynchronicznie, równolegle i asynchronicznie-równolegle. 🙂 Również nie chcemy zmieniać kodu samych konektorów. Obecnie dostępnych jest kilka rozszerzeń, które pozwalają osiągnąć zamierzony cel:

  • Swoole — wysokowydajny asynchroniczny framework dla PHP. Używany przez takie giganty internetowe jak Alibaba i Baidu. Od wersji 4.1.0 pojawiła się magiczna metoda SwooleRuntime::enableCoroutine(), która pozwala «jednym wierszem kodu przekształcić synchronizowane biblioteki sieciowe PHP w asynchroniczne».
  • Async — do niedawna obiecujące rozszerzenie do asynchronicznej pracy w PHP. Dlaczego do niedawna? Niestety, z nieznanych mi powodów, autor usunął repozytorium, a przyszłość projektu jest niepewna. Będziemy musieli skorzystać z jednego z forków. Podobnie jak Swoole, to rozszerzenie pozwala w prosty sposób włączyć asynchroniczność, zamieniając standardowe implementacje strumieni TCP i TLS na ich asynchroniczne wersje. Robi się to za pomocą opcji «async.tcp = 1«.
  • Parallel — dość nowe rozszerzenie od nie mniej znanego Joe Watkinsa, autora takich bibliotek jak phpdbg, apcu, pthreads, pcov, uopz. Rozszerzenie udostępnia API do wielowątkowej pracy w PHP i jest pozycjonowane jako alternatywa dla pthreads. Istotnym ograniczeniem biblioteki jest to, że działa tylko z wersją PHP ZTS (Zend Thread Safe).

Jak będziemy testować?

Uruchomimy instancję Tarantoola z wyłączonym dziennikiem zapisu wstępnego (wal_mode = none) i zwiększonym buforem sieciowym (readahead = 1 * 1024 * 1024). Pierwsza opcja wykluczy pracę z dyskiem, druga — umożliwi odczyt większej liczby zapytań z bufora systemu operacyjnego, minimalizując tym samym liczbę wywołań systemowych.

Dla benchmarków, które działają na danych (wstawianie, usuwanie, odczyt itp.) przed rozpoczęciem benchmarku będzie (prze)tworzona przestrzeń memtx, w której wartości indeksu pierwotnego są generowane przez generator uporządkowanych wartości liczb całkowitych (sequence).
DDL przestrzeni wygląda tak:

space = box.schema.space.create(config.space_name, {id = config.space_id, temporary = true})
space:create_index('primary', {type = 'tree', parts = {1, 'unsigned'}, sequence = true})
space:format({{name = 'id', type = 'unsigned'}, {name = 'name', type = 'string', is_nullable = false}})

W razie potrzeby, przed uruchomieniem benchmarku, przestrzeń jest wypełniana 10 000 krotkami rodzaju

{id, "tuplе_<id>"}

Dostęp do krotek realizowany jest na podstawie losowej wartości klucza.

Sam benchmark stanowi pojedyncze zapytanie do serwera, które wykonywane jest 10 000 razy (rewolucji), które z kolei realizowane są w iteracjach. Iteracje powtarzane są, dopóki wszystkie odchylenia czasowe między 5 iteracjami nie będą mieścić się w dopuszczalnym błędzie wynoszącym 3 %*. Po tym brany jest średni wynik. Między iteracjami jest przerwa 1 sekundy, aby nie dopuścić do throttlingu procesora. Zbieracz śmieci Lua jest wyłączany przed każdą iteracją i wymuszenie uruchamiane po jej zakończeniu. Proces PHP uruchamiany jest tylko z niezbędnymi do benchmarku rozszerzeniami, z włączoną buforowaniem wyjścia i wyłączonym zbieraczem śmieci.

* Liczba rewolucji, iteracji i próg błędu mogą być zmieniane w ustawieniach benchmarku.

Środowisko testowe

Opublikowane poniżej wyniki uzyskano na MacBookPro (2015), system operacyjny — Fedora 30 (wersja jądra 5.3.8-200.fc30.x86_64). Tarantool uruchamiano w Dockerze z parametrem "--network host".

Wersje pakietów:

Tarantool: 2.3.0-115-g5ba5ed37e
Docker: 19.03.3, build a872fc2f86
PHP: 7.3.11 (cli) (zbudowany: Oct 22 2019 08:11:04)
tarantool/client: 0.6.0
rybakit/msgpack: 0.6.1
ext-tarantool: 0.3.2 (+ poprawka dla 7.3)*
ext-msgpack: 2.0.3
ext-async: 0.3.0-8c1da46
ext-swoole: 4.4.12
ext-parallel: 1.1.3

* Niestety, oficjalny konektor nie działa z wersją PHP > 7.2. Aby skompilować i uruchomić rozszerzenie na PHP 7.3, trzeba było skorzystać z łatkami.

Wyniki

Tryb synchroniczny

Protokół Tarantoola używa formatu binarnego MessagePack do serializacji wiadomości. W konektorze PECL serializacja jest ukryta głęboko w czeluściach biblioteki i nie można wpływać na proces kodowania z poziomu kodu userland. nie ma możliwości.. Konektor w czystym PHP, przeciwnie, zapewnia możliwość dostosowania procesu kodowania poprzez rozszerzenie standardowego kodera lub możliwość wykorzystania własnej implementacji. Z pudełka dostępne są dwa kodery, z których jeden oparty jest na msgpack/msgpack-php (oficjalne rozszerzenie MessagePack PECL), a drugi — na rybakit/msgpack (na czystym PHP).

Przed porównaniem konektorów, zmierzmy wydajność koderów MessagePack dla konektora PHP i w dalszych testach zastosujemy ten, który osiągnie lepszy wynik:

Przyspieszamy PHP-konektory do Tarantoola za pomocą Async, Swoole i Parallel
Chociaż wersja PHP (Pure) ustępuje rozszerzeniu PECL pod względem szybkości, w rzeczywistych projektach nadal zalecałbym używanie właśnie rybakit/msgpack, ponieważ w oficjalnym rozszerzeniu MessagePack specyfikacja formatu została zrealizowana jedynie częściowo (na przykład brak wsparcia dla typów danych użytkownika, bez którego nie będziesz mógł użyć Decimal — nowego typu danych wprowadzonego w Tarantool 2.3) i ma szereg innych problemach (w tym problemy z kompatybilnością z PHP 7.4). Ogólnie projekt wygląda na porzucony.

Zatem, zmierzmy wydajność konektorów w trybie synchronicznym:

Przyspieszamy PHP-konektory do Tarantoola za pomocą Async, Swoole i Parallel
Jak widać z wykresu, konektor PECL (Tarantool) pokazuje lepszą wydajność w porównaniu z konektorem w PHP (Client). Ale to nie jest zaskakujące, biorąc pod uwagę, że ostatni, oprócz tego, że został zaimplementowany w wolniejszym języku, wykonuje, w zasadzie, więcej pracy: przy każdym wywołaniu tworzy nowy obiekt Request i Odpowiedź (w przypadku Select — jeszcze i Criteria, a w przypadku Update/Upsert — Operations), oddzielne byty Connection, Packer i Handler również dodają narzut. Oczywiste jest, że za elastyczność trzeba płacić. Niemniej jednak, ogólnie rzecz biorąc, interpreter PHP pokazuje dobrą wydajność, chociaż różnica istnieje, jest ona nieznaczna i być może będzie jeszcze mniejsza przy użyciu preloading w PHP 7.4, nie mówiąc już o JIT w PHP 8.

Przechodzimy dalej. W Tarantool 2.0 pojawiło się wsparcie dla SQL. Spróbujmy wykonać operacje Select, Insert, Update i Delete używając protokołu SQL i porównać wyniki z noSQL (binarnymi) odpowiednikami:

Przyspieszamy PHP-konektory do Tarantoola za pomocą Async, Swoole i Parallel
Wyniki SQL nie są zbyt imponujące (przypominam, że wciąż testujemy tryb synchroniczny). Jednak nie sądzę, aby warto było się tym martwić za wcześnie, wsparcie dla SQL wciąż znajduje się w aktywnym rozwoju (niedawno na przykład dodano wsparcie dla prepared statements) i, sądząc po liście issues, silnik SQL czeka na szereg optymalizacji.

Async

Cóż, zobaczmy teraz, jak rozszerzenie Async może pomóc nam poprawić wyniki powyżej. Do pisania programów asynchronicznych rozszerzenie udostępnia API oparte na korutynach (coroutines), z tego skorzystamy. Doświadczeniem ustalamy, że optymalna liczba korutyn w naszym środowisku wynosi 25:

Przyspieszamy PHP-konektory do Tarantoola za pomocą Async, Swoole i Parallel
Rozdzielamy 10,000 operacji na 25 korutyn i patrzymy, co się wydarzyło:

Przyspieszamy PHP-konektory do Tarantoola za pomocą Async, Swoole i Parallel
Liczba operacji na sekundę wzrosła ponad 3 razy dla tarantool-php/client!

Smutne, ale konektor PECL nie uruchomił się z ext-async.

A co z SQL?

Przyspieszamy PHP-konektory do Tarantoola za pomocą Async, Swoole i Parallel
Jak widać, w trybie asynchronicznym różnica między protokołem binarnym a SQL stała się w granicach błędu.

Swoole

Ponownie ustalamy optymalną liczbę korutyn, tym razem dla Swoole:
Przyspieszamy PHP-konektory do Tarantoola za pomocą Async, Swoole i Parallel
Zatrzymujemy się na 25. Powtórzymy ten sam trick, co z rozszerzeniem Async - rozdzielimy 10,000 operacji między 25 korutynami. Ponadto, dodamy jeszcze test, w którym podzielimy całą pracę na 2 procesy (czyli każdy proces będzie wykonywał 5,000 operacji w 25 korutynach). Procesy będą tworzone za pomocą SwooleProcess.

Wyniki:

Przyspieszamy PHP-konektory do Tarantoola za pomocą Async, Swoole i Parallel
Swole pokazuje nieco niższy wynik w porównaniu do Async przy uruchomieniu w jednym procesie, ale z 2 procesami sytuacja zmienia się diametralnie (liczba 2 nie jest przypadkowa, na mojej maszynie właśnie 2 procesy pokazały najlepszy wynik).

Apropos, w rozszerzeniu Async również znajduje się API do pracy z procesami, jednak nie zauważyłem jakiejś różnicy w uruchamianiu benchmarków w jednym lub wielu procesach (nie wykluczone, że coś popsułem).

SQL vs protokół binarny:

Przyspieszamy PHP-konektory do Tarantoola za pomocą Async, Swoole i Parallel
Tak jak z Async, różnica między operacjami binarnymi a SQL jest neutralizowana w trybie asynchronicznym.

Parallel

Ponieważ rozszerzenie Parallel nie dotyczy korutyn, ale wątków, zmierzymy optymalną liczbę równoległych wątków:

Przyspieszamy PHP-konektory do Tarantoola za pomocą Async, Swoole i Parallel
Wynosi ona 16 na mojej maszynie. Uruchomimy benchmarki konektorów na 16 równoległych wątkach:

Przyspieszamy PHP-konektory do Tarantoola za pomocą Async, Swoole i Parallel
Jak widzicie, wynik jest nawet lepszy niż w przypadku asynchronicznych rozszerzeń (nie licząc Swoole uruchomionego na 2 procesach). Zauważcie, że dla konektora PECL w miejscu operacji Update i Upsert jest pusto. Jest to związane z tym, że te operacje zakończyły się błędem - nie wiem, czy to wina ext-parallel, ext-tarantool, czy obu.

Teraz porównajmy wydajność SQL:

Przyspieszamy PHP-konektory do Tarantoola za pomocą Async, Swoole i Parallel
Zauważyliście podobieństwo do wykresu dla konektorów uruchomionych synchronnie?

Wszystko razem

Na koniec zestawmy wszystkie wyniki w jednym wykresie, aby zobaczyć ogólny obraz testowanych rozszerzeń. Dodamy do wykresu tylko jeden nowy test, którego jeszcze nie przeprowadzaliśmy — uruchomimy korutyny Async równolegle przy pomocy Parallel*. Idea integracji wcześniej wspomnianych rozszerzeń już była omawiana przez autorów, jednak konsensus nie został osiągnięty, będziemy musieli zrobić to samodzielnie.

* Nie udało się uruchomić korutyn Swoole z Parallel, wydaje się, że te rozszerzenia są niekompatybilne.

Oto finalne wyniki:

Przyspieszamy PHP-konektory do Tarantoola za pomocą Async, Swoole i Parallel

Zamiast zakończenia

Moim zdaniem wyniki są bardzo obiecujące i mam jakieś przeczucie, że to jeszcze nie koniec! Czy potrzebujesz tego w rzeczywistym projekcie, decydujesz wyłącznie ty sam, powiem tylko, że dla mnie to był interesujący eksperyment, który pozwolił ocenić, ile można „wycisnąć” z synchronizowanego TCP-łącznika z minimalnym wysiłkiem. Jeśli masz pomysły na ulepszenie benchmarków — chętnie rozważę twój pull request. Cały kod z instrukcjami uruchomienia i wynikami jest opublikowany w osobnym repozytorium.

Ź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