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 می‌شد.”

محدوده فاجعه: چه کسانی affect شدن

این 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 شدن:

  • Core CDN: نمایش خطاهای HTTP 5xx به end userها
  • Turnstile: به طور کامل fail شد، که باعث شد خیلی‌ها نتونن login کنن
  • Workers KV: افزایش ناگهانی خطاهای 5xx
  • Cloudflare Access: خطاهای authentication گسترده
  • Dashboard: اکثر کاربرها نتونستن login کنن

جالبه که 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 درصد افت کرد.

Timeline دقیق: از شروع تا پایان

  • 11:05 UTC - تغییرات permission دیپلوی شد (trigger event)
  • 11:20 UTC - شبکه شروع به significant failures کرد
  • 11:28 UTC - اولین customer impacts
  • 11:31 UTC - اولین automated test مشکل رو تشخیص داد
  • 11:32 UTC - تحقیقات manual شروع شد
  • 11:35 UTC - آغاز Incident call
  • 12:37 UTC - اعلام “Services recovering”
  • 13:04 UTC - ورکرهای KV و Access bypass پیاده شد
  • 13:09 UTC - اعلام شناسایی مشکل، fix در حال پیاده‌سازی
  • 14:24 UTC - انتشار فایل configuration بد متوقف شد
  • 14:30 UTC - تاثیرات اصلی resolve شد؛ configuration صحیح deploy شد
  • 14:42 UTC - اعلام “Incident resolved”
  • 17:06 UTC - تمام سرویس‌ها fully operational

کل مدت: 5 ساعت و 46 دقیقه (تاثیر اصلی: 3 ساعت و 10 دقیقه)

Root cause از نگاه SRE: چند لایه failure

اگه بخوایم این 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” می‌نامه. او می‌گه:

“Design your application such that a half-failed transaction rolls back and does not leave the database in a nonsensical state.”
در این مورد، کد Cloudflare با فرض این که file size هیچ‌وقت از limit رد نمی‌شه، نوشته شده بود.

Layer 3: Lack of Circuit Breakers - آقای Martin Fowler که circuit breaker pattern رو محبوب کرده، توضیح می‌ده که یک circuit breaker باید

“once the failures reach a certain threshold, the circuit breaker trips, and all further calls return with an error, without the protected call being made at all.”
اگه Cloudflare یک circuit breaker برای Bot Management module داشت، می‌تونست بعد از اولین panicها این feature رو disable کنه و از cascading failure جلوگیری کنه.

Layer 4: Global Propagation Speed - سیستم Cloudflare’s Quicksilver که می‌تونه در p99 حدود 2.3 ثانیه یک کانفیگ رو به تمام دنیا برسونه، همون چیزیه که معمولاً قدرت بزرگشونه. اما در کتاب “Designing Data-Intensive Applications”، Martin Kleppmann به درستی اشاره می‌کنه که در سیستم‌های توزیع‌شده

“There is a fundamental trade-off between consistency, availability, and partition tolerance” (CAP theorem).”
سرعت بالای propagation یعنی failureها هم سریع‌تر propagate می‌شن.

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 و سیستم‌های توزیع‌شده استفاده می‌کنم.

Graceful degradation و circuit breakers

یکی از اصول اساسی که در “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 توضیح می‌ده:

“The circuit breaker monitors for failures. Once the failures reach a certain threshold, the circuit breaker trips.”
شرکت Netflix با Hystrix library این الگو رو عملی کرد و نشون داد که چطور می‌تونه در مقیاس بالا جلوی cascading failures رو بگیره.

Multi-region failover با static stability

کتاب Google SRE یک مفهوم مهم رو معرفی می‌کنه به اسم Static Stability:

“Systems should remain operational even when control plane is unavailable.”
این یعنی data plane (چیزی که ترافیک واقعی رو serve می‌کنه) باید بتونه مستقل از control plane (چیزی که configurationها رو مدیریت می‌کنه) کار کنه.

AWS Route53 یک مثال عالی از این اصل هستش. Route53 اسمهاش رو هفته‌ها از قبل pre-sign می‌کنه، به این معنی که حتی اگه control plane کاملاً down بشه، DNS queries همچنان جواب داده می‌شن.

برای Cloudflare، این می‌تونست به این معنی باشه که:

  • فایل‌های Configuration چندین نسخه قدیمی‌شون رو cache کنه
  • اگه نسخه جدید fail کرد، خودکار به آخرین نسخه سالم برگره
  • Data plane نباید به realtime config generation وابسته باشه
  • Pre-validated configurations چند ساعت قبل از propagation انجام بشه

Martin Kleppmann در کتاب درباره replication strategies توضیح می‌ده که چطور می‌شه با asynchronous replication و eventual consistency معیار availability رو حفظ کرد حتی وقتی که بخشی از سیستم دچار مشکل میشه.

Staged rollout و progressive deployment

یکی از مهم‌ترین درس‌هایی که از این incident می‌گیریم اینه که هیچ تغییری نباید یکباره global بشه. در کتاب Google SRE، یک فصل کامل به release engineering اختصاص داره که تاکید می‌کنه روی canary deployments و gradual rollouts.

الگو استاندارد باید این باشه:

  1. Internal testing (DOG): دیپلوی روی internal infrastructure
  2. Small customer subset (PIG): دیپلوی برای یک درصد کوچیک از customerها
  3. Geographic canaries: دیپلوی در 3-5 location در سراسر دنیا
  4. Progressive rollout: تدریجی 10% و 25% و 50% و 100%
  5. Automated monitoring: در هر مرحله، metrics رو مانیتور کن
  6. Automated rollback: اگه error rate spike کرد، خودکار برگرد

