Skip to content

فصل ۱ — برنامه‌های قابل‌اعتماد، مقیاس‌پذیر و قابل‌نگهداری

اینترنت آن‌قدر خوب ساخته شد که بیشتر مردم آن را مانند یک منبع طبیعی مثل اقیانوس آرام می‌بینند، نه چیزی که انسان ساخته باشد. آخرین باری که فناوری با چنین مقیاسی این‌قدر بدون خطا بود کی بود؟

— Alan Kay، در مصاحبه با Dr Dobb's Journal (۲۰۱۲)

بسیاری از برنامه‌های امروزی داده‌محور (data-intensive) هستند، نه محاسبه‌محور (compute-intensive). قدرت خام CPU به‌ندرت عامل محدودکنندهٔ این برنامه‌هاست — مشکلات بزرگ‌تر معمولاً مقدار داده، پیچیدگی داده و سرعتی است که داده تغییر می‌کند.

یک برنامهٔ داده‌محور معمولاً از بلوک‌های ساختمانی استانداردی ساخته می‌شود که قابلیت‌های رایج موردنیاز را فراهم می‌کنند. برای مثال، بسیاری از برنامه‌ها به موارد زیر نیاز دارند:

  • ذخیرهٔ داده تا خودشان یا برنامهٔ دیگری بتوانند بعداً آن را پیدا کنند (پایگاه‌های داده)
  • به‌خاطر سپردن نتیجهٔ یک عملیات پرهزینه برای تسریع خواندن‌ها (کش‌ها)
  • امکان جستجوی داده بر اساس کلیدواژه یا فیلتر کردن آن به روش‌های مختلف (نمایه‌های جستجو)
  • ارسال پیام به فرایند دیگری برای پردازش ناهمگام (پردازش جریانی)
  • پردازش دوره‌ای حجم زیادی از دادهٔ انباشته‌شده (پردازش دسته‌ای)

اگر این‌ها به‌ظاهر بدیهی به نظر می‌رسند، فقط به این دلیل است که این سیستم‌های داده چنان انتزاع موفقی هستند که بدون فکر زیاد از آن‌ها استفاده می‌کنیم. هنگام ساخت برنامه، بیشتر مهندسان حتی به ذهنشان هم خطور نمی‌کند موتور ذخیره‌سازی دادهٔ جدیدی از صفر بنویسند، چون پایگاه‌های داده ابزار کاملاً مناسبی برای این کار هستند.

اما واقعیت این‌قدر ساده نیست. پایگاه‌های دادهٔ زیادی با ویژگی‌های متفاوت وجود دارند، چون برنامه‌های مختلف نیازمندی‌های متفاوتی دارند. رویکردهای گوناگونی برای کش وجود دارد، چندین روش برای ساخت نمایه‌های جستجو و غیره. هنگام ساخت برنامه، هنوز باید مشخص کنیم کدام ابزارها و کدام رویکردها برای کار در دست مناسب‌ترند. و وقتی کاری لازم است که یک ابزار به‌تنهایی نمی‌تواند انجام دهد، ترکیب ابزارها می‌تواند سخت باشد.

این کتاب سفری است در اصول و جنبه‌های عملی سیستم‌های داده و اینکه چگونه می‌توانید از آن‌ها برای ساخت برنامه‌های داده‌محور استفاده کنید. بررسی می‌کنیم ابزارهای مختلف چه چیزهایی مشترک دارند، چه چیزی آن‌ها را متمایز می‌کند و چگونه به ویژگی‌هایشان دست می‌یابند.

در این فصل، با اصول آنچه می‌خواهیم به دست آوریم شروع می‌کنیم: سیستم‌های دادهٔ قابل‌اعتماد، مقیاس‌پذیر و قابل‌نگهداری. معنای این مفاهیم را روشن می‌کنیم، برخی روش‌های فکر کردن دربارهٔ آن‌ها را ترسیم می‌کنیم و مبانی لازم برای فصل‌های بعد را مرور می‌کنیم. در فصل‌های بعدی لایه‌به‌لایه پیش می‌رویم و تصمیم‌های طراحی مختلفی را که هنگام کار روی برنامهٔ داده‌محور باید در نظر گرفت، بررسی می‌کنیم.

تفکر دربارهٔ سیستم‌های داده

معمولاً پایگاه‌های داده، صف‌ها، کش‌ها و غیره را دسته‌های بسیار متفاوتی از ابزارها می‌دانیم. اگرچه پایگاه داده و صف پیام از نظر ظاهری شباهت‌هایی دارند — هر دو داده را برای مدتی ذخیره می‌کنند — الگوهای دسترسی بسیار متفاوتی دارند؛ یعنی ویژگی‌های عملکردی متفاوت و در نتیجه پیاده‌سازی‌های بسیار متفاوت.

پس چرا همه را زیر چتر اصطلاحی مثل «سیستم‌های داده» جمع کنیم؟

ابزارهای جدید زیادی برای ذخیره و پردازش داده در سال‌های اخیر ظهور کرده‌اند. آن‌ها برای کاربردهای مختلف بهینه شده‌اند و دیگر به‌خوبی در دسته‌های سنتی جا نمی‌گیرند [۱]. برای مثال، datastoreهایی هستند که هم‌زمان به‌عنوان صف پیام استفاده می‌شوند (Redis)، و صف‌های پیامی با تضمین‌های دوام شبیه پایگاه داده (Apache Kafka). مرزهای بین دسته‌ها محو می‌شوند.

دوم، به‌طور فزاینده‌ای برنامه‌های زیادی نیازمندی‌های سنگین یا گسترده‌ای دارند که یک ابزار دیگر نمی‌تواند همهٔ نیازهای پردازش و ذخیرهٔ دادهٔ آن‌ها را برآورده کند. در عوض، کار به وظایفی تقسیم می‌شود که روی یک ابزار به‌طور مؤثر انجام می‌شوند و ابزارهای مختلف با کد برنامه به هم متصل می‌شوند.

برای مثال، اگر لایهٔ کشی مدیریت‌شده توسط برنامه (با Memcached یا مشابه) یا سرور جستجوی تمام‌متن (مانند Elasticsearch یا Solr) جدا از پایگاه دادهٔ اصلی داشته باشید، معمولاً مسئولیت کد برنامه است که آن کش‌ها و نمایه‌ها را با پایگاه دادهٔ اصلی همگام نگه دارد. شکل ۱-۱ نمایی از این وضعیت می‌دهد (در فصل‌های بعد با جزئیات بیشتر بررسی می‌کنیم).

شکل ۱-۱. یک معماری ممکن برای سیستم داده که چندین مؤلفه را ترکیب می‌کند.

وقتی چند ابزار را برای ارائهٔ یک سرویس ترکیب می‌کنید، رابط یا API سرویس معمولاً جزئیات پیاده‌سازی را از کلاینت‌ها پنهان می‌کند. اکنون اساساً یک سیستم دادهٔ ویژه‌منظورهٔ جدید از مؤلفه‌های عمومی‌منظورهٔ کوچک‌تر ساخته‌اید. سیستم دادهٔ ترکیبی شما ممکن است تضمین‌هایی ارائه دهد؛ مثلاً اینکه کش هنگام نوشتن به‌درستی باطل یا به‌روز شود تا کلاینت‌های بیرونی نتایج سازگار ببینند. اکنون نه‌تنها توسعه‌دهندهٔ برنامه، بلکه طراح سیستم داده هم هستید.

اگر سیستم یا سرویس داده طراحی می‌کنید، سؤالات دشوار زیادی پیش می‌آید. چگونه اطمینان می‌دهید داده حتی وقتی درون سیستم مشکلی پیش می‌آید درست و کامل بماند؟ چگونه حتی وقتی بخش‌هایی از سیستم تخریب شده‌اند، عملکرد خوبی به‌طور پایدار به کلاینت‌ها ارائه می‌دهید؟ چگونه برای افزایش بار مقیاس می‌دهید؟ API خوب برای سرویس چه شکلی است؟

عوامل زیادی ممکن است بر طراحی سیستم داده تأثیر بگذارند، از جمله مهارت و تجربهٔ افراد درگیر، وابستگی‌های سیستم‌های legacy، بازهٔ زمانی تحویل، میزان تحمل سازمان شما نسبت به انواع مختلف ریسک، محدودیت‌های قانونی و غیره. این عوامل بسیار به موقعیت بستگی دارند.

در این کتاب بر سه نگرانی تمرکز می‌کنیم که در بیشتر سیستم‌های نرم‌افزاری مهم‌اند:

قابل‌اعتماد (Reliability) سیستم باید حتی در برابر سختی‌ها (خرابی سخت‌افزار یا نرم‌افزار و حتی خطای انسانی) به‌درستی کار کند (عملکرد صحیح را در سطح عملکرد مطلوب انجام دهد). «قابل‌اعتماد» را ببینید.

مقیاس‌پذیر (Scalability) با رشد سیستم (در حجم داده، حجم ترافیک یا پیچیدگی)، باید راه‌های معقولی برای برخورد با آن رشد وجود داشته باشد. «مقیاس‌پذیر» را ببینید.

