دیدن و فهمیدن رفتار سرور؛ لاگ، سلامت و عیبیابی لایهبهلایه
در این درس ۷ بخش
ساعت ده صبح نشانی بکاند را باز میکنید و بهجای فهرست کارها، خطای ۵۰۲ میبینید. اولین اقدام مناسب، نصب دوبارهٔ Node یا بازکردن همهٔ پورتها نیست؛ باید بفهمید درخواست تا کدام لایه رسیده است. در معماری این دوره، نام دامنه به نشانی سرور تبدیل میشود، اتصال شبکه به Nginx میرسد و Nginx درخواست مجاز را به برنامهٔ محلی روی پورت ۳۰۰۰ میفرستد. هر لایه شاهد متفاوتی تولید میکند. هدف این درس ساختن یک روش تکرارپذیر است: نشانه را ثبت کنید، با فرمان محدود آن را بررسی کنید، یک فرضیه بسازید و فقط همان فرضیه را بیازمایید. همهٔ فرمانهای این درس، بهجز بخشی که صریحاً «کلاینت بیرونی» نام دارد، در Bash سرور آزمایشی اوبونتو اجرا میشوند. درسهای قبلی باید سرویس و پیکربندی Nginx را ساخته باشند؛ اگر هنوز نصب کامل نیست، خروجی خطا را با وضعیت میانی پروژه اشتباه نگیرید.
لاگ، سنجه و آزمون سلامت، سه نوع شاهد متفاوتاند
لاگ یک رویداد ثبتشده است؛ مثلاً برنامه در زمان مشخص شروع شده یا Nginx برای یک درخواست پاسخ ۴۰۴ فرستاده است. سنجه یک اندازهگیری عددی در طول زمان است؛ مانند تعداد پاسخهای ناموفق در دقیقه یا سهم حافظهٔ مصرفی. آزمون سلامت، پرسشی فعال از سامانه است: آیا همین حالا مسیر مشخصی پاسخ مورد انتظار را میدهد؟ «مشاهدهپذیری» یعنی با کنار هم گذاشتن این شاهدها بتوانید دربارهٔ وضعیت داخلی سامانه استدلال کنید. یک لاگ سبز بهتنهایی کیفیت همهٔ درخواستها را ثابت نمیکند. در پروژهٔ فعلی، برنامه در شروع، نشانی اجرا را چاپ میکند و در شکست شروع، کد خطا میدهد؛ برای هر درخواست، لاگ ساختیافتهٔ اختصاصی در Node نساختهایم. در عوض Nginx درخواستهای رسیده به خودش را در فایل دسترسی ثبت میکند. شناخت این تفاوت ضروری است: نبودن نام یک درخواست در journal الزاماً به معنی نرسیدن آن به برنامه نیست؛ شاید برنامه اصلاً برای آن رویداد، دستور ثبت لاگ ندارد. در کدکس، #debugging-console را برای نقش چاپ تشخیصی و #throw-error را برای خطای کنترلشده مرور کنید.
قدم اول: زمان، اثر و وضعیت سرویس را مشخص کنید
پیش از اجرای فرمان، بنویسید چه چیزی انتظار داشتهاید، چه چیزی دیدهاید و مسئله از چه زمانی آغاز شده است. عبارت «سرور خراب است» قابل آزمون نیست؛ عبارت «در ساعت ۱۰:۱۵، درخواست GET به /health از رایانهٔ بیرونی خطای ۵۰۲ داد» دقیقتر است. زمان دستگاهها را با منطقهٔ زمانی ثبت کنید تا رخدادهای دو ماشین را نادرست کنار هم نگذارید. در Bash اوبونتو، date -Is زمان فعلی را همراه اختلاف منطقه نشان میدهد. سپس وضعیت همین واحد را بخوانید. systemctl is-active دربارهٔ چرخهٔ عمر فرایند گزارش میدهد؛ نمیگوید پاسخ API از نظر قرارداد درست است. journalctl رویدادهای همین واحد را در یک بازهٔ محدود میخواند. گزینهٔ --no-pager خروجی را از نمایشگر صفحهای مستقل میکند و -n تعداد سطرها را محدود نگه میدارد. اجرای این فرمانها پیکربندی سرویس را تغییر نمیدهد. اگر اجازهٔ خواندن journal را ندارید، برای همین فرمان از sudo استفاده کنید؛ نیازی نیست کل جلسهٔ کاری را به پوستهٔ root تبدیل کنید.
date -Is
sudo systemctl is-active darsavand-node.service
sudo systemctl status darsavand-node.service --no-pager --full
sudo journalctl -u darsavand-node.service --since '-15 minutes' \
-n 80 --no-pager -o short-iso
sudo ss -ltnp '( sport = :3000 )'در وضعیت سالم، is-active کلمهٔ active میدهد و بررسی سوکت باید نشانی 127.0.0.1:3000 را نشان دهد. اگر active است ولی سوکت دیگری میبینید، شمارهٔ پورت و نسخهٔ واقعاً در حال اجرا را بررسی کنید. عبارت 0.0.0.0:3000 با قرارداد این دوره یکسان نیست، چون برنامه را به همهٔ واسطهای IPv4 متصل میکند. اگر وضعیت failed بود، نخستین خط مرتبط با شکست را بخوانید: EADDRINUSE معمولاً اشغالبودن نشانی و پورت را نشان میدهد؛ 203/EXEC به اجرای فایل اجرایی و 200/CHDIR به ورود به پوشهٔ کاری مربوط است. از روی کد، علت قطعی را بدون بررسی مسیر و مجوز حدس نزنید. خطای Restart request repeated too quickly معمولاً نتیجهٔ تکرار شکست است؛ خودِ علت اصلی اغلب چند سطر قبل دیده میشود. reset-failed علت را تعمیر نمیکند. تا زمانی که یک توضیح قابل آزمون برای خطای اولیه ندارید، سرویس را پشت سر هم restart نکنید و به فرایندهای Node نامرتبط پایان ندهید.
قدم دوم: برنامه را از داخل همان سرور آزمایش کنید
آزمون داخلی، دامنه و Nginx را کنار میگذارد و مستقیماً برنامه را میسنجد. مسیر /health در پروژه، پاسخ JSON با ویژگی ok برابر true دارد. تنها دیدن HTTP ۲۰۰ کافی نیست: ممکن است درخواست به برنامهای دیگر رسیده باشد یا یک صفحهٔ HTML اشتباه برگشته باشد. فرمان بعدی هم موفقیت HTTP را میسنجد و هم ساختار حداقلی پاسخ را. --connect-timeout زمان برقراری اتصال و --max-time کل زمان این درخواست را محدود میکند. set -o pipefail باعث میشود شکست curl پشت موفقیت ظاهری مرحلهٔ بعد پنهان نشود. قطعهٔ Node از ورودی استاندارد متن را میخواند، آن را به #json تبدیل میکند و قرارداد پاسخ را بررسی میکند. این همان فاصلهٔ بین «متنی دریافت شد» و «دادهٔ مورد انتظار دریافت شد» است که در #validation-contracts تمرین کردهایم. در پاسخ سالم باید application health: PASS ببینید. فرمان رمز، بدنهٔ درخواست کاربران یا محتوای فایل محیطی را چاپ نمیکند.
set -o pipefail
curl --fail --silent --show-error --connect-timeout 2 --max-time 4 \
http://127.0.0.1:3000/health |
/opt/node-current/bin/node --input-type=module -e '
let text = "";
for await (const chunk of process.stdin) text += chunk;
const result = JSON.parse(text);
if (!result || result.ok !== true) throw new Error("Unexpected health response");
console.log("application health: PASS");'Connection refused معمولاً یعنی در این نشانی و پورت شنوندهای در دسترس نیست؛ آن را با ۴۰۴ یکی ندانید. در ۴۰۴، یک پاسخ HTTP وجود دارد و باید مسیر و برنامهٔ پاسخدهنده بررسی شوند. شکست JSON.parse یعنی پاسخ با قرارداد JSON سازگار نبوده است؛ پیش از تغییر کد، هدر Content-Type و مقصد درخواست را بررسی کنید. پاسشدن این آزمون ثابت میکند همین فرایند به همین مسیر پاسخ درست میدهد. در سامانهٔ آینده که پایگاه داده دارد، این health ساده سلامت اتصال پایگاه داده، مجوز کاربر یا امکان ثبت سفارش را بررسی نخواهد کرد. «زندهبودن» و «آمادهبودن برای یک وظیفهٔ واقعی» باید قراردادهای جداگانه داشته باشند. در پروژهٔ آموزشی، /health فقط وضعیت پاسخگویی اولیه را نشان میدهد. اگر این مرحله شکست خورد، بررسی DNS بیرونی هنوز اولویت ندارد؛ ابتدا واحد، پورت، مسیر فایل و پیکربندی برنامه را اصلاح کنید.
قدم سوم: پاسخ Nginx و خطای کلاینت را با هم مقایسه کنید
وقتی آزمون داخلی سالم شد، از همان سرور مسیر عبور از Nginx را بررسی کنید و سپس از یک کلاینت بیرونی ادامه دهید. نام api.example.com در متن دوره نمونه است و باید با دامنهٔ واقعی آزمایشگاه خودتان جایگزین شود. برای HTTPS، نشانی کامل https و دامنهٔ داخل گواهی را استفاده کنید. حذف بررسی گواهی با -k تشخیص را مخدوش میکند؛ اگر نام یا زنجیرهٔ گواهی نادرست است، همان موضوع باید حل شود. خطای ۵۰۲ در این معماری یعنی Nginx نتوانسته از upstream پاسخ مناسب بگیرد؛ journal برنامه و error log خود Nginx را در بازهٔ همان درخواست بخوانید. ۴۰۴ ممکن است برای ریشهٔ دامنه کاملاً عمدی باشد، چون در درس هفتم مسیر عمومی / را مسدود کردهایم. درخواست POST به /api/tasks باید ۴۰۳ بگیرد، زیرا نمای عمومی فقط خواندنی است. همچنین HEAD در backend فعلی پیادهسازی نشده و ممکن است ۴۰۵ بدهد؛ برای آزمون سلامت از GET استفاده کنید. هر کد را با مسیر و روش درخواست تفسیر کنید، نه جدا از آنها.
# Bash on the Ubuntu lab: replace api.example.com first.
curl --silent --show-error --connect-timeout 3 --max-time 8 \
--dump-header - https://api.example.com/health
sudo tail -n 30 /var/log/nginx/darsavand-node.access.log
sudo tail -n 30 /var/log/nginx/darsavand-node.error.logفایل دسترسی درس هفتم، هر رویداد را به شکل یک شیء JSON در یک سطر مینویسد. time زمان، status وضعیت نهایی، method روش، uri مسیر بدون بخش query، upstream وضعیت گزارششده از برنامهٔ پشت پراکسی و duration مدت پردازش درخواست را نشان میدهند. upstream یک متن است و در برخی پاسخهایی که خود Nginx تولید میکند، مقدار خط تیره دارد؛ آن را همیشه عدد فرض نکنید. request_id یک شناسه برای پیوند دادن پاسخ به لاگ همان درخواست است. در هدر پاسخ، X-Request-Id را پیدا کنید و همان مقدار را در فایل دسترسی جستوجو کنید. این شناسه برای پیگیری درخواست است و هویت کاربر یا مدرک مجوز محسوب نمیشود. در یک سامانهٔ چندلایه، برای دنبالکردن مسیر کامل باید برنامهها نیز آن را مسئولانه منتقل و ثبت کنند؛ پروژهٔ فعلی چنین زنجیرهٔ کاملی پیاده نکرده است. تمرین #object و #json کمک میکند هنگام نوشتن یک ابزار تحلیل لاگ، ساختار داده را از متن ظاهری تشخیص دهید.
تفاوت شکست DNS، TLS و HTTP را در تصمیم بعدی به کار ببرید
اگر نام دامنه اصلاً حل نمیشود، ممکن است هیچ درخواستی به Nginx نرسیده باشد؛ خالیبودن access log در آن بازه قابل انتظار است. رکوردهای A و AAAA را از درس هشتم بررسی کنید و نتیجهٔ resolver کلاینت را با نشانی سرور تطبیق دهید. اگر اتصال برقرار میشود اما بررسی گواهی شکست میخورد، هنوز یک پاسخ معمول API نگرفتهاید؛ نام دامنه، تاریخ اعتبار، ساعت دستگاه و زنجیرهٔ TLS را بررسی کنید. اگر پاسخ HTTP دارید، روش، مسیر و وضعیت را ثبت کنید و بین پاسخ مستقیم برنامه و پاسخ پراکسی مقایسه انجام دهید. این تقسیمبندی مانع اتلاف وقت میشود: خطای حل نام با تغییر تابع #async-await در برنامه رفع نمیشود و خطای ورودی ۴۲۲ با بازکردن پورت تازه درمان نخواهد شد. پس از هر تغییر محدود، همان درخواست اولیه را دوباره با همان شرایط اجرا کنید. اگر همزمان دامنه، فایروال، کد و سرویس را عوض کنید، حتی موفقیت بعدی هم روشن نمیکند کدام فرضیه درست بوده است.
لاگ را مخزن بیقاعدهٔ همهٔ ورودیها نکنید. رمز، توکن، کوکی، رشتهٔ اتصال پایگاه داده و بدنهٔ درخواستهای واقعی نباید صرفاً برای راحتی عیبیابی چاپ شوند. حتی مسیر یا عنوان یک کار ممکن است در یک محصول واقعی اطلاعات شخصی داشته باشد. قالب این دوره query string و بدنهٔ درخواست را ثبت نمیکند، اما اگر در مسیر URL اطلاعات حساس بگذارید، همچنان ممکن است همان مسیر وارد لاگ شود. به همین دلیل از ابتدا دادهٔ محرمانه را در نشانی قرار ندهید. دورهٔ نگهداری لاگ، سطح دسترسی و چرخش فایل باید متناسب با نیاز محصول تعریف شود. journal ممکن است فقط بخشی از تاریخچه را نگه دارد؛ نبودن رویداد قدیمی، دلیل رخندادن آن نیست. فضای دیسک و سیاست rotation را بررسی کنید و هیچوقت برای خالیکردن سریع فضا، فایلهای ناشناخته یا شواهد رخداد را کورکورانه پاک نکنید. در گزارش تحویل، فقط سطرهای لازم و پاکسازیشده را بیاورید و نسخهٔ کامل را در فضای مناسب خودتان نگه دارید.
تمرین با پاسخ و معیار تحویل
سه سناریو را بدون تغییر واقعی سیستم تحلیل کنید. یک: سرویس active است، درخواست مستقیم /health سالم است، ولی درخواست دامنه ۵۰۲ دارد. پاسخ راهنما: ابتدا پیکربندی upstream و error log Nginx را در زمان همان درخواست بررسی کنید؛ ممکن است پراکسی به پورت دیگری اشاره کند. دو: /health دامنه سالم است ولی POST به /api/tasks کد ۴۰۳ میدهد. پاسخ: این نتیجه با محدودیت عمدی نمای عمومی سازگار است؛ برای تمرین ایجاد کار از مسیر محلی یا تونل SSH درس قبل استفاده میکنیم و مجوز نوشتن عمومی را بدون طراحی احراز هویت باز نمیکنیم. سه: after restart فهرست کارها خالی است، اما health سالم است. پاسخ: مدل برنامه در حافظه است؛ این موضوع را با خرابی TLS یا از دسترفتن فایل کد اشتباه نگیرید. تحویل این درس، یک گزارش شامل زمان و منطقه، درخواست دقیق، خروجی آزمون داخلی، سطر مرتبط Nginx، فرضیه و نتیجهٔ آزمون فرضیه است. در گزارش توضیح دهید کدام ادعا با شاهد پشتیبانی شده و کدام هنوز نامعلوم است.
برای مرور مرتبط، ابتدا روش خواندن journal و سپس عیبیابی اتصال را از مسیرهای قبلی بخوانید. این ارجاعها برای تکرار همهٔ درسها نیستند؛ یک مهارت مشخص را با توجه به خطای مشاهدهشده تقویت میکنند.
تحقیق درباره شکست با لاگهای ساختیافته journal: https://www.darsavand.com/courses/linux-advanced-administration?lesson=32265068-21de-4ef1-86c7-6ae5d7f00827
عیبیابی Node.js در ویندوز؛ از breakpoint تا خطاهای اتصال: https://www.darsavand.com/courses/nodejs-windows-backend-start?lesson=7f46ca6f-c14d-45dd-b9b9-2ef88ae773aa
منابع رسمی انگلیسی برای مطالعهٔ تکمیلی
systemd journalctl: https://www.freedesktop.org/software/systemd/man/255/journalctl.html
NGINX HTTP logging: https://nginx.org/en/docs/http/ngx_http_log_module.html
NGINX upstream variables: https://nginx.org/en/docs/http/ngx_http_upstream_module.html
Node.js HTTP: https://nodejs.org/docs/latest-v24.x/api/http.html
تألیف و سناریوهای اختصاصی: تحریریه درسآوند. تمرینها برای آزمایشگاه مستقل و دادهٔ ساختگیاند.
متن درسهای عمومی را بدون ورود بخوانید.