
W ekosystemie PHP obecnie istnieją dwa konektory do pracy z serwerem Tarantool — to oficjalne rozszerzenie PECL , napisane w C, oraz , 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:
- — 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 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«.
- — 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 .
Wyniki
Tryb synchroniczny
Protokół Tarantoola używa formatu binarnego 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. . 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 (oficjalne rozszerzenie MessagePack PECL), a drugi — na (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:

Chociaż wersja PHP (Pure) ustępuje rozszerzeniu PECL pod względem szybkości, w rzeczywistych projektach nadal zalecałbym używanie właśnie , 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 (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:

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:

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 ) i, sądząc po liście , 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:

Rozdzielamy 10,000 operacji na 25 korutyn i patrzymy, co się wydarzyło:

Liczba operacji na sekundę wzrosła ponad 3 razy dla !
Smutne, ale konektor PECL nie uruchomił się z ext-async.
A co z SQL?

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:

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:

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:

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:

Wynosi ona 16 na mojej maszynie. Uruchomimy benchmarki konektorów na 16 równoległych wątkach:

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:

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

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 .
Źródło: habr.com