Cloudflare در postmortem خودش اعتراف کرد که تغییرات کانفیگ‌های مدیریت بات شامل این پروسه staged rollout نبودن. این یک الگو خطرناکیه که در خیلی از outageها دیده می‌شه - تغییرات ضروری و امنیتی normal safety gates رو دور می‌زنن.

Chaos engineering و proactive failure testing

Netflix در 2010 مفهوم Chaos Engineering رو با Chaos Monkey معرفی کرد. ایده سادست: عمداً همه چیز رو خراب کن تا ببینی سیستمت چطور واکنش نشون می‌ده. گوگل این رو DiRT (Disaster Recovery Testing) می‌نامه و به صورت منظم در پروداکشن انجامش می‌ده.

اگه Cloudflare به صورت منظم chaos testing انجام می‌داد، می‌تونست این scenarioها رو پیدا کنه:

  • “چی میشه اگه bot management config file خیلی بزرگ بشه؟”
  • “چی میشه اگه یه ماژول panic کنه؟ بقیه سیستم نجات پیدا می‌کنه؟”
  • “آیا monitoring ما می‌تونه این failure modes رو تشخیص بده؟”
“Chaos engineering is firmly rooted in the belief that only a production environment with real traffic and dependencies can provide an accurate picture of resiliency.”

این یعنی نمی‌شه فقط در محیط تست همه چیز رو امتحان کرد - باید در محیط پروداکشن (با کنترل سطح تاثیر محدود) تست کرد.</p>

Automated validation و defensive programming

در “Release It!”، آقای Nygard یک اصل اساسی رو توضیح می‌ده: “Be skeptical of everything”. این یعنی حتی به داده‌ای که از سیستم داخلی‌ت هم میاد باید شک کنی و validate کنی.

Cloudflare در postmortem خودش گفت که یکی از action itemهاش اینه:

“Harden ingestion of Cloudflare-generated configuration files - treat them like user-generated input.”
این دقیقاً همون چیزیه که defensive programming می‌گه: حتی اگه یک فایل کانفیگ از سیستم داخلی تولید شده، باید مثل untrusted input باهاش برخورد کنی.

کتاب Google SRE در بخش درباره handling overload می‌گه:

“Load shedding - drop requests when capacity exceeded” and “Graceful degradation - maintain partial functionality.”
اگه Cloudflare ولیدیشن داشت که می‌دید فایل خیلی بزرگه، می‌تونست reject کنه و از آخرین نسخه سالم قبلی استفاده کنه.

Blameless postmortem culture

یکی از مهم‌ترین چیزهایی که Google SRE Book تدریس می‌کنه، فرهنگ Blameless Postmortem هستش. Google می‌گه:

“For a postmortem to be truly blameless, it must focus on identifying the contributing causes of the incident without indicting any individual or team for bad or inappropriate behavior.”</p>

Postmortem باید این سوالات رو جواب بده:

  • چی اتفاق افتاد؟ (بدون سرزنش کردن شخص خاصی)
  • چرا اتفاق افتاد؟ (multiple contributing factors)
  • چطور detect شد؟
  • چطور fix شد؟
  • چطور می‌تونیم جلوشو بگیریم؟ (با action items مشخص)

Google می‌گه: “The cost of failure is education.” وقتی یک incident اتفاق می‌افته، باید بیشترین یادگیری رو ازش داشته باشی، و این فقط وقتی ممکنه که افراد بدون ترس از مجازات بتونن راحت صحبت کنن.

Lessons learned: چی یاد گرفتیم

این 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 رو محدود کنی و ریکاوری رو سریع کنی.

جمع‌بندی: معماری برای failure

وقتی که 20 درصد از اینترنت روی یک پروایدر متکی هست، یک incident مثل این یادآور این واقعیته که سیستم‌های توزیع‌شده ذاتاً شکننده هستن. اما این شکنندگی قابل اجتناب نیست - بخشی از ذات پیچیدگی هست.

با پیاده‌سازی اصولی که در کتاب‌هایی مثل “Site Reliability Engineering”، “Designing Data-Intensive Applications”، و “Release It!” توضیح داده شده، می‌تونیم سیستم‌هایی بسازیم که:

  • Fail gracefully - با circuit breakers و graceful degradation
  • Recover quickly - با automated rollbacks و clear runbooks
  • Minimize blast radius - با staged rollouts و proper isolation
  • Learn continuously - با blameless postmortems و chaos engineering

این incident به ما یاد داد که حتی بهترین مهندسین، در بهترین شرکت‌ها، با بهترین ابزارها، هنوز می‌تونن اشتباه کنن. تفاوت بین یک سازمان خوب و عالی نه تو اینه که اشتباه نمی‌کنن، بلکه در اینه که چطور پاسخ می‌دن، چطور ریکاور می‌کنن، و چطور یاد می‌گیرن.

در نهایت، این incident یک یادآوری بود: در دنیای سیستم‌های توزیع‌شده، رخ دادن failure یک ویژگی هست، نه یک باگ. باید براش طراحی کنی، براش برنامه‌ریزی کنی، و ازش یاد بگیری. چون همونطور که CTO شرکت آمازون آقای Werner Vogels، می‌گه: “Everything fails, all the time.” سوال اینه که آیا آماده‌ای یا نه!