قابل‌نگهداری (Maintainability) در طول زمان، افراد مختلفی روی سیستم کار می‌کنند (مهندسی و عملیات، هم نگهداری رفتار فعلی و هم تطبیق سیستم با کاربردهای جدید) و همه باید بتوانند به‌طور مؤثر روی آن کار کنند. «قابل‌نگهداری» را ببینید.

این واژه‌ها اغلب بدون درک روشن از معنایشان به کار می‌روند. به‌خاطر مهندسی آگاهانه، بقیهٔ این فصل را صرف کاوش روش‌های فکر کردن دربارهٔ قابلیت اطمینان، مقیاس‌پذیری و قابلیت نگهداری می‌کنیم. سپس در فصل‌های بعد، تکنیک‌ها، معماری‌ها و الگوریتم‌های مختلفی را که برای دستیابی به این اهداف به کار می‌روند بررسی می‌کنیم.

قابل‌اعتماد

همه تصور شهودی از معنای قابل‌اعتماد یا غیرقابل‌اعتماد بودن چیزی دارند. برای نرم‌افزار، انتظارات معمول عبارت‌اند از:

  • برنامه عملکردی را که کاربر انتظار دارد انجام دهد.
  • تحمل اشتباهات کاربر یا استفادهٔ غیرمنتظره از نرم‌افزار.
  • عملکرد به‌اندازهٔ کافی خوب برای کاربرد موردنیاز، تحت بار و حجم دادهٔ مورد انتظار.
  • جلوگیری از هرگونه دسترسی غیرمجاز و سوءاستفاده.

اگر همهٔ این‌ها با هم به معنای «درست کار کردن» باشند، می‌توانیم قابلیت اطمینان را تقریباً به معنای «ادامهٔ کار درست، حتی وقتی مشکلی پیش می‌آید» بفهمیم.

چیزهایی که ممکن است اشتباه پیش بروند خرابی (fault) نامیده می‌شوند، و سیستم‌هایی که خرابی را پیش‌بینی می‌کنند و با آن کنار می‌آیند تحمل‌کنندهٔ خرابی (fault-tolerant) یا تاب‌آور (resilient) نامیده می‌شوند. اصطلاح اول کمی گمراه‌کننده است: القا می‌کند می‌توانیم سیستمی تحمل‌کنندهٔ هر نوع خرابی ممکن بسازیم، که در واقع امکان‌پذیر نیست. اگر کل سیارهٔ زمین (و همهٔ سرورهای روی آن) توسط یک سیاه‌چاله بلعیده شود، تحمل آن خرابی به میزبانی وب در فضا نیاز دارد — موفق باشید آن قلم بودجه تأیید شود. پس فقط دربارهٔ تحمل انواع خاصی از خرابی‌ها منطقی است صحبت کنیم.

توجه کنید خرابی (fault) با شکست (failure) یکی نیست [۲]. خرابی معمولاً به‌عنوان انحراف یک مؤلفه از سیستم از مشخصاتش تعریف می‌شود، در حالی که شکست وقتی است که سیستم به‌عنوان یک کل ارائهٔ سرویس موردنیاز به کاربر را متوقف می‌کند. کاهش احتمال خرابی به صفر غیرممکن است؛ بنابراین معمولاً بهتر است مکانیزم‌های تحمل خرابی طراحی کنیم که از تبدیل خرابی‌ها به شکست جلوگیری کنند. در این کتاب چندین تکنیک برای ساخت سیستم‌های قابل‌اعتماد از قطعات غیرقابل‌اعتماد پوشش می‌دهیم.

برخلاف شهود، در چنین سیستم‌های تحمل‌کنندهٔ خرابی، گاهی افزایش نرخ خرابی با تحریک عمدی آن‌ها — مثلاً کشتن تصادفی فرایندهای منفرد بدون هشدار — منطقی است. بسیاری از باگ‌های بحرانی در واقع به دلیل مدیریت ضعیف خطا هستند [۳]؛ با القای عمدی خرابی، اطمینان می‌دهید ماشین‌آلات تحمل خرابی مداوماً تمرین و آزمایش می‌شوند، که می‌تواند اطمینان شما را از مدیریت صحیح خرابی‌ها هنگام وقوع طبیعی افزایش دهد. Netflix Chaos Monkey [۴] نمونه‌ای از این رویکرد است.

اگرچه معمولاً تحمل خرابی را بر پیشگیری از خرابی ترجیح می‌دهیم، مواردی هست که پیشگیری بهتر از درمان است (مثلاً چون درمانی وجود ندارد). این در مورد امنیت صادق است: اگر مهاجمی سیستم را به خطر انداخته و به دادهٔ حساس دسترسی یافته باشد، آن رویداد قابل بازگشت نیست. با این حال، این کتاب عمدتاً با انواع خرابی‌هایی سروکار دارد که قابل درمان‌اند، همان‌طور که در بخش‌های بعد توضیح داده می‌شود.

خرابی‌های سخت‌افزاری

وقتی به علل شکست سیستم فکر می‌کنیم، خرابی‌های سخت‌افزاری سریعاً به ذهن می‌رسند. دیسک‌های سخت خراب می‌شوند، RAM دچار نقص می‌شود، شبکهٔ برق قطع می‌شود، کسی کابل شبکهٔ اشتباه را می‌کشد. هر کسی که با دیتاسنترهای بزرگ کار کرده می‌تواند بگوید وقتی ماشین‌های زیادی دارید، این‌ها همیشه اتفاق می‌افتند.

گزارش شده دیسک‌های سخت میانگین زمان تا خرابی (MTTF) حدود ۱۰ تا ۵۰ سال دارند [۵، ۶]. بنابراین در خوشهٔ ذخیره‌سازی با ۱۰٬۰۰۰ دیسک، به‌طور میانگین باید انتظار یک دیسک در روز بمیریم.

اولین واکنش ما معمولاً افزودن افزونگی به مؤلفه‌های سخت‌افزاری منفرد برای کاهش نرخ شکست سیستم است. دیسک‌ها ممکن است در پیکربندی RAID تنظیم شوند، سرورها منبع تغذیهٔ دوگانه و CPUهای قابل تعویض گرم داشته باشند، و دیتاسنترها باتری و ژنراتور دیزل برای برق پشتیبان. وقتی یک مؤلفه می‌میرد، مؤلفهٔ افزونه جای آن را می‌گیرد تا مؤلفهٔ خراب تعویض شود. این رویکرد نمی‌تواند کاملاً از بروز مشکلات سخت‌افزاری که منجر به شکست می‌شوند جلوگیری کند، اما به‌خوبی فهمیده شده و اغلب می‌تواند ماشین را سال‌ها بدون وقفه نگه دارد.

تا اخیراً، افزونگی مؤلفه‌های سخت‌افزاری برای بیشتر برنامه‌ها کافی بود، چون شکست کامل یک ماشین منفرد را نسبتاً نادر می‌کند. تا وقتی بتوانید پشتیبان را نسبتاً سریع روی ماشین جدید بازیابی کنید، زمان توقف در صورت خرابی در بیشتر برنامه‌ها فاجعه‌بار نیست. بنابراین افزونگی چندماشینه فقط برای تعداد کمی از برنامه‌هایی که در دسترس‌پذیری بالا کاملاً ضروری بود لازم بود.

با این حال، با افزایش حجم داده و نیازهای محاسباتی برنامه‌ها، برنامه‌های بیشتری از تعداد ماشین‌های بیشتری استفاده می‌کنند که متناسباً نرخ خرابی‌های سخت‌افزاری را افزایش می‌دهد. علاوه بر این، در برخی پلتفرم‌های ابری مانند Amazon Web Services (AWS) نسبتاً رایج است که نمونه‌های ماشین مجازی بدون هشدار در دسترس نباشند [۷]، چون پلتفرم‌ها برای انعطاف و کشسانیi بر قابلیت اطمینان تک‌ماشینه اولویت دارند.

بنابراین گرایشی به سمت سیستم‌هایی که می‌توانند از دست دادن کل ماشین‌ها را تحمل کنند وجود دارد، با استفاده از تکنیک‌های تحمل خرابی نرم‌افزاری به‌جای یا علاوه بر افزونگی سخت‌افزاری. چنین سیستم‌هایی مزایای عملیاتی هم دارند: سیستم تک‌سروری برای اعمال وصلهٔ امنیتی سیستم‌عامل (مثلاً) به زمان توقف برنامه‌ریزی‌شده نیاز دارد، در حالی که سیستمی که شکست ماشین را تحمل می‌کند می‌تواند یک گره در هر بار وصله شود، بدون توقف کل سیستم (ارتقای rolling؛ فصل ۴ را ببینید).

خطاهای نرم‌افزاری

معمولاً خرابی‌های سخت‌افزاری را تصادفی و مستقل از یکدیگر می‌دانیم: خرابی دیسک یک ماشین به معنای خرابی دیسک ماشین دیگر نیست. ممکن است همبستگی‌های ضعیفی وجود داشته باشد (مثلاً به‌دلیل علت مشترک مانند دمای رک سرور)، اما در غیر این صورت بعید است تعداد زیادی مؤلفهٔ سخت‌افزاری هم‌زمان خراب شوند.

دستهٔ دیگری از خرابی، خطای سیستماتیک درون سیستم است [۸]. چنین خطاهایی پیش‌بینی‌شان سخت‌تر است و چون در گره‌ها همبسته‌اند، معمولاً شکست‌های سیستمی بیشتری نسبت به خرابی‌های سخت‌افزاری غیرهمبسته ایجاد می‌کنند [۵]. نمونه‌ها عبارت‌اند از:

  • باگ نرم‌افزاری که هر نمونهٔ سرور برنامه را هنگام دریافت ورودی بد خاصی خراب می‌کند. برای مثال، ثانیهٔ کبیسه در ۳۰ ژوئن ۲۰۱۲ را در نظر بگیرید که به‌دلیل باگ در هستهٔ Linux باعث آویزان ماندن هم‌زمان بسیاری از برنامه‌ها شد [۹].
  • فرایند فرار که منبع مشترکی — زمان CPU، حافظه، فضای دیسک یا پهنای باند شبکه — را مصرف می‌کند.
  • سرویسی که سیستم به آن وابسته است و کند می‌شود، پاسخ نمی‌دهد یا پاسخ‌های خراب برمی‌گرداند.
  • شکست‌های آبشاری (cascading failures)، جایی که خرابی کوچک در یک مؤلفه خرابی در مؤلفهٔ دیگر را تحریک می‌کند که خود خرابی‌های بیشتری را تحریک می‌کند [۱۰].

باگ‌هایی که این نوع خرابی‌های نرم‌افزاری را ایجاد می‌کنند اغلب مدت‌ها خاموش می‌مانند تا با مجموعه‌ای غیرمعمول از شرایط تحریک شوند. در آن شرایط، مشخص می‌شود نرم‌افزار فرضی دربارهٔ محیطش دارد — و اگرچه آن فرض معمولاً درست است، سرانجام به‌دلیلی دیگر درست نمی‌ماند [۱۱].

راه‌حل سریعی برای مشکل خطاهای سیستماتیک در نرم‌افزار وجود ندارد. چیزهای کوچک زیادی کمک می‌کنند: فکر دقیق دربارهٔ فرضیات و تعاملات در سیستم؛ آزمایش کامل؛ جداسازی فرایند؛ اجازهٔ خرابی و راه‌اندازی مجدد فرایندها؛ اندازه‌گیری، پایش و تحلیل رفتار سیستم در تولید. اگر از سیستم انتظار تضمینی دارید (مثلاً در صف پیام، تعداد پیام‌های ورودی برابر تعداد پیام‌های خروجی باشد)، می‌تواند هنگام اجرا مداوم خود را بررسی کند و در صورت اختلاف هشدار بدهد [۱۲].

خطاهای انسانی

انسان‌ها سیستم‌های نرم‌افزاری را طراحی و می‌سازند، و اپراتورهایی که سیستم‌ها را نگه می‌دارند هم انسان‌اند. حتی با بهترین نیت‌ها، انسان‌ها غیرقابل‌اعتماد شناخته شده‌اند. برای مثال، یک مطالعه از سرویس‌های بزرگ اینترنتی نشان داد خطاهای پیکربندی توسط اپراتورها علت اصلی قطعی‌ها بودند، در حالی که خرابی‌های سخت‌افزاری (سرور یا شبکه) فقط در ۱۰–۲۵٪ قطعی‌ها نقش داشتند [۱۳].

چگونه سیستم‌هایمان را قابل‌اعتماد کنیم، با وجود انسان‌های غیرقابل‌اعتماد؟ بهترین سیستم‌ها چند رویکرد را ترکیب می‌کنند:

  • طراحی سیستم به‌گونه‌ای که فرصت خطا را به حداقل برساند. برای مثال، انتزاع‌ها، APIها و رابط‌های مدیریتی خوب طراحی‌شده انجام «کار درست» را آسان و «کار اشتباه» را دشوار می‌کنند. با این حال، اگر رابط‌ها بیش‌ازحد محدودکننده باشند، مردم دورشان می‌زنند و فایدهٔ آن‌ها را از بین می‌برند؛ پس تعادل دشواری است.
  • جداسازی جاهایی که مردم بیشترین اشتباه را می‌کنند از جاهایی که می‌توانند باعث شکست شوند. به‌ویژه، محیط‌های sandbox غیرتولیدی کامل فراهم کنید که مردم بتوانند با دادهٔ واقعی به‌امن کاوش و آزمایش کنند، بدون تأثیر بر کاربران واقعی.
  • آزمایش کامل در همهٔ سطوح، از تست واحد تا تست‌های یکپارچه‌سازی کل سیستم و تست‌های دستی [۳]. آزمایش خودکار به‌طور گسترده استفاده می‌شود، به‌خوبی فهمیده شده و به‌ویژه برای پوشش موارد گوشه‌ای که به‌ندرت در عمل عادی پیش می‌آیند ارزشمند است.
  • امکان بازیابی سریع و آسان از خطاهای انسانی، برای به حداقل رساندن تأثیر در صورت شکست. برای مثال، بازگشت سریع تغییرات پیکربندی، rollout تدریجی کد جدید (تا باگ‌های غیرمنتظره فقط زیرمجموعهٔ کوچکی از کاربران را تحت تأثیر قرار دهند)، و ابزارهایی برای محاسبهٔ مجدد داده (اگر محاسبهٔ قبلی نادرست بود).
  • راه‌اندازی پایش دقیق و روشن، مانند معیارهای عملکرد و نرخ خطا. در رشته‌های مهندسی دیگر به این تله‌متری (telemetry) گفته می‌شود. (وقتی موشک از زمین بلند شده، تله‌متری برای ردیابی آنچه می‌افتد و درک شکست‌ها ضروری است [۱۴].) پایش می‌تواند سیگنال‌های هشدار زودهنگام نشان دهد و اجازه دهد بررسی کنیم آیا فرضیات یا محدودیت‌ها نقض شده‌اند. وقتی مشکلی پیش می‌آید، معیارها برای تشخیص مسئله بسیار ارزشمندند.
  • پیاده‌سازی شیوه‌های مدیریتی و آموزش خوب — جنبه‌ای پیچیده و مهم، و خارج از محدودهٔ این کتاب.

قابلیت اطمینان چقدر مهم است؟

قابلیت اطمینان فقط برای نیروگاه‌های هسته‌ای و نرم‌افزار کنترل ترافیک هوایی نیست — برنامه‌های عادی‌تر هم انتظار می‌رود قابل‌اعتماد کار کنند. باگ‌ها در برنامه‌های کسب‌وکار باعث از دست رفتن بهره‌وری (و ریسک‌های حقوقی اگر ارقام نادرست گزارش شوند) می‌شوند، و قطعی سایت‌های تجارت الکترونیک می‌تواند هزینهٔ عظیمی از دست‌رفتن درآمد و آسیب به شهرت داشته باشد.

حتی در برنامه‌های «غیربحرانی» مسئولیتی در قبال کاربران داریم. والدی را در نظر بگیرید که همهٔ عکس‌ها و ویدیوهای فرزندانش را در برنامهٔ عکس شما ذخیره کرده [۱۵]. اگر آن پایگاه داده ناگهان خراب شود چه حسی پیدا می‌کند؟ آیا می‌داند چگونه از پشتیبان بازیابی کند؟

موقعیت‌هایی هست که ممکن است قابلیت اطمینان را برای کاهش هزینهٔ توسعه (مثلاً هنگام توسعهٔ نمونهٔ اولیه برای بازار اثبات‌نشده) یا هزینهٔ عملیاتی (مثلاً برای سرویسی با حاشیهٔ سود بسیار باریک) قربانی کنیم — اما باید کاملاً آگاه باشیم چه زمانی گوشه می‌بریم.

مقیاس‌پذیر

حتی اگر سیستمی امروز قابل‌اعتماد کار کند، لزوماً به معنای کار قابل‌اعتماد در آینده نیست. یک دلیل رایج تخریب، افزایش بار است: شاید سیستم از ۱۰٬۰۰۰ کاربر هم‌زمان به ۱۰۰٬۰۰۰ کاربر هم‌زمان رشد کرده، یا از ۱ میلیون به ۱۰ میلیون. شاید حجم دادهٔ بسیار بیشتری نسبت به قبل پردازش می‌کند.

مقیاس‌پذیری (scalability) اصطلاحی است که برای توصیف توانایی سیستم در برخورد با افزایش بار به کار می‌بریم. توجه کنید، برچسب یک‌بعدی نیست که به سیستم بچسبانیم: گفتن «X مقیاس‌پذیر است» یا «Y مقیاس نمی‌گیرد» بی‌معناست. در عوض، بحث دربارهٔ مقیاس‌پذیری به معنای بررسی سؤالاتی مانند «اگر سیستم به روش خاصی رشد کند، چه گزینه‌هایی برای برخورد با رشد داریم؟» و «چگونه می‌توانیم منابع محاسباتی اضافه کنیم تا بار اضافی را مدیریت کنیم؟» است.

توصیف بار

ابتدا باید بار فعلی سیستم را به‌اختصار توصیف کنیم؛ فقط آنگاه می‌توانیم دربارهٔ رشد سؤال کنیم (اگر بار دو برابر شود چه می‌شود؟). بار را می‌توان با چند عدد توصیف کرد که پارامترهای بار (load parameters) می‌نامیم. بهترین انتخاب پارامترها به معماری سیستم شما بستگی دارد: ممکن است درخواست در ثانیه به سرور وب، نسبت خواندن به نوشتن در پایگاه داده، تعداد کاربران فعال هم‌زمان در اتاق چت، نرخ hit روی کش، یا چیز دیگری باشد. شاید حالت میانگین برای شما مهم باشد، یا شاید گلوگاه شما توسط تعداد کمی از موارد شدید غالب باشد.

برای ملموس‌تر کردن این ایده، Twitter را با داده‌های منتشرشده در نوامبر ۲۰۱۲ [۱۶] مثال می‌زنیم. دو عملیات اصلی Twitter عبارت‌اند از:

ارسال توییت (Post tweet) کاربر می‌تواند پیام جدیدی برای دنبال‌کنندگانش منتشر کند (به‌طور میانگین ۴٫۶k درخواست در ثانیه، بیش از ۱۲k درخواست در ثانیه در اوج).

خط زمانی خانه (Home timeline) کاربر می‌تواند توییت‌های افرادی که دنبال می‌کند را ببیند (۳۰۰k درخواست در ثانیه).

مدیریت سادهٔ ۱۲٬۰۰۰ نوشتن در ثانیه (نرخ اوج ارسال توییت) نسبتاً آسان است. با این حال، چالش مقیاس‌پذیری Twitter عمدتاً به‌خاطر حجم توییت نیست، بلکه به‌خاطر fan-outii است — هر کاربر افراد زیادی را دنبال می‌کند و هر کاربر توسط افراد زیادی دنبال می‌شود. به‌طور کلی دو روش برای پیاده‌سازی این دو عملیات وجود دارد:

۱. ارسال توییت فقط توییت جدید را در مجموعهٔ سراسری توییت‌ها درج می‌کند. وقتی کاربر خط زمانی خانه را درخواست می‌کند، همهٔ افرادی که دنبال می‌کند را پیدا کنید، همهٔ توییت‌های هر یک از آن کاربران را بیابید و ادغام کنید (مرتب‌شده بر اساس زمان). در پایگاه دادهٔ رابطه‌ای مانند شکل ۱-۲، می‌توانید کوئریی مانند زیر بنویسید:

sql
SELECT tweets.*, users.* FROM tweets
  JOIN users ON tweets.sender_id      = users.id
  JOIN follows ON follows.followee_id = users.id
  WHERE follows.follower_id = current_user

شکل ۱-۲. طرحوارهٔ رابطه‌ای ساده برای پیاده‌سازی خط زمانی خانهٔ Twitter.

۲. برای هر کاربر کش خط زمانی خانه نگه دارید — مانند صندوق پستی توییت‌ها برای هر کاربر گیرنده (شکل ۱-۳ را ببینید). وقتی کاربر توییت می‌فرستد، همهٔ افرادی که آن کاربر را دنبال می‌کنند را پیدا کنید و توییت جدید را در کش خط زمانی خانهٔ هر یک درج کنید. درخواست خواندن خط زمانی خانه ارزان است، چون نتیجه از قبل محاسبه شده.

شکل ۱-۳. خط لولهٔ دادهٔ Twitter برای تحویل توییت‌ها به دنبال‌کنندگان، با پارامترهای بار تا نوامبر ۲۰۱۲ [۱۶].

نسخهٔ اول Twitter از رویکرد ۱ استفاده کرد، اما سیستم‌ها در همگام ماندن با بار کوئری‌های خط زمانی خانه مشکل داشتند، بنابراین شرکت به رویکرد ۲ تغییر کرد. این بهتر کار می‌کند چون نرخ میانگین انتشار توییت تقریباً دو مرتبه کمتر از نرخ خواندن خط زمانی خانه است، و در این مورد ترجیحاً کار بیشتری در زمان نوشتن و کمتر در زمان خواندن انجام می‌دهیم.

با این حال، نقطهٔ ضعف رویکرد ۲ این است که ارسال توییت اکنون کار اضافی زیادی می‌طلبد. به‌طور میانگین، یک توییت به حدود ۷۵ دنبال‌کننده تحویل داده می‌شود، پس ۴٫۶k توییت در ثانیه به ۳۴۵k نوشتن در ثانیه در کش‌های خط زمانی خانه تبدیل می‌شود. اما این میانگین این واقعیت را پنهان می‌کند که تعداد دنبال‌کنندگان هر کاربر بسیار متفاوت است و برخی کاربران بیش از ۳۰ میلیون دنبال‌کننده دارند. یعنی یک توییت ممکن است بیش از ۳۰ میلیون نوشتن در خطوط زمانی خانه ایجاد کند! انجام این کار به‌موقع — Twitter تلاش می‌کند توییت‌ها را ظرف پنج ثانیه به دنبال‌کنندگان برساند — چالش قابل‌توجهی است.

در مثال Twitter، توزیع دنبال‌کنندگان هر کاربر (شاید وزن‌دار با میزان توییت آن کاربران) پارامتر بار کلیدی برای بحث مقیاس‌پذیری است، چون بار fan-out را تعیین می‌کند. برنامهٔ شما ممکن است ویژگی‌های بسیار متفاوتی داشته باشد، اما می‌توانید اصول مشابهی را برای استدلال دربارهٔ بار آن به کار ببرید.

پیچیدگی نهایی حکایت Twitter: اکنون که رویکرد ۲ به‌طور محکم پیاده‌سازی شده، Twitter به ترکیبی از هر دو رویکرد می‌رود. توییت‌های بیشتر کاربران همچنان در زمان ارسال به خطوط زمانی خانه fan-out می‌شوند، اما تعداد کمی از کاربران با دنبال‌کنندگان بسیار زیاد (یعنی مشاهیر) از این fan-out مستثنی‌اند. توییت‌های هر مشهوری که کاربر دنبال می‌کند جداگانه واکشی و هنگام خواندن خط زمانی خانه با آن ادغام می‌شود، مانند رویکرد ۱. این رویکرد ترکیبی می‌تواند عملکرد مداوماً خوبی ارائه دهد. این مثال را در فصل ۱۲، پس از پوشش مبانی فنی بیشتر، دوباره بررسی می‌کنیم.

توصیف عملکرد

وقتی بار سیستم را توصیف کردید، می‌توانید بررسی کنید با افزایش بار چه می‌شود. می‌توانید از دو زاویه نگاه کنید:

  • وقتی یک پارامتر بار را افزایش می‌دهید و منابع سیستم (CPU، حافظه، پهنای باند شبکه و غیره) را ثابت نگه می‌دارید، عملکرد سیستم چگونه تحت تأثیر قرار می‌گیرد؟
  • وقتی یک پارامتر بار را افزایش می‌دهید، برای حفظ عملکرد ثابت چقدر باید منابع را افزایش دهید؟

هر دو سؤال به اعداد عملکرد نیاز دارند، پس به‌اختصار توصیف عملکرد سیستم را مرور می‌کنیم.

در سیستم پردازش دسته‌ای مانند Hadoop، معمولاً به توان عبوری (throughput) اهمیت می‌دهیم — تعداد رکوردهایی که در ثانیه پردازش می‌کنیم، یا کل زمان اجرای یک job روی مجموعه‌داده‌ای با اندازهٔ مشخص.iii در سیستم‌های آنلاین، معمولاً مهم‌تر زمان پاسخ (response time) سرویس است — یعنی زمان بین ارسال درخواست توسط کلاینت و دریافت پاسخ.

تأخیر و زمان پاسخ

تأخیر (latency) و زمان پاسخ اغلب مترادف به کار می‌روند، اما یکی نیستند. زمان پاسخ آنچه کلاینت می‌بیند است: علاوه بر زمان واقعی پردازش درخواست (زمان سرویس)، شامل تأخیرهای شبکه و صف‌بندی هم می‌شود. تأخیر مدت زمانی است که درخواست منتظر رسیدگی می‌ماند — در این مدت در حالت انتظار (latent) است [۱۷].

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

در شکل ۱-۴، هر میلهٔ خاکستری یک درخواست به سرویس را نشان می‌دهد و ارتفاع آن مدت زمان آن درخواست را. بیشتر درخواست‌ها نسبتاً سریع‌اند، اما گاهی outlierهایی هستند که بسیار بیشتر طول می‌کشند. شاید درخواست‌های کند ذاتاً پرهزینه‌ترند، مثلاً دادهٔ بیشتری پردازش می‌کنند. اما حتی در سناریویی که فکر می‌کنید همهٔ درخواست‌ها باید یکسان طول بکشند، تغییرپذیری دارید: تأخیر اضافی تصادفی ممکن است به‌دلیل context switch به فرایند پس‌زمینه، از دست رفتن بستهٔ شبکه و retransmission TCP، مکث garbage collection، page fault که خواندن از دیسک را اجبار می‌کند، لرزش مکانیکی در رک سرور [۱۸] یا علل بسیار دیگر باشد.

شکل ۱-۴. نمایش میانگین و صدک‌ها: زمان‌های پاسخ برای نمونه‌ای از ۱۰۰ درخواست به یک سرویس.

گزارش میانگین زمان پاسخ یک سرویس رایج است. (به‌طور دقیق، اصطلاح «میانگین» به فرمول خاصی اشاره ندارد، اما در عمل معمولاً به‌عنوان میانگین حسابی فهمیده می‌شود: با n مقدار، همه را جمع کنید و بر n تقسیم کنید.) با این حال، میانگین معیار خوبی نیست اگر بخواهید زمان پاسخ «معمول» را بدانید، چون نمی‌گوید چند کاربر واقعاً آن تأخیر را تجربه کرده‌اند.

معمولاً بهتر است از صدک‌ها (percentiles) استفاده کنیم. اگر فهرست زمان‌های پاسخ را از سریع‌ترین به کندترین مرتب کنید، میانه (median) نقطهٔ میانی است: برای مثال، اگر میانهٔ زمان پاسخ ۲۰۰ میلی‌ثانیه باشد، یعنی نیمی از درخواست‌ها در کمتر از ۲۰۰ میلی‌ثانیه برمی‌گردند و نیمی بیشتر طول می‌کشند.

این میانه را به معیار خوبی تبدیل می‌کند اگر بخواهید بدانید کاربران معمولاً چقدر منتظر می‌مانند: نیمی از درخواست‌های کاربر در کمتر از میانهٔ زمان پاسخ سرویس می‌شود و نیمی بیشتر. میانه همچنین صدک پنجاهم (50th percentile) نامیده می‌شود و گاهی p50 مخفف می‌شود. توجه کنید میانه به یک درخواست اشاره دارد؛ اگر کاربر چند درخواست انجام دهد (در طول یک session یا چون چند منبع در یک صفحه گنجانده شده)، احتمال اینکه حداقل یکی کندتر از میانه باشد بسیار بیشتر از ۵۰٪ است.

برای فهمیدن شدت outlierها، می‌توانید صدک‌های بالاتر را ببینید: صدک‌های ۹۵، ۹۹ و ۹۹٫۹ رایج‌اند (مخفف p95، p99 و p999). آستانه‌های زمان پاسخی که ۹۵٪، ۹۹٪ یا ۹۹٫۹٪ درخواست‌ها سریع‌تر از آن هستند. برای مثال، اگر صدک ۹۵ زمان پاسخ ۱٫۵ ثانیه باشد، یعنی ۹۵ از ۱۰۰ درخواست کمتر از ۱٫۵ ثانیه طول می‌کشند و ۵ از ۱۰۰ درخواست ۱٫۵ ثانیه یا بیشتر. این در شکل ۱-۴ نشان داده شده.

صدک‌های بالای زمان پاسخ، که تأخیر دم (tail latencies) هم نامیده می‌شوند، مهم‌اند چون مستقیماً بر تجربهٔ کاربران از سرویس تأثیر می‌گذارند. برای مثال، Amazon الزامات زمان پاسخ سرویس‌های داخلی را بر اساس صدک ۹۹٫۹ بیان می‌کند، اگرچه فقط ۱ از هر ۱٬۰۰۰ درخواست را تحت تأثیر قرار می‌دهد. چون مشتریانی با کندترین درخواست‌ها اغلب کسانی‌اند که بیشترین داده را در حسابشان دارند چون خریدهای زیادی کرده‌اند — یعنی ارزشمندترین مشتریان‌اند [۱۹]. نگه داشتن رضایت آن‌ها با اطمینان از سرعت سایت برایشان مهم است: Amazon همچنین مشاهده کرده افزایش ۱۰۰ میلی‌ثانیه‌ای زمان پاسخ فروش را ۱٪ کاهش می‌دهد [۲۰]، و دیگران گزارش داده‌اند کندی ۱ ثانیه‌ای معیار رضایت مشتری را ۱۶٪ کاهش می‌دهد [۲۱، ۲۲].

از سوی دیگر، بهینه‌سازی صدک ۹۹٫۹۹ (کندترین ۱ از ۱۰٬۰۰۰ درخواست) برای اهداف Amazon بسیار پرهزینه و کم‌بازده تلقی شد. کاهش زمان پاسخ در صدک‌های بسیار بالا دشوار است چون به‌راحتی تحت تأثیر رویدادهای تصادفی خارج از کنترل شما قرار می‌گیرند و فایده‌ها کاهش می‌یابد.

برای مثال، صدک‌ها اغلب در اهداف سطح سرویس (SLO) و توافق‌نامه‌های سطح سرویس (SLA) به کار می‌روند — قراردادهایی که عملکرد و در دسترس‌بودن مورد انتظار سرویس را تعریف می‌کنند. یک SLA ممکن است بگوید سرویس در دسترس است اگر میانهٔ زمان پاسخ کمتر از ۲۰۰ میلی‌ثانیه و صدک ۹۹ زیر ۱ ثانیه باشد (اگر زمان پاسخ بیشتر باشد، بهتر است پایین در نظر گرفته شود)، و سرویس باید حداقل ۹۹٫۹٪ زمان در دسترس باشد. این معیارها انتظارات کلاینت‌های سرویس را تعیین می‌کنند و به مشتریان اجازه می‌دهند در صورت نقض SLA بازپرداخت بخواهند.

تأخیرهای صف‌بندی اغلب بخش بزرگی از زمان پاسخ در صدک‌های بالا را تشکیل می‌دهند. چون سرور فقط تعداد کمی کار را به‌طور موازی پردازش می‌کند (مثلاً محدود به تعداد هسته‌های CPU)، تعداد کمی درخواست کند کافی است تا پردازش درخواست‌های بعدی را متوقف کند — اثری که گاهی انسداد سر صف (head-of-line blocking) نامیده می‌شود. حتی اگر آن درخواست‌های بعدی روی سرور سریع پردازش شوند، کلاینت زمان پاسخ کلی کند می‌بیند به‌دلیل انتظار برای تکمیل درخواست قبلی. به‌خاطر این اثر، اندازه‌گیری زمان پاسخ در سمت کلاینت مهم است.

وقتی برای آزمایش مقیاس‌پذیری سیستم به‌طور مصنوعی بار تولید می‌کنید، کلاینت تولیدکنندهٔ بار باید مستقل از زمان پاسخ به ارسال درخواست ادامه دهد. اگر کلاینت منتظر تکمیل درخواست قبلی بماند تا بعدی را بفرستد، این رفتار اثر مصنوعی کوتاه‌تر نگه داشتن صف‌ها در آزمایش نسبت به واقعیت دارد و اندازه‌گیری‌ها را منحرف می‌کند [۲۳].

صدک‌ها در عمل

صدک‌های بالا به‌ویژه در سرویس‌های backend که چندین بار برای سرویس‌دهی به یک درخواست end-user فراخوانی می‌شوند مهم می‌شوند. حتی اگر فراخوانی‌ها را موازی انجام دهید، درخواست end-user همچنان باید منتظر کندترین فراخوانی موازی بماند. فقط یک فراخوانی کند کافی است تا کل درخواست end-user کند شود، همان‌طور که شکل ۱-۵ نشان می‌دهد. حتی اگر فقط درصد کمی از فراخوانی‌های backend کند باشند، اگر درخواست end-user به چند فراخوانی backend نیاز داشته باشد، احتمال دریافت فراخوانی کند بیشتر می‌شود و در نتیجه نسبت بیشتری از درخواست‌های end-user کند می‌شوند (اثری که تقویت تأخیر دم (tail latency amplification) نامیده می‌شود [۲۴]).

اگر می‌خواهید صدک‌های زمان پاسخ را به داشبوردهای پایش سرویس‌هایتان اضافه کنید، باید آن‌ها را به‌طور مداوم به‌صورت کارآمد محاسبه کنید. مثلاً ممکن است بخواهید پنجرهٔ rolling از زمان‌های پاسخ درخواست‌های ۱۰ دقیقهٔ گذشته را نگه دارید. هر دقیقه میانه و صدک‌های مختلف را روی مقادیر آن پنجره محاسبه و روی نمودار رسم کنید.

پیاده‌سازی ساده نگه داشتن فهرست زمان‌های پاسخ همهٔ درخواست‌ها در پنجرهٔ زمانی و مرتب‌سازی آن فهرست هر دقیقه است. اگر ناکارآمد است، الگوریتم‌هایی هستند که تقریب خوبی از صدک‌ها با حداقل هزینهٔ CPU و حافظه محاسبه می‌کنند، مانند forward decay [۲۵]، t-digest [۲۶] یا HdrHistogram [۲۷]. مراقب باشید میانگین‌گیری از صدک‌ها — مثلاً برای کاهش وضوح زمانی یا ترکیب داده از چند ماشین — از نظر ریاضی بی‌معناست؛ روش درست تجمیع دادهٔ زمان پاسخ، افزودن histogramها است [۲۸].

شکل ۱-۵. وقتی چند فراخوانی backend برای سرویس‌دهی به یک درخواست لازم است، فقط یک فراخوانی backend کند کافی است تا کل درخواست end-user کند شود.

رویکردها برای برخورد با بار

اکنون که پارامترهای توصیف بار و معیارهای اندازه‌گیری عملکرد را بررسی کردیم، می‌توانیم جدی‌تر دربارهٔ مقیاس‌پذیری بحث کنیم: چگونه حتی وقتی پارامترهای بار تا حدی افزایش می‌یابند عملکرد خوبی حفظ کنیم؟

معماری مناسب برای یک سطح بار، بعید است با ۱۰ برابر آن بار کنار بیاید. اگر روی سرویسی با رشد سریع کار می‌کنید، احتمالاً باید معماری را در هر افزایش یک مرتبه‌ای بار — یا شاید بیش از آن — بازاندیشی کنید.

مردم اغلب دوگانگی بین مقیاس‌دهی عمودی (scaling up / vertical scaling — انتقال به ماشین قدرتمندتر) و مقیاس‌دهی افقی (scaling out / horizontal scaling — توزیع بار روی چند ماشین کوچک‌تر) را مطرح می‌کنند. توزیع بار روی چند ماشین همچنین معماری shared-nothing نامیده می‌شود. سیستمی که روی یک ماشین اجرا می‌شود اغلب ساده‌تر است، اما ماشین‌های high-end می‌توانند بسیار گران شوند، پس بارهای بسیار سنگین اغلب نمی‌توانند از مقیاس‌دهی افقی اجتناب کنند. در واقع، معماری‌های خوب معمولاً ترکیب عمل‌گرایانه‌ای از رویکردها دارند: مثلاً استفاده از چند ماشین نسبتاً قدرتمند هنوز می‌تواند ساده‌تر و ارزان‌تر از تعداد زیادی ماشین مجازی کوچک باشد.

برخی سیستم‌ها کشسان (elastic) دارند، یعنی هنگام تشخیص افزایش بار به‌طور خودکار منابع محاسباتی اضافه می‌کنند، در حالی که برخی دیگر به‌صورت دستی مقیاس می‌گیرند (انسان ظرفیت را تحلیل می‌کند و تصمیم می‌گیرد ماشین بیشتری اضافه کند). سیستم کشسان برای بار بسیار غیرقابل پیش‌بینی مفید است، اما سیستم‌های مقیاس‌دهی دستی ساده‌ترند و ممکن است سورپرایزهای عملیاتی کمتری داشته باشند («تعادل مجدد پارتیشن‌ها» در صفحه ۲۰۹ را ببینید).

اگرچه توزیع سرویس‌های بدون حالت (stateless) روی چند ماشین نسبتاً ساده است، بردن سیستم‌های دادهٔ دارای حالت (stateful) از یک گره به تنظیمات توزیع‌شده پیچیدگی اضافی زیادی معرفی می‌کند. به‌همین دلیل، حکمت رایج تا اخیر این بود که پایگاه داده را روی یک گره نگه دارید (مقیاس‌دهی عمودی) تا هزینهٔ مقیاس یا الزامات در دسترس‌پذیری بالا شما را مجبور به توزیع کند.

با بهتر شدن ابزارها و انتزاع‌ها برای سیستم‌های توزیع‌شده، این حکمت رایج ممکن است برای برخی انواع برنامه‌ها تغییر کند. قابل تصور است سیستم‌های دادهٔ توزیع‌شده در آینده پیش‌فرض شوند، حتی برای کاربردهایی که حجم زیاد داده یا ترافیک ندارند. در ادامهٔ این کتاب انواع مختلف سیستم‌های دادهٔ توزیع‌شده را پوشش می‌دهیم و بررسی می‌کنیم نه‌تنها از نظر مقیاس‌پذیری، بلکه از نظر سهولت استفاده و قابلیت نگهداری چگونه عمل می‌کنند.

معماری سیستم‌هایی که در مقیاس بزرگ کار می‌کنند معمولاً بسیار خاص برنامه است — معماری مقیاس‌پذیر عمومی و یک‌اندازه‌برای‌همه (به‌صورت غیررسمی سس جادویی مقیاس (magic scaling sauce) نامیده می‌شود) وجود ندارد. مشکل ممکن است حجم خواندن، حجم نوشتن، حجم داده برای ذخیره، پیچیدگی داده، الزامات زمان پاسخ، الگوهای دسترسی، یا (معمولاً) ترکیبی از همهٔ این‌ها به‌علاوهٔ مسائل بسیار بیشتر باشد.

برای مثال، سیستمی که برای ۱۰۰٬۰۰۰ درخواست در ثانیه، هر کدام ۱ کیلوبایت طراحی شده، بسیار متفاوت از سیستمی است که برای ۳ درخواست در دقیقه، هر کدام ۲ گیگابایت طراحی شده — حتی اگر هر دو توان عبوری داده یکسان داشته باشند.

معماری‌ای که برای یک برنامهٔ خاص به‌خوبی مقیاس می‌گیرد حول فرضیاتی از اینکه کدام عملیات رایج و کدام نادر خواهند بود — پارامترهای بار — ساخته می‌شود. اگر آن فرضیات اشتباه باشند، تلاش مهندسی برای مقیاس‌دهی در بهترین حالت هدر رفته و در بدترین حالت ضد‌تولید است. در استارتاپ مرحلهٔ اولیه یا محصول اثبات‌نشده، معمولاً مهم‌تر است که بتوانید سریع روی ویژگی‌های محصول تکرار کنید تا مقیاس‌دهی به بار فرضی آینده.

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

قابل‌نگهداری

شناخته‌شده است که بیشتر هزینهٔ نرم‌افزار در توسعهٔ اولیه نیست، بلکه در نگهداری مداوم — رفع باگ، نگه داشتن سیستم‌ها در حال کار، بررسی شکست‌ها، تطبیق با پلتفرم‌های جدید، تغییر برای کاربردهای جدید، بازپرداخت بدهی فنی و افزودن ویژگی‌های جدید.

با این حال، متأسفانه بسیاری از افرادی که روی سیستم‌های نرم‌افزاری کار می‌کنند از نگهداری سیستم‌های به‌اصطلاح legacy بیزارند — شاید شامل رفع اشتباهات دیگران، کار با پلتفرم‌های منسوخ، یا سیستم‌هایی که مجبور شده‌اند کاری انجام دهند که برای آن طراحی نشده‌اند. هر سیستم legacy به روش خودش ناخوشایند است، پس دادن توصیه‌های عمومی برای برخورد با آن‌ها دشوار است.

با این حال، می‌توانیم و باید نرم‌افزار را طوری طراحی کنیم که امیدوارانه درد نگهداری را به حداقل برساند و از ساخت نرم‌افزار legacy توسط خودمان جلوگیری کنیم. برای این منظور، به‌ویژه به سه اصل طراحی برای سیستم‌های نرم‌افزاری توجه می‌کنیم:

قابلیت بهره‌برداری (Operability) آسان کردن کار برای تیم‌های عملیات تا سیستم به‌روان اجرا شود.

سادگی (Simplicity) آسان کردن درک سیستم برای مهندسان جدید، با حذف تا حد امکان پیچیدگی از سیستم. (توجه کنید این با سادگی رابط کاربری یکی نیست.)

تکاملی‌پذیری (Evolvability) آسان کردن تغییر سیستم در آینده برای مهندسان، تطبیق آن با کاربردهای پیش‌بینی‌نشده با تغییر نیازمندی‌ها. همچنین قابلیت گسترش (extensibility)، قابلیت تغییر (modifiability) یا انعطاف‌پذیری (plasticity) نامیده می‌شود.

همان‌طور که با قابلیت اطمینان و مقیاس‌پذیری، راه‌حل آسان برای دستیابی به این اهداف وجود ندارد. در عوض، سعی می‌کنیم دربارهٔ سیستم‌ها با در نظر گرفتن قابلیت بهره‌برداری، سادگی و تکاملی‌پذیری فکر کنیم.

قابلیت بهره‌برداری: آسان کردن زندگی برای عملیات

پیشنهاد شده «عملیات خوب اغلب می‌تواند محدودیت‌های نرم‌افزار بد (یا ناقص) را دور بزند، اما نرم‌افزار خوب نمی‌تواند با عملیات بد به‌طور قابل‌اعتماد اجرا شود» [۱۲]. اگرچه برخی جنبه‌های عملیات می‌توانند و باید خودکار شوند، هنوز بر عهدهٔ انسان است که آن خودکارسازی را در ابتدا راه‌اندازی کند و مطمئن شود درست کار می‌کند.

تیم‌های عملیات برای نگه داشتن سیستم نرم‌افزاری به‌روان حیاتی‌اند. یک تیم عملیات خوب معمولاً مسئول موارد زیر و بیشتر است [۲۹]:

  • پایش سلامت سیستم و بازیابی سریع سرویس اگر به وضعیت بد رفت
  • ردیابی علت مشکلات، مانند شکست‌های سیستم یا تخریب عملکرد
  • به‌روز نگه داشتن نرم‌افزار و پلتفرم‌ها، از جمله وصله‌های امنیتی
  • پیگیری تأثیر متقابل سیستم‌های مختلف، تا تغییر مشکل‌ساز قبل از ایجاد آسیب اجتناب شود
  • پیش‌بینی مشکلات آینده و حل آن‌ها قبل از وقوع (مثلاً برنامه‌ریزی ظرفیت)
  • ایجاد شیوه‌ها و ابزارهای خوب برای استقرار، مدیریت پیکربندی و موارد بیشتر
  • انجام وظایف نگهداری پیچیده، مانند انتقال برنامه از یک پلتفرم به پلتفرم دیگر
  • حفظ امنیت سیستم هنگام تغییرات پیکربندی
  • تعریف فرایندهایی که عملیات را قابل پیش‌بینی می‌کند و به پایداری محیط تولید کمک می‌کند
  • حفظ دانش سازمان دربارهٔ سیستم، حتی با آمدن و رفتن افراد

قابلیت بهره‌برداری خوب به معنای آسان کردن وظایف روزمره است، تا تیم عملیات روی فعالیت‌های باارزش تمرکز کند. سیستم‌های داده می‌توانند کارهای مختلفی برای آسان کردن وظایف روزمره انجام دهند، از جمله:

  • ارائهٔ دید به رفتار زمان اجرا و درون سیستم، با پایش خوب
  • پشتیبانی خوب از خودکارسازی و یکپارچه‌سازی با ابزارهای استاندارد
  • اجتناب از وابستگی به ماشین‌های منفرد (اجازهٔ خاموش کردن ماشین‌ها برای نگهداری در حالی که کل سیستم بدون وقفه اجرا می‌شود)
  • ارائهٔ مستندات خوب و مدل عملیاتی قابل فهم («اگر X انجام دهم، Y اتفاق می‌افتد»)
  • ارائهٔ رفتار پیش‌فرض خوب، اما همچنین آزادی مدیران برای لغو پیش‌فرض‌ها در صورت نیاز
  • خود-ترمیمی در جاهای مناسب، اما همچنین کنترل دستی مدیران بر وضعیت سیستم در صورت نیاز
  • رفتار قابل پیش‌بینی، به حداقل رساندن سورپرایزها

سادگی: مدیریت پیچیدگی

پروژه‌های نرم‌افزاری کوچک می‌توانند کد ساده و بیانگر دلپذیری داشته باشند، اما با بزرگ‌تر شدن پروژه‌ها، اغلب بسیار پیچیده و دشوار برای درک می‌شوند. این پیچیدگی همهٔ کسانی را که باید روی سیستم کار کنند کند می‌کند و هزینهٔ نگهداری را بیشتر می‌کند. پروژهٔ نرم‌افزاری غرق در پیچیدگی گاهی توپ گل بزرگ (big ball of mud) [۳۰] نامیده می‌شود.

علائم مختلفی برای پیچیدگی وجود دارد: انفجار فضای حالت، coupling تنگ بین ماژول‌ها، وابستگی‌های درهم‌تنیده، نام‌گذاری و اصطلاحات ناسازگار، hackهایی برای حل مشکلات عملکرد، موارد خاص برای دور زدن مسائل جای دیگر و بسیاری بیشتر. در این موضوع قبلاً زیاد گفته شده [۳۱، ۳۲، ۳۳].

وقتی پیچیدگی نگهداری را سخت می‌کند، بودجه و زمان‌بندی اغلب از برنامه خارج می‌شوند. در نرم‌افزار پیچیده، ریسک معرفی باگ هنگام تغییر هم بیشتر است: وقتی سیستم برای توسعه‌دهندگان سخت‌تر برای درک و استدلال است، فرضیات پنهان، پیامدهای ناخواسته و تعاملات غیرمنتظره راحت‌تر نادیده گرفته می‌شوند. برعکس، کاهش پیچیدگی به‌طور چشمگیری قابلیت نگهداری نرم‌افزار را بهبود می‌دهد، و بنابراین سادگی باید هدف کلیدی سیستم‌هایی باشد که می‌سازیم.

ساده کردن سیستم لزوماً به معنای کاهش قابلیت‌ها نیست؛ می‌تواند به معنای حذف پیچیدگی تصادفی (accidental complexity) هم باشد. Moseley و Marks [۳۲] پیچیدگی را تصادفی تعریف می‌کنند اگر ذات مسئله‌ای که نرم‌افزار حل می‌کند (از دید کاربران) نباشد، بلکه فقط از پیاده‌سازی ناشی شود.

یکی از بهترین ابزارهای ما برای حذف پیچیدگی تصادفی انتزاع (abstraction) است. انتزاع خوب می‌تواند مقدار زیادی جزئیات پیاده‌سازی را پشت نمای ساده و قابل فهم پنهان کند. انتزاع خوب همچنین برای طیف گسترده‌ای از کاربردهای مختلف قابل استفاده است. نه‌تنها این استفادهٔ مجدد از پیاده‌سازی مجدد چیز مشابه چندباره کارآمدتر است، بلکه به نرم‌افزار با کیفیت بالاتر منجر می‌شود، چون بهبود کیفیت در مؤلفهٔ انتزاعی به همهٔ برنامه‌هایی که از آن استفاده می‌کنند سود می‌رساند.

برای مثال، زبان‌های برنامه‌نویسی سطح‌بالا انتزاع‌هایی هستند که کد ماشین، رجیسترهای CPU و syscallها را پنهان می‌کنند. SQL انتزاعی است که ساختارهای دادهٔ روی دیسک و در حافظه، درخواست‌های هم‌زمان از کلاینت‌های دیگر و ناسازگاری‌ها پس از خرابی را پنهان می‌کند. البته هنگام برنامه‌نویسی با زبان سطح‌بالا، هنوز از کد ماشین استفاده می‌کنیم؛ فقط مستقیماً استفاده نمی‌کنیم، چون انتزاع زبان برنامه‌نویسی ما را از فکر کردن به آن نجات می‌دهد.

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

در سراسر این کتاب، به‌دنبال انتزاع‌های خوبی می‌گردیم که اجازه دهند بخش‌هایی از سیستم بزرگ را به مؤلفه‌های تعریف‌شده و قابل استفادهٔ مجدد استخراج کنیم.

تکاملی‌پذیری: آسان کردن تغییر

بسیار بعید است نیازمندی‌های سیستم شما برای همیشه ثابت بمانند. بسیار محتمل‌تر است که دائماً در نوسان باشند: حقایق جدید می‌آموزید، کاربردهای پیش‌بینی‌نشده ظهور می‌کنند، اولویت‌های کسب‌وکار تغییر می‌کند، کاربران ویژگی‌های جدید می‌خواهند، پلتفرم‌های جدید جایگزین قدیمی‌ها می‌شوند، الزامات قانونی یا نظارتی تغییر می‌کند، رشد سیستم تغییرات معماری را اجبار می‌کند و غیره.

از نظر فرایندهای سازمانی، الگوهای کاری Agile چارچوبی برای تطبیق با تغییر فراهم می‌کنند. جامعهٔ Agile همچنین ابزارها و الگوهای فنی مفیدی برای توسعهٔ نرم‌افزار در محیط پرتغییر توسعه داده، مانند توسعهٔ رانش‌محور تست (TDD) و بازآرایی (refactoring).

بیشتر بحث‌های این تکنیک‌های Agile روی مقیاس نسبتاً کوچک و محلی متمرکز است (چند فایل کد منبع در همان برنامه). در این کتاب، به‌دنبال راه‌های افزایش چابکی در سطح یک سیستم دادهٔ بزرگ‌تر می‌گردیم، شاید شامل چند برنامه یا سرویس با ویژگی‌های مختلف. برای مثال، چگونه معماری Twitter برای مونتاژ خطوط زمانی خانه («توصیف بار» در صفحه ۱۱) را از رویکرد ۱ به رویکرد ۲ «بازآرایی» می‌کنید؟

سهولت تغییر سیستم داده و تطبیق آن با نیازمندی‌های در حال تغییر، به‌طور نزدیک با سادگی و انتزاع‌هایش مرتبط است: سیستم‌های ساده و قابل فهم معمولاً آسان‌تر از سیستم‌های پیچیده تغییر می‌کنند. اما چون این ایده بسیار مهم است، از واژهٔ دیگری برای اشاره به چابکی در سطح سیستم داده استفاده می‌کنیم: تکاملی‌پذیری (evolvability) [۳۴].

جمع‌بندی

در این فصل، برخی روش‌های بنیادی فکر کردن دربارهٔ برنامه‌های داده‌محور را کاوش کردیم. این اصول ما را در بقیهٔ کتاب هدایت می‌کنند، جایی که به جزئیات فنی عمیق می‌رویم.

یک برنامه برای مفید بودن باید نیازمندی‌های مختلفی را برآورده کند. نیازمندی‌های عملکردی (functional requirements) وجود دارد (چه کاری باید انجام دهد، مانند امکان ذخیره، بازیابی، جستجو و پردازش داده به روش‌های مختلف)، و نیازمندی‌های غیرعملکردی (nonfunctional requirements) (ویژگی‌های کلی مانند امنیت، قابلیت اطمینان، انطباق، مقیاس‌پذیری، سازگاری و قابلیت نگهداری). در این فصل قابلیت اطمینان، مقیاس‌پذیری و قابلیت نگهداری را با جزئیات بررسی کردیم.

قابلیت اطمینان به معنای درست کار کردن سیستم‌ها، حتی وقتی خرابی رخ می‌دهد. خرابی می‌تواند در سخت‌افزار (معمولاً تصادفی و غیرهمبسته)، نرم‌افزار (باگ‌ها معمولاً سیستماتیک و سخت‌برخورد) و انسان (که گاهی اشتباه می‌کنند) باشد. تکنیک‌های تحمل خرابی می‌توانند انواع خاصی از خرابی را از کاربر نهایی پنهان کنند.

مقیاس‌پذیری به معنای داشتن استراتژی برای حفظ عملکرد خوب، حتی با افزایش بار. برای بحث مقیاس‌پذیری، ابتدا به روش‌های توصیف کمی بار و عملکرد نیاز داریم. به‌اختصار خطوط زمانی خانهٔ Twitter را به‌عنوان مثال توصیف بار و صدک‌های زمان پاسخ را به‌عنوان روش اندازه‌گیری عملکرد دیدیم. در سیستم مقیاس‌پذیر، می‌توانید ظرفیت پردازش اضافه کنید تا تحت بار بالا قابل‌اعتماد بمانید.

قابلیت نگهداری جنبه‌های زیادی دارد، اما در ماهیت آن دربارهٔ بهتر کردن زندگی تیم‌های مهندسی و عملیاتی است که باید با سیستم کار کنند. انتزاع‌های خوب می‌توانند پیچیدگی را کاهش دهند و سیستم را برای تغییر و تطبیق با کاربردهای جدید آسان‌تر کنند. قابلیت بهره‌برداری خوب به معنای دید خوب به سلامت سیستم و روش‌های مؤثر مدیریت آن است.

متأسفانه راه‌حل آسان برای قابل‌اعتماد، مقیاس‌پذیر یا قابل‌نگهداری کردن برنامه‌ها وجود ندارد. با این حال، الگوها و تکنیک‌هایی هستند که در انواع مختلف برنامه‌ها بارها ظاهر می‌شوند. در چند فصل بعد، نمونه‌هایی از سیستم‌های داده را می‌بینیم و تحلیل می‌کنیم چگونه به سمت این اهداف کار می‌کنند.

بعدتر در کتاب، در بخش III، الگوهایی برای سیستم‌هایی که از چند مؤلفهٔ همکاری‌کننده تشکیل شده‌اند — مانند شکل ۱-۱ — را بررسی می‌کنیم.

منابع

[۱] Michael Stonebraker and Uğur Çetintemel: "'One Size Fits All': An Idea Whose Time Has Come and Gone," at 21st International Conference on Data Engineering (ICDE), April 2005.

[۲] Walter L. Heimerdinger and Charles B. Weinstock: "A Conceptual Framework for System Fault Tolerance," Technical Report CMU/SEI-92-TR-033, Software Engineering Institute, Carnegie Mellon University, October 1992.

[۳] Ding Yuan, Yu Luo, Xin Zhuang, et al.: "Simple Testing Can Prevent Most Critical Failures: An Analysis of Production Failures in Distributed Data-Intensive Systems," at 11th USENIX Symposium on Operating Systems Design and Implementation (OSDI), October 2014.

[۴] Yury Izrailevsky and Ariel Tseitlin: "The Netflix Simian Army," techblog.netflix.com, July 19, 2011.

[۵] Daniel Ford, François Labelle, Florentina I. Popovici, et al.: "Availability in Globally Distributed Storage Systems," at 9th USENIX Symposium on Operating Systems Design and Implementation (OSDI), October 2010.

[۶] Brian Beach: "Hard Drive Reliability Update – Sep 2014," backblaze.com, September 23, 2014.

[۷] Laurie Voss: "AWS: The Good, the Bad and the Ugly," blog.awe.sm, December 18, 2012.

[۸] Haryadi S. Gunawi, Mingzhe Hao, Tanakorn Leesatapornwongsa, et al.: "What Bugs Live in the Cloud?," at 5th ACM Symposium on Cloud Computing (SoCC), November 2014. doi:10.1145/2670979.2670986

[۹] Nelson Minar: "Leap Second Crashes Half the Internet," somebits.com, July 3, 2012.

[۱۰] Amazon Web Services: "Summary of the Amazon EC2 and Amazon RDS Service Disruption in the US East Region," aws.amazon.com, April 29, 2011.

[۱۱] Richard I. Cook: "How Complex Systems Fail," Cognitive Technologies Laboratory, April 2000.

[۱۲] Jay Kreps: "Getting Real About Distributed System Reliability," blog.empathybox.com, March 19, 2012.

[۱۳] David Oppenheimer, Archana Ganapathi, and David A. Patterson: "Why Do Internet Services Fail, and What Can Be Done About It?," at 4th USENIX Symposium on Internet Technologies and Systems (USITS), March 2003.

[۱۴] Nathan Marz: "Principles of Software Engineering, Part 1," nathanmarz.com, April 2, 2013.

[۱۵] Michael Jurewitz: "The Human Impact of Bugs," jury.me, March 15, 2013.

[۱۶] Raffi Krikorian: "Timelines at Scale," at QCon San Francisco, November 2012.

[۱۷] Martin Fowler: Patterns of Enterprise Application Architecture. Addison Wesley, 2002. ISBN: 978-0-321-12742-6

[۱۸] Kelly Sommers: "After all that run around, what caused 500ms disk latency even when we replaced physical server?" twitter.com, November 13, 2014.

[۱۹] Giuseppe DeCandia, Deniz Hastorun, Madan Jampani, et al.: "Dynamo: Amazon's Highly Available Key-Value Store," at 21st ACM Symposium on Operating Systems Principles (SOSP), October 2007.

[۲۰] Greg Linden: "Make Data Useful," slides from presentation at Stanford University Data Mining class (CS345), December 2006.

[۲۱] Tammy Everts: "The Real Cost of Slow Time vs Downtime," webperformancetoday.com, November 12, 2014.

[۲۲] Jake Brutlag: "Speed Matters for Google Web Search," googleresearch.blogspot.co.uk, June 22, 2009.

[۲۳] Tyler Treat: "Everything You Know About Latency Is Wrong," bravenewgeek.com, December 12, 2015.

[۲۴] Jeffrey Dean and Luiz André Barroso: "The Tail at Scale," Communications of the ACM, volume 56, number 2, pages 74–80, February 2013. doi:10.1145/2408776.2408794

[۲۵] Graham Cormode, Vladislav Shkapenyuk, Divesh Srivastava, and Bojian Xu: "Forward Decay: A Practical Time Decay Model for Streaming Systems," at 25th IEEE International Conference on Data Engineering (ICDE), March 2009.

[۲۶] Ted Dunning and Otmar Ertl: "Computing Extremely Accurate Quantiles Using t-Digests," github.com, March 2014.

[۲۷] Gil Tene: "HdrHistogram," hdrhistogram.org.

[۲۸] Baron Schwartz: "Why Percentiles Don't Work the Way You Think," vividcortex.com, December 7, 2015.

[۲۹] James Hamilton: "On Designing and Deploying Internet-Scale Services," at 21st Large Installation System Administration Conference (LISA), November 2007.

[۳۰] Brian Foote and Joseph Yoder: "Big Ball of Mud," at 4th Conference on Pattern Languages of Programs (PLoP), September 1997.

[۳۱] Frederick P Brooks: "No Silver Bullet – Essence and Accident in Software Engineering," in The Mythical Man-Month, Anniversary edition, Addison-Wesley, 1995. ISBN: 978-0-201-83595-3

[۳۲] Ben Moseley and Peter Marks: "Out of the Tar Pit," at BCS Software Practice Advancement (SPA), 2006.

[۳۳] Rich Hickey: "Simple Made Easy," at Strange Loop, September 2011.

[۳۴] Hongyu Pei Breivold, Ivica Crnkovic, and Peter J. Eriksson: "Analyzing Software Evolvability," at 32nd Annual IEEE International Computer Software and Applications Conference (COMPSAC), July 2008. doi:10.1109/COMPSAC.2008.50


i. در «رویکردها برای برخورد با بار» در صفحه ۱۷ تعریف شده است.

ii. اصطلاحی وام‌گرفته از مهندسی الکترونیک، جایی که تعداد ورودی‌های گیت منطقی متصل به خروجی گیت دیگر را توصیف می‌کند. خروجی باید جریان کافی برای راه‌اندازی همهٔ ورودی‌های متصل تأمین کند. در سیستم‌های پردازش تراکنش، برای توصیف تعداد درخواست‌هایی که برای سرویس‌دهی به یک درخواست ورودی باید به سرویس‌های دیگر بفرستیم به کار می‌بریم.

iii. در دنیای ایده‌آل، زمان اجرای یک job دسته‌ای اندازهٔ مجموعه‌داده تقسیم بر توان عبوری است. در عمل، زمان اجرا اغلب بیشتر است، به‌دلیل skew (داده به‌طور یکنواخت بین فرایندهای worker پخش نشده) و نیاز به انتظار برای تکمیل کندترین وظیفه.