Artykuły o technologiach IT, programowaniu, testowaniu i nie tylko

Jakiś czas temu w mojej głowie

Gracjan

zrodził się pomysł dotyczący Raspberry PI i innych podobnych mini-komputerów, który polega na wystawieniu prostego interfejsu, do którego będzie można się odwoływać z dowolnego miejsca i z jego pomocą sterować np. GPIO oraz innymi zasobami takiej płytki. Jest to tak naprawdę część mojego hobbystycznego projektu, więc nie wzięło się to znikąd.

Docelowe rozwiązanie powinno być wydajne (nie określiłem sobie konkretnych liczb) i zajmować mało pamięci RAM, nawet pod dużym obciążeniem.

Jak to bywa – opcji mamy mnóstwo: WebSockety, TCP, sockety Unixowe, HTTP, itd.

Mój wybór padł na HTTP. Dlaczego? Podszedłem do tego eksperymentalnie – sprawdzę, zobaczę, a w razie problemów zmienię koncepcję. Do tego wydaje mi się, że jest to jeden z najprostszych, jeśli nie najprostszy interfejs do komunikacji od strony klienta. Natomiast co do samej implementacji aplikacji webowej, każdy zrobi po swojemu – jeden użyje API, np. REST do zapalania i gaszenia diody w Raspberry PI, a innemu wystarczy prosta strona z przyciskiem.

Ok, ok, ale aplikacja ma być wydajna. Jak zapewne wszyscy wiemy, PHP do najbardziej wydajnych technologii nie należy (tak, w wersji 7 też). Inne języki skryptowe już na wstępie odpuściłbym. Cóż – Java? Odpada, bo RAM, więc zostaje C/C++, zwłaszcza że do GPIO jest całkiem przyjemna biblioteka dla tych języków – WiringPI. NodeJS także wydaje się rozsądną opcją, ze względu na to, że napisany jest w C na bazie wydajnego silnika JavaScript, także w dalszej części tego artykułu wezmę na warsztat Node 7 i C++.

O ile w Node napiszemy około 5 linijek i już mamy wystawiony serwer HTTP, który zwraca stały string, to jak wystawić aplikację C++ przez HTTP? Z PHP-FPM kojarzymy FastCGI, do tego dochodzi np. Nginx, tak? Bingo! Trzeba po prostu zaimplementować interfejs FastCGI w naszej aplikacji C/C++. Z pomocą przychodzi biblioteka libfcgi, która jest implementacją protokołu FastCGI.

Mamy aplikację, która zwraca na razie stały ciąg znaków, więc musimy jeszczę ją uruchomić w kontekście FastCGI i skonfigurować Nginx. Do wystawienia socketu FastCGI można użyć przykładowo spawn-fcgi.

Odpytujemy odpowiedni host i port przez przeglądarkę, nasza aplikacja działa, jest super, czujemy się świetnie, prawie tak jakbyśmy pozbyli się głodu na świecie, ale czegoś tu brakuje…frameworka! Przecież w Node mamy do wyboru XYZ+ różnych frameworków webowych. Lecz spokojnie, bez paniki, dla C/C++ także są gotowe frameworki/biblioteki do tworzenia aplikacji webowych, ale ja postanowiłem napisać coś swojego – ot tak dla zabawy i w celu podskillowania C++, a przyznam, że frajdy miałem sporo oraz wiele się nauczyłem 🙂

Przedstawiam Wam Hetach

Hetach jest jeszcze w fazie developmentu, więc do pierwszego release’a trochę mu brakuje, ale prostą aplikację webową można już na nim postawić. W osobnym repozytorium znajdują się przykłady.

Teraz chyba najciekawsza część tego artykułu – liczby, testy, ciekawostki, taka sytuacja 🙂

Zacznę od Node’a więc. Na początek wydajność “gołego” serwera http, zatem pierwsza aplikacja, jaką się posłużę wygląda następująco:

Jak sprawdzam wydajność poszczególnych programów? Mam przygotowanego JMetera, który:

  • uruchamia 50 wątków w ciągu 1 sekundy
  • odpytuje jeden konkretny url (/api/rest/companies/1)
  • działa przez 30 sekund

Wynik po tych 30 sekundach działania JMetera umieszczam tutaj. Każda użyta aplikacja jest jednowątkowa (także % przy użyciu CPU to wykorzystanie jednego rdzenia). No to do dzieła – uruchamiam kontener:

I nasyłam na aplikację legion, po czym dostaję taki oto wynik:

Min [ms] Max [ms] Średnia [ms] Przepustowość [req/s] Użycie CPU i RAM
0 49 39 1223,9 10% CPU
40Mb RAM

W tym momencie nie wiem czy to źle, czy dobrze. Pora na jakiś framework, więc odpalam Google, wpisuję “node js web frameworks”, klikam pierwszy wynik i wybieram pierwszy z brzegu framework o dumnej nazwie “Hapi”. Kolejna aplikacja wygląda tak:

Można tutaj zauważyć jeden parametr w linku, który potem jest wyświetlany, identyczną rzecz będą robić pozostałe aplikacje wykorzystujące jakiś framework. No więc ponownie uruchamiam kontener, JMetera i oto wynik:

Min [ms] Max [ms] Średnia [ms] Przepustowość [req/s] Użycie CPU i RAM
1 67 41 1186 90% CPU
85Mb RAM

W tym momencie coś mi zaczęło śmierdzieć…i to wcale nie był oddech mojego psa przebywającego obok. Mało możliwe, aby narzut frameworka (zerknąłem w jego kod i uwierzcie mi – MAŁO możliwe) spowodował utratę jedynie około 40 req/s… Tak więc ze strony wybieram kolejny framework z listy (na drugim miejscu Socket.IO, więc skip) i trafiłem na “Express”. Copy-paste i mamy:

Testy pokazują już coś zupełnie innego:

Min [ms] Max [ms] Średnia [ms] Przepustowość [req/s] Użycie CPU i RAM
0 29 6 7695,7 100% CPU
70Mb RAM

Chwila konsternacji…no to rzucam okiem w jego kod i widzę, że zamiast response.write() z pierwszego przykładu, używają response.end(). Mhm, no to zmieniam ten pierwszy przykład, a konkretnie response.write() zastępuję res.end(„test”, „utf8”), odpalam testy i:

Min [ms] Max [ms] Średnia [ms] Przepustowość [req/s] Użycie CPU i RAM
0 24 3 14951,8 100% CPU
65Mb RAM

Myślę: całkiem nieźle, wow, uszanowanko 😀 No ale nie oszukujmy się, to jest jednak JavaScript, więc przy takim ruchu aplikacja napisana w C++ będzie – zakładam – sobie odpoczywać, prawda? Na razie pomijam Hetach i analogicznie jak przy Node, sprawdzam najpierw najprostszą możliwą aplikację wystawiającą socket FastCGI, która wygląda tak:

Za dużo się tu nie dzieje, ostatecznie w przeglądarce dostaniemy “test”. Tak więc przez dockera uruchamiam jeden kontener z powyższą aplikacją wystawioną za pomocą spawn-fcgi, a w drugim kontenerze Nginx z podstawową konfiguracją:

Oczywiście uruchamiam JMetera z myślą, że Node zostanie za chwilę zaorany…a tu zonk:

Min [ms] Max [ms] Średnia [ms] Przepustowość [req/s] Użycie CPU i RAM
0 19 6 7406,7 Nginx:
100% CPU
10Mb RAM
Aplikacja:
40% CPU
5Mb RAM

…tego to się nie spodziewałem. Zacząłem kombinować z ustawieniami Nginxa – dodawać workery, zwiększać maksymalną ilość połączeń, ale nic to nie dało. Co bym nie zrobił, to przepustowość i tak utrzymywała się na poziomie 7-8 tysięcy na sekundę…trochę bieda, nie?

Ale zaraz, zaraz – chwila rozkminy – przecież FastCGI jest protokołem do komunikacji pomiędzy dwoma różnymi procesami. Serwer http Node’a i aplikacja, którą napisaliśmy działa przecież w obrębie jednego procesu. Dodatkowo nie wiem, czy to Nginx jest tutaj wąskim gardłem, czy socket wystawiony przez spawn-fcgi (powyżej widać, że Nginx pracuje na 100%, ale przy 4 workerach było około 50-60% użycia CPU). Wygooglałem, że ludzie wyciskają z Nginx grubo ponad 100k requestów i się zastanawiają czemu tak wolno. Postanowiłem jednak nie drążyć tego tematu. Pora pogooglować w poszukiwaniu informacji o serwerach http napisanych w C++. Znalazłem gotowca, ale miał zależność w postaci boosta – za czym nie przepadam. Ostatecznie zdecydowałem się na bibliotekę libevent, która udostępnia odpowiednie funkcjonalności potrzebne do postawienia serwera http. Uruchomiłem prosty przykład w postaci:

Moim oczom ukazały się takie oto liczby:

Min [ms] Max [ms] Średnia [ms] Przepustowość [req/s] Użycie CPU i RAM
0 25 0 38702 100% CPU
1-2Mb RAM

I to mi się podoba! 🙂 W Hetach, oprócz FastCGI zaimplementowałem także prosty serwer http, właśnie na bazie libevent. Tak więc skupię się teraz na testowaniu Hetach. Od razu powiem, że ostatecznie przez FastCGI i tak osiągam tylko 7-8 tysięcy zapytań na sekundę, więc uwagę poświęcę wbudowanemu serwerowi. Każdy test odbywa się na aplikacji z przykładów.

Min [ms] Max [ms] Średnia [ms] Przepustowość [req/s] Użycie CPU i RAM
1 81 19 2494,8 100% CPU
5Mb RAM

Co tu dużo mówić…zbyt różowo to nie wygląda. Po sprofilowaniu aplikacji Valgrindem okazało się, że wyrażenia w C++ jednak do najszybszego mechanizmu nie należą. Używałem ich w dwóch miejsach: w routerze i w przykładowym programie (klucz id w zwrotce JSON pobierany z url). Tak więc router przepisałem, przykład również zmodyfikowałem, do tego doszło jeszcze kilka optymalizacji samego Hetach. Rezultat tej optymalizacji znajduje się poniżej:

Min [ms] Max [ms] Średnia [ms] Przepustowość [req/s] Użycie CPU i RAM
0 26 3 14422,7 100% CPU
5Mb RAM

Dużo lepiej, prawda? Zapewne nadal jest coś, co można tam zrobić lepiej, ale ale…zerkam w nagłówki odpowiedzi z aplikacji Node’a i puszczonej przez Nginx i moim oczom ukazuje się nagłówek: Connection: keep-alive. Takie cwaniaczki jeden z drugim 🙂 Ja używam w serwerze Hetach Connection: closed z tego względu, że z nim zacząłem optymalizować framework, więc potraktowałem to jako punkt odniesienia. To nie będę gorszy:

Min [ms] Max [ms] Średnia [ms] Przepustowość [req/s] Użycie CPU i RAM
0 19 2 20000,1 100% CPU
5Mb RAM

Mam jeszcze jednego asa w rękawie – optymalizacja kodu podczas kompilowania. Włączyłem w bibliotece Hetach i w aplikacji testowej optymalizację na poziomie 3 – tak od razu z grubej rury, a co 😀

Min [ms] Max [ms] Średnia [ms] Przepustowość [req/s] Użycie CPU i RAM
0 23 1 27558,6 100% CPU
5Mb RAM

Jeden parametr w g++, a zyskaliśmy prawie 8 tysięcy requestów profitu.

Na początku artykułu wspomniałem o Raspberry PI. Ja mam podobną płytkę – Orange PI One (dla której zamiennikiem WiringPI jest WiringOP) z procesorem 600MHz, mój komputer posiada procesor z taktowaniem 3,5GHz. Użyję aplikacji, które osiągnęły najlepszy wynik w powyższych testach. Sprawdźmy zatem różnicę w przypadku Node’a (akurat tutaj w wersji 0.10):

Min [ms] Max [ms] Średnia [ms] Przepustowość [req/s] Użycie CPU i RAM
7 321 80 608,4 100% CPU
25Mb RAM

Oraz Hetach:

Min [ms] Max [ms] Średnia [ms] Przepustowość [req/s] Użycie CPU i RAM
1 31 18 2581,3 100% CPU
2Mb RAM

Wnioski są następujące:

  1. Nie zależy Ci na zużyciu pamięci, ale jednocześnie aplikacja nie musi być wysoko wydajna? Polecam Node, jednakże warto zwrócić uwagę na wybrany/wybierany framework,
  2. Zależy Ci bardzo na pamięci lub wydajności i możesz poświęcić trochę więcej czasu na programowanie? Świetną opcją jest C++,
  3. Potrzebujesz wystawić interfejs API-REST lub inny webowy biblioteki, np. WiringPI? Polecam Hetach lub podobne rozwiązania,
  4. Masz zamiar uruchomić swoją aplikację na nisko wydajnej platformie? Obie omawiane technologie są dobrym rozwiązaniem, wszystko zależy od Twoich potrzeb.

  • kutyba.it

    Fajna sprawa. Ja chcę zbierać dane z czujników, więc pewnie przyda mi się interfejs webowy do prezentowania wartości z czujników.
    Nie napisałeś na czym robiłeś benchmarki, to był jakiś intel 3,5GHz?

Nawigacja