Błąd w Node.js, którego nie wykryje Twój monitoring

Twój health check twierdzi, że wszystko jest w porządku. Trwa jedną milisekundę. Potem ruch rośnie. Nagle Twoje p99 latency skacze do 400 ms. Patrzysz na swoje dashboardy. Wszystko świeci się na zielono.

Użycie CPU jest umiarkowane. Event loop lag jest płaski. Pamięć jest w normie. Twoje APM pokazuje wolne żądanie, ale nie mówi nic o przyczynie. Nie ma wolnych wywołań bazy danych. Nie ma błędów.

Czas jest zużywany w puli wątków libuv.

Standardowa obserwowalność Node skupia się na event loopie. Pula to oddzielna kolejka. Znajduje się poza Twoim zasięgiem.

Node uruchamia JavaScript na event loopie. Przekazuje ciężkie zadania do puli wątków libuv. Obejmuje to:

  • Operacje na systemie plików (fs.readFile, fs.writeFile).
  • Zadania kryptograficzne (bcrypt, scrypt, pbkdf2).
  • Kompresję (zlib gzip, deflate).
  • Zapytania DNS (dns.lookup).

Domyślnie pula posiada tylko cztery wątki. Dotyczy to niezależnie od liczby rdzeni CPU w Twojej maszynie.

Cztery wątki to za mało. Oto trzy sposoby, w jakie pula zawodzi:

  1. Bcrypt podczas logowania. Pojedyncze haszowanie bcrypt może zająć 250 ms. Jeśli cztery osoby zalogują się jednocześnie, wszystkie sloty zostaną zajęte. Piąta osoba czeka w kolejce. Traci 250 ms tylko na samo rozpoczęcie. Twoje opóźnienie podwaja się.

  2. Duże operacje gzip. Kompresowanie dużej odpowiedzi zajmuje slot w puli. Jeśli cztery żądania robią to jednocześnie, każde inne zadanie musi czekać. Zapytania DNS i odczyty plików utykają w kolejce.

  3. Zapytania DNS. Większość aplikacji Node korzysta z dns.lookup. Wykorzystuje to blokujące wywołanie systemowe. Umieszcza to zadanie w puli. Jeśli Twoja sieć ma chwilowe zakłócenia, zapytania te blokują całą pulę.

Żądanie utknięte w kolejce puli jest niewidoczne. Nie zużywa CPU. Nie uruchamia JavaScriptu. Jest po prostu „zaparkowane”.

Jak to znaleźć:

Jeśli Twoje p99 latency rośnie pod obciążeniem, ale event loop lag pozostaje płaski, sprawdź pulę.

Najszybszy test: Zwiększ UV_THREADPOOL_SIZE. Ustaw go na 64 w swoim środowisku i zrestartuj aplikację. Jeśli opóźnienie spadnie, znalazłeś problem.

Jak to naprawić właściwie:

  • Używaj worker_threads dla ciężkiej kryptografii, takiej jak bcrypt. Pozwala to uniknąć obciążania puli libuv.
  • Używaj dns.resolve zamiast dns.lookup. Jest to prawdziwy asynchroniczny resolver.
  • Używaj strumieni (streams) do operacji zlib, aby szybciej zwalniać sloty.
  • Unikaj ciężkich operacji na systemie plików w głównych ścieżkach żądań.

Przestań wpatrywać się w zielone dashboardy, podczas gdy Twoi użytkownicy czekają.

Źródło: https://dev.to/r9v/the-nodejs-bug-thats-invisible-to-your-monitoring-oo8