GoCasts آموزش Go به زبان ساده
دیروز ساعت 11:20 UTC صبح، Cloudflare - شرکتی که بیش از 20 درصد از کل وبسایتهای دنیا را سرویسدهی میکند - دچار بدترین outage خودش از سال 2019 شد. مشکل از یک تغییر ساده در دسترسیهای database شروع شد که باعث شد یک configuration file دو برابر بزرگ بشه، و چون کد Rust با یک .unwrap() بیپروا نوشته شده بود، تمام سرویس panic کرد و HTTP 5xx برگشت داد. نتیجه؟ X (توییتر سابق)، ChatGPT، Spotify، League of Legends و هزاران سرویس دیگه برای تقریباً 6 ساعت مشکل داشتن. ضرر اقتصادی تخمین زده شده 5 تا 15 میلیارد دلار در هر ساعت بوده.
این incident یک درس مهم در معماری سیستمهای توزیعشده به ما میده: حتی یک خط کد بد، وقتی در مقیاس جهانی باشه، میتونه فاجعهآفرین باشه. Cloudflare’s Quicksilver system که میتونه در p99 کمتر از 2.3 ثانیه یک configuration رو به تمام دنیا برسونه، همون سرعتی که قدرتش بود، تبدیل به ضعفش شد. چیزی که اتفاق افتاد نه یک حمله سایبری بود، نه یک DDoS، بلکه یک ترکیب کلاسیک از تغییر configuration، باگ پنهان در کد، و نبود safeguardهای کافی بود.
در ساعت 11:05 UTC روز 18 نوامبر، یک تیم در Cloudflare تصمیم گرفت امنیت دیتابیس ClickHouse رو بهبود بده. کار سادهای به نظر میرسید: یک تغییر در قسمت permissions که implicit table access رو به explicit تبدیل میکرد. تیم همه چیز رو review کرد، تست کرد، و deploy زد. اما 15 دقیقه بعد، شبکه Cloudflare شروع به ریختن کرد.
مشکل اینجا بود: این تغییر دسترسی باعث شد که یک کوئری SQL ناگهان duplicate rows برگردونه. query از دو دیتابیس - یکی default و یکی r0 - دادهها رو استخراج میکرد، و با تغییر جدید، نتیجه چی شد؟ یک فایل feature که برای سیستم Bot Management استفاده میشد، از حدود 60 ویژگی به بیش از 200 ویژگی رسید.
حالا بیایم ببینم چرا این مهم بود. Cloudflare برای مدیریت بات یک محدودیت hardcoded داشت: حداکثر 200 ویژگی. این محدودیت برای memory allocation و safety در نظر گرفته شده بود. کد Rust که FL2 proxy engine جدید Cloudflare رو میساخت، طوری نوشته شده بود که با این فرض کار میکرد که file هیچوقت از این محدودیت رد نمیشه. و اینجا نقطه ضعف اصلی بود: استفاده از Result::unwrap() در کد پروداکشن.
برای کسایی که با Rust آشنا نیستن، باید بگم که .unwrap() در Rust مثل این میمونه که بگی “من مطمئنم این error نمیده، اگه داد که برنامه crash کنه”. در توسعه شاید مشکلی نباشه، اما در کد پروداکشن، این یک آنتیپترن حساب میشه. وقتی فایل از 200 فیچر رد شد، کد Rust هم panic کرد با این خطا:
thread fl2_worker_thread panicked: called Result::unwrap() on an Err value
و این panic در تمام edge serverهای Cloudflare در سرتاسر دنیا اتفاق افتاد.
یکی از جالبترین و گیجکنندهترین بخشهای این مشکل، رفتار on-off آن بود. برای بیش از 2 ساعت، سرویسها ریکاور میشدن و دوباره fail میشدن. این الگو باعث شد که تیم incident response اول فکر کنن که شاید یک DDoS attack در حال انجامه.
دلیلش این بود: ClickHouse cluster به صورت تدریجی در حال update شدن بود. این bot management feature file هر 5 دقیقه یکبار به صورت خودکار generate میشد و به تمام ماشینها توزیع میشد. اما چون cluster به صورت تدریجی بروزرسانی شده بود، بعضی اوقات query از nodeهای update شده دادههارو دریافت میکرد (که duplicate rows میدادن) و بعضی اوقات از nodeهای قدیمی (که درست کار میکردن).
Matthew Prince، که CEO شرکت Cloudflare هست، توضیح داد: “دادههای بد فقط زمانی تولید میشد که کوئری روی بخشی از کلاستر اجرا میشد که بروزرسانی شده بود. این fluctuation باعث شد که مشخص نباشه چی داره اتفاق میافته چون کل سیستم ریکاور میشد و دوباره fail میشد.”
این incident یک global outage کامل بود. تمام دیتاسنترهای Cloudflare در سراسر دنیا affect شدن. لیست سرویسهایی که down شدن یا مشکل داشتن شامل اینها میشد:
سرویسهای X (Twitter)، OpenAI (ChatGPT، DALL-E، Sora)، Claude AI، Spotify، Discord، League of Legends، RuneScape، Shopify، Indeed، Canva، Uber، و هزاران وبسایت دیگه. حتی McDonald’s self-service kiosks و nuclear plant background check systems هم گزارش دادن که مشکل دارن. به قول یکی از مهندسین در Reddit:
صدای هزاران توسعهدهنده شنیده میشد که یکجا دارن از ترس میمیرن چون فکر میکنن یه بیلد مشکلدار دیپلوی کردن!
سرویسهای مختلف Cloudflare به روشهای متفاوتی affect شدن:
جالبه که proxy engineهای قدیمی و جدید Cloudflare به روشهای متفاوتی fail شدن. FL2 (engine جدید) که با Rust نوشته شده crash میکرد، در حالی که FL (engine قدیمی) bot scoreها رو به اشتباه روی zero میذاشت که باعث false positive شدن bot-blocking ruleها میشد.
ضرر اقتصادی هم قابل توجه بود: تقریباً 35 درصد شرکتهای Fortune 500 ضرر کردن، و تخمین زده شده که ضرر 5 تا 15 میلیارد دلار در هر ساعت بوده. سهام Cloudflare (NET) هم بیش از 2 درصد افت کرد.
کل مدت: 5 ساعت و 46 دقیقه (تاثیر اصلی: 3 ساعت و 10 دقیقه)
اگه بخوایم این incident رو از دیدگاه Site Reliability Engineering تحلیل کنیم، میبینیم که این یک cascading failure کلاسیک بود که از چندین نقطه ضعف ساخته شده:
Layer 1: Configuration Management - در کتاب Site Reliability Engineering که توسط Google SRE team نوشته شده، یکی از مهمترین اصول اینه که “treat configuration as code”. این یعنی configuration changes باید همون سطح testing و staged rollout رو داشته باشن که code changes دارن. اینجا این اتفاق نیفتاد. تغییر database permission به اندازه کافی تست نشد برای edge caseای که duplicate rows برگردونه.
Layer 2: Error Handling - استفاده از .unwrap() در Rust production code یک نقض آشکار defensive programming principles هستش. Michael Nygard در کتاب معروفش “Release It!” یکی از anti-patternهای اصلی رو “expecting the best case” مینامه. او میگه:
Layer 3: Lack of Circuit Breakers - آقای Martin Fowler که circuit breaker pattern رو محبوب کرده، توضیح میده که یک circuit breaker باید
Layer 4: Global Propagation Speed - سیستم Cloudflare’s Quicksilver که میتونه در p99 حدود 2.3 ثانیه یک کانفیگ رو به تمام دنیا برسونه، همون چیزیه که معمولاً قدرت بزرگشونه. اما در کتاب “Designing Data-Intensive Applications”، Martin Kleppmann به درستی اشاره میکنه که در سیستمهای توزیعشده
Layer 5: Monitoring Blind Spots - کتاب Google SRE در فصل ششم از “Four Golden Signals” صحبت میکنه: Latency، Traffic، Errors، و Saturation. اینجا مشکل این بود که مانیتورینگی که میتونست config file size رو track کنه وجود نداشت. اگه یک هشدار برای “feature file size approaching 200” تعریف شده بود، میتونست incident رو قبل از اینکه catastrophic بشه، کشف کنه.
حالا که دیدیم چی اتفاق افتاد، بیاید ببینیم چطور میشد با معماری و practiceهای بهتر از این مشکل جلوگیری کرد. اینجا از اصول کتابهای معتبر SRE و سیستمهای توزیعشده استفاده میکنم.
یکی از اصول اساسی که در “Release It!” به تفصیل توضیح داده شده، Circuit Breaker Pattern هستش. این pattern از electrical circuit breakerها الهام گرفته شده و ایدهاش سادست: وقتی یک سرویس دچار مشکل میشه، به جای اینکه بقیه سرویسها منتظر بمونن یا retry کنن (که work amplification ایجاد میکنه)، circuit breaker “میشکنه” و درخواستها فوری با خطا مواجه میشن.
Circuit breaker سه state داره: Closed (normal operation)، Open (failing fast)، و Half-Open (testing recovery). اگه Cloudflare برای Bot Management module یک circuit breaker پیاده کرده بود، بعد از threshold مشخصی از panics (مثلاً پنج failure پیدرپی)، circuit میشکست و به جای crash کردن، Bot Management رو غیرفعال میکرد. ترافیک همچنان serve میشد، فقط بدون bot detection - که خیلی بهتر از برگردوندن خطاهای 5xx بود.
Martin Fowler توضیح میده:
کتاب Google SRE یک مفهوم مهم رو معرفی میکنه به اسم Static Stability:
AWS Route53 یک مثال عالی از این اصل هستش. Route53 اسمهاش رو هفتهها از قبل pre-sign میکنه، به این معنی که حتی اگه control plane کاملاً down بشه، DNS queries همچنان جواب داده میشن.
برای Cloudflare، این میتونست به این معنی باشه که:
Martin Kleppmann در کتاب درباره replication strategies توضیح میده که چطور میشه با asynchronous replication و eventual consistency معیار availability رو حفظ کرد حتی وقتی که بخشی از سیستم دچار مشکل میشه.
یکی از مهمترین درسهایی که از این incident میگیریم اینه که هیچ تغییری نباید یکباره global بشه. در کتاب Google SRE، یک فصل کامل به release engineering اختصاص داره که تاکید میکنه روی canary deployments و gradual rollouts.
الگو استاندارد باید این باشه:
Cloudflare در postmortem خودش اعتراف کرد که تغییرات کانفیگهای مدیریت بات شامل این پروسه staged rollout نبودن. این یک الگو خطرناکیه که در خیلی از outageها دیده میشه - تغییرات ضروری و امنیتی normal safety gates رو دور میزنن.
Netflix در 2010 مفهوم Chaos Engineering رو با Chaos Monkey معرفی کرد. ایده سادست: عمداً همه چیز رو خراب کن تا ببینی سیستمت چطور واکنش نشون میده. گوگل این رو DiRT (Disaster Recovery Testing) مینامه و به صورت منظم در پروداکشن انجامش میده.
اگه Cloudflare به صورت منظم chaos testing انجام میداد، میتونست این scenarioها رو پیدا کنه:
این یعنی نمیشه فقط در محیط تست همه چیز رو امتحان کرد - باید در محیط پروداکشن (با کنترل سطح تاثیر محدود) تست کرد.</p>
در “Release It!”، آقای Nygard یک اصل اساسی رو توضیح میده: “Be skeptical of everything”. این یعنی حتی به دادهای که از سیستم داخلیت هم میاد باید شک کنی و validate کنی.
Cloudflare در postmortem خودش گفت که یکی از action itemهاش اینه:
کتاب Google SRE در بخش درباره handling overload میگه:
یکی از مهمترین چیزهایی که Google SRE Book تدریس میکنه، فرهنگ Blameless Postmortem هستش. Google میگه:
Postmortem باید این سوالات رو جواب بده:
Google میگه: “The cost of failure is education.” وقتی یک incident اتفاق میافته، باید بیشترین یادگیری رو ازش داشته باشی، و این فقط وقتی ممکنه که افراد بدون ترس از مجازات بتونن راحت صحبت کنن.
این incident چند درس کلیدی داره که برای هر کسی که داره distributed system طراحی میکنه، حیاتی هست:
درس اول: سرعت deployment یک شمشیر دولبهست. توانایی push کردن changes در چند ثانیه به global network یک قدرت فوقالعادهست برای پاسخگویی به تهدیدات. اما همین سرعت یعنی failureها هم با همین سرعت propagate میشن. این یعنی safeguardها - validation، staged rollout، automated rollback - حیاتیتر از همیشه هستن.
درس دوم: defensive programming هیچوقت اختیاری نیست. استفاده از .unwrap() در Rust production code یک قمار بود که باخت. حتی اگه “مطمئن” باشی که یک خطا اتفاق نمیافته، باید gracefully مدیریت کنی. Michael Nygard در “Release It!” میگه: “Expect the worst and code for it.” این یعنی همیشه فرض کن که چیزها میتونن fail بشن و برای اون بدترین سناریو برنامهریزی کن.
درس سوم: configuration changes خطرناکتر از code changes هستن. در تحقیقات Uptime Institute، تغییرات کانفیگ به عنوان شماره یک علت network outagesها شناسایی شدن. چرا؟ چون معمولاً کمتر تست میشن، کمتر ریویو میشن، و اغلب از staged rollout processها عبور میکنن. باید تغییرات کانفیگ رو با همون سطح قوانین که تغییرات کد رو باهاش رفتار میکنی، رفتار کنی.
درس چهارم: پیچیدگی قابل اجتناب نیست، اما قابل مدیریت هست. سیستمهای distributed ذاتاً پیچیده هستن. Martin Kleppmann در “Designing Data-Intensive Applications” میگه که این پیچیدگی unavoidable هستش، اما میتونی با proper abstractions، clear interfaces، و comprehensive testing، اون رو مدیریت کنی. نمیتونی از failure جلوگیری کنی، اما میتونی blast radius رو محدود کنی و ریکاوری رو سریع کنی.
وقتی که 20 درصد از اینترنت روی یک پروایدر متکی هست، یک incident مثل این یادآور این واقعیته که سیستمهای توزیعشده ذاتاً شکننده هستن. اما این شکنندگی قابل اجتناب نیست - بخشی از ذات پیچیدگی هست.
با پیادهسازی اصولی که در کتابهایی مثل “Site Reliability Engineering”، “Designing Data-Intensive Applications”، و “Release It!” توضیح داده شده، میتونیم سیستمهایی بسازیم که:
این incident به ما یاد داد که حتی بهترین مهندسین، در بهترین شرکتها، با بهترین ابزارها، هنوز میتونن اشتباه کنن. تفاوت بین یک سازمان خوب و عالی نه تو اینه که اشتباه نمیکنن، بلکه در اینه که چطور پاسخ میدن، چطور ریکاور میکنن، و چطور یاد میگیرن.
در نهایت، این incident یک یادآوری بود: در دنیای سیستمهای توزیعشده، رخ دادن failure یک ویژگی هست، نه یک باگ. باید براش طراحی کنی، براش برنامهریزی کنی، و ازش یاد بگیری. چون همونطور که CTO شرکت آمازون آقای Werner Vogels، میگه: “Everything fails, all the time.” سوال اینه که آیا آمادهای یا نه!