Szybki benchmark ReactPHP vs Swoole vs NodeJS

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

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 !

  1. wrk -t4 -c100 -d60s (4 wątki, 100 aktywnych połączeń, czas trwania 60s)
  2. wrk -t4 -c200 -d60s (4 wątki, 200 aktywnych połączeń, czas trwania 60s)
  3. wrk -t6 -c250 -d60s (6 wątków, 250 aktywnych połączeń, czas trwania 60s)
  4. 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>

  1. ReactPHP:     197114/60s    3283/1s
    NodeJS:         478912/60s    7977/1s
    Swoole:        1597235/60s  26576/1s
    _____________________________________
  2. ReactPHP:     198542/60s    3297/1s
    NodeJS:         493146/60s    8217/1s
    Swoole:        1917588/60s  26934/1s
    _____________________________________
  3. ReactPHP:     177450/60s    2952/1s wrk zgłosił 213 błędnych pakietów oraz 180 przedwcześnie zamkniętych połączeń
    NodeJS:         502589/60s    8369/1s
    Swoole:        1591360/60s  26496/1s
    _____________________________________
  4. ReactPHP:     183295/60s    3050/1s wrk zgłosił 423 błędnych pakietów oraz 517 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.

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.

ReactPHP , Swoole , NodeJS

Photo by Martin Damboldt from Pexels