تست سلامتی که از خود سرویس خرابتر بود: مسیر تست با مسیر واقعی فرق داشت
اسکریپت smoke test اعلام کرد همه حسابهای سرویس هوش مصنوعی از کار افتادهاند، ولی سرویس سالم بود. ماجرای تستی که مسیر واقعی کد را نمیرفت.
یک اسکریپت smoke test هر چند ساعت یکبار سرویس هوش مصنوعی ما را بررسی میکرد. یک روز نتیجهاش این بود که همه حسابها از کار افتادهاند. در همان لحظه سرویس اصلی داشت به درخواستها جواب میداد. چند ساعت صرف پیدا کردن مشکلی شد که وجود نداشت. خطا در تست بود، نه در سرویس. این یادداشت درباره همین اتفاق است و درباره اینکه چرا تستی که مسیر واقعی کد را نمیرود، از نبودن تست بدتر است.
ماجرا و علامتها
در ابزارهایی که با اتوماسیون هوش مصنوعی کار میکنند، چند حساب مختلف برای صدا زدن سرویس هوش مصنوعی تنظیم شده است. برای اینکه از سلامت آنها بیخبر نمانیم، یک اسکریپت smoke test نوشته بودیم که هر چند ساعت یکبار اجرا میشد و به هر حساب یک درخواست ساده میفرستاد.
یک روز نتیجه این اسکریپت قرمز بود، آن هم برای همه حسابها با هم. وقتی همه چیز با هم خراب میشود، معمولاً یک عامل مشترک پشتش است. از همینجا شروع کردیم به دنبال آن عامل گشتن.
نکته عجیب این بود که سرویس اصلی همان لحظه کار میکرد و درخواستهای واقعی جواب میگرفتند. یعنی دو منبع اطلاعات داشتیم که با هم نمیخواندند: تست میگفت همه حسابها مردهاند، و رفتار واقعی سرویس میگفت چیزی نشده است. در چنین موقعیتی باید یکی از این دو را باور کرد، و ما ابتدا تست را باور کردیم.
نکات کلیدی
- تست برای همه حسابها همزمان قرمز شد
- سرویس اصلی در همان لحظه درخواستها را جواب میداد
- ابتدا به تست اعتماد کردیم، نه به رفتار واقعی سرویس
ردگیری علت
چون تست میگفت مشکل فراگیر است، طبیعتاً خطایابی را از سمت سرویس و حسابها شروع کردیم، یعنی همان جایی که فکر میکردیم مشکل است. چند ساعت صرف این مسیر شد. هر چه جلوتر میرفتیم، شواهد بیشتری میدیدیم که سرویس سالم است، ولی چون خروجی تست روشن و قطعی بود، دنبال توضیحی میگشتیم که هر دو را سازگار کند.
نقطه عطف وقتی بود که بهجای پرسیدن «چرا حسابها خراب شدهاند؟» پرسیدیم «تست دقیقاً چه چیزی را صدا میزند؟». جواب این سؤال نشان داد که اسکریپت از همان مسیر کدی که سرویس واقعی استفاده میکند نمیرود.
این اشتباه روششناختی ما بود: مشکل را فرض کردیم و دنبال علتش گشتیم، بهجای اینکه اول اعتبار خودِ ابزار اندازهگیری را بسنجیم.
نکات کلیدی
- خطایابی را از سمت سرویس شروع کردیم، چون به تست اعتماد داشتیم
- سؤال درست این بود: تست دقیقاً چه چیزی را صدا میزند؟
علت اصلی: تستی که مسیر دیگری را میرفت
مسیر کد تولید دو ویژگی داشت که تست نداشت. اول، retry: درخواستهای واقعی در صورت شکست موقت دوباره تلاش میشدند. دوم، تنظیمی که مسیر واقعی هنگام صدا زدن سرویس اعمال میکرد. اسکریپت smoke test درخواست را مستقیم و خام میفرستاد، بدون retry و بدون آن تنظیم. پس تست در عمل چیز دیگری را امتحان میکرد، نه همان کاری که کاربر واقعی از سیستم میخواست.
نتیجه این بود که یک خطای گذرا یا نبودنِ آن تنظیم، که مسیر واقعی با آنها کنار میآمد، در تست مستقیماً به «حساب از کار افتاده» تبدیل میشد. تست از سرویس سختگیرتر نبود، فقط با آن متفاوت بود. تفاوت هم در جهتی بود که همیشه شکست گزارش میکرد.
مفهوم عمومی پشت این ماجرا تمایز بین مانیتورینگ جعبهسیاه و بررسی داخلی است. فصل مانیتورینگ کتاب SRE گوگل، مانیتورینگ جعبهسیاه را آزمودن رفتار قابل مشاهده بیرونی «همانطور که کاربر میبیند» تعریف میکند. کلمه کلیدی «همانطور که کاربر میبیند» است. تست ما شبیه کاربر رفتار نمیکرد، شبیه یک کلاینت سادهشده بود که هیچکس در تولید ندارد. چنین تستی نه جعبهسیاه واقعی است و نه بررسی داخلی دقیق، و فقط یک سیگنال قابل اعتماد به نظر میرسد.
مقاله «Implementing health checks» در Amazon Builders' Library هم از زاویه دیگری به همین موضوع میرسد: انواع بررسی سلامت (liveness، محلی، وابستگی) هر کدام چیز متفاوتی را میسنجند و واکنش به آنها باید با همین تفاوت تنظیم شود. بررسی سطحی که فقط میگوید «پورت جواب میدهد» با بررسی کار واقعی فرق دارد، و هیچکدام جای دیگری را نمیگیرد.
نکات کلیدی
- مسیر تولید retry و یک تنظیم اضافه داشت؛ تست هیچکدام را نداشت
- خطای گذرا در تست مستقیماً «حساب خراب» گزارش میشد
- تست باید رفتار را مثل کاربر واقعی بسنجد، نه مثل یک کلاینت سادهشده
راهحل: همان مسیر تولید، و یک دکمه تست زنده
دو تغییر دادیم. اول، تست سلامت دیگر درخواست خودش را نمیسازد و دقیقاً همان تابعی را صدا میزند که کد تولید صدا میزند، با همان retry و همان تنظیمات. هر تغییری در مسیر واقعی خودکار در تست هم اعمال میشود، چون کد مشترک است و دو نسخه موازی وجود ندارد.
دوم، در پنل مدیریت یک دکمه «تست زنده» گذاشتیم. قبلاً وضعیت نمایشدادهشده در پنل نتیجه آخرین اجرای زمانبندیشده بود، یعنی چند ساعت قبل. حالا مدیر میتواند همان لحظه تست را اجرا کند و نتیجه همین الان را ببیند، بهجای اعتماد به عددی کهنه. این دکمه به همین دلیل مفید است که مستقیم روی همان مسیر تولید کار میکند.
نمونه زیر ایده را با مقادیر ساختگی نشان میدهد:
// lib/ai-client.ts: تنها نقطهای که سرویس هوش مصنوعی را صدا میزند
export async function callAI(account: Account, prompt: string) {
return withRetry(
() => rawRequest(account, prompt, { timeoutMs: DEFAULT_TIMEOUT }),
{ attempts: 3 }
);
}
// health.ts: تست سلامت از همان تابع استفاده میکند
export async function checkAccount(account: Account) {
const started = Date.now();
try {
await callAI(account, 'ping');
return { ok: true, ms: Date.now() - started };
} catch (err) {
return { ok: false, error: String(err) };
}
}
// API پنل: دکمه «تست زنده»
// POST /api/admin/health/live -> checkAccount(...) همان لحظهدرسها و چکلیست
تستی که مسیر واقعی را نمیرود، از نبودن تست بدتر است. نبودن تست را همه میدانند و محتاطانه رفتار میکنند. تست اشتباه اعتماد میسازد و بعد آن اعتماد را علیه تو به کار میبرد، همانطور که ما چند ساعت را صرف مشکلی کردیم که وجود نداشت.
در مقالهی مربوط به گواهی SSL منقضیشده پشت CDN هم دیدیم که بررسی ناقص میتواند وضعیت را برعکس نشان دهد. آنجا سیستم سالم به نظر میرسید و خراب بود؛ اینجا خراب به نظر میرسید و سالم بود. ریشه هر دو یکی است: چیزی که اندازه میگیریم با چیزی که کاربر تجربه میکند فرق دارد. برای نگاه کلیتر به اتصال مدل زبانی به سیستمهای واقعی هم میتوانید ساخت ایجنت هوش مصنوعی متصل به دیتابیس را ببینید، و نمونهای از این حس در پیجیار هست که چند حساب و سرویس هوش مصنوعی را کنار هم اداره میکند.
یک نکته دیگر دربارهی اینکه چه چیزی را باید باور کنیم. کتاب SRE تأکید میکند هشدار باید روی علامتهایی باشد که کاربر واقعاً حس میکند. وقتی تست قرمز میگوید همه چیز خراب است ولی ترافیک واقعی سالم است، این تناقض خودش یک داده است و باید قبل از دنبال کردن «علت خرابی» بررسی شود.
نکات کلیدی
- تست سلامت باید دقیقاً همان تابع مسیر تولید را صدا بزند، نه یک کپی سادهشده
- retry و تنظیمات مسیر واقعی باید در تست هم اعمال شوند
- نتیجه چند ساعت پیش را بهجای وضعیت فعلی نمایش نده؛ دکمه «تست زنده» بگذار
- وقتی تست و رفتار واقعی با هم نمیخوانند، اول اعتبار تست را بررسی کن
- اگر تست جدید مینویسی، یک بار عمداً مسیر واقعی را خراب کن و ببین تست قرمز میشود یا نه
جمعبندی
مشکل این ماجرا خرابی سرویس نبود، خرابی ابزار اندازهگیری بود. تست سلامت فقط وقتی ارزش دارد که از همان مسیری برود که کاربر میرود. از آن به بعد، هم تست خودکار و هم دکمه تست زنده در پنل از همان کد تولید استفاده میکنند و هر بار که تست و واقعیت اختلاف دارند، اول به تست شک میکنیم.