هشدارهایی که یک بار فعال میشوند، نه یک بار برای هر سرور
یکی از هشدارهای عملیاتی ما — «اعتبار کم» برای اپ منتشرشدهی یک سازنده — قرار است حداکثر یک بار در روز به ما برسد. یک بعدازظهر این هشدار پنج بار آمد. علت این بود که هر نسخه از API ما فهرست هشدارهایی را که قبلاً فرستاده بود در حافظهی خودش نگه میداشت؛ پس دو سرور هر کدام آن را یک بار فرستادند، و هر دیپلوی آن حافظه را پاک میکرد و اجازه میداد دوباره فرستاده شود. همین الگو باعث شده بود یک کار پسزمینه دو بار انجام شود. حالا هر دو از طریق یک فروشگاه مشترک هماهنگ میشوند، و هر دو اگر آن فروشگاه در دسترس نباشد، به سمت امن شکست میخورند.
علامت مشکل
Mythex وقتی چیزی نیاز به بررسی دارد، هشدارهای داخلی به تیم میفرستد: یک سرویس از کار افتاده، یک کار متوقف شده، یا موجودی اعتبار یک سازنده در حالی که اپش منتشر است رو به اتمام است. هر هشدار یک پنجرهی dedupe دارد، یعنی بازهای که در آن همان هشدار فقط یک بار فرستاده میشود. برای هشدار اعتبار کم این پنجره ۲۴ ساعت است.
پنج نسخه از یک هشدار در یک بعدازظهر یعنی پنجره کار نمیکرد. بدتر اینکه به صورت جفت میآمدند، تقریباً ۱۱ دقیقه با هم فاصله.
چرا این اتفاق افتاد
بستهی هشدارهای ما dedupe را در حافظه نگه میداشت: جدولی از «کلید هشدار ← زمان آخرین ارسال»، درون پروسهی در حال اجرا. این وقتی درست کار میکند که یک پروسه باشد که هرگز راهاندازی مجدد نشود. تولید هیچکدام از اینها را ندارد:
- دو replica از API. تولید دو نسخه از API اجرا میکند. هر کدام جدول خودشان را داشتند، پس هر کدام هشدار را یک بار فرستادند. این همان جفتها بودند.
- دیپلویها. هر دیپلوی هر دو replica را با پروسههای تازه و جدولهای خالی جایگزین میکند. بعد از دیپلوی، هشدار دوباره رفت، انگار هرگز فرستاده نشده بود.
فاصلهی حدود ۱۱ دقیقه بین دو هشدار در هر جفت از کاری میآمد که این هشدار را تولید میکند. آن کار روی هر دو replica اجرا میشود و یک قفل میگیرد، پس replicaها به نوبت برنده میشوند، و هر کدام وقتی نوبتش میشد هشدار را تولید میکرد.
یک باگ دوم و آرامتر در همان کد بود. برای اینکه جدول تا ابد بزرگ نشود، وقتی به ۵۰۰ کلید میرسید خودش را هرس میکرد؛ یعنی هر ورودی قدیمیتر از ۱۵ دقیقه را حذف میکرد، بدون توجه به پنجرهاش. زیر بار، یک dedupe ۲۴ ساعته میتوانست فقط یک ربع ساعت دوام بیاورد.
راهحل
یک رکورد مشترک برای هر هشدار
حالا قبل از ارسال هشدار، API کلید dedupe آن را در Redis میگیرد؛ یک فروشگاه درونحافظهای مشترک که هر دو replica به آن وصلاند و با راهاندازی مجدد API از بین نمیرود. این گرفتن یک عملیات واحد است: «این کلید را فقط اگر وجود ندارد تنظیم کن، و بعد از پایان پنجره منقضیاش کن.» هر replica که اول بگیرد هشدار را میفرستد؛ دیگری کلید را گرفته میبیند و ساکت میماند. یک پروسهی تازه بعد از دیپلوی کلید را هنوز آنجا میبیند.
کلید و پنجره از همان قاعدهای میآیند که بستهی هشدارها استفاده میکند، که حالا بین آنها مشترک است، پس دو لایه نمیتوانند دربارهی اینکه «همان هشدار» یعنی چه با هم اختلاف داشته باشند.
در صورت خرابی باز بمان، بسته نه
مهمترین تصمیم این بود که وقتی Redis در دسترس نیست چه کنیم. تصمیم گرفتیم هشدار در هر حال فرستاده شود. اگر Redis خاموش باشد یا ظرف ۱.۵ ثانیه پاسخ ندهد، هشدار میرود. هشدار تکراری فقط مزاحمت است. هشدار گمشده یعنی کسی خبردار نشده است.
جدیترین هشدارها، یعنی P0، هرگز اصلاً توسط dedupe نگه داشته نمیشوند.
هر ورودی پنجرهی خودش را نگه میدارد
در dedupe درونحافظهای خود بستهی هشدارها، هر ورودی حالا زمان پایان پنجرهاش را ذخیره میکند و هرس فقط ورودیهایی را حذف میکند که پنجرهشان تمام شده. یک کلید ۲۴ ساعته، هر تعداد هشدار دیگری که برسد، ۲۴ ساعت میماند.
همان باگ، جای دیگر
همان الگو یک کار پسزمینهی مهم را هم دوبرابر کرده بود: یک job تطبیق که سابقهی وضعیت هر workspace را با ارائهدهندهی sandbox ما هماهنگ نگه میدارد. هر اجرا همهی sandboxها در حساب را فهرست میکند — در تولید هزاران مورد، حدود پنج ثانیه — بهعلاوهی هر پروژهی در حال اجرا یا متوقف. هر دو replica هر دو دقیقه این کار را اجرا میکردند.
نوشتنهایش idempotent هستند، یعنی اجرای دوباره همان نتیجهی یک بار اجرا را میدهد، پس چیزی خراب نشد. فقط دو برابر کار بود. حالا هر پنجرهی دو دقیقهای در Redis گرفته میشود، همانطور که متر مصرف ما پیشتر پنجرههای خودش را گرفته بود، و فقط replicaای که پنجره را میگیرد اجرا میکند. اگر Redis در دسترس نباشد، اجرا در هر حال انجام میشود: انجام دوبارهی کار بیضرر است، انجام ندادنش نه.
چطور آزمایشش کردیم
تستها راهاندازی واقعی را مدل میکنند. ماژول هشدار را دو بار بارگذاری میکنند، بهعنوان دو «replica» جدا، هر دو را به یک فروشگاه مشترک وصل میکنند، همان هشدار را از هر کدام تولید میکنند، سپس یک نسخهی سوم و تازه بارگذاری میکنند تا بهجای «بعد از دیپلوی» بایستد و دوباره تولیدش کند. دقیقاً یک هشدار فرستاده میشود. تستهای دیگر بررسی میکنند که هشدار سازندهی دیگری همچنان میرود، که یک P0 از هر دو replica فرستاده میشود، و هشدار وقتی فروشگاه خاموش است یا از مهلت زمانی بیشتر طول میکشد باز هم فرستاده میشود. اصلاح هرس تست خودش را دارد: یک کلید ۲۴ ساعته، ۲۰ دقیقه بعد از ورود ۶۰۰ هشدار دیگر، همچنان زنده میماند.
برای job تطبیق، دو replica که همزمان تیک میزنند، بهعلاوهی یک تیک سوم در همان پنجره، فقط یک فهرستبرداری تولید میکنند.
آنچه از این ماجرا یاد گرفتیم
- وضعیت درونحافظهای به هر پروسه تعلق دارد. هر چیزی که باید بین سرورها درست باشد — «یک بار فرستاده شد»، «یک بار اجرا شد» — باید جایی باشد که همه به آن دسترسی دارند.
- دیپلویها یعنی راهاندازی مجدد. وضعیتی که فقط تا راهاندازی بعدی دوام دارد، در یک روز کاری عادی دوام نمیآورد.
- تصمیم بگیرید به کدام سمت شکست بخورید. برای هشدارها، خاموش بودن یک فروشگاه مشترک باید به معنای هشدار بیشتر باشد، نه کمتر.
- الگو را پیدا کنید، نه فقط نمونه را. هشدار صدای بلندتر بود؛ کار تطبیق دو برابرشده همان باگ بود که آرام هزینهی کار را میداد.
این هیچکدام از داخل Mythex دیده نمیشود. برای اطلاعات بیشتر دربارهی اینکه چطور پلتفرم را پایدار نگه میداریم، چطور دیپلویها را به صفر خطای ۵۰۲ رساندیم و فایل swap گمشده را ببینید. اگر با ایدهی اجرای بیش از یک نسخه از یک سرویس تازهکار هستید، راهنمای ما دربارهی سرور چیست مبانی را پوشش میدهد.