ساخت یه صف قابلاعتماد با PostgreSQL
خلاصهٔ کاملتر
خیلی از پروژهها یه صف کار (queue) لازم دارن: فرستادن ایمیل، پردازش پرداخت، سینک داده و... . به گفته نویسنده، ساختن یه صف ساده رو PostgreSQL راحته - یه جدول جاب میسازی، چند ورکر باهاش کار میکنن - ولی وقتی ورکر زیاد میشه، یکی کرش میکنه، یا جدول بزرگ میشه، نگهداریش سخت میشه.
اولین مشکل، ریس کاندیشنه: وقتی چند ورکر همزمان میخوان یه جاب pending رو بردارن. راهحل اینه که کوئری با FOR UPDATE SKIP LOCKED نوشته بشه، تا هر ورکر یه ردیف قفلنشده رو برداره و بقیه معطل نمونن:
SELECT * FROM queue_jobs
WHERE status = 'pending'
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 1;تو تست ۱۰۰ هزار جابی نویسنده، این روش صفر برداشت تکراری و صفر جاب گمشده داشته.
مشکل بعدی مرگ ورکره: اگه یه ورکر وسط کار بمیره، جاب باید بعد یه مدت مشخص (lease) آزاد بشه تا یکی دیگه بگیرتش. اما این خودش یه سوراخ داره: ممکنه ورکر قدیمی بعد از اینکه یکی دیگه جاب رو گرفته، بیدار بشه و وضعیتش رو خراب کنه. برای همین از fencing token استفاده میشه - یه شماره نسخه (lease_version) که اگه با نسخه فعلی جاب نخونه، آپدیت اون ورکر رد میشه.
نویسنده تاکید میکنه فنسینگ توکن جلوی side effectهای بیرونی مثل ایمیل دوبار فرستادهشده یا پرداخت تکراری رو نمیگیره؛ برای اون handlerها باید idempotent باشن. برای ریترای هم روش fixed-delay باعث retry storm میشه، ولی exponential backoff + jitter ریترایها رو پخش میکنه؛ تو تست نویسنده fixed-delay تو یک ثانیه به ۱۰۰۰ ریترای رسیده ولی jitter همون تعداد رو رو ۸۹ ثانیه پخش کرده.
یه نکته جالب دیگه درباره ایندکسه: وقتی کوئری با queue = ANY(...) چند صف رو باهم چک میکنه، پستگرس نمیتونه از ایندکس مرتب استفاده کنه و باید نتیجهها رو sort کنه که رو ۱۰۰ هزار ردیف pending حدود ۵۲ میلیثانیه طول کشیده. وقتی نویسنده کوئری رو به یه صف تنها تغییر داده، زمان به حدود ۵ میلیثانیه رسیده - نزدیک ۱۰ برابر سریعتر:
SELECT state.job_id FROM queue_job_state AS state
WHERE state.queue = $1::text AND state.status = 'pending'
ORDER BY state.priority DESC, state.job_id
FOR UPDATE SKIP LOCKED LIMIT 20;برای همین API ورکرها هم عوض شده که هر بار فقط یه صف رو کلایم کنن.
نکات کلیدی:
- SKIP LOCKED جلوی برداشت تکراری جاب توسط چند ورکر رو میگیره، بدون قفل پشتسرهم
- lease + fencing token (ستون lease_version) مشکل ورکر کرشکرده و رایت قدیمی رو حل میکنه
- برای side effectهای بیرونی مثل ایمیل و پرداخت، handler باید idempotent باشه
- exponential backoff + jitter جلوی retry storm رو میگیره: ۱۰۰۰ ریترای در ثانیه در برابر پخششدن رو ۸۹ ثانیه
- کوئری با یه صف تنها بهجای ANY(...) با ایندکس مرتب ۱۰ برابر سریعتره: ۵ میلیثانیه در برابر ۵۲ میلیثانیه




