Pewnie słyszeliście, że nodejs jest jednowątkowym środowiskiem do uruchomienia JS na serwerze, i być może zastanawialiście się, dlaczego serwisy uruchomione w tym jednowątkowym środowisku są takie szybkiej w porównaniu do innych środowisk uruchomieniowych.
Odpowiedź jest prosta – Event Loop. Jest to jednocześnie zaletą i wadą tego całego noda.
Choć twierdzenie, że nodejs jest jednowątkowym środowiskiem z pierwszego zdania tego wpisu, już nie jest aktualnym, gdyż GC i kilka innych krytycznych funkcjonalności już działają w swoich osobnych wątkach, ale dla programistów piszących na tej platformie jest dostępny tylko jeden wątek. Wątek ten uruchamia kod umieszczony w stacku wywołań – są to wszystkie callbacki i kod JS umieszczony w tych callbackach. Dzięki temu, że jest jeden wątek na uruchomienia tego kodu dla całego kodu serwisu, procesor traci mniej czasu na przełączenia się między zadaniami i optymalnej wykorzystuje czas w przypadku obsługiwania dużej ilości połączeń do naszego http serwera uruchomionego na nodejs.
Otóż, callbacki – nodejs odkrył jeden prosty trick i wszyscy go nienawidzą. Każdy callback rejestruje się w tzw macroqueue skąd callback trafia do event loopa (wątku event loopa) kiedy przed nim nie ma innych zadań w macroqueue. Z nazwy widzimy że to jest stos, więc cała struktura jest oparta na FIFO – im szybciej zarejestruje się zadanie tym szybciej zostanie wykonane. Ale, zawsze jest jakieś małe „ale” – w V8 jest coś takiego jak microqueue, są to callbacki z wszelakich promisów, try/catch/finaly, async/await (przecież to promisy tbh). Wszystkie te zadania z microqueue mają priorytet przed zadaniami z macroqueue i są wykonywane przez silnik przed podjęciem się innego zadania z macroqueue – wszystkie są wykonywane naraz.
Pamiętając kilka tych szczegółach opisanych wyżej, możemy dojść do odpowiedzi na pytanie z tytułu tego wpisu:
No a więc, jak pisać na node tak, żeby potem opowiadać na konferencjach jak to Wasz zespół techniczny wytrzymuje wyprzedaży na black friday zwracając odpowiedzi do klientów w mniej niż sekunda? Odpowiedz jest prosta, i dotyczy nie tylko node ale ogółem każdego frameworka czy środowiska – trzeba znać szczegóły platformy i wykorzystywać je na swoją korzyść, dla node np – cały kod który nie wpływa na wynik zwrócony klientowi, np. logi – wynieść do osobnego wątku/procesu, skomplikowane wyliczenia przenieść na zewnątrz – najlepiej nie do nodejs a jakiś C/C++/C#/Java, wykorzystywać klasteryzację aplikacji oraz nauczyć się skalować aplikację horyzontalnie.
Na koniec mały przykład: pobieranie danych z bazy postgresql wraz z JOINami tablic za pomocą ręcznego napisanego zapytania i biblioteki node-progres oraz wykorzystanie prisma.io ORM dla tego samego celu – różnica circa 2 razy mnie obsłużonych requestów:
~/src/test/src master ✗
» autocannon http://localhost:8080/submission/09a9eb11-5ebc-4dba-b871-2323dd500715
Running 10s test @ http://localhost:8080/submission/09a9eb11-5ebc-4dba-b871-2323dd500715
10 connections
┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤
│ Latency │ 335 ms │ 405 ms │ 526 ms │ 535 ms │ 410.01 ms │ 45.32 ms │ 571 ms │
└─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘
┌───────────┬─────────┬─────────┬───────┬─────────┬─────────┬─────────┬─────────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼─────────┼─────────┼───────┼─────────┼─────────┼─────────┼─────────┤
│ Req/Sec │ 20 │ 20 │ 24 │ 28 │ 23.7 │ 2.8 │ 20 │
├───────────┼─────────┼─────────┼───────┼─────────┼─────────┼─────────┼─────────┤
│ Bytes/Sec │ 10.9 kB │ 10.9 kB │ 13 kB │ 15.2 kB │ 12.9 kB │ 1.52 kB │ 10.9 kB │
└───────────┴─────────┴─────────┴───────┴─────────┴─────────┴─────────┴─────────┘
Req/Bytes counts sampled once per second.
247 requests in 10.02s, 129 kB read
~/src/test/src master ✗
» autocannon http://localhost:8080/submission/09a9eb11-5ebc-4dba-b871-2323dd500715
Running 10s test @ http://localhost:8080/submission/09a9eb11-5ebc-4dba-b871-2323dd500715
10 connections
┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤
│ Latency │ 524 ms │ 564 ms │ 622 ms │ 630 ms │ 563.58 ms │ 27.68 ms │ 630 ms │
└─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘
┌───────────┬─────┬──────┬───────┬─────────┬─────────┬─────────┬───────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼─────┼──────┼───────┼─────────┼─────────┼─────────┼───────┤
│ Req/Sec │ 0 │ 0 │ 1 │ 20 │ 8.31 │ 8.56 │ 1 │
├───────────┼─────┼──────┼───────┼─────────┼─────────┼─────────┼───────┤
│ Bytes/Sec │ 0 B │ 0 B │ 543 B │ 10.9 kB │ 4.51 kB │ 4.65 kB │ 543 B │
└───────────┴─────┴──────┴───────┴─────────┴─────────┴─────────┴───────┘
Req/Bytes counts sampled once per second.
93 requests in 10.04s, 45.1 kB read