Jakiś czas temu, miałem możliwość przyjrzenia się dostępnym na rynku technologiom a bardziej precyzując frameworkom, które dostarczają nam możliwości poradzenia sobie z problemem C10k. Po kilkudniowym researchu, wybrałem trzy dostępne rozwiązania, mniej lub bardziej znane, warunkiem klasyfikacyjnym było spełnienie minimum jednego z poniższych wymogów:
- Event-driven
- Async
- non blocking I/O
- Concurrency
- Http/TCP/UDP/Socket server
Kandydatów było wielu, natomiast z racji, że moim głównym obszarem zainteresowania jest PHP oraz JS, gdy tylko pojawili się pierwsi faworyci z tego obszaru oraz po głębszej weryfikacji, postanowiłem pozostać w tym obszarze.
ReactPHP, Swoole, NodeJs
To właśnie te trzy rozwiązania wziąłem pod lupę, aby sprawdzić ich wydajność oraz w jakiś sposób je zaklasyfikować.
Wersje wykorzystywanych frameworków:
ReactPHP: 0.4.1
Swoole: 1.9.23
NodeJS: 8.9.0 LTS
Procedura testowa
Na potrzeby testów udało mi się uzyskać maszynę fizyczną (nie maszynę wirtualną) o parametrach
8 core 8GB RAM 250GB HDD
Test dzielił się na 3 etapy, każdy z etapów wymagał przygotowania odpowiedniego programu w taki sposób aby mógł korzystać z 4 rdzeni procesora, w każdym z powyższych rozwiązań. Każdy z testów opierał się na protokole HTTP/1.1
obsługiwanego przez dostarczony serwer HTTP danego rozwiązania.

Etap 1:
- Odebranie żądania
GET
- Konwersja 100-elementowej tablicy na
JSON
- Wysłanie w odpowiedzi przygotowanego
JSON'a
- Zamknięcie połączenia
Etap 2:
- Odebranie żądania
POST
z plikiem graficznym o wadze 1 mb - Wygenerowanie
UUID
w wersji 4 - Zapisanie pliku graficznego na dysku pod nazwą wcześniej wygenerowanego
UUID'a
- Wysłanie w odpowiedzi wcześniej wygenerowanego
UUID'a
- Usunięcie pliku z dysku
- Zamknięcie połączenia
Etap 3:
- Odebranie żądania
POST
z plikiem graficznym o wadze 0,3 mb - Wygenerowanie
UUID
w wersji 4 - Zapisanie pliku graficznego na dysku pod nazwą wcześniej wygenerowanego
UUID'a
- Odczytanie pliku z dysku
- Wysłanie w odpowiedzi odczytanego pliku
- Usunięcie pliku z dysku
- Zamknięcie połączenia
Czym testować
Wykorzystanym narzędziem testowym był wrk, który był uruchamiany na tej samej maszynie w celu zniwelowania błędu pomiarowego wynikającego z opóźnień sieci. Każdy z etapów był wykonywany w tej samej konfiguracji uruchomieniowej narzędzia, a tych konfiguracji było aż 4 !
wrk -t4 -c100 -d60s
(4 wątki, 100 aktywnych połączeń, czas trwania 60s)wrk -t4 -c200 -d60s
(4 wątki, 200 aktywnych połączeń, czas trwania 60s)wrk -t6 -c250 -d60s
(6 wątków, 250 aktywnych połączeń, czas trwania 60s)wrk -t6 -c550 -d60s
(6 wątków, 550 aktywnych połączeń, czas trwania 60s)
A każdy z etapów był powtarzany 5 razy.
Wyniki
Wyniki tego benchmarku są najbardziej kontrowersyjnym akapitem tego wpisu, a więc do sedna.
Wartości pokazane poniżej są średnią arytmetyczną z każdego etapu ((etap1_proba1 + etap1_proba2 + etap1_proba3 + etap1_proba4 + etap1_proba5) / 5)
Etap 1 zaowocował uśrednionymi wartościami rzędu (wyniki dla każdej konfiguracji wrk):
<nr konfiguracji> <nazwa frameworka>: <ilość połączeń w trakcie 60s> <ilość połączeń w trakcie 1s>
- ReactPHP:
197114/60s
3283/1s
NodeJS:478912/60s
7977/1s
Swoole:1597235/60s
26576/1s
_____________________________________ - ReactPHP:
198542/60s
3297/1s
NodeJS:493146/60s
8217/1s
Swoole:1917588/60s
26934/1s
_____________________________________ - ReactPHP:
177450/60s
2952/1s
wrk zgłosił213
błędnych pakietów oraz180
przedwcześnie zamkniętych połączeń
NodeJS:502589/60s
8369/1s
Swoole:1591360/60s
26496/1s
_____________________________________ - ReactPHP:
183295/60s
3050/1s
wrk zgłosił423
błędnych pakietów oraz517
przedwcześnie zamkniętych połączeń
NodeJS:507302/60s
8450/1s
Swoole:1786041/60s
29743/1s
Wartości liczbowych dla etapu 2 oraz 3 nie ma sensu podawać, ponieważ każde z pięciu wywołań danej konfiguracji w tych etapach wykazywały duże różnice w wynikach (różnice między wywołaniami a nie między technologiami) mogące wynikać z problemów dysku twardego, przeciążenia sieci lub też systemowego GC
. Niemniej jednak końcowe wyniki w tych etapach wskazywały zbieżność z wynikami z etapu pierwszego, gdzie to NodeJS
wykazywał wydajność od ReactPHP
od 2 do 3 razy większą, natomiast Swoole
wykazywał zwiekszoną wydajność względem NodeJS
o minimum 3 razy.
Warto zaznaczyć fakt, że tylko ReactPHP
wykazał problemy z pakietami i przedwcześnie zamkniętymi połączeniami, ze względu na ich zbyt dużą ilość. Dodatkowo udało mi się ustalić wartość graniczną ilości stabilnych połączeń na testowanej maszynie tylko dla ReactPHP
oraz NodeJS
. I tak też, dla Reacta stabilną 3297/1s
dla Noda 8450/1s
, dla Swooliego nie udało się wyznaczyć wartości granicznej, po kilkunastu próbach postanowiłem zakończyć poszukiwania kończąc na wartości ponad 37500/1s
.
Podsumowanie
Tak więc wnioski wysuwają się same. Kompromisowym rozwiązaniem okazał się NodeJS, oferujący dobrą wydajność, która większości z nas bez problemu wystarczy, rekompensując chwilowe potknięcia bardzo dużą bazą dostępnych gotowych rozwiązań, mogących pochwalić się także bardzo dużym wsparciem społeczności.
Jeśli natomiast nasza aplikacja jest klasy enterprise musi cechować się bardzo dużą wydajnością kosztem ciut dłuższego wytwarzania aplikacji w przypadku braku gotowego rozwiązania zaproponowanego przez społeczność, która jeszcze jest mała, ale ciągle rośnie. Wybrałbym Swoole.
Natomiast ReactPHP pozostawiłbym raczej w sferze domowych zabaw z kodem i wstępu do powyższych technologii. React może przydać nam się, aby zrozumieć jak działają eventy i poeksperymentować z operacjami blokującymi i nieblokującymi.
Wybraliśmy Swoole jako technologię dla naszego Authorization Service autoryzującego ruch pomiędzy microserwisami. Czy można nazwać kompromisem coś, co jest prawie 4x wolniejsze?