باگی در Node.js که از دید ابزارهای مانیتورینگ شما پنهان است

بررسی سلامت (health check) شما می‌گوید همه چیز مرتب است. فقط یک میلی‌ثانیه طول می‌کشد. سپس ترافیک افزایش می‌یابد. ناگهان، p99 latency شما به ۴۰۰ میلی‌ثانیه می‌پرد. به داشبوردهای خود نگاه می‌کنید. همه چیز سبز است.

میزان استفاده از CPU متوسط است. تأخیر حلقه رویداد (event loop lag) ثابت است. حافظه (memory) سالم است. APM شما یک درخواست کند را نشان می‌دهد اما چیزی درباره علت آن نمی‌گوید. هیچ فراخوانی کندی در دیتابیس وجود ندارد. هیچ خطایی وجود ندارد.

زمان در libuv thread pool صرف می‌شود.

قابلیت مشاهده (observability) استاندارد در Node بر روی event loop تمرکز دارد. اما thread pool یک صف جداگانه است که خارج از دسترس شما قرار دارد.

Node جاوااسکریپت را روی event loop اجرا می‌کند. وظایف سنگین را به libuv thread pool می‌فرستد. این موارد شامل:

  • کارهای مربوط به سیستم فایل (fs.readFile, fs.writeFile).
  • وظایف رمزنگاری (bcrypt, scrypt, pbkdf2).
  • فشرده‌سازی (zlib gzip, deflate).
  • جستجوهای DNS (dns.lookup).

این pool به صورت پیش‌فرض فقط دارای چهار ترد (thread) است. این موضوع صرف‌نظر از اینکه ماشین شما چند هسته CPU دارد، صادق است.

چهار ترد کافی نیست. در اینجا سه روش وجود دارد که باعث از کار افتادن pool می‌شود:

۱. استفاده از bcrypt هنگام ورود. یک هش bcrypt واحد می‌تواند ۲۵۰ میلی‌ثانیه طول بکشد. اگر چهار نفر همزمان وارد شوند، تمام ظرفیت‌ها پر می‌شود. نفر پنجم در صف منتظر می‌ماند. او فقط برای شروع کار، ۲۵۰ میلی‌ثانیه معطل می‌شود. تأخیر (latency) شما دو برابر می‌شود.

۲. عملیات‌های سنگین gzip. فشرده‌سازی یک پاسخ بزرگ، یک ظرفیت از pool را اشغال می‌کند. اگر چهار درخواست همزمان این کار را انجام دهند، تمام وظایف دیگر منتظر می‌مانند. جستجوهای DNS و خواندن فایل‌ها در صف گیر می‌کنند.

۳. جستجوهای DNS. اکثر اپلیکیشن‌های Node از dns.lookup استفاده می‌کنند. این متد از یک فراخوانی سیستم (system call) مسدودکننده استفاده می‌کند. این کار وظیفه را در pool قرار می‌دهد. اگر شبکه شما دچار اختلال شود، این جستجوها کل pool را متوقف می‌کنند.

درخواستی که در صف pool گیر کرده باشد، نامرئی است. از CPU استفاده نمی‌کند. جاوااسکریپت اجرا نمی‌کند. فقط در حالت انتظار (parked) قرار دارد.

چطور آن را پیدا کنیم:

اگر p99 latency شما تحت بار (load) افزایش می‌یابد اما event loop lag ثابت می‌ماند، pool را بررسی کنید.

سریع‌ترین تست: مقدار UV_THREADPOOL_SIZE را افزایش دهید. آن را در محیط (environment) خود روی ۶۴ تنظیم کرده و برنامه را ری‌استارت کنید. اگر تأخیر کاهش یافت، مشکل را پیدا کرده‌اید.

چطور آن را به درستی حل کنیم:

  • از worker_threads برای کارهای سنگین رمزنگاری مثل bcrypt استفاده کنید. این کار باعث می‌شود آن‌ها از libuv pool خارج بمانند.
  • به جای dns.lookup از dns.resolve استفاده کنید. این یک resolver واقعاً ناهمگام (async) است.
  • برای کارهای zlib از streamها استفاده کنید تا ظرفیت‌ها سریع‌تر آزاد شوند.
  • از انجام کارهای سنگین سیستم فایل در مسیرهای اصلی درخواست (request paths) خودداری کنید.

از خیره شدن به داشبوردهای سبز، در حالی که کاربران شما منتظر هستند، دست بردارید.

منبع: https://dev.to/r9v/the-nodejs-bug-thats-invisible-to-your-monitoring-oo8