حالت تاریک
فصل ۳ — میکروسرویسها در مقیاس
وقتی با مثالهای کوچک و کتابانداز سر و کار دارید، همهچیز ساده به نظر میرسد. اما دنیای واقعی فضای پیچیدهتری است. وقتی معماریهای میکروسرویس ما از آغازهای سادهتر و فروتنتر به چیزی پیچیدهتر رشد میکنند چه میشود؟ وقتی باید failure چند سرویس جداگانه را handle کنیم یا صدها سرویس را مدیریت کنیم چه؟ الگوهای مقابلهای وقتی میکروسرویسهایتان بیشتر از تعداد افرادتان است چیست؟ بیایید بفهمیم.
Failure همهجا هست
میدانیم چیزها میتوانند اشتباه پیش بروند. دیسکهای سخت fail میکنند. نرمافزارمان crash میکند. و همانطور که هر کسی که fallacies of distributed computing را خوانده میداند، شبکه unreliable است. میتوانیم تا حدی تلاش کنیم علل failure را محدود کنیم، اما در مقیاس خاصی failure اجتنابناپذیر میشود. دیسکهای سخت مثلاً اکنون قابلاعتمادتر از همیشهاند، اما در نهایت خراب میشوند. هرچه دیسکهای سخت بیشتر داشته باشید، احتمال failure برای هر واحد بالاتر است؛ failure در مقیاس به یک قطعیت آماری تبدیل میشود.
حتی برای کسانی که در مقیاس extreme فکر نمیکنند، اگر بتوانیم امکان failure را بپذیریم بهتر خواهیم بود. مثلاً اگر بتوانیم failure یک سرویس را gracefully handle کنیم، میتوانیم upgrade in-place سرویس را هم انجام دهیم، چون outage برنامهریزیشده خیلی آسانتر از برنامهریزینشده است.
همچنین میتوانیم کمی کمتر وقت صرف جلوگیری از اجتنابناپذیر کنیم و کمی بیشتر به gracefully handle کردن آن بپردازیم. از تعداد سازمانهایی که فرآیندها و کنترلها میگذارند تا failure رخ ندهد شگفتزدهام، اما کم یا اصلاً به آسانتر کردن recovery از failure فکر نمیکنند.
فرض کردن اینکه همهچیز میتواند و خواهد fail شد، شما را متفاوت دربارهٔ حل مسائل فکر میکند.
چند سال پیش در campus گوگل نمونهای از این تفکر دیدم. در reception یکی از ساختمانهای Mountain View رک قدیمی ماشینها بود، بهعنوان نمایشگاه. چند نکته دیدم. اول، این سرورها در enclosure سرور نبودند؛ فقط motherboardهای برهنه در رک بودند. نکتهٔ اصلی این بود که دیسکهای سخت با velcro وصل شده بودند. از یکی از Googlerها پرسیدم چرا. گفت: «دیسکهای سخت آنقدر fail میکنند که نمیخواهیم پیچ شوند. فقط درمیآوریم، دور میاندازیم و با velcro یکی جدید میگذاریم.»
پس تکرار کنم: در مقیاس، حتی اگر بهترین تجهیزات و گرانترین سختافزار را بخرید، نمیتوانید از این واقعیت که چیزها میتوانند و خواهند fail شد اجتناب کنید. پس باید فرض کنید failure ممکن است. اگر این تفکر را در همهٔ کارها بگنجانید و برای failure برنامهریزی کنید، trade-offهای متفاوتی میتوانید بگیرید. اگر بدانید سیستمتان میتواند failure یک سرور را handle کند، چرا زیاد روی آن هزینه کنید؟ چرا مثل گوگل از motherboard برهنه با قطعات ارزانتر (و کمی velcro) استفاده نکنید، بهجای نگرانی زیاد دربارهٔ resiliency یک node؟
چقدر زیاد، زیاد است؟
در (ارجاع آینده) به cross-functional requirements اشاره کردیم. درک cross-functional requirements یعنی در نظر گرفتن جنبههایی مثل durability داده، availability سرویسها، throughput و latency قابلقبول سرویسها. بسیاری از تکنیکهای این فصل و جاهای دیگر دربارهٔ پیادهسازی این نیازهاست، اما فقط شما دقیقاً میدانید نیازها چیست.
سیستم autoscaling که به افزایش بار یا failure nodeهای فردی واکنش نشان دهد ممکن است عالی باشد، اما برای سیستم گزارشدهی که ماهی دو بار اجرا میشود و down بودن یک یا دو روز مهم نیست، overkill باشد. همینطور blue/green deployment برای حذف downtime سرویس برای ecommerce آنلاین معنا دارد، اما برای intranet پایگاه دانش شرکت احتمالاً یک قدم زیاد است.
دانستن چقدر failure را تحمل میکنید یا سیستم چقدر باید سریع باشد، توسط کاربران سیستم تعیین میشود. این به شما کمک میکند بفهمید کدام تکنیکها برایتان معنا دارند. با این حال، کاربران همیشه نمیتوانند نیازهای دقیق را بیان کنند. پس باید سؤال بپرسید تا اطلاعات درست استخراج شود و به آنها هزینهٔ نسبی سطوح مختلف سرویس را بفهمانید.
همانطور که گفتم، این cross-functional requirements میتوانند سرویس به سرویس متفاوت باشند، اما پیشنهاد میکنم چند مورد عمومی تعریف کنید و برای use caseهای خاص override کنید. وقتی به scale out سیستم برای handle بهتر بار یا failure فکر میکنید، ابتدا سعی کنید این نیازها را بفهمید:
Response time/latency
عملیات مختلف چقدر باید طول بکشند؟ مفید است با تعداد کاربران مختلف اندازه بگیرید تا بفهمید افزایش بار چگونه response time را تحت تأثیر قرار میدهد. با توجه به ماهیت شبکهها همیشه outlier دارید؛ پس تعیین target برای percentile مشخصی از پاسخهای monitorشده مفید است. target باید تعداد connection/user همزمانی که نرمافزار handle میکند را هم شامل شود. مثلاً بگویید: «انتظار داریم وبسایت در ۹۰th-percentile زمان پاسخ ۲ ثانیه با ۲۰۰ connection همزمان در ثانیه داشته باشد.»
Availability
آیا میتوان انتظار داشت سرویس down باشد؟ آیا سرویس ۲۴/۷ است؟ برخی دورههای downtime قابلقبول را برای اندازهگیری availability نگاه میکنند، اما برای کسی که سرویس شما را صدا میزند چقدر مفید است؟ باید بتوانم به پاسخ سرویستان اعتماد کنم یا نه. اندازهگیری دورههای downtime بیشتر برای گزارش تاریخی مفید است.
Durability of data
چقدر از دست رفتن داده قابلقبول است؟ داده چقدر باید نگه داشته شود؟ این احتمالاً case به case متفاوت است. مثلاً logهای session کاربر را یک سال یا کمتر نگه دارید تا فضا صرفهجویی شود، اما رکوردهای تراکنش مالی ممکن است سالها لازم باشد.
وقتی این نیازها را دارید، راهی برای اندازهگیری سیستماتیک مداوم میخواهید. ممکن است performance test استفاده کنید تا سیستم به targetهای قابلقبول برسد، اما در production هم این آمار را monitor کنید!
Degrading Functionality
بخش ضروری ساخت سیستم resilient، بهویژه وقتی functionality روی چند میکروسرویس مختلف پخش است که ممکن است up یا down باشند، توانایی safely degrade کردن functionality است. صفحهٔ وب استاندارد سایت ecommerce را تصور کنید. برای جمع کردن بخشهای مختلف وبسایت، چند میکروسرویس لازم است. یکی جزئیات آلبوم فروشی را نشان دهد. دیگری قیمت و موجودی. احتمالاً محتوای سبد خرید را هم نشان میدهیم که میکروسرویس دیگری است.
اگر یکی از این سرویسها down باشد و کل صفحهٔ وب unavailable شود، سیستمی ساختهایم که کمتر resilient از سیستمی است که فقط یک سرویس لازم دارد.
باید impact هر outage را بفهمیم و نحوهٔ degrade درست functionality را مشخص کنیم. اگر سرویس سبد خرید unavailable باشد، احتمالاً دردسر داریم، اما میتوانیم صفحهٔ لیست را نشان دهیم. شاید سبد خرید را پنهان کنیم یا با آیکون «به زودی برمیگردیم!» جایگزین کنیم.
در یک برنامهٔ monolithic واحد، انتخاب زیادی نداریم. سلامت سیستم binary است. اما در معماری میکروسرویس باید وضعیت nuancedتر را در نظر بگیریم. کار درست در هر موقعیت اغلب تصمیم فنی نیست. ممکن است بدانیم وقتی سبد خرید down است چه فنی ممکن است، اما بدون context کسبوکار نمیفهمیم چه اقدامی بگیریم. مثلاً شاید کل سایت را ببندیم، اجازهٔ مرور کاتالوگ بدهیم، یا بخش UI سبد را با شمارهٔ تلفن سفارش جایگزین کنیم. اما برای هر interface مشتریمحور که چند میکروسرویس استفاده میکند، یا هر میکروسرویسی که به چند collaborator downstream وابسته است، باید بپرسید «اگر این down باشد چه میشود؟» و بدانید چه کنید.
با فکر کردن به criticality هر capability از نظر cross-functional requirements، برای دانستن چه میتوانیم انجام دهیم بهتر قرار میگیریم. حالا از نظر فنی چیزهایی را در نظر بگیریم که وقتی failure رخ میدهد بتوانیم gracefully handle کنیم.
Architectural Safety Measures
چند الگو وجود دارد که جمعاً architectural safety measures میناممشان و میتوانیم استفاده کنیم تا اگر چیزی اشتباه پیش رفت، ripple-out effects ناخوشایند ایجاد نشود. درک این نکات ضروری است و باید در سیستم استاندارد کنید تا یک bad citizen کل دنیا را فرو نریزد. به زودی چند safety measure کلیدی را میبینیم، اما قبلش داستانی کوتاه از چیزی که میتواند اشتباه پیش برود.
technical lead پروژهٔ سایت آگهیهای آنلاین بودم. وبسایت حجم بالایی داشت و درآمد خوبی برای کسبوکار تولید میکرد. برنامهٔ هسته نمایش آگهیها را handle میکرد و callهای سرویسهای دیگر را proxy میکرد، مانند شکل ۳-۱. این مثال strangler application است که سیستم جدید callهای legacy را intercept میکند و تدریجاً جایگزین میکند. بخشی از پروژه در حال retire کردن برنامههای قدیمیتر بودیم. تازه محصول با بیشترین حجم و درآمد را منتقل کرده بودیم، اما بخش زیادی از آگهیها هنوز با چند برنامهٔ قدیمیتر serve میشد. از نظر تعداد جستجو و درآمد، tail بسیار بلندی بود.
شکل ۳-۱. وبسایت آگهیهای طبقهبندیشده که برنامههای legacy قدیمیتر را strangle میکند
سیستم مدتی live بود و خوب رفتار میکرد و بار قابلتوجهی handle میکرد. آن زمان حدود ۶۰۰۰–۷۰۰۰ request در ثانیه در peak داشتیم. بیشترشان توسط reverse proxyهای جلوی application serverها بهشدت cache شده بود، اما جستجوهای محصول (مهمترین بخش سایت) عمدتاً uncached بودند و round-trip کامل سرور میخواست.
یک صبح، کمی قبل از peak ناهار روزانه، سیستم کند شد و تدریجاً fail کرد. monitoring روی برنامهٔ هستهٔ جدید داشتیم، کافی بود بگوید هر node به spike ۱۰۰٪ CPU رسیده، بالاتر از سطح عادی حتی در peak. در مدت کوتاه کل سایت down شد.
مقصر را پیدا کردیم و سایت را برگرداندیم. یکی از سیستمهای آگهی downstream، از قدیمیترین و کمنگهداریترینها، خیلی کند پاسخ میداد. پاسخ خیلی کند یکی از بدترین failure modeهاست. اگر سیستم اصلاً نباشد، سریع میفهمید. وقتی فقط کند است، مدتی منتظر میمانید تا تسلیم شوید. هر علت failure، سیستمی ساخته بودیم که به cascading failure آسیبپذیر بود. سرویس downstream که کنترل کمی روی آن داشتیم توانست کل سیستم را down کند.
یک تیم مشکل downstream را بررسی کرد و بقیهٔ ما به مشکل برنامه نگاه کردیم. چند مشکل پیدا کردیم. از HTTP connection pool برای connectionهای downstream استفاده میکردیم. threadهای pool برای مدت انتظار در call HTTP downstream timeout داشتند که خوب است. مشکل این بود که workerها بهخاطر سیستم downstream کند مدتی برای timeout طول میکشید. در انتظار بودند، requestهای بیشتری به pool میآمدند و worker میخواستند. بدون worker در دسترس، این requestها هم hang میکردند. کتابخانهٔ connection pool timeout برای انتظار worker داشت، اما بهطور پیشفرض غیرفعال بود! این باعث build-up عظیم threadهای blocked شد. برنامه معمولاً ۴۰ connection همزمان داشت. در پنج دقیقه به حدود ۸۰۰ connection رسید و سیستم را down کرد.
بدتر اینکه سرویس downstream کمتر از ۵٪ مشتریان را پوشش میداد و درآمد کمتری از آن تولید میکرد. فهمیدیم سیستمهایی که فقط کند عمل میکنند خیلی سختتر از fail fast هستند. در سیستم توزیعشده، latency میکشد.
حتی با timeout درست pool، یک HTTP connection pool واحد برای همهٔ requestهای outbound داشتیم. یعنی یک سرویس کند میتواند همهٔ workerهای در دسترس را exhaust کند حتی اگر بقیه سالم باشند. همچنین سرویس downstream سالم نبود اما همچنان traffic میفرستادیم و وضعیت بد را بدتر میکردیم و فرصت recovery نداشت. سه fix پیاده کردیم: timeout درست، bulkhead برای جدا کردن connection poolها، و circuit breaker برای ارسال نکردن call به سیستم ناسالم.
The Antifragile Organization
در کتاب Antifragile (Random House)، Nassim Taleb از چیزهایی میگوید که از failure و disorder سود میبرند. Ariel Tseitlin این مفهوم را برای نحوهٔ عملکرد Netflix به antifragile organization تعبیر کرد.
مقیاس Netflix معروف است و Netflix کاملاً روی infrastructure AWS است. این دو یعنی باید failure را خوب بپذیرد. Netflix فراتر میرود و failure را عمداً ایجاد میکند تا سیستمها tolerant باشند.
برخی سازمانها با game dayها راضیاند که failure با خاموش کردن سیستمها simulate میشود و تیمها واکنش نشان میدهند. در گوگل این برای سیستمهای مختلف رایج بود و بسیاری از سازمانها از تمرینهای منظم سود میبرند. گوگل فراتر از تست سادهٔ failure سرور میرود و در DiRT (Disaster Recovery Test) سالانه بلایای بزرگ مثل زلزله را simulate میکند. Netflix تهاجمیتر است و برنامههایی مینویسد که failure ایجاد میکنند و روزانه در production اجرا میشوند.
معروفترینشان Chaos Monkey است که در ساعات مشخصی ماشینهای تصادفی را خاموش میکند. دانستن اینکه در production میتواند و خواهد اتفاق افتاد یعنی توسعهدهندگان واقعاً آماده باشند. Chaos Monkey بخشی از Simian Army Netflix است. Chaos Gorilla کل availability center (معادل data center در AWS) را از کار میاندازد و Latency Monkey connectivity کند شبکه بین ماشینها را simulate میکند. Netflix این ابزارها را تحت open source license منتشر کرده. برای بسیاری، تست نهایی robust بودن سیستم ممکن است رها کردن Simian Army خودتان روی production infrastructure باشد.
پذیرش و ایجاد failure از طریق نرمافزار و ساخت سیستمهایی که handle کنند فقط بخشی از کار Netflix است. اهمیت یادگیری از failure و فرهنگ blameless وقتی اشتباه رخ میدهد را هم میفهمند. توسعهدهندگان برای یادگیری و تکامل توانمندند؛ هر توسعهدهنده مسئول مدیریت سرویسهای production خودش است.
با ایجاد failure و ساخت برای آن، Netflix سیستمهایی دارد که بهتر scale میکنند و نیاز مشتریان را بهتر پشتیبانی میکنند.
همه به extreme گوگل یا Netflix نیاز ندارند، اما mindset shift لازم با سیستمهای توزیعشده مهم است. چیزها fail میشوند. پخش شدن روی چند ماشین (که fail میشوند) در شبکه (که unreliable است) میتواند سیستم را آسیبپذیرتر کند نه کمتر. پس چه در مقیاس گوگل یا Netflix باشید چه نه، آمادهسازی برای failureهای معماریهای توزیعشده مهم است. برای handle failure در سیستمها چه باید کرد؟
Timeouts
timeoutها چیزی است که راحت نادیده گرفته میشود، اما در سیستم downstream مهم است درست تنظیم شوند. چقدر میتوانم صبر کنم قبل از اینکه سیستم downstream را down بدانم؟
خیلی دیر تصمیم بگیرید call fail شده، کل سیستم را کند میکنید. خیلی زود timeout کنید، callای که شاید کار میکرد را failed میدانید. بدون timeout، downstream down میتواند کل سیستم را hang کند.
روی همهٔ callهای out-of-process timeout بگذارید و default timeout برای همه انتخاب کنید. وقتی timeout رخ داد log کنید، ببینید چه میشود و متناسب تغییر دهید.
Circuit Breakers
در خانه، circuit breakerها از دستگاههای برقی در برابر spike برق محافظت میکنند. اگر spike باشد، circuit breaker میپرد و لوازم گران محافظت میشوند. میتوانید دستی circuit breaker را disable کنید تا برق بخشی از خانه قطع شود و ایمن روی برق کار کنید. کتاب Release It! (Pragmatic Programmers) اثر Michael Nygard نشان میدهد همین ایده برای نرمافزار چه معجزهای میکند.
داستانی که گفتم را در نظر بگیرید. برنامهٔ legacy آگهی خیلی کند پاسخ میداد و بعد error برمیگرداند. حتی با timeout درست، مدت زیادی قبل از error صبر میکردیم و دفعهٔ بعد دوباره امتحان میکردیم. بدتر از malfunctioning downstream این است که ما را هم کند میکند.
با circuit breaker، بعد از تعداد مشخصی request ناموفق به منبع downstream، circuit breaker میپرد. همهٔ requestهای بعدی fail fast میکنند تا circuit breaker در حالت blown باشد. بعد از مدتی، client چند request میفرستد تا ببیند downstream recover شده یا نه؛ اگر پاسخهای سالم کافی گرفت، circuit breaker reset میشود. نمای کلی در شکل ۳-۲.
شکل ۳-۲. نمای کلی circuit breakerها
پیادهسازی circuit breaker به معنای failed request بستگی دارد؛ برای HTTP معمولاً timeout یا کد 5XX است. وقتی منبع downstream down، timeout یا error میدهد، بعد از threshold بهطور خودکار traffic را قطع و fail fast میکنیم. وقتی سالم شد دوباره خودکار شروع میکنیم.
تنظیم درست کمی tricky است. نه خیلی زود بپرد، نه خیلی دیر. همچنین مطمئن شوید downstream دوباره سالم است. مثل timeout، defaultهای منطقی انتخاب کنید و برای موارد خاص تغییر دهید.
وقتی circuit breaker blown است، گزینه دارید. یکی queue کردن requestها و retry بعداً است. برای برخی use caseها، بهویژه کار asynchronous، مناسب است. اگر در call chain همزمان است، بهتر fail fast کنید — error را propagate کنید یا functionality را subtly degrade کنید.
با این مکانیزم (مثل circuit breaker خانه) میتوانیم دستی برای ایمنتر کردن کار استفاده کنیم. مثلاً برای maintenance میکروسرویس، circuit breakerهای سیستمهای وابسته را دستی blow کنیم تا fail fast کنند. وقتی برگشت، reset کنیم و همه عادی شود.
Bulkheads
در الگوی دیگر Release It!، Nygard مفهوم bulkhead را برای ایزوله شدن از failure معرفی میکند. در کشتی، bulkhead بخشی از کشتی است که میتوان بست تا بقیه محافظت شود. اگر نشتی باشد، درهای bulkhead را میبندید. بخشی از کشتی را از دست میدهید اما بقیه سالم میماند.
در معماری نرمافزار bulkheadهای مختلفی داریم. در تجربهٔ خودمان فرصت bulkhead را از دست دادیم. باید connection pool جدا برای هر connection downstream داشتیم. اگر یک pool exhaust شود، بقیه تأثیر نمیگیرند، مانند شکل ۳-۳. اگر سرویس downstream کند شود، فقط همان pool تأثیر میگیرد و callهای دیگر عادی ادامه مییابند.
شکل ۳-۳. استفاده از connection pool به ازای هر سرویس downstream برای bulkhead
separation of concerns هم bulkhead است. با جدا کردن functionality به میکروسرویسهای جدا، احتمال outage یک بخش روی دیگری کمتر میشود.
همهٔ جنبههایی که میتوانند اشتباه شوند را ببینید، درون و بین میکروسرویسها. bulkhead دارید؟ حداقل connection pool جدا برای هر downstream پیشنهاد میکنم. شاید circuit breaker هم. circuit breaker را مکانیزم خودکار بستن bulkhead میبینیم — هم consumer را از مشکل downstream محافظت میکند هم شاید downstream را از callهای بیشتر. با خطر cascading failure، circuit breaker برای همهٔ callهای همزمان downstream را اجباری پیشنهاد میکنم. لازم نیست خودتان بنویسید. Hystrix کتابخانهٔ JVM circuit breaker با monitoring قوی است؛ پیادهسازیهای دیگر برای stackهای مختلف مثل Polly برای .NET یا circuit_breaker mixin برای Ruby وجود دارد.
بهطرق مختلف bulkhead مهمترین این سه الگوست. timeout و circuit breaker وقتی منابع constrained میشوند آزاد میکنند، اما bulkhead میتواند از constrained شدن جلوگیری کند. Hystrix bulkheadهایی دارد که در شرایطی request را reject میکنند تا منابع بیشتر saturate نشوند؛ load shedding. گاهی reject کردن request بهترین راه جلوگیری از overwhelmed شدن سیستم مهم و bottleneck شدن برای چند سرویس upstream است.
Isolation
هرچه یک سرویس به up بودن دیگری وابستهتر باشد، سلامت یکی بر توان دیگری برای کار تأثیر بیشتری دارد. اگر integration techniques اجازه دهند سرور downstream offline باشد، سرویسهای upstream کمتر از outage برنامهریزیشده یا نشده تأثیر میگیرند.
ایزولاسیون بیشتر بین سرویسها فایدهٔ دیگر دارد. وقتی سرویسها از هم ایزولهاند، coordination کمتری بین مالکان سرویس لازم است. coordination کمتر یعنی autonomy بیشتر تیمها برای عمل و تکامل آزادانهتر سرویسها.
Idempotency
در عملیات idempotent، نتیجه بعد از اولین اعمال تغییر نمیکند حتی اگر عملیات چند بار اعمال شود. اگر idempotent باشند، میتوانیم call را چند بار تکرار کنیم بدون اثر نامطلوب. برای replay پیامهایی که مطمئن نیستیم پردازش شدهاند، راه رایج recovery از error است.
call ساده برای افزودن امتیاز بهخاطر سفارش مشتری را در نظر بگیرید. payloadی مانند مثال ۳-۱ ممکن است بفرستیم.
مثال ۳-۱. اعتبار امتیاز به حساب
xml
<credit>
<amount>100</amount>
<forAccount>1234</account>
</credit>اگر این call چند بار دریافت شود، ۱۰۰ امتیاز چند بار اضافه میشود. پس idempotent نیست. با اطلاعات بیشتر، بانک امتیاز میتواند idempotent کند، مانند مثال ۳-۲.
مثال ۳-۲. افزودن اطلاعات برای idempotent کردن اعتبار امتیاز
xml
<credit>
<amount>100</amount>
<forAccount>1234</account>
<reason>
<forPurchase>4567</forPurchase>
</reason>
</credit>حالا میدانیم این اعتبار مربوط به سفارش ۴۵۶۷ است. فرض کنیم فقط یک اعتبار برای هر سفارش میگیریم؛ میتوانیم دوباره اعمال کنیم بدون افزایش کل امتیاز.
این با event-based collaboration هم کار میکند و وقتی چند instance از همان نوع سرویس به eventها subscribe میکنند مفید است. حتی اگر eventهای پردازششده را ذخیره کنیم، در برخی تحویل asynchronous پیام پنجرههای کوچکی هست که دو worker همان پیام را میبینند. پردازش idempotent اطمینان میدهد مشکلی پیش نمیآید.
برخی در این مفهوم گیر میکنند و فکر میکنند یعنی callهای بعدی با همان پارامترها هیچ اثری ندارند. هنوز میخواهیم در log ثبت کنیم call دریافت شد. response time را ثبت و برای monitoring جمع کنیم. نکته این است که عملیات کسبوکار underlying idempotent است، نه کل state سیستم.
برخی HTTP verbها مثل GET و PUT در مشخصات HTTP idempotent تعریف شدهاند، اما سرویس باید این callها را idempotent handle کند. اگر این verbها را nonidempotent کنید اما callerها فکر کنند میتوانند ایمن تکرار کنند، دردسر دارید. یادتان باشد HTTP بهعنوان پروتکل زیرین همهچیز را رایگان نمیدهد!
Scaling
سیستمها را عموماً به یکی از دو دلیل scale میکنیم. اول برای مقابله با failure: اگر نگران failure چیزی هستیم، داشتن بیشتر کمک میکند، درست است؟ دوم برای performance، چه handle بار بیشتر، کاهش latency یا هر دو. تکنیکهای رایج scaling و کاربردشان در معماری میکروسرویس را ببینیم.
Go Bigger
برخی عملیات از قدرت بیشتر سود میبرند. جعبهٔ بزرگتر با CPU سریعتر و I/O بهتر اغلب latency و throughput را بهبود میدهد. اما این vertical scaling میتواند گران باشد — گاهی یک سرور بزرگ از دو سرور کوچکتر با همان قدرت خام گرانتر است، بهویژه در ماشینهای خیلی بزرگ. گاهی نرمافزار از منابع اضافی استفادهٔ زیادی نمیکند. ماشینهای بزرگتر اغلب فقط core CPU بیشتر میدهند و نرمافزار ما برای استفاده نوشته نشده. مشکل دیگر: اگر فقط یکی داریم، resiliency سرور شاید زیاد بهبود نیابد! با این حال برای quick win خوب است، بهویژه با virtualization provider که resize آسان است.
Splitting Workloads
همانطور که در (ارجاع آینده) گفتیم، یک میکروسرویس به ازای هر host ترجیح دارد به مدل چند سرویس روی یک host. ابتدا بسیاری چند میکروسرویس روی یک box میگذارند تا هزینه کم شود یا مدیریت host سادهتر شود (که بحثبرانگیز است). چون میکروسرویسها process مستقل هستند که از شبکه ارتباط میگیرند، جابهجایی به host جدا برای throughput و scaling آسان است. resiliency هم بیشتر میشود چون outage یک host تعداد کمتری میکروسرویس را تحت تأثیر میگذارد.
البته میتوانید برای scale نیاز، میکروسرویس موجود را به بخشها بشکنید. مثال ساده: سرویس accounts ایجاد و مدیریت حساب مالی مشتریان و API query برای گزارش دارد. query بار قابلتوجهی میگذارد. ظرفیت query غیرحیاتی است چون برای جریان سفارش روز لازم نیست. مدیریت رکوردهای مالی مشتریان حیاتی است و نمیتوانیم down باشد. با جدا کردن این دو capability به سرویسهای جدا، بار روی سرویس accounts حیاتی کم میشود و سرویس گزارشدهی accounts جدید برای query طراحی میشود (شاید با تکنیکهای فصل ۲) و بهعنوان سیستم غیرحیاتی نیاز به deploy بهقدر سرویس هستهٔ accounts resilient ندارد.
Spreading Your Risk
یک راه scale برای resilience این است که همهٔ تخممرغها را در یک سبد نگذارید. مثال ساده: چند سرویس روی یک host نباشند که outage چند سرویس را تحت تأثیر بگذارد. host یعنی چه؟ امروز host اغلب مفهوم مجازی است. اگر همهٔ سرویسها روی hostهای مختلف باشند اما همه virtual host روی یک physical box باشند، اگر آن box down شود چند سرویس از دست میرود. برخی platformهای virtualization توزیع hostها روی physical boxهای مختلف را ممکن میکنند.
برای platformهای virtualization داخلی، root partition ماشین مجازی اغلب به یک SAN (storage area network) map میشود. اگر SAN down شود همهٔ VMهای متصل down میشوند. SANها بزرگ، گران و طوری طراحی شدهاند که fail نشوند. با این حال SANهای بزرگ و گران حداقل دو بار در ۱۰ سال برایم fail کردند و هر بار جدی بود.
جداسازی رایج دیگر: همهٔ سرویسها در یک rack نباشند یا در بیش از یک data center پخش شوند. با service provider باید بدانید SLA هست یا نه و برنامهریزی کنید. اگر سرویسها بیش از چهار ساعت در هر فصل down نباشند اما hosting provider فقط هشت ساعت تضمین کند، SLA را عوض کنید یا راهحل جایگزین.
AWS به regionها تقسیم شده که cloudهای متمایزند. هر region به دو یا بیشتر availability zone (AZ) تقسیم میشود. AZ معادل data center در AWS است. توزیع سرویسها روی چند AZ ضروری است چون AWS تضمینی برای availability یک node یا کل AZ نمیدهد. برای compute فقط ۹۹.۹۵٪ uptime ماهانه region بهعنوان کل میدهد؛ پس workload را روی چند AZ در یک region پخش کنید. برخی روی چند region هم اجرا میکنند.
چون provider SLA میدهد، liability را محدود میکند! اگر از دست دادن target برایتان مشتری و پول زیاد هزینه دارد، شاید در قراردادها دنبال جبران بگردید. impact شکست supplier را بفهمید و plan B (یا C) داشته باشید. بیش از یک مشتری platform disaster recovery با supplier دیگر داشتند.
Load Balancing
برای resilient بودن سرویس، از single point of failure اجتناب کنید. برای میکروسرویس معمولی با endpoint همزمان HTTP، چند host با instance میکروسرویس پشت load balancer سادهترین راه است، مانند شکل ۳-۴. برای consumer نمیدانید با یک instance صحبت میکنید یا صد تا.
شکل ۳-۴. مثال load balancing برای scale تعداد instanceهای سرویس مشتری
load balancerها اشکال و اندازههای مختلف دارند، از appliance سختافزاری بزرگ و گران تا نرمافزاری مثل mod_proxy. قابلیتهای کلیدی مشترک: توزیع callها به instanceها بر اساس الگوریتم، حذف instance ناسالم، و امیدواریم برگرداندن وقتی سالم شد.
برخی ویژگی مفید دارند. SSL termination رایج است: HTTPS ورودی به load balancer به HTTP روی instance تبدیل میشود. سابقاً overhead SSL قابلتوجه بود؛ امروز بیشتر برای سادهسازی setup hostهاست. نکتهٔ HTTPS جلوگیری از man-in-the-middle است (در ارجاع آینده)؛ با SSL termination کمی آسیبپذیر میشویم. mitigation: همهٔ instanceها در یک VLAN، مانند شکل ۳-۵. VLAN شبکهٔ محلی مجازی ایزوله است که از بیرون فقط از router بیاید؛ اینجا router همان load balancer با SSL termination است. ارتباط بیرون VLAN فقط HTTPS؛ داخل همه HTTP.
شکل ۳-۵. HTTPS termination در load balancer با VLAN برای امنیت بهتر
AWS ELB (elastic load balancer) با HTTPS termination دارد و security group یا VPC برای VLAN. mod_proxy نقش مشابه نرمافزاری دارد. بسیاری hardware load balancer دارند که automate سخت است؛ گاهی load balancer نرمافزاری پشت hardware برای آزادی تیمها پیشنهاد میکنم. مراقب باشید hardware load balancerها خودشان single point of failure باشند!
هر رویکردی، configuration load balancer را مثل configuration سرویس در version control نگه دارید و خودکار اعمال کنید.
load balancerها اضافه کردن instance بدون آگاهی consumer را ممکن میکنند. handle بار بیشتر و کاهش impact failure یک host. اما بسیاری از میکروسرویسها persistent data store دارند، احتمالاً database روی ماشین دیگر. چند instance روی ماشینهای مختلف اما یک host برای database یعنی database هنوز single source of failure است. الگوها را به زودی میبینیم.
Worker-Based Systems
load balancing تنها راه share بار و کاهش fragility با چند instance نیست. بسته به ماهیت عملیات، سیستم worker-based مؤثر است. مجموعه instanceها روی backlog مشترک کار میکنند — چند process Hadoop یا listener صف مشترک. برای batch یا jobهای asynchronous مناسب است: thumbnail تصویر، ایمیل، گزارش.
برای بار peaky هم خوب است: instance اضافی on demand. اگر صف کار resilient باشد، برای throughput و resiliency scale میشود — impact failure worker آسان است؛ کار طولانیتر میشود اما چیزی گم نمیشود.
در سازمانهایی با ظرفیت compute بلااستفاده در بخشی از روز دیدهام. شب که برای ecommerce ماشین کمتر لازم است، موقتاً برای reporting worker استفاده کنید.
در سیستمهای worker-based workerها لازم نیست خیلی reliable باشند، اما سیستم نگهداری کار باید reliable باشد. message broker persistent یا Zookeeper. مزیت: نرمافزار موجود؛ اما باید resilient راهاندازی و نگهداری کنید.
Starting Again
معماری شروع ممکن است معماری ادامه وقتی سیستم باید حجم بار بسیار متفاوت handle کند نباشد. Jeff Dean در «Challenges in Building Large-Scale Information Retrieval Systems» (WSDM 2009) گفت: «برای رشد ~۱۰× طراحی کنید، اما قبل از ~۱۰۰× برنامهریزی rewrite کنید.» در نقاطی باید کاری radical برای سطح بعدی رشد انجام دهید.
داستان Gilt در (ارجاع آینده): Rails monolithic ساده دو سال خوب بود. موفقیت بیشتر یعنی مشتری و بار بیشتر. در tipping point باید redesign کرد.
redesign ممکن است شکستن monolith باشد مثل Gilt. یا data store جدید. یا تکنیک جدید: از request/response همزمان به event-based، platform deployment جدید، تغییر stack یا همه.
خطر این است که نیاز rearchitect در thresholdهای scaling دلیل ساخت برای massive scale از ابتدا شود. فاجعهبار است. در شروع پروژه اغلب دقیق نمیدانیم چه میسازیم یا موفق میشود. باید سریع experiment کنیم و capabilityها را بفهمیم. ساخت برای massive scale از اول یعنی کار زیاد برای باری که شاید نیاید و دور شدن از فهمیدن آیا کسی محصول را میخواهد. Eric Ries شش ماه محصول ساخت که کسی download نکرد. میگفت میتوانست لینک 404 بگذارد و شش ماه ساحل باشد و همان یاد بگیرد!
نیاز تغییر سیستم برای scale نشانهٔ failure نیست. نشانهٔ success است.
Scaling Databases
scale کردن میکروسرویسهای stateless نسبتاً straightforward است. اما اگر در database ذخیره میکنیم؟ باید آن را هم scale کنیم. انواع database اشکال مختلف scaling دارند؛ درک مناسب use case از ابتدا technology درست را انتخاب میکند.
Availability of Service Versus Durability of Data
مهم است availability سرویس را از durability داده جدا کنیم. دو چیز متفاوت با راهحلهای متفاوت.
مثلاً کپی همهٔ دادهٔ نوشتهشده به database در filesystem resilient. اگر database down شود داده گم نشده اما database و شاید میکروسرویس unavailable است. مدل رایجتر standby: داده به replica کپی میشود. primary down یعنی داده امن اما بدون مکانیزم برگرداندن primary یا promote replica، database available نیست هرچند داده امن است.
Scaling for Reads
بسیاری از سرویسها read-mostly هستند. سرویس کاتالوگ اطلاعات کالا. رکورد جدید گاهبهگاه؛ بیش از ۱۰۰ read به ازای هر write عجیب نیست. scaling برای read از write آسانتر است. caching نقش بزرگ دارد. مدل دیگر read replica.
در RDBMS مثل MySQL یا Postgres داده از primary به replica کپی میشود. اغلب برای امنیت کپی؛ میتوانیم read را توزیع کنیم. سرویس همهٔ writeها به primary و readها به replicaها، مانند شکل ۳-۶. replication بعد از write است؛ readها گاهی stale میبینند تا replication تمام شود. eventually consistent است؛ اگر inconsistency موقت را handle کنید راه رایج و آسان scale است. با CAP theorem بیشتر میبینیم.
شکل ۳-۶. استفاده از read replica برای scale readها
سالها پیش read replica برای scale مد بود؛ امروز اول caching را ببینید — بهبود performance بیشتر با کار کمتر.
Scaling for Writes
read نسبتاً آسان scale میشود. write؟ sharding. چند node database. داده را میگیرید، hashing روی key، بر اساس نتیجه میدانید کجا بفرستید. مثال ساده (و بد): رکورد A–M به یک instance و N–Z به دیگری. خودتان در application یا Mongo بخشی را handle میکند.
پیچیدگی sharding برای write در query است. lookup رکورد تکی آسان — hash و retrieve از shard درست. query روی چند node؟ مثلاً همهٔ مشتریان بالای ۱۸ سال. یا هر shard را query و join در memory، یا read store جایگزین. اغلب query بین shardها asynchronous با نتایج cacheشده. Mongo map/reduce.
سؤال: اگر node اضافه کنیم چه؟ سابقاً downtime زیاد برای rebalance. اخیراً سیستمهایی مثل Cassandra rebalance در پسزمینه. اضافه کردن shard به cluster زنده برای ضعیفدلان نیست — test کامل.
sharding برای write شاید volume write را scale کند اما resiliency را بهبود ندهد. A–M همیشه به Instance X؛ X unavailable یعنی دسترسی A–M از دست میرود. Cassandra replication به چند node در ring.
از این نمای کوتاه، scaling database برای write جایی است که سخت میشود و قابلیت databaseها تمایز مییابد. وقتی به limit scale write میرسید، بسیاری technology را عوض میکنند. جعبهٔ بزرگتر اغلب سریعترین fix موقت است؛ در پسزمینه Cassandra، Mongo یا Riak را برای مدل scaling بلندمدت ببینید.
Shared Database Infrastructure
برخی database مثل RDBMS سنتی database و schema را جدا میکنند. یک database در حال اجرا چند schema مستقل، یکی برای هر میکروسرویس. تعداد ماشین کمتر؛ اما single point of failure بزرگ. اگر infrastructure down شود چند میکروسرویس همزمان — outage فاجعهبار. ریسک را در نظر بگیرید و database تا حد ممکن resilient باشد.
CQRS
الگوی Command-Query Responsibility Segregation (CQRS) مدل جایگزین ذخیره و query است. در database معمولی یک سیستم برای تغییر و query داده. در CQRS بخشی commandها (درخواست تغییر state) و بخشی query.
commandها درخواست تغییر state. validate و اعمال به model. commandها intent دارند. sync یا async پردازش — مثلاً queue و پردازش بعداً.
نکتهٔ کلیدی: modelهای داخلی command و query کاملاً جدا هستند. commandها را بهعنوان event handle کنم و فقط لیست commandها را ذخیره کنم (event sourcing). model query از event store projection بسازد یا feed از بخش command store دیگر را update کند. مزایای read replica بدون اینکه backing store replica همان store تغییر باشد.
جداسازی scaling متفاوت command و query — سرویسها یا hardware یا data store متفاوت. formatهای read مختلف با چند پیادهسازی query.
هشدار: این الگو دور از مدل تک data store برای همهٔ CRUD است. بیش از یک تیم باتجربه برای درست گرفتنش struggle کردهاند!
Caching
caching بهینهسازی performance رایج است: نتیجهٔ قبلی عملیات ذخیره میشود تا requestهای بعدی از مقدار ذخیرهشده استفاده کنند نه محاسبهٔ مجدد. اغلب حذف round-trip بیمورد به database یا سرویس. استفادهٔ درست مزیت performance عظیم. HTTP بهخاطر caching درونساخته برای requestهای زیاد scale میکند.
حتی در monolithic ساده انتخابهای زیادی برای cache دارید. در میکروسرویس هر سرویس منبع داده و رفتار خودش است — انتخابهای بیشتر. در سیستم توزیعشده معمولاً client-side یا server-side. کدام بهتر؟
Client-Side, Proxy, and Server-Side Caching
در client-side caching، client نتیجه cacheشده را نگه میدارد و تصمیم میگیرد چه موقع نسخهٔ تازه بگیرد. ایدهآل: سرویس downstream hint بدهد client بداند با response چه کند. در proxy caching، proxy بین client و server — reverse proxy یا CDN. در server-side caching، سرور مسئول cache است — Redis، Memcache یا cache in-memory.
کدام معنا دارد به آنچه optimize میکنید بستگی دارد. client-side network call را کم میکند و بار downstream را سریع کاهش میدهد؛ اما تغییر رفتار cache روی consumerها سخت و invalidation دادهٔ stale trickier است.
proxy caching برای client و server opaque است. راه ساده cache به سیستم موجود. proxy عمومی چند سرویس را cache میکند — Squid یا Varnish برای HTTP. hop شبکه اضافه میکند؛ نادر است مشکل باشد چون بهبود cache از هزینهٔ شبکه بیشتر است.
server-side برای clientها opaque است. cache نزدیک یا داخل boundary سرویس invalidation و cache hit را آسانتر میکند. با چند نوع client، server-side cache سریعترین بهبود performance ممکن است.
برای هر وبسایت public-facing ترکیب هر سه را داشتهام. برای بیش از یک سیستم توزیعشده بدون cache هم گذشتهام. به بار، تازگی داده و توان فعلی سیستم بستگی دارد. دانستن ابزارها فقط آغاز است.
Caching in HTTP
HTTP کنترلهای مفیدی برای cache client یا server دارد.
با cache-control directive در response به client میگوید cache کند یا نه و چند ثانیه. Expires header زمان و تاریخ stale شدن. منابع static مثل CSS و تصویر با TTL ساده cache-control. اگر زمان بهروزرسانی از قبل معلوم است Expires. جلوگیری از request به سرور.
Entity Tag یا ETag برای تشخیص تغییر مقدار resource. URI همان است اما مقدار عوض شده ETag عوض میشود. conditional GET: GET با header اضافی — resource فقط اگر شرط برقرار باشد.
مثلاً رکورد مشتری با ETag o5t6fkd2sa. بعداً با stale شدن طبق cache-control، GET بعدی با If-None-Match: o5t6fkd2sa. اگر بهروز است 304 Not Modified؛ وگرنه 200 OK با resource و ETag جدید.
این کنترلها در مشخصات پرکاربرد HTTP یعنی نرمافزار زیادی cache را handle میکند. Squid و Varnish بین client و server. CDN مثل CloudFront یا Akamai به cache نزدیک client. کتابخانهها و client cache هم.
ETag، Expires و cache-control کمی overlap دارند — مراقب اطلاعات متناقض باشید. REST In Practice (O'Reilly) یا بخش ۱۳ HTTP 1.1.
حتی اگر HTTP پروتکل interservice نباشد، cache در client و کاهش round-trip ارزش دارد. پروتکل دیگر: hint به client برای مدت cache.
Caching for Writes
اغلب cache برای read است؛ گاهی write. write-behind cache: نوشتن به cache محلی و بعد flush به منبع canonical. برای burst write یا نوشتن مکرر همان داده مفید است. buffer و batch write بهینهسازی performance.
با write-behind اگر writeهای buffered persistent باشند، حتی downstream unavailable میتوانیم queue و بعداً بفرستیم.
Caching for Resilience
cache برای resiliency در failure. client-side: downstream unavailable، client از دادهٔ cacheشده شاید stale استفاده کند. reverse proxy دادهٔ stale. برای برخی سیستمها available با stale بهتر از بدون نتیجه — judgment call شما. اگر داده در cache نیست کمک محدود است.
تکنیک Guardian و جاهای دیگر: crawl دورهای سایت live برای نسخهٔ static در outage. نسخهٔ crawl بهاندازهٔ live تازه نبود اما در pinch نسخهای نمایش میداد.
Hiding the Origin
در cache عادی، cache miss request به origin میرود و caller منتظر میماند. اگر cache miss عظیم — مثلاً ماشین یا گروهی که cache میدهند fail — تعداد زیادی request به origin میخورد.
برای دادهٔ highly cacheable، origin اغلب برای کسری از کل traffic scale شده چون بیشتر از cache در memory serve میشود. thundering herd وقتی کل region cache ناپدید شود origin را از بین میبرد.
محافظت: هرگز request به origin نرود. origin بهصورت asynchronous cache را populate کند، مانند شکل ۳-۷. cache miss event ایجاد میکند و origin rebuild میکند. اگر shard ناپدید شد cache در پسزمینه rebuild. میتوانید request اصلی را block کنید تا repopulate — ممکن است contention روی cache ایجاد کند. محتملتر برای پایداری سیستم request را fail fast کنید.
شکل ۳-۷. پنهان کردن origin از client و populate ناهمزمان cache
برای برخی موقعیتها معنا ندارد؛ راهی برای up ماندن وقتی بخشی fail میشود. fail fast منابع و latency را افزایش نمیدهد و فرصت recovery میدهد.
Keep It Simple
مراقب cache در جاهای زیاد باشید! هرچه cache بین شما و منبع تازه بیشتر، داده staleتر و سختتر تعیین تازگی برای client. در میکروسرویس با چند سرویس در call chain بدتر است. اگر cache خوب است، ساده نگه دارید، یکی بماند و قبل از اضافه کردن فکر کنید!
Cache Poisoning: A Cautionary Tale
با cache فکر میکنیم بدترین حالت دادهٔ stale کمی است. اگر برای همیشه stale serve کنید؟ پروژهٔ strangler که گفتم: proxy به legacy؛ در برگشت HTTP cache header درست اعمال میکردیم.
یک روز بعد از release عادی، عجیب شد. باگ: زیرمجموعهای از صفحات از شرط logic cache header رد نمیشد و header عوض نمیشد. downstream قبلاً Expires: Never داشت که override میکردیم؛ حالا نه.
Squid زیاد برای cache HTTP. مشکل را زود دیدیم — requestهای بیشتر از Squid به application server. cache header را fix و release و دستی پاک کردن region Squid. کافی نبود.
cache در چند جا است. CDN، cache ISP، و cache مرورگر کاربر — کنترل محدود. صفحات با Expires: Never در cache کاربران ماند تا پر شود یا دستی پاک کنند. تنها راه تغییر URL برای refetch.
caching قدرتمند است اما مسیر کامل داده از منبع تا مقصد را بفهمید تا پیچیدگی و خطا را ببینید.
Autoscaling
اگر provisioning ماشین مجازی و deploy instance میکروسرویس کاملاً خودکار است، بلوکهای autoscaling میکروسرویسها را دارید.
scaling با trend شناختهشده: peak ۹ صبح تا ۵ عصر — instance اضافی ۸:۴۵ و خاموش ۵:۱۵. AWS autoscaling خوب دارد؛ خاموش کردن instance پول صرفهجویی. دادهٔ تغییر بار روزانه، هفتگی، فصلی لازم است.
یا reactive: instance اضافی با افزایش بار یا failure instance. سرعت scale up بعد از تشخیص trend کلیدی است. اگر فقط چند دقیقه اخطار دارید اما scale ۱۰ دقیقه طول میکشد، ظرفیت اضافی برای پل لازم است. suite load test تقریباً ضروری — autoscaling ruleها را test کنید. بدون test که بارهای مختلف trigger کند، فقط در production میفهمید rule اشتباه بود و عواقب خوب نیست!
سایت خبری مثال ترکیب predictive و reactive: trend روزانه صبح تا ناهار و کاهش؛ weekend کمتر — proactive scale. خبر بزرگ spike غیرمنتظره با اخطار کوتاه.
autoscaling بیشتر برای failure instance دیده میشود تا بار. AWS rule «حداقل ۵ instance» — یکی down یکی جدید. گاهی whack-a-mole وقتی rule را فراموش خاموش کنند و برای maintenance down بگیرند و دوباره spin up شوند!
هر دو مفید و برای cost effectiveness روی platform pay-per-use. نیاز به مشاهدهٔ دقیق داده. اول autoscaling برای failure؛ داده جمع کنید. برای scale بار محتاط در scale down — قدرت محاسباتی بیشتر از نیاز اغلب بهتر از کمبود است!
CAP Theorem
میخواهیم همهچیز را داشته باشیم اما نمیتوانیم. در سیستمهای توزیعشده مثل میکروسرویسها حتی اثبات ریاضی داریم. CAP theorem را شنیدهاید، بهویژه در بحث data storeها. در قلبش: در سیستم توزیعشده سه چیز trade-off داریم: consistency، availability و partition tolerance. در failure mode فقط دو تا را نگه میداریم.
Consistency: پاسخ یکسان از چند node. Availability: هر request پاسخ میگیرد. Partition tolerance: توانایی سیستم وقتی ارتباط بین بخشها گاهی غیرممکن است.
از زمان conjecture اریک بروئر، اثبات ریاضی آمده. به ریاضی اثبات نمیپردازم. با مثالهای کار شده CAP را میفهمیم.
تکنیک scaling database ساده را probe کنیم. سرویس inventory در دو data center، مانند شکل ۳-۸. پشت هر instance یک database؛ دو database sync میکنند. read و write از node محلی؛ replication برای sync.
شکل ۳-۸. استفاده از multiprimary replication برای اشتراک داده بین دو node database
وقتی چیزی fail میشود؟ لینک شبکه بین data centerها قطع. sync fail. write به primary در DC1 به DC2 propagate نمیشود و برعکس. بیشتر databaseها queuing برای recovery بعدی دارند؛ در این فاصله چه؟
Sacrificing Consistency
فرض کنیم inventory را کاملاً down نمیکنیم. تغییر در DC1 — database DC2 نمیبیند. request به node inventory در DC2 دادهٔ stale میبیند. سیستم available است — هر دو node پاسخ میدهند — partition را نگه داشتیم اما consistency از دست رفت. اغلب AP system مینامند. هر سه را نداریم.
در partition اگر write بپذیریم میپذیریم بعداً resync لازم است. partition طولانیتر resync سختتر.
حتی بدون failure شبکه بین nodeها، replication فوری نیست. سیستمهایی که consistency را برای partition tolerance و availability میدهند eventually consistent هستند — انتظار داریم همه nodeها دادهٔ بهروز را ببینند اما نه فوری؛ کاربران ممکن است دادهٔ قدیمی ببینند.
Sacrificing Availability
اگر consistency لازم است و چیز دیگری را میدهیم؟ برای consistency هر node باید بداند کپیاش با دیگری یکی است. در partition اگر nodeها صحبت نکنند coordinate برای consistency ممکن نیست. تضمین consistency نداریم — تنها گزینه پاسخ ندادن به request. availability قربانی شد. CP: consistent و partition tolerant. سرویس باید functionality را degrade کند تا partition heal و resync شود.
Consistency بین چند node واقعاً سخت است. شاید سختترین چیز در سیستمهای توزیعشده. رکورد را از node محلی بخوانیم — چطور up to date بدانیم؟ از node دیگر بپرسیم و بگوییم در حین read update نشود — transactional read بین nodeها. اما transactional read کند است، lock میخواهد، read میتواند سیستم را block کند. سیستمهای consistent به locking نیاز دارند.
سیستمهای توزیعشده failure را انتظار دارند. transactional read: از node دور میخواهیم lock کند؛ read تمام شد release — اما حالا صحبت نمیکنیم. lock در single process سخت است؛ در توزیعشده سختتر.
distributed transaction در (ارجاع آینده) بهخاطر همین consistency بین nodeها challenging است.
multinode consistency را درست گرفتن آنقدر سخت است که اگر لازم دارید خودتان invent نکنید. data store یا lock service مثل Consul — key/value store strongly consistent برای configuration بین nodeها. کنار «دوستان اجازه نمیدهند دوستان crypto خودشان بنویسند» بگذارید «دوستان اجازه نمیدهند distributed consistent data store خودشان بنویسند». اگر فکر میکنید CP data store خودتان لازم است، همهٔ paperها را بخوانید، PhD بگیرید، چند سال اشتباه کنید. من off-the-shelf یا AP eventually consistent میسازم.
Sacrificing Partition Tolerance?
دو تا انتخاب میکنیم. AP eventually consistent. CP سخت ساخت و scale. چرا CA؟ بدون partition tolerance سیستم روی شبکه اجرا نمیشود — باید single process محلی باشد. CA در سیستم توزیعشده وجود ندارد.
AP or CP?
کدام درست؟ بستگی دارد. ما trade-off را میدانیم. AP آسانتر scale و سادهتر ساخت؛ CP کار بیشتر برای distributed consistency. اما impact کسبوکار را شاید نفهمیم. inventory پنج دقیقه stale باشد OK است؟ موجودی بانک؟ بدون context نمیدانیم. CAP فقط trade-off و سؤالهایی که بپرسید را میفهماند.
It's Not All or Nothing
کل سیستم لازم نیست AP یا CP باشد. catalog میتواند AP باشد؛ inventory CP چون نمیخواهیم چیزی بفروشیم که نداریم.
حتی هر سرویس لازم نیست CP یا AP باشد. سرویس موجودی امتیاز: نمایش balance stale OK؛ بهروزرسانی balance باید consistent تا بیش از امتیاز استفاده نشود. CP، AP یا هر دو؟ trade-off CAP را به capabilityهای فردی service فرو بردهایم.
consistency و availability همه یا هیچ نیستند. Cassandra trade-off متفاوت per call: read که تا همه replica پاسخ دهند block کند، یا quorum یا یک node. اگر همه replica و یکی unavailable، مدت block. با quorum کمتر inconsistency بپذیرید و کمتر آسیبپذیر یک replica.
پستهای «شکست CAP» معمولاً سیستمی با capabilityهای CP و AP ساختهاند. اثبات ریاضی CAP برقرار است. ریاضی را شکست نمیدهید.
And the Real World
خیلی از بحث دنیای الکترونیکی است — bit و byte در حافظه. consistency را کودکانه تصور میکنیم؛ در scope سیستم دنیا را متوقف کنیم و همه معنا داشته باشد. اما خیلی از آنچه میسازیم بازتاب دنیای واقعی است و کنترل نداریم.
inventory به اشیای فیزیکی map میشود. ۱۰۰ نسخه Give Blood The Brakes. یکی فروختیم — ۹۹. اگر هنگام ارسال سفارش یکی روی زمین افتاد و شکست؟ سیستم ۹۹، قفسه ۹۸.
اگر inventory AP باشد و گاهی به کاربر بگوییم out of stock — بدترین چیز جهان؟ ساخت، scale و صحت آسانتر.
سیستمها هرچقدر consistent باشند همهٔ اتفاقات را نمیدانند، بهویژه رکورد دنیای واقعی. یکی از دلایل AP در بسیاری موقعیتها. علاوه بر پیچیدگی CP، همهٔ مشکلات را fix نمیکنند.
Service Discovery
با بیش از چند میکروسرویس، توجه به اینکه همه کجا هستند. شاید بدانید در محیط چه اجرا میشود برای monitoring. شاید ساده: accounts service کجاست تا بقیه پیدا کنند. شاید برای توسعهدهندگان APIهای موجود تا چرخ را دوباره نسازند. همه تحت service discovery. گزینههای مختلف داریم.
راهحلها دو بخش: مکانیزم register «اینجام!» و راه پیدا کردن بعد از register. با destroy و deploy مداوم instanceها پیچیدهتر میشود — راهحل باید cope کند.
DNS
ساده شروع کنیم. DNS نام را به IP یک یا چند ماشین وصل میکند. accounts service همیشه accounts.musiccorp.com. به IP host سرویس یا load balancer که load را پخش میکند. بهروزرسانی entryها بخشی از deploy.
برای instance در محیطهای مختلف، template دامنه convention-based خوب است: <servicename>-<environment>.musiccorp.com مثل accounts-uat.musiccorp.com یا accounts-dev.musiccorp.com.
پیشرفتهتر: DNS server جدا per environment. accounts.musiccorp.com همیشه accounts اما resolve به hostهای مختلف بسته به محل lookup. اگر محیطها segment شبکه جدا و مدیریت DNS خودتان راحتید، neat است؛ کار زیاد اگر فقط برای این setup نباشید.
مزیت اصلی DNS: استاندارد شناختهشده که تقریباً هر stack پشتیبانی میکند. سرویسهای مدیریت DNS داخل سازمان کم است برای hostهای highly disposable — بهروزرسانی painful. Route53 آمازون خوب است؛ self-hosted بهاندازه آن ندیدم؛ Consul شاید کمک کند. مشکل بهروزرسانی entry و TTL در مشخصات DNS: مدت تازگی entry. تغییر host — clientها حداقل تا TTL IP قدیمی را نگه میدارند. cache در چند جا (حتی JVM مگر غیرفعال کنید). هرچه cache بیشتر staleتر.
راهحل: دامنه به load balancer که به instanceها اشاره میکند، مانند شکل ۳-۹. deploy instance جدید — قدیمی از load balancer out و جدید in. DNS round-robin مشکلساز — client از host پنهان است و نمیتواند traffic به host بیمار را قطع کند.
شکل ۳-۹. DNS به load balancer برای اجتناب از entryهای DNS stale
DNS شناختهشده و پشتیبانی گسترده. downside دارد. قبل از پیچیدهتر بررسی کنید fit است یا نه. single node — DNS مستقیم به host شاید OK. چند instance — DNS به load balancer که hostها را in/out of service میکند.
Dynamic Service Registries
downside DNS در محیط پویا منجر به سیستمهای جایگزین شده: سرویس خودش را در registry مرکزی register میکند و lookup بعداً. اغلب بیش از register و discovery — خوب یا بد. میدان شلوغ — چند گزینه برای حس موضوع.
Zookeeper
بخشی از Hadoop. use caseهای فراوان: configuration management، sync داده بین سرویسها، leader election، message queue، naming service.
مثل بسیاری از این سیستمها، Zookeeper چند node در cluster برای guaranteeها. حداقل سه node Zookeeper. smarts حول replicate امن داده و consistent ماندن در failure node.
در قلب، namespace سلسلهمراتبی برای ذخیره اطلاعات. client node اضافه، تغییر، query و watch برای تغییر. محل سرویسها در این ساختار؛ client وقتی عوض شد خبر میگیرد. configuration store عمومی — configuration خاص سرویس، log level یا feature toggle. شخصاً از Zookeeper بهعنوان configuration source دوری میکنم — reasoning رفتار سرویس سختتر میشود.
Zookeeper generic است — درخت اطلاعات replicate با alert تغییر. معمولاً روی آن برای use case میسازید. client library برای اکثر زبانها.
در مقیاس بزرگ Zookeeper قدیمی به نظر میرسد و برای service discovery کمتر از جایگزینهای جدید out of the box. tried and tested و widely used. الگوریتمهای underlying سخت درست گرفتن. یک vendor database فقط برای leader election از Zookeeper استفاده میکرد؛ heavyweight دید و وقت زیاد برای جایگزین PAXOS خودش. cryptography library خودتان ننویسید — distributed coordination system هم ننویسید. استفاده از چیز موجود که کار میکند.
Consul
مثل Zookeeper configuration management و service discovery؛ فراتر از Zookeeper برای این use caseها. رابط HTTP برای discovery. killer feature: DNS server out of the box؛ SRV record با IP و port برای نام. اگر بخشی از سیستم DNS دارد و SRV را پشتیبانی میکند، Consul را drop in کنید بدون تغییر.
قابلیتهای دیگر: health check روی nodeها. ممکن است با monitoring tools overlap؛ احتمالاً Consul منبع و pull به dashboard. طراحی fault-tolerant و focus روی ephemeral nodeها — شاید جایگزین Nagios و Sensu برای برخی use caseها.
رابط RESTful HTTP برای register، key/value و health check. integration با stackهای مختلف straightforward. Serf زیر Consul: cluster management، failure، alerting. Consul discovery و configuration. separation of concerns مطابق تم کتاب.
Consul خیلی جدید است؛ با پیچیدگی الگوریتمها معمولاً hesitant میشوم. Hashicorp (Packer، Vagrant) track record خوب؛ توسعهٔ فعال؛ چند نفر production happy. ارزش نگاه دارد.
Eureka
Eureka اوپنسورس Netflix برخلاف Consul و Zookeeper general-purpose configuration store نیست — use case هدفمند.
load-balancing پایه: round-robin lookup instance. endpoint REST برای client خودتان یا Java client Netflix. Java client health check instance. بدون client Netflix و REST مستقیم — خودتان.
client مستقیم service discovery — process جدا لازم نیست. هر client باید discovery پیاده کند. Netflix روی JVM استاندارد — همه Eureka. polyglot چالشبرانگیزتر.
Rolling Your Own
خودمان ساختهایم. پروژهٔ AWS-heavy: tag روی instance هنگام launch:
service = accountsenvironment = productionversion = 154
APIهای AWS برای query instanceهای account. AWS metadata و query را handle میکند. CLI و dashboard monitoring آسان با health check per instance.
آخرین بار سرویسها از API AWS برای dependency پیدا نکردند — میتوانید. برای alert upstream وقتی محل downstream عوض شد خودتان.
Don't Forget the Humans!
سیستمها register و lookup برای instance را آسان میکنند. انسانها هم گاهی این اطلاعات را میخواهند. هر سیستمی ابزار report و dashboard روی registry برای انسان نه فقط ماشین.
Documenting Services
تجزیه به میکروسرویس fine-grained امید seamهای API برای کارهای زیبا. discovery درست — میدانیم کجا هستند. چه میکنند و چطور استفاده کنیم؟ documentation API. documentation اغلب out of date. ایدهآل: documentation همیشه با API همگام و آسان دیدن وقتی endpoint را میدانیم. Swagger و HAL این را ممکن میکنند.
Swagger
Swagger API را describe میکند تا UI وب زیبا برای documentation و تعامل از مرورگر. اجرای request: template POST برای محتوای مورد انتظار سرور.
Swagger نیاز به sidecar file فرمت Swagger دارد. library برای زبانها — Java با annotate methodهای API و generate خودکار.
تجربهٔ end-user خوب؛ برای incremental exploration hypermedia کم میکند. راه خوب expose documentation سرویسها.
HAL and the HAL Browser
Hypertext Application Language (HAL) استاندارد hypermedia controlها. فصل ۲: hypermedia controlها کاوش تدریجی API با coupling کمتر. HAL: client libraryهای زیاد (۵۰+ در wiki) و HAL browser برای کاوش از مرورگر.
مثل Swagger living documentation و اجرای call — اجرا بهاندازهٔ Swagger slick نیست. Swagger template POST؛ HAL بیشتر خودتان. hypermedia کاوش API مؤثرتر — مرورگر لینک را خوب دنبال میکند!
برخلاف Swagger همهٔ اطلاعات در hypermedia controlها embed است. شمش دوسر: اگر hypermedia دارید، HAL browser با کمترین تلاش. بدون hypermedia HAL ندارید یا retrofit که consumerهای موجود را میشکند.
HAL استاندارد hypermedia با client library — دلیل uptake بیشتر HAL نسبت به Swagger برای کسانی که hypermedia دارند. hypermedia دارید: HAL بر Swagger. hypermedia دارید اما switch نمیتوانید justify کنید: Swagger امتحان کنید.
The Self-Describing System
اوایل SOA استانداردهایی مثل Universal Description, Discovery, and Integration (UDDI) برای فهم سرویسها. heavyweight — humane registry سبکتر: wiki برای ثبت اطلاعات سرویسها.
تصویر سیستم و رفتار در scale مهم است. تکنیکهایی که فهم مستقیم از سیستم میدهند: health سرویسهای downstream با correlation ID برای call chain؛ Consul برای محل میکروسرویسها؛ HAL برای capability روی endpoint؛ health-check و monitoring برای سلامت کل و فردی.
همهٔ این اطلاعات programmatic است. humane registry قویتر از wiki که out of date میشود. harness و نمایش اطلاعاتی که سیستم emit میکند. dashboard سفارشی برای درک ecosystem.
با صفحهٔ وب static یا wiki که کمی از سیستم live scrape کند شروع کنید. با زمان اطلاعات بیشتر. دسترسی آسان ابزار کلیدی مدیریت پیچیدگی emerging در scale.
Summary
بهعنوان design approach، میکروسرویسها هنوز جواناند — تجربهٔ notable داریم اما سالهای بعد الگوهای مفیدتر. امیدوارم این فصل گامهایی برای سفر به میکروسرویس در مقیاس که پایدار بمانند outline کرده باشد.
علاوه بر اینجا، کتاب عالی Release It! اثر Michael Nygard را پیشنهاد میکنم — داستانهای failure سیستم و الگوها برای handle خوب. essential reading برای هر کسی سیستم در scale میسازد.
زمین زیادی پوشش دادیم و به پایان نزدیکیم. فصل نهایی همه را جمع میکند و خلاصهٔ کل کتاب.