آنچه در این مقاله میخوانید [پنهانسازی]
برای دیباگ خطاهای تصادفی، ابتدا مسیر اجرای درخواست را قابل ردیابی کنید: لاگ ساخت یافته با شناسه همبستگی (request/trace id)، Trace با spanهای مرزبانی، ثبت زمان با ساعت یکنواخت، و نمونه برداری هوشمند در لحظه خطا. سپس با شواهد کامل، فرضیه بسازید و بازتولید را با کنترل منبع تصادف (seed)، ثابت کردن زمان و باز کردن پنجره رقابت در کد انجام دهید. این ترکیب، خطا را از «گاهی اوقات» به «قابل تکرار و قابل رفع» تبدیل می کند.
چرا خطاهای «تصادفی» سخت گیر می افتند؟
خطای تصادفی معمولا ناشی از ناپایداری های بیرونی یا همزمانی است: رقابت بین تردها/ریکوئست ها، تاخیر شبکه و صف، سازگاری نهایی دیتابیس، زمان بندی تایمرها، کمبود منابع، درهم ریختگی وضعیت اشتراکی، درهم ریختگی زمان (time skew)، و بازپخش یا تکرارهای ناخواسته ناشی از retry. نشانه مشترک این باگ ها این است که با افزایش لاگ ساده یا اجرای دیباگر تغییر رفتار می دهند و به سختی تکرار می شوند.
راهبرد کلان: از مشاهده تا بازتولید
- شواهد را جمع کنید: لاگ ساخت یافته، Trace، متریک های کلیدی (نرخ خطا، تاخیر، retry).
- مرزها را مشخص کنید: نقطه های ورود/خروج سرویس ها، صف ها، تراکنش ها و قفل ها را علامت بزنید.
- همبستگی بسازید: هر درخواست یک شناسه یکتا داشته باشد و در همه لاگ ها و spanها تکرار شود.
- فضای جستجو را کوچک کنید: بازه زمانی، کاربر/tenant خاص، مسیر API یا نسخه دیپلوی را ایزوله کنید.
- فرضیه محور پیش بروید: برای هر علت محتمل، یک مشاهده قابل ابطال تعریف کنید.
- بازتولید کنترل شده: seed، زمان ثابت، تزریق تاخیر و بارگذاری مصنوعی برای آشکار کردن رقابت.
- تایید رفع: با trace و متریک، نبودِ رخداد را نشان دهید؛ فقط «ندیدن خطا» کافی نیست.
طراحی Log که خطاهای تصادفی را لو می دهد
لاگ باید پاسخ بدهد «چه شد، کجا شد، برای چه کسی، در چه بافتی و با چه تاخیری». نکات کلیدی:
- ساخت یافته (JSON): پارس پذیر، قابل فیلتر و پایدار در طول زمان.
- شناسه همبستگی: request_id، trace_id/span_id (در صورت وجود Trace)، user_id/tenant، و کلید idempotency اگر دارید.
- مرزبانی روشن: ورود/خروج به تابع های کلیدی، شروع/پایان تراکنش، ارسال/دریافت پیام.
- زمان سنجی درست: از ساعت یکنواخت (monotonic) برای مدت زمان و از timestamp برای زمان دیواری استفاده کنید.
- سطوح و rate limit: خطاها را کامل ثبت کنید، اما اطلاعات تکراری debug را با نمونه برداری یا تریگر مبتنی بر خطا فعال کنید.
- کاردینالیتی کنترل شده: از قرار دادن مقادیر بی نهایت متنوع در عنوان event بپرهیزید؛ آن ها را در فیلدهای داده بگذارید.
- حریم خصوصی: PII را حذف یا ماسک کنید؛ نشتی داده خود می تواند منشا باگ های بعدی باشد.
نمونه کد: وابستگی همبستگی و زمان سنجی در Node.js (Express)
هدف: افزودن request_id و مدت زمان اجرای هندلر به هر لاگ و سازگار کردن آن با Trace در آینده.
const { AsyncLocalStorage } = require('async_hooks');
const { randomUUID } = require('crypto');
const express = require('express');
const als = new AsyncLocalStorage();
function withRequestContext(req, res, next) {
const ctx = {
request_id: req.headers['x-request-id'] || randomUUID(),
user_id: req.headers['x-user-id'] || null
};
als.run(ctx, () => {
res.setHeader('x-request-id', ctx.request_id);
next();
});
}
function log(level, msg, data = {}) {
const ctx = als.getStore() || {};
const record = {
ts: new Date().toISOString(),
level,
msg,
...ctx,
...data
};
console.log(JSON.stringify(record));
}
function timed(name, fn) {
return async function wrapped(req, res, next) {
const start = process.hrtime.bigint();
try {
log('info', name + ' start', { path: req.path, method: req.method });
await fn(req, res, next);
const dur_ms = Number(process.hrtime.bigint() - start) / 1e6;
log('info', name + ' done', { dur_ms });
} catch (err) {
const dur_ms = Number(process.hrtime.bigint() - start) / 1e6;
log('error', name + ' error', { dur_ms, error: String(err) });
next(err);
}
};
}
const app = express();
app.use(withRequestContext);
app.get('/checkout', timed('checkout', async (req, res) => {
// نمونه: منطق پرداخت
// log('debug', 'charge.attempt', { amount: 42000, currency: 'IRR' });
res.json({ ok: true });
}));
app.listen(3000, () => log('info', 'server.started', { port: 3000 }));چگونه بررسی کنیم؟ یک درخواست با هدر x-request-id بفرستید و ببینید همه لاگ ها همان شناسه را دارند و رویدادهای start/done با مدت زمان منطقی ثبت شده اند.
Trace درست: Span، Attribute، Event و Context Propagation
Trace توزیع شده با OpenTelemetry یا ابزار مشابه نشان می دهد هر درخواست از کدام سرویس و تابع گذشته، هر کدام چقدر طول کشیده و در کجا خطا رخ داده است. اجزا:
- Span: واحد کار با نام واضح (مثلا "db.query.orders")، دارای مدت زمان و وضعیت.
- Attributes: کلید-مقدارهای ثابت مانند user_id، order_id، route.
- Events: لحظه های درون span مانند "retry" یا "cache_miss".
- Links: ارتباط بین spanهای همزمان (مثلا پردازش دو پیام مرتبط).
- Context propagation: انتقال trace_id/span_id بین تردها، async callbacks و سرویس ها (HTTP header "traceparent").
نمونه کد: افزودن span و همبستگی با لاگ در Node.js (OpenTelemetry API)
هدف: ساخت span برای عملیات حساس و نوشتن trace_id در لاگ برای اتصال Log و Trace.
const { trace, context } = require('@opentelemetry/api');
function withSpan(name, fn, attrs = {}) {
const tracer = trace.getTracer('app');
return async function wrapped(...args) {
const span = tracer.startSpan(name, undefined, context.active());
Object.entries(attrs).forEach(([k, v]) => span.setAttribute(k, v));
try {
const res = await fn.apply(this, args);
span.setStatus({ code: 1 }); // OK
return res;
} catch (e) {
span.recordException(e);
span.setStatus({ code: 2, message: String(e) }); // ERROR
throw e;
} finally {
// نمونه همبستگی: trace_id را در لاگ بگذارید
const traceId = span.spanContext().traceId;
log('debug', 'span.finish', { trace_id: traceId, name });
span.end();
}
};
}
// استفاده:
// const chargeWithTrace = withSpan('payment.charge', chargeFn, { provider: 'xpay' });
// await chargeWithTrace(params);نکته: برای ارسال Trace باید یک Provider و Exporter تنظیم کنید. اگر ابزار آماده ندارید، Console exporter برای شروع مناسب است. Sampling را طوری تنظیم کنید که خطاها همیشه ثبت شوند و موفق ها بر اساس نرخ نمونه برداری.
ترکیب Log و Trace: سه الگوی موثر
- شناسه مشترک: trace_id و span_id را در لاگ وارد کنید تا با یک کلیک از لاگ به Trace برسید.
- eventهای کلیدی دوطرفه: رخدادهای مهم (retry، cache_miss، timeout) را هم در span به عنوان event و هم در لاگ ثبت کنید.
- مرزبانی سازگار: نام spanها و نام eventها را با الگوی ثابت انتخاب کنید تا جستجو و گروه بندی ساده شود.
سناریوی عملی: باگ ناپایدار در سبد خرید
نشانه: گاهی پرداخت انجام می شود، اما پاسخ سرویس دیر می رسد و کلاینت دوباره درخواست می فرستد؛ نتیجه، دوبار شارژ یا خطای "order state mismatch" است.
- لاگ ساخت یافته: request_id، user_id، order_id، payment_attempt را اضافه کنید. eventهایی مانند "charge.attempt"، "charge.ok"، "callback.received" را ثبت کنید.
- Trace: span "checkout"، زیرspan "payment.charge" و "db.update.order" را بسازید. event "retry" و "timeout" را اضافه کنید.
- مشاهده: در Trace می بینید "payment.charge" 2900ms طول کشیده، timeout کلاینت 2500ms است، کلاینت retry کرده در حالی که callback دیرتر رسیده. در لاگ ها دو request_id به یک order_id مرتبط شده اند.
- رفع: کلید idempotency در API پرداخت، افزایش مهلت یا الگوی async confirmation، و قفل خوش رفتار روی order_id. سپس verify: هیچ order با attempt > 1 در یک trace_id وجود ندارد و metric retry کاهش یافته است.
روش های بازتولید: Seed، زمان ثابت و Record/Replay
- کنترل تصادف: منابع تصادف را seed پذیر کنید (مولد اعداد تصادفی، انتخاب سرور، shuffle).
- انجماد زمان: از ساعت قابل کنترل استفاده کنید تا تایمرها و expirationها قابل پیش بینی شوند.
- گسترش رقابت: تاخیرهای کوچک هدفمند در نقاط بحرانی تزریق کنید تا race تکرار شود.
- Record/Replay ورودی ها: بدنه HTTP، پیام های صف و پاسخ سرویس های بیرونی را در محیط staging ضبط و بازپخش کنید.
- پرچم لاگ پویا: سطح debug را برای یک user_id یا request_id خاص به صورت موقت فعال کنید تا نویز سراسری ایجاد نشود.
- شبیه سازی خطا: قطع موقت شبکه، خطای DNS، پاسخ کند دیتابیس یا خطای partial را با ابزار آزمایش خطا (chaos) تست کنید.
چک لیست اجرایی سریع
- برای هر درخواست request_id تولید و در هدر پاسخ برگردانید.
- trace_id/span_id را در لاگ ها درج کنید و مرزبانی ورود/خروج را بسازید.
- durations را با ساعت یکنواخت اندازه بگیرید؛ timestamp را برای زمان دیواری ثبت کنید.
- eventهای retry، timeout، cache_miss و state_change را استاندارد کنید.
- Sampling: خطاها را ۱۰۰٪، موفق ها را به صورت نرخ محور ثبت کنید.
- برای user/tenant مشکوک، لاگ debug پویا فعال کنید.
- فرضیه بسازید و با تزریق تاخیر/seed/زمان ثابت بازتولید کنید.
- پس از رفع، با Trace و متریک تایید کنید و یک بررسی پس از حادثه بنویسید.
خطاهای رایج هنگام دیباگ این باگ ها
- تغییر زمان بندی با لاگ زیاد: لاگ پرحجم می تواند رقابت را پنهان کند؛ از نمونه برداری و رویدادهای هدفمند استفاده کنید.
- اعتماد به timestamp سیستم های مختلف: skew بین ماشین ها ترتیب واقعی را به هم می زند؛ روی durations و Trace تکیه کنید.
- عدم انتقال context: نبود request_id/trace_id در سرویس بعدی، همبستگی را می شکند.
- نشتی اطلاعات: درج PII خام در لاگ؛ ماسک/حذف کنید.
- کاردینالیتی بی کنترل: event با نام دارای شناسه پویا قابل جستجو نیست؛ شناسه ها را در فیلدها بگذارید.
- نادیده گرفتن retry: فقط به اولین خطا نگاه کردن؛ چرخه کامل retry/backoff را ثبت کنید.
ارزیابی نتیجه و پیشگیری
نتیجه باید با شواهد تایید شود: Trace بدون شاخه های غیرعادی، کاهش متریک retry/timeout، و نبودِ eventهای مغایر با قواعد دامنه. برای پیشگیری:
- Idempotency روی عملیات غیراتمی و ارتباطات غیرقابل اعتماد.
- آزمون های همزمانی و بار؛ تزریق تاخیر در تست های یکپارچه.
- Property-based testing برای قواعد دامنه (مثلا وضعیت سفارش هرگز از "paid" به "new" برنمی گردد).
- هشدار روی invariantهای حیاتی (تلاش بیش از ۱، پرش حالت غیرمجاز، تراکنش های طولانی).
گام بعدی چیست؟
اگر امروز فقط یک کار می کنید، شناسه همبستگی و لاگ ساخت یافته را اضافه کنید و برای یک مسیر بحرانی Trace را فعال کنید. سپس با یک باگ واقعی، چرخه فرضیه سازی، بازتولید کنترل شده و تایید را تمرین کنید. این پایه ساده بیشترین بازده را در دیباگ خطاهای تصادفی دارد.



