Building Microservices
Designing Fine-Grained Systems
توضیحات
در کتاب Building Microservices، سام نیومن به بررسی عمیق مفهوم میکروسرویسها و نحوه طراحی سیستمهای نرمافزاری با استفاده از این معماری میپردازد. این کتاب به توسعهدهندگان نرمافزار، معماران سیستم و مدیران فناوری اطلاعات کمک میکند تا درک بهتری از مزایا و چالشهای میکروسرویسها داشته باشند و بتوانند آنها را به طور مؤثر در پروژههای خود پیادهسازی کنند.
نظر
نظر
امتیاز: 08/10به دیگران توصیه میکنم: بلهدوباره میخوانم: بلهایده برجسته:تاثیر در من:نکات مثبت:نکات منفی:
مشخصات
نویسنده: sam newmanانتشارات: orlyصفحه مشخصات:
بخشهایی از کتاب
ریشههای تکاملی و ظهور ریزسرویسها (The Evolutionary Roots and Emergence of Microservices)
در مهندسی نرمافزار، معماریها به یکباره و از خلأ اختراع نمیشوند، بلکه در پاسخ به نیازهای واقعی و از ترکیب رویکردهای پیشین ظهور (Emerge) میکنند. معماری ریزسرویس (Microservices) نیز حاصل همگرایی و بلوغ چندین مفهوم بنیادین در دنیای توسعه نرمافزار است:
- Domain-Driven Design (DDD): ایده Eric Evans مبنی بر اهمیت مدلسازی کدهای نرمافزار بر اساس مفاهیم دنیای واقعی کسبوکار.
- Continuous Delivery: نگرشی که نشان داد چگونه میتوان نرمافزار را بهطور مؤثرتری به محیط عملیاتی منتقل کرد و هر تغییر در کد (Check-in) را یک کاندیدای بالقوه برای انتشار (Release Candidate) در نظر گرفت.
- Hexagonal Architecture: مفهوم Alistair Cockburn برای هدایت معماری به سمتی که منطق کسبوکار (Business Logic) از لایههای زیرساختی جدا شده و در معماریهای لایهای سنتی پنهان نشود.
- Virtualization & Infrastructure Automation: امکان تخصیص و تغییر اندازه منابع بر اساس تقاضا (On-demand) و مدیریت مقیاسپذیر زیرساختها در سطح کلان.
- Small Autonomous Teams: رویکرد سازمانهای پیشرویی مانند Amazon و Google در واگذاری مالکیت کامل چرخه حیات سرویسها به تیمهای کوچک.
- Antifragile Systems: تجربیات Netflix در ساخت سیستمهایی که در مقیاسهای بسیار بزرگ در برابر خرابیها مقاوم هستند و خود را ترمیم میکنند.
بر اساس این پایهها، معماری ریزسرویسها به سازمانها آزادی عمل چشمگیری برای واکنش سریع به تغییرات اجتنابناپذیر میدهد و امکان استفاده مؤثرتر از تکنولوژیهای جدیدتر را فراهم میسازد.
ماهیت ریزسرویسها: کوچک و خودمختار (Small and Autonomous)
تعریف پایه و بنیادین کتاب از ریزسرویسها بدین شرح است: “سرویسهای کوچک و خودمختاری که با یکدیگر همکاری میکنند”.
۱. مفهوم «کوچک بودن» و اصل تکمسئولیتی (Single Responsibility Principle): در سیستمهای یکپارچه (Monolithic)، علیرغم تلاش برای ایجاد ماژولار بودن، مرزهای درونپردازشی (In-process boundaries) به مرور زمان از بین میروند و کدهای مرتبط با عملکردهای مشابه در سراسر سیستم پخش میشوند. این امر رفع باگها و پیادهسازی ویژگیهای جدید را به شدت دشوار میکند. برای مقابله با این پدیده، تمرکز بر انسجام (Cohesion) ضروری است؛ یعنی گردآوری کدهای مرتبط در یک مکان واحد.
این رویکرد دقیقاً با تعریف رابرت سی. مارتین (Robert C. Martin) از SRP همخوانی دارد: “چیزهایی که به یک دلیل تغییر میکنند را کنار هم قرار دهید و چیزهایی که به دلایل مختلف تغییر میکنند را از هم جدا کنید”. در معماری ریزسرویس، مرز سرویسها بر اساس مرزهای کسبوکار (Business Boundaries) تعیین میشود تا مشخص باشد کد مربوط به هر قابلیت دقیقاً کجا قرار دارد و از رشد بیرویه و غیرقابلکنترل آن جلوگیری شود.
۲. اما یک سرویس چقدر باید کوچک باشد؟ تعیین اندازه بر اساس «تعداد خطوط کد» (LOC) رویکردی اشتباه است، زیرا زبانهای برنامهنویسی در میزان فشردگی و قدرت بیان (Expressiveness) متفاوت هستند و وابستگیها (Dependencies) نیز خود حاوی کدهای فراوانی میباشند. نویسنده دو معیار کاربردیتر را مطرح میکند:
- معیار Jon Eaves: سرویسی که بتوان آن را در طی دو هفته به طور کامل بازنویسی کرد.
- معیار ساختار تیمی: تناسب اندازه کدبیس با توانایی مدیریت آن توسط یک تیم کوچک؛ اگر مدیریت کدبیس برای یک تیم کوچک دشوار شده باشد، آن سرویس بیش از حد بزرگ است.
به بیان ساده، یک سرویس باید “به اندازهای کوچک باشد و نه کوچکتر”. با کوچکتر شدن سرویسها، مزایای استقلال افزایش مییابد اما در عین حال پیچیدگیِ ناشی از مدیریتِ بخشهای متحرکِ متعدد (Moving parts) نیز به شدت بالا میرود.
۱. تحلیل انتقادی/فنی
- چالش تعیین مرزها (Boundary Definition Challenge): گره زدن اندازه یک سرویس به بازه زمانی ذهنی مانند “دو هفته برای بازنویسی” یک معیار به شدت متغیر و وابسته به سطح تخصص برنامهنویس، زبان برنامهنویسی و ابزارهای موجود است و نمیتواند یک استاندارد مهندسی دقیق باشد. برای شما به عنوان یک توسعهدهنده ارشد که در اکوسیستم NET. فعالیت میکنید، تمرکز بر روی کشف دقیق Bounded Contextها در DDD بسیار منطقیتر و ساختاریافتهتر از تمرکز بر زمان بازنویسی است.
- تضاد پیچیدگی و کارایی (Complexity vs. Performance): نویسنده اشاره میکند که هرچه سرویسها کوچکتر شوند، مزایای عدم وابستگی بیشتر میشود. اما باید دقت کرد که در محیطهای High Performance، خرد کردن بیش از حد سرویسها منجر به ارتباطات شبکهای پرتکرار (Chatty Communication) میشود. این مسئله به خصوص در طراحی APIها و هنگام سریالایز/دیسریالایز کردن مداوم دادهها، به شدت باعث افت پرفورمنس (Performance Bottlenecks) و نقض اصول معماری تمیز (Clean Architecture) در زمینه کنترل هزینهی عبور از مرزها (Boundary Crossing) خواهد شد.
مفهوم خودمختاری (Autonomy) و جداسازی (Decoupling)
ریزسرویسها موجودیتهای مستقلی (Separate Entities) هستند که میتوانند به عنوان یک سرویس ایزوله روی یک پلتفرم (PaaS) یا در یک پردازشگر سیستمعامل (OS Process) مجزا مستقر شوند. نویسنده تأکید میکند که باید از تجمیع چندین سرویس روی یک ماشین واحد پرهیز کرد. هرچند این سطح از ایزولهسازی، سربارهایی (Overhead) به همراه دارد، اما سادگیِ حاصل از آن باعث میشود استدلال و درک رفتار سیستم توزیعشده بسیار آسانتر گردد.
در این معماری، ارتباط بین سرویسها منحصراً از طریق فراخوانیهای شبکه (Network Calls) انجام میشود تا تفکیک فیزیکی و منطقی تضمین شده و از خطر وابستگی شدید (Tight Coupling) جلوگیری گردد. کلاینتها (Consumers) با استفاده از APIهای ارائهشده توسط سرویس با آن ارتباط برقرار میکنند. برای حفظ خودمختاری، سرویسها باید جزئیات پیادهسازی داخلی خود را پنهان کنند؛ زیرا به اشتراکگذاری بیش از حد مدلهای داخلی باعث میشود کلاینتها به ساختار داخلی سرویس وابسته (Coupled) شوند. این وابستگی، خودمختاری را از بین میبرد، زیرا هر تغییری در سرویس نیازمند هماهنگی با کلاینتها خواهد بود. همچنین، APIها باید از نظر تکنولوژی خنثی (Technology-agnostic) باشند تا انتخابهای کلاینتها را محدود نکنند.
در اینجا یک قانون طلایی (Golden Rule) مطرح میشود: “آیا میتوانید یک تغییر در سرویس ایجاد کرده و آن را به تنهایی، بدون نیاز به تغییر در هیچ جای دیگری دیپلوی کنید؟”. اگر پاسخ منفی باشد، دستیابی به مزایای کلیدی ریزسرویسها غیرممکن خواهد بود.
مزیت کلیدی: ناهمگونی تکنولوژی (Technology Heterogeneity)
در یک سیستم متشکل از سرویسهای متعدد، میتوانیم از تکنولوژیهای متفاوتی برای هر سرویس استفاده کنیم. این قابلیت به ما اجازه میدهد به جای استفاده از یک رویکرد یکپارچه و تحمیلی که معمولاً ضعیفترین وجه مشترک (Lowest Common Denominator) را هدف قرار میدهد، ابزار دقیق و مناسب را برای هر نیازمندی (Right tool for each job) انتخاب کنیم.
نویسنده دو کاربرد اصلی برای این ناهمگونی ذکر میکند:
- بهینهسازی عملکرد (Performance) و ذخیرهسازی: اگر بخشی از سیستم به عملکرد بالاتری نیاز داشته باشد، میتوانیم Stack تکنولوژی آن بخش را تغییر دهیم. در سطح دیتابیس نیز، یک شبکه اجتماعی میتواند تعاملات کاربران را در یک پایگاه داده گرافی (Graph DB) و پستها را در یک پایگاه داده سندمحور (Document-oriented) ذخیره کند.
- کاهش ریسک در پذیرش تکنولوژیهای جدید: در معماری یکپارچه (Monolithic)، آزمایش یک زبان یا فریمورک جدید بسیار پرریسک است زیرا کل سیستم را تحتالشعاع قرار میدهد. اما در ریزسرویسها میتوان یک سرویس کمریسک را انتخاب و تکنولوژی جدید را در آن آزمایش کرد.
با این وجود، استفاده از تکنولوژیهای متعدد بدون هزینه نیست. شرکتهایی مانند Netflix و Twitter برای کنترل این پیچیدگیها، عمدتاً پلتفرم JVM را هدف قرار میدهند تا از درک بالای خود نسبت به پایداری آن و ابزارهای مانیتورینگ موجود بهره ببرند، اما هرگز خود را به یک تکنولوژی واحد محدود نمیکنند.
۱. تحلیل انتقادی/فنی
- تلهی Polyglot Persistence و Polyglot Programming: نویسنده ناهمگونی تکنولوژی را یک مزیت برجسته میداند، اما از منظر مهندسی نرمافزار، تنوع بیش از حد زبانها و دیتابیسها منجر به ظهور کابوس عملیاتی (Operational Nightmare) میشود. برای تیمی متشکل از توسعهدهندگان ارشد .NET Core، سربارِ نگهداری، راهاندازی فرآیندهای CI/CD، و آموزش نیروهای جدید برای مدیریت تکنولوژیهای گوناگون (مانند مدیریت همزمان SQL Server و دیتابیسهای گرافی با زبانهای مختلف) معمولاً بسیار بیشتر از مزایای تئوریک “انتخاب بهترین ابزار” است.
- هزینه پنهان Network Calls و نقض محرمانگی (Encapsulation): تاکید بر استفاده از فراخوانیهای شبکه برای جلوگیری از Coupling، از دیدگاه Clean Code و OOP معادل تبدیل متدکالهای پردازنده به فراخوانیهای پرهزینه شبکه است. اگر APIها به درستی و به صورت Coarse-grained (دانهدرشت) طراحی نشوند، مشکل N+1 از سطح ORM به سطح شبکه منتقل میشود. این امر نه تنها Performance سیستم را به شدت کاهش میدهد، بلکه به دلیل نیاز به سریالایز/دیسریالایز مداوم دادهها، مصرف منابع را بیرویه بالا میبرد.
مقیاسپذیری هدفمند و تابآوری سیستم (Targeted Scaling and System Resilience)
نویسنده در ادامه به بررسی مزایای ساختاری ریزسرویسها در مدیریت بار پردازشی و خرابیها میپردازد:
۱. مفهوم دیوارههای حائل (Bulkhead) و تابآوری: یکی از مفاهیم کلیدی در مهندسی تابآوری (Resilience Engineering)، الگوی Bulkhead است. در یک سیستم یکپارچه (Monolithic)، خرابی در یک بخش به راحتی به کل سیستم سرایت کرده و باعث از کار افتادن آن میشود. در معماری ریزسرویس، مرزهای بین سرویسها دقیقاً به عنوان دیوارههای حائل عمل میکنند؛ به طوری که اگر یک سرویس به طور کامل از کار بیفتد، خرابی به صورت آبشاری (Cascading Failure) گسترش نیافته و سایر بخشهای سیستم میتوانند با تنزل سطح عملکرد (Degraded Functionality) به کار خود ادامه دهند. با این حال، نویسنده هشدار میدهد که برای دستیابی به این تابآوری، باید با منابع جدیدِ خرابی در سیستمهای توزیعشده (مانند قطعی شبکهها و ماشینها) آشنا باشیم و آنها را مدیریت کنیم.
۲. مقیاسپذیری (Scaling) و کنترل هزینهها: در برنامههای بزرگ و یکپارچه، اگر تنها یک بخش کوچک با محدودیت منابع مواجه شود، مجبور هستیم کل سیستم را با هم مقیاسدهی کنیم که به شدت ناکارآمد است. در مقابل، ریزسرویسها به ما اجازه میدهند مقیاسپذیری را صرفاً بر روی سرویسهایی متمرکز کنیم که به آن نیاز دارند (Targeted Scaling). این امر اجازه میدهد سایر بخشهای سیستم روی سختافزارهای ضعیفتر و ارزانتر اجرا شوند. ترکیب این قابلیت با زیرساختهای ابری (مانند AWS)، امکان کنترل بسیار دقیقتر هزینهها را فراهم میکند.
چابکی در استقرار، ترکیبپذیری و قابلیت جایگزینی (Agility, Composability, and Replaceability)
در این بخش، تاثیر معماری بر روی فرآیندهای توسعه و چرخه حیات نرمافزار بررسی میشود:
۱. سهولت استقرار (Ease of Deployment): در سیستمهای یکپارچهی چند میلیون خطی، تغییر یک خط کد نیازمند استقرار (Deployment) کل برنامه است؛ فرآیندی که ریسک بالایی دارد و به دلیل ترس از خرابی، با فواصل زمانی طولانی انجام میشود. این تاخیر باعث تجمع تغییرات بین نسخهها شده و ریسک خطا را به شدت بالا میبرد. ریزسرویسها اجازه میدهند یک تغییر در یک سرویس به صورت مستقل مستقر شود و در صورت بروز خطا، بازگشت به نسخه قبل (Rollback) به سرعت انجام پذیرد.
۲. همراستایی سازمانی و ترکیبپذیری (Organizational Alignment & Composability): ریزسرویسها به سازمانها کمک میکنند تا معماری خود را با ساختار تیمها همراستا کنند، که این امر بهرهوری تیمهای کوچک روی کدبیسهای کوچک را به حداکثر میرساند. از منظر مصرفکنندگان (Clients)، ریزسرویسها درزها و نقاط اتصالی (Seams) ایجاد میکنند که میتوان عملکردهای مختلف را برای پلتفرمهای گوناگون (وب، موبایل، گجتهای پوشیدنی) به شکلی جدید با یکدیگر ترکیب (Compose) کرد.
۳. بهینهسازی برای جایگزینی (Optimizing for Replaceability): سیستمهای Legacy به این دلیل در سازمانها باقی میمانند که بسیار بزرگ هستند و جایگزینی آنها ریسک وحشتناکی دارد. اما در ریزسرویسها، هزینه دور انداختن یا بازنویسی کامل یک سرویسِ چند صد خطی بسیار پایین است. توسعهدهندگان به کدهای بسیار کوچک دلبستگی عاطفی پیدا نمیکنند و جایگزینی آنها بدون دغدغه انجام میشود.
۱. تحلیل انتقادی/فنی
- سرابِ ترکیبپذیری مستقیم (The Illusion of Direct Composability): نویسنده به قابلیت دسترسی مستقیم (Addressable) سرویسها از سمت پلتفرمهای مختلف به عنوان یک مزیت اشاره میکند. از منظر الگوهای طراحی سیستمهای توزیعشده، افشای مستقیم ریزسرویسها به کلاینتهای خارجی (مثل موبایل یا وب) یک Anti-Pattern به شدت مخرب است که باعث نشت منطق تجمیع داده (Aggregation Logic) به سمت کلاینت و افزایش شدید تاخیر شبکه (Network Latency) میشود. در یک معماری اصولی، باید از الگوی API Gateway یا BFF (Backend for Frontend) استفاده کرد تا کلاینت با یک مرز تجمیعیافته صحبت کند، نه آنکه به طور مستقیم با Seamهای داخلی درگیر شود.
- تضاد دیوارههای حائل و ارتباطات همگام (Bulkhead vs. Synchronous Calls): نویسنده ایده Bulkhead را به عنوان راهکار جلوگیری از Cascading Failure مطرح میکند. اما باید دقت داشت که مرز فیزیکی به تنهایی تضمینکننده این موضوع نیست. اگر سرویس A سرویس B را از طریق یک درخواست همگام (مانند HTTP REST) فراخوانی کند و سرویس B دچار کندی شود (Thread Starvation)، سرویس A نیز به سرعت منابع خود را از دست داده و از کار میافتد. برای تحقق وعدهی Bulkhead، استفاده از الگوهای پیشرفتهای نظیر Circuit Breaker، محدودیت زمانی سختگیرانه (Timeouts) و ترجیحاً معماریهای رویدادمحور (Event-Driven) با استفاده از Message Brokerها (نظیر RabbitMQ یا Kafka) الزامی است.
معماری سرویسگرا (SOA) در برابر ریزسرویسها (SOA vs. Microservices)
معماری سرویسگرا (SOA) رویکردی طراحی است که در آن چندین سرویس برای ارائه قابلیتهای نهایی با هم همکاری میکنند و ارتباط آنها عموماً از طریق فراخوانیهای شبکه صورت میگیرد. در تئوری، SOA با هدف ترویج استفاده مجدد از نرمافزار (Reusability) و سهولت در نگهداری سیستمها شکل گرفت، اما در عمل نتوانست جایگزین موفقی برای برنامههای بزرگ یکپارچه ارائه دهد.
نویسنده استدلال میکند که بسیاری از مشکلات نسبت داده شده به SOA، در واقع ناشی از پیادهسازیهای نادرست، پروتکلهای ارتباطی سنگین (مانند SOAP)، میانافزارهای تجاری (Vendor Middleware)، و فقدان راهنمای عملی برای تعیین اندازه و مرزهای سرویسها بوده است. از این منظر، ریزسرویسها رویکردی جدید و متمایز نیستند، بلکه فرم تکاملیافته و پیادهسازی اصولیِ SOA بر اساس تجربیات دنیای واقعی محسوب میشوند؛ دقیقاً همانطور که XP و Scrum رویکردهای مشخصی برای پیادهسازی تفکر Agile هستند.
جایگزینهای تجزیه سیستم (Other Decompositional Techniques)
نویسنده بررسی میکند که آیا تکنیکهای درونپردازشی (In-process) میتوانند به جای ریزسرویسها استفاده شوند:
۱. کتابخانههای مشترک (Shared Libraries): اشتراکگذاری کد در قالب کتابخانهها (مانند Nuget در داتنت) روشی مرسوم برای جلوگیری از تکرار کدهای پایه است. اما این روش چهار محدودیت جدی دارد: اول، ناهمگونی تکنولوژی را از بین میبرد (محدودیت به یک زبان/پلتفرم)؛ دوم، امکان مقیاسدهی مستقل را سلب میکند؛ سوم، برای اعمال تغییرات نیازمند استقرار مجدد کل سیستم (Redeployment) است؛ و چهارم، فاقد درزهای معماری (Seams) برای اعمال الگوهای تابآوری است. نویسنده هشدار میدهد که استفاده از کتابخانههای مشترک برای کدهای ارتباطی بین سرویسها میتواند به سرعت به یک نقطه وابستگی خطرناک (Coupling Point) تبدیل شود.
۲. مدیریت ماژولها (Modules): ابزارهایی مانند OSGI در اکوسیستم جاوا تلاش کردند مدیریت چرخه حیات ماژولها را در زمان اجرا (Runtime) فراهم کنند، اما به دلیل عدم پشتیبانی ذاتی زبان، بار کاری مضاعفی برای برنامهنویسان ایجاد کرده و به منبعی از پیچیدگی تبدیل شدند. در مقابل، زبانی مانند Erlang ماژولها را به صورت ذاتی در هسته خود دارد و امکان ارتقای بدون توقف آنها را فراهم میکند. با این حال، حتی در کاملترین پیادهسازیها نیز ماژولهای درونپردازشی به ندرت در دنیای واقعی مستقل باقی میمانند و معمولاً به شدت با یکدیگر Coupled میشوند.
ریزسرویسها اکسیر اعظم نیستند (No Silver Bullet)
در پایان این بخش، نویسنده صراحتاً اعلام میکند که ریزسرویسها یک راهحل جادویی یا یک ابزار همهکاره (Golden Hammer) نیستند. این معماری تمام پیچیدگیهای ذاتی سیستمهای توزیعشده را به همراه دارد. توسعهدهندگانی که از سیستمهای یکپارچه مهاجرت میکنند، برای آزادسازی پتانسیل ریزسرویسها باید در زمینههای استقرار، تست نرمافزار و مانیتورینگ بسیار توانمندتر شوند. همچنین چالشهای سنگینی مانند تراکنشهای توزیعشده (Distributed Transactions) و قضیه کپ (CAP Theorem) به مشکلات روزمره طراحی تبدیل خواهند شد.
۱. تحلیل انتقادی/فنی
- تلهی اصل DRY در کدهای مشترک: نویسنده به درستی از خطرات Shared Libraries یاد میکند. در معماری Monolithic، اصل DRY (Don’t Repeat Yourself) یک قانون بنیادین در Clean Code است. اما در معماری ریزسرویس، پافشاری کورکورانه بر این اصل و تلاش برای قرار دادن موجودیتهای Domain در کتابخانههای مشترک، منجر به Logical Coupling میشود. در اینجا یک پارادایم شیگرایی تغییر میکند: تکرار هدفمند کد (Intentional Duplication) بین سرویسهای مستقل، به مراتب ساختاریافتهتر و ارزانتر از وابستگی به یک Shared Library است که با هر تغییرش باعث نقض اصل OCP (Open-Closed Principle) و شکسته شدن بیلد در دهها سرویس میشود.
- کابوس تراکنشهای توزیعشده (Distributed Transactions): نویسنده صرفاً به دردسرساز بودن تراکنشهای توزیعشده اشاره میکند، اما به عنوان یک مهندس نرمافزار باید بدانید که تلاش برای اعمال مفاهیم ACID روی پایگاهدادههای پراکنده، یک Anti-Pattern مطلق (مثل Two-Phase Commit) است که باعث قفل شدن منابع (Locking) و نابودی پرفورمنس میشود. در این معماری، ما قطعیّت آنی پایگاهداده را فدای مقیاسپذیری میکنیم و بر اساس قضیه CAP، با پذیرش Eventual Consistency (سازگاری نهایی)، سیستم را به کمک الگوهایی نظیر Saga Pattern و معماری Event-Driven پیادهسازی میکنیم.
نقش معمار تکاملی و حاکمیت از طریق کد (The Evolutionary Architect & Governance Through Code)
در فصل دوم، نویسنده از مباحث ساختاری فراتر رفته و به نقش «معمار سیستم» و نحوه اعمال حاکمیت (Governance) در معماری ریزسرویسها میپردازد. یکی از ابزارهای مهم برای هدایت توسعهدهندگان در مسیر درست، استفاده از «قالبهای سرویس سفارشی» (Tailored Service Template) است.
۱. پیادهسازی حاکمیت با الگوهای کدنویسی: به جای آنکه از توسعهدهندگان بخواهیم تمام قوانین (مانند بررسی سلامت سیستم، ایجاد سرور HTTP یا خروجی متریکها) را از صفر پیادهسازی کنند، میتوان از میکروکانتینرهایی نظیر Dropwizard یا Karyon استفاده کرد. معماران میتوانند این ابزارها را سفارشیسازی کرده و کتابخانههایی مانند Circuit Breaker (مثل Hystrix) یا ارسالکننده متریک به سرور متمرکز (مثل Graphite) را در آنها تعبیه کنند. این رویکرد تضمین میکند که توسعهدهندگان برای ایجاد یک «سرویس با رفتار مخرب»، باید به زحمت بیفتند و مسیر پیشفرض، مسیرِ درست و ایمن است.
۲. چالش چندزبانگی و الگوی Sidecar: اگر سازمان از زبانهای برنامهنویسی متعددی استفاده کند، بازنویسی کدهای مربوط به تابآوری (Fault Tolerance) در تمامی زبانها به شدت پرهزینه و پرریسک خواهد بود. نتفلیکس (Netflix) برای حل این مشکل از سرویسهای Sidecar استفاده میکند؛ به این معنا که یک پراسس مجزا (مبتنی بر JVM که حاوی کتابخانههای استاندارد است) در کنار سرویس اصلی (با هر زبانی) قرار میگیرد و ارتباطات محلی (Local Communication) بین آنها شکل میگیرد تا نیاز به تکرار کد از بین برود.
۳. خطرات کدهای مشترک و فریمورکهای متمرکز: نویسنده هشدار میدهد که این قالبهای سفارشی نباید توسط یک «تیم معماری متمرکز و دیکتاتور» ساخته شوند، بلکه باید حاصل تلاش جمعی تیمها (با رویکرد Internal Open Source) باشند. از سوی دیگر، تلاش افراطی برای استفاده مجدد از کدها (Code Reuse) میتواند منجر به ایجاد فریمورکهای غولپیکر و درهمتنیدهای شود که تیمها را نابود میکند. برخی تیمها برای جلوگیری از Coupling، حتی حاضرند کدهای پایه را به صورت دستی (Copy-Paste) در سرویسها تکثیر کنند تا از وابستگی به یک نسخه باینری مشترک (Shared Binary) رها شوند.
مدیریت بدهی فنی، استثنائات و رهبری تیمی (Technical Debt & Leadership)
۱. بدهی فنی و لاگ استثنائات: گاهی به دلیل فشارهای زمانی یا تغییر در چشمانداز سیستم، مجبور به عبور از اصول معماری میشویم که این امر منجر به انباشت بدهی فنی (Technical Debt) میشود. معمار وظیفه دارد این بدهیها را در یک لاگ ثبت کرده و برای بازپرداخت آنها برنامهریزی کند. همچنین، اگر یک استثنا به طور مداوم تکرار شود، نشاندهنده نیاز به تغییر اصول است؛ به عنوان مثال، قانونی که میگوید “همیشه از MySQL استفاده کنید”، با دیدن حجم بالای داده میتواند به “از MySQL استفاده کنید، مگر برای مقیاسهای بسیار بزرگ که Cassandra مناسبتر است” تغییر یابد.
۲. حاکمیت به عنوان یک فعالیت گروهی: حاکمیت فنی (Technical Governance) نباید یک کار فردی باشد. بهترین مدل این است که معمار، رهبری گروهی متشکل از افراد ارشدِ تیمهای توسعه (Tech Leads) را بر عهده بگیرد. این گروه تصمیمگیری میکند و اطلاعات را بین تیمها جریان میدهد.
۳. استعاره برکه اردک (The Duck Pond Analogy): زمانی که تصمیم گروه با نظر معمار مغایرت دارد، معمار چه باید بکند؟ نویسنده این موقعیت را به آموزش دوچرخهسواری به یک کودک تشبیه میکند: شما اجازه میدهید کودک تلوتلو بخورد و حتی زمین بیفتد تا یاد بگیرد، اما اگر ببینید در حال رفتن به سمت ترافیک خیابان یا افتادن در «برکه اردکها» است، قاطعانه مداخله میکنید. معمار نیز باید بداند چه زمانی باید به تیم اعتماد کند (حتی اگر اشتباه کنند) و چه زمانی خطر به قدری سیستمیک و بزرگ است که باید نظر تیم را وتو کند.
۱. تحلیل انتقادی/فنی
- تکامل الگوی Sidecar به Service Mesh: نویسنده ایده Sidecar را برای اشتراکگذاری قابلیتهای زیرساختی (مانند مانیتورینگ و Circuit Breaker) مطرح میکند. از منظر معماری نوین، قرار دادن این منطق در داخل کتابخانههای کد (حتی در قالبهای سفارشی) ناقض اصل SRP در سطح سیستم است. امروزه در اکوسیستم .NET و فضای Cloud-Native، ما این مسئولیتها را به طور کامل از کد نرمافزار خارج کرده و به ابزارهای Service Mesh (نظیر Istio یا Linkerd) یا Dapr (Distributed Application Runtime) میسپاریم. این ابزارها دقیقاً با الگوی Sidecar مستقر میشوند و چالشهای Cross-cutting concerns (مثل رمزنگاری، Service Discovery، و رترای) را بدون آلوده کردن کدهای C#، در سطح زیرساخت حل میکنند.
- ناکارآمدی مدیریت دستی بدهی فنی: نویسنده پیشنهاد میکند بدهیهای فنی یا انحراف از قوانین در لاگها ثبت شوند. در مهندسی نرمافزار مدرن، مدیریت دستی این موارد محکوم به شکست است. به عنوان یک توسعهدهنده ارشد، شما باید حاکمیت معماری را از طریق Fitness Functions و ابزارهایی مانند NetArchTest در داتنت یا SonarQube پیادهسازی کنید. قوانینی نظیر “هیچ سرویسی حق ندارد مستقیماً با دیتابیس سرویس دیگر صحبت کند” یا “لایه Domain نباید به لایه Infrastructure وابستگی داشته باشد” باید به صورت Unit Test نوشته شوند تا در صورت نقض، Pipeline بیلد (CI) بلافاصله متوقف (Fail) شود. حاکمیت واقعی با کدِ قابلاجرا (Executable Code) انجام میشود، نه با مستندات متنی.
مدلسازی سرویسها: دو اصل بنیادین (Loose Coupling & High Cohesion)
پیش از آنکه تیم MusicCorp شروع به ساخت سرویسها کند، نویسنده بر دو مفهوم حیاتی تأکید میکند که اگر این دو را اشتباه پیادهسازی کنیم، تمام تکنیکهای دیگر بیفایده خواهند بود :
۱. اتصال سست (Loose Coupling): تغییر در یک سرویس نباید نیازمند تغییر در سرویس دیگری باشد .
۲. انسجام بالا (High Cohesion): رفتارهای مرتبط باید کنار هم، و رفتارهای نامرتبط باید از هم جدا باشند .
مفهوم کلیدی: Bounded Context در DDD
اریک ایوانز در کتاب Domain-Driven Design مفهومی معرفی میکند که اهمیت آن در ابتدا به سادگی از دست میرود: Bounded Context .
مثال MusicCorp: انبار کالا (Warehouse) و بخش مالی (Finance) دو Bounded Context مجزا هستند .
از Bounded Context به ریزسرویس (Modules to Services)
نویسنده مسیر تکاملی مهمی را پیشنهاد میدهد: ابتدا Bounded Contextها را به عنوان ماژول در داخل یک سیستم یکپارچه مدل کنید، و سپس این ماژولها را به ریزسرویسهای مجزا تبدیل کنید .
قانون طلایی: هرگز از ابتدا برای یک دامنه ناشناخته شروع به ساخت ریزسرویس نکنید. داشتن یک کدبیس موجود برای تجزیه، بسیار آسانتر از شروع مستقیم با ریزسرویسها از صفر است .
قابلیتهای کسبوکاری، نه داده (Business Capabilities, Not Data)
نویسنده هشدار میدهد که وقتی در مورد Bounded Contextها فکر میکنید، نباید ذهنیتتان «اشتراکگذاری داده» باشد، بلکه باید از خود بپرسید: «این Context چه قابلیتی به بقیه سیستم ارائه میدهد؟» . مثلاً انبار «توانایی ارائه لیست موجودی» را عرضه میکند، نه اینکه صرفاً «داده موجودی را در اختیار میگذارد».
تودرتویی Bounded Contextها (Turtles All the Way Down)
Bounded Contextها میتوانند در درون خود، Contextهای ریزتری داشته باشند .
۱. تحلیل انتقادی/فنی
خطر Anemic Domain Model در ریزسرویسها: نویسنده به درستی از سرویسهای CRUD-based به عنوان Anti-Pattern یاد میکند. در اکوسیستم .NET، این الگوی مخرب به شکل پروژههایی ظاهر میشود که تنها یک لایه Repository در قالب یک Controller HTTP هستند. برای جلوگیری از این مشکل، هر ریزسرویس باید یک Aggregate Root از دیدگاه DDD داشته باشد که منطق کسبوکار را درون خود캡سوله کند، نه اینکه صرفاً یک CRUD Layer روی دیتابیس باشد.
تله «Nano-Services» در تودرتویی بیش از حد: نویسنده پیشنهاد میدهد Bounded Contextهای تودرتو را به ریزسرویسهای جداگانه تبدیل کنید. اما باید هوشیار باشید که تقسیم بیش از حد میتواند به Nano-Services منجر شود؛ یعنی سرویسهایی به قدری کوچک که بیشتر وقتشان صرف Network Roundtrips و Orchestration میشود تا منطق واقعی. معیار تشخیص این Anti-Pattern در داتنت ساده است: اگر یک سرویس هیچ Domain Logic ای ندارد و تنها دادهای را از یک جدول میخواند و برمیگرداند، احتمالاً باید با سرویس بزرگتر مرتبطش ادغام شود.
یکپارچهسازی (Integration): مهمترین چالش فنی ریزسرویسها
نویسنده در ابتدای فصل چهارم صریحاً اعلام میکند که یکپارچهسازی صحیح بین سرویسها، مهمترین جنبه فنی در معماری ریزسرویس است .
نویسنده چهار معیار اساسی برای انتخاب تکنولوژی یکپارچهسازی تعریف میکند :
- جلوگیری از تغییرات شکننده (Avoid Breaking Changes): تکنولوژی انتخابی باید تضمین کند که افزودن یک فیلد جدید به یک سرویس، مصرفکنندگان فعلی را مختل نکند .
- خنثی بودن نسبت به تکنولوژی (Technology-Agnostic APIs): APIها نباید مصرفکننده را به یک زبان یا پلتفرم خاص وابسته کنند .
- پنهان کردن جزئیات پیادهسازی داخلی (Hide Internal Details): مصرفکنندهها نباید بدانند سرویس چگونه دادههایش را ذخیره میکند یا از چه تکنولوژیای استفاده میکند .
- سهولت مصرف (Easy to Consume): یک API خوب باید ساخت یک کلاینت با هر زبانی را آسان کند .
یکپارچهسازی از طریق پایگاه داده مشترک (Shared Database): بدترین Anti-Pattern
رایجترین الگویی که نویسنده با آن مواجه شده، یکپارچهسازی از طریق پایگاه داده است؛ یعنی سرویسهای مختلف مستقیماً به دیتابیس یکدیگر دسترسی دارند :
- هر سرویسی که به دیتابیس سرویس دیگری دسترسی داشته باشد، به ساختار داخلی آن وابسته (Coupled) میشود. تغییر هر ستون یا جدول، میتواند تعداد زیادی سرویس را خراب کند.
- منطق کسبوکار (Business Logic) مربوط به یک موجودیت (مثل Customer) بین چندین سرویس پراکنده میشود و هیچکس مالکیت واقعی آن را ندارد.
- امکان تغییر تکنولوژی ذخیرهسازی به کلی از بین میرود، زیرا تمام سرویسها به ساختار فیزیکی دیتابیس وابستهاند.
ارتباط همگام در برابر ناهمگام (Synchronous vs. Asynchronous)
پیش از بررسی تکنولوژیهای مشخص، نویسنده بر یک انتخاب بنیادین تأکید میکند: سبک ارتباطی بین سرویسها .
| ویژگی | همگام (Synchronous) | ناهمگام (Asynchronous) |
|---|---|---|
| سادگی درک | بالا، نتیجه فوری مشخص است | پیچیدهتر |
| مناسب برای | تراکنشهای کوتاه، نیاز فوری به نتیجه | کارهای طولانی، تأخیر شبکه بالا |
| کوپلینگ | بیشتر (کلاینت منتظر میماند) | کمتر (کلاینت رها میکند و ادامه میدهد) |
| تابآوری | کمتر (خرابی سرویس = بلاک کلاینت) | بیشتر (پیام در صف منتظر میماند) |
این دو سبک، دو مدل همکاری ایجاد میکنند :
- Request/Response: کلاینت درخواست میفرستد و منتظر پاسخ میماند (یا یک Callback ثبت میکند).
- Event-Based (Choreography): سرویس یک رویداد (Event) منتشر میکند و برایش اهمیت ندارد که چه کسی آن را مصرف میکند. این رویکرد وابستگی را به شدت کاهش میدهد .
۱. تحلیل انتقادی/فنی
تلهی ارتباط همگام زنجیرهای (Synchronous Chain Problem): نویسنده تفاوت همگام و ناهمگام را به خوبی توضیح میدهد، اما یک خطر پنهان و مرگبار را به صراحت مطرح نمیکند: زنجیرهی همگام عمیق (Deep Synchronous Chain). تصور کنید سرویس A سرویس B را فراخوانی میکند، B منتظر C میماند، C منتظر D. در این حالت، Latency نهایی برابر با مجموع تمام تأخیرها است و خرابی D، تمام زنجیره را مسدود میکند. در .NET Core، ابزارهایی مانند HttpClientFactory با تنظیم دقیق Timeout و Policy (از طریق Polly) میتوانند این خطر را کنترل کنند، اما بهترین راهحل بنیادی، شکستن زنجیره با استفاده از رویکرد ناهمگام (مثل استفاده از MassTransit با RabbitMQ یا Azure Service Bus) است.
خطر Shared Database در پروژههای .NET میراث (Legacy): در پروژههای .NET قدیمی، اشتراکگذاری DbContext یا حتی Connection String بین چندین سرویس یک Anti-Pattern بسیار رایج است. حتی اگر قرارداد در لایه Application متفاوت باشد، وجود یک دیتابیس مشترک (Shared Database) عملاً هر تلاشی برای استقلال سرویسها را خنثی میکند. اولین گام در مهاجرت از Monolith به Microservice در .NET، جداسازی Schema است، نه جداسازی کد.
فراخوانی رویه راهدور (Remote Procedure Call - RPC)
RPC تکنیکی است که یک فراخوانی محلی را به گونهای جلوه میدهد که گویی روی یک سرویس راهدور اجرا میشود .
نویسنده سه نقطه ضعف اساسی RPC را بررسی میکند:
۱. وابستگی تکنولوژیکی (Technology Coupling): برخی پیادهسازیهای RPC مانند Java RMI هم کلاینت و هم سرور را به یک پلتفرم خاص وابسته میکنند . این خود نوعی افشای جزئیات پیادهسازی داخلی است که دقیقاً با اصل خودمختاری سرویسها در تضاد است.
۲. فراخوانی محلی با فراخوانی راهدور تفاوت بنیادین دارد: تعداد زیادی از فراخوانیهای درونپردازشی (In-process) بدون نگرانی از پرفورمنس قابل اجرا هستند، اما در RPC، هزینه سریالایز و دیسریالایز پیامها (Marshalling/Unmarshalling) به علاوه تأخیر شبکه، قابل چشمپوشی نیست .
۳. شکنندگی (Brittleness): مثال عملی از کتاب: یک شیء Customer داریم که فیلد age دارد. هیچ کلاینتی این فیلد را استفاده نمیکند، اما وقتی سرور آن را حذف میکند، کد دیسریالایزر در سمت تمام کلاینتها خراب میشود .
انتقال وضعیت نمایشی (REST) و HTTP
REST یک سبک معماری است که از وب الهام گرفته شده و بر مفهوم منبع (Resource) بنا شده است؛ هر موجودیتی که سرویس از آن آگاه است (مثل Customer) یک منبع محسوب میشود .
ترکیب HTTP و REST: پروتکل HTTP به خوبی با REST همخوانی دارد. فعلهای HTTP (GET، POST، PUT، DELETE) معنای از پیش تعریفشدهای دارند که با عملیات روی منابع منطبق است .
HATEOAS (Hypermedia as the Engine of Application State): یکی از اصول پیشرفتهتر REST که نویسنده بر آن تأکید ویژه دارد این است که پاسخهای سرور باید حاوی لینکهایی به عملیات و منابع مرتبط باشند .
هماهنگسازی در برابر کوریوگرافی (Orchestration vs. Choreography)
برای پیادهسازی فرآیندهای کسبوکاری که چندین سرویس را درگیر میکنند، نویسنده دو رویکرد متفاوت را با مثال ثبتنام مشتری جدید در MusicCorp مقایسه میکند (ایجاد رکورد امتیاز وفاداری + ارسال بسته خوشآمدگویی + ارسال ایمیل خوشآمدگویی) :
| ویژگی | Orchestration | Choreography |
|---|---|---|
| ساختار | یک سرویس مرکزی (مغز) تمام مراحل را هدایت میکند | هر سرویس به رویدادها واکنش مستقل نشان میدهد |
| وضوح فرآیند | بالا؛ flow کسبوکار در کد منعکس است | پایینتر؛ flow به صورت ضمنی توزیعشده است |
| کوپلینگ | بیشتر؛ سرویس مرکزی به همه وابسته است | کمتر؛ سرویسها فقط رویدادها را میشناسند |
| ریسک | سرویس مرکزی تبدیل به «God Service» میشود | نیاز به مانیتورینگ پیچیدهتر برای ردیابی فرآیند |
نویسنده صراحتاً رویکرد Choreography را ترجیح میدهد، زیرا سیستمهای مبتنی بر Choreography از نظر وابستگی سستتر، منعطفتر و آسانتر برای تغییر هستند .
همکاری ناهمگام مبتنی بر رویداد (Asynchronous Event-Based Collaboration)
برای پیادهسازی ارتباطات ناهمگام، نویسنده دو گزینه اصلی را بررسی میکند :
- Message Broker (مثل RabbitMQ): یک سیستم مستقل که انتشار و اشتراک رویدادها را مدیریت میکند. مقیاسپذیر، قابلاطمینان و امکانات غنی دارد، اما پیچیدگی عملیاتی اضافه میکند .
- ATOM (مبتنی بر HTTP): یک فید REST-compliant که مصرفکنندگان آن را Poll میکنند. سادهتر است، اما برای نیازهای پیچیده مانند Competing Consumer Pattern، باید بسیاری از رفتارهایی که Broker به صورت آماده ارائه میدهد را دستی پیادهسازی کنید .
نویسنده یک داستان هشداردهنده (Cautionary Tale) نقل میکند: در یک سیستم قیمتگذاری بانکی، یک باگ باعث میشد که Worker پردازشگر پیام خراب شود و آن پیام دوباره به صف بازگردد تا Worker بعدی هم خراب شود — یک «خرابی فاجعهبار آبشاری» (Catastrophic Failover). راهحل: تعیین حداکثر تعداد تلاش مجدد (Retry Limit) و پیادهسازی «صف نامه مرده» (Dead Letter Queue) برای پیامهای معیوب .
اصل DRY در دنیای ریزسرویسها (DRY and the Perils of Code Reuse)
نویسنده یک پارادوکس مهم را مطرح میکند: اصل DRY (Don’t Repeat Yourself) در داخل یک سرویس واحد کاملاً درست است، اما بین سرویسهای مجزا میتواند خطرناک باشد .
۱. تحلیل انتقادی/فنی
ضعف HATEOAS در دنیای واقعی: نویسنده خودش هم اذعان میکند که HATEOAS در عمل کمتر از چیزی که باید استفاده میشود ** + Versioned Endpoints استفاده میکنند. این رویکرد عملیتر است، اما Coupling بیشتری ایجاد میکند. اگر در پروژه شما کلاینتهای خارجی و متعدد وجود دارند، سرمایهگذاری روی HATEOAS توجیه دارد؛ در غیر این صورت، Versioning دقیق و مستندسازی با OpenAPI کافی است.
Dead Letter Queue؛ اجبار، نه اختیار: داستان بانک که نویسنده نقل میکند، در حقیقت یک قانون طلایی را آشکار میکند: در هر سیستم مبتنی بر Message Queue، پیادهسازی Dead Letter Queue و مکانیزم مانیتورینگ آن اجباری است. در داتنت با MassTransit (روی RabbitMQ یا Azure Service Bus)، این قابلیت به صورت توکار (Built-in) وجود دارد. اما بسیاری از تیمها در هیجان پیادهسازی ارتباط ناهمگام، این ملاحظه را فراموش میکنند و تا زمانی که در پروداکشن به مشکل نخورند، متوجه اهمیت آن نمیشوند.
نسخهبندی و مدیریت تغییرات شکننده (Versioning & Breaking Changes)
در هر صحبتی درباره ریزسرویسها، نویسنده همیشه با این سوال مواجه میشود: «نسخهبندی را چطور مدیریت میکنید؟» . این سوال نگرانی مشروعی دارد، چون دیر یا زود باید رابط یک سرویس تغییر کند.
۱. به تعویق انداختن تغییرات شکننده (Defer Breaking Changes): بهترین راه مقابله با تغییرات شکننده، اجتناب از وقوع آنها در وهله اول است . نویسنده دو تکنیک اساسی برای این منظور معرفی میکند:
اصل Tolerant Reader (خواننده بردبار): کلاینت باید فقط فیلدهایی را که واقعاً نیاز دارد از پیام بخواند و مابقی را نادیده بگیرد .
قانون Postel (اصل استحکام - Robustness Principle): «در آنچه انجام میدهی محافظهکار باش، در آنچه از دیگران میپذیری آزاداندیش.» این اصل که در ابتدا برای پروتکلهای شبکه تعریف شده، در دنیای ریزسرویسها نیز کاملاً صدق میکند.
۲. نسخهبندی معنایی (Semantic Versioning): شکل هر نسخه به صورت MAJOR.MINOR.PATCH است :
- MAJOR: تغییرات ناسازگار با نسخه قبل (Breaking Changes)
- MINOR: افزودن قابلیت جدید با حفظ سازگاری عقبگرد (Backward Compatible)
- PATCH: رفع باگ در عملکرد موجود
یعنی اگر سرویس Customer نسخه 1.2.0 دارد، اپلیکیشن Helpdesk بدون هیچ تغییری با نسخه 1.3.0 کار میکند، اما باید برای نسخه 2.0.0 بازنویسی شود .
۳. همزیستی چند Endpoint (Coexist Different Endpoints): اگر از تغییر شکننده گریزی نبود، راهحل توصیهشده نویسنده این است که هر دو نسخه قدیم و جدید API را به طور همزمان در همان سرویس در دسترس بگذارید .
این رویکرد در داخل از الگوی Expand and Contract پیروی میکند: ابتدا قابلیتهای جدید را در کنار قدیمی اضافه کنید (Expand)، و پس از اتمام مهاجرت مشتریان، قابلیتهای قدیمی را حذف کنید (Contract) .
گزینهای که نویسنده توصیه نمیکند: اجرای نسخههای مختلف از یک سرویس به صورت کاملاً جدا در محیط Production (مثل سرویس v1 در کنار سرویس v2). این کار باعث میشود که برای رفع یک باگ مجبور شوید دو Codebase مجزا را نگه دارید، مدیریت دادههای مشترک پیچیده شود و منطق مسیریابی به Middleware منتقل شود — که همگی از استقلال سرویسها میکاهند .
رابط کاربری: لایه ترکیب (User Interface as Compositional Layer)
نویسنده رابط کاربری را نه یک موجودیت مستقل، بلکه «لایهای ترکیبی» میداند که قابلیتهای سرویسهای مختلف را به هم میبافد . سه مدل اصلی برای این ترکیب وجود دارد:
۱. ترکیب مستقیم API (API Composition): رابط کاربری مستقیماً با APIهای هر سرویس صحبت میکند .
۲. ترکیب قطعات UI (UI Fragment Composition): هر سرویس بخشی از رابط کاربری را مستقیماً سرو میکند (مانند یک Widget توصیه محصولات) .
۳. الگوی Backends for Frontends (BFF): برترین گزینه از دید نویسنده این است که برای هر نوع رابط کاربری (موبایل، وب، پنل مدیریت) یک Backend اختصاصی تعریف شود .
یکپارچهسازی با نرمافزار شخص ثالث (Third-Party Software Integration)
سازمانها همیشه نرمافزارهایی میخرند که نمیتوانند آنها را تغییر دهند (COTS یا SaaS). نویسنده یک قانون ساده ارائه میدهد :
- اگر قابلیتی در کسبوکار شما منحصربهفرد و مزیت رقابتی محسوب میشود → آن را خودتان بسازید.
- اگر استفاده از ابزار در سازمان شما ویژگی خاصی ندارد (مثل سیستم حقوق و دستمزد) → آن را بخرید.
۱. تحلیل انتقادی/فنی
تلهی BFF که به God Service تبدیل میشود: نویسنده درباره خطر اضافه شدن منطق کسبوکار به BFF هشدار میدهد . در پروژههای .NET با ASP.NET Core، این Anti-Pattern معمولاً به این شکل ظاهر میشود که تدریجاً Validation Logic، Orchestration Logic و حتی تراکنشهای چندسرویسی وارد BFF میشوند. برای پیشگیری، یک قانون سختگیرانه تعریف کنید: BFF فقط مجاز به Map کردن، Filtering و Aggregation پاسخهاست. هر منطقی که شامل عبارت «اگر کاربر شرط X را داشت آنگاه Y را انجام بده» باشد، باید در داخل سرویس Domain مربوطه قرار گیرد.
اصل Tolerant Reader و ابزارهای مدرن .NET: نویسنده از XPath برای پیادهسازی Tolerant Reader یاد میکند. در دنیای .NET Core،
System.Text.Jsonبا تنظیمJsonSerializerOptions.UnknownTypeHandling = JsonUnknownTypeHandling.JsonNodeو استفاده از[JsonExtensionData]همین رفتار را به صورت توکار فراهم میکند. با این حال، در پروژههایی که از MassTransit برای ارتباطات ناهمگام استفاده میکنید، باید توجه داشته باشید که Contractهای پیام را به صورت مستقل در یک پروژه مجزایContractsنگهداری کنید و هرگز مدلهای Domain را مستقیماً در پیامها استفاده نکنید؛ در غیر این صورت، یکی از مخربترین اشکال Coupling را دقیقاً در لایهای ایجاد کردهاید که باید آزادترین لایه باشد.
فصل پنجم: تجزیه مونولیت (Splitting the Monolith)
نویسنده این فصل را با یک واقعیت تلخ شروع میکند: اکثر ما با کدبیسهایی سروکار داریم که قبلاً وجود داشتهاند و ساختار ریزسرویسی ندارند ، این موجودیتهای بزرگ را تجزیه کنیم؟
همهچیز درباره درزها (It’s All About Seams)
مایکل فدرز در کتاب Working Effectively with Legacy Code مفهوم درز (Seam) را تعریف میکند: بخشی از کد که میتوان آن را جداگانه دید و بدون تأثیر بر بقیه Codebase روی آن کار کرد .
بهترین درزها، Bounded Contextهایی هستند که در فصل سوم بررسی کردیم، زیرا به تعریف هم انسجام بالا و هم کوپلینگ پایین دارند :
- Catalog: همه چیز درباره متادیتای آیتمهای قابل فروش
- Finance: گزارشات مالی، پرداختها و بازپرداختها
- Warehouse: ارسال سفارشات، مدیریت موجودی
- Recommendation: سیستم پیشنهاد محصول
پس از این سازماندهی، ابزارهایی مانند Structure101 میتوانند وابستگیهای بین Packageها را به صورت بصری نمایش دهند .
کجا تجزیه را شروع کنیم؟ (The Reasons to Split the Monolith)
نویسنده هشدار میدهد که تجزیه مونولیت را تدریجی و هدفمند انجام دهید، نه به خاطر تجزیه صرف . چهار دلیل اصلی برای اولویتبندی انتخاب اولین سرویس وجود دارد:
| دلیل | توضیح |
|---|---|
| سرعت تغییر (Pace of Change) | اگر بخشی از سیستم تغییرات زیادی دارد، جداسازی آن سرعت را بالا میبرد |
| ساختار تیم (Team Structure) | اگر تیمها به صورت جغرافیایی یا سازمانی جدا هستند، مرزهای سرویس را با آنها همراستا کنید |
| امنیت (Security) | دادههای حساس مالی میتوانند در یک سرویس مجزا با کنترل دسترسی سختگیرانهتر محافظت شوند |
| تکنولوژی (Technology) | اگر یک تیم میخواهد زبان یا فریمورک جدیدی را امتحان کند، جداسازی آن بخش این آزادی را میدهد |
یک نکته فنی مهم: کمترین وابستگی را اول جدا کنید ببینید، راحتتر میتوانید آنهایی را که کمتر به بقیه وابستهاند، شناسایی کنید.
چالش اصلی: پایگاه داده (The Database Problem)
نویسنده میگوید پایگاه داده «مادر همه وابستگیهای درهمتنیده» است . سه مشکل رایج در پایگاهداده مشترک وجود دارد که باید حل شوند:
۱. شکستن روابط کلید خارجی (Breaking Foreign Key Relationships): تصور کنید کد Finance برای تولید گزارش ماهانه، مستقیماً وارد جدول line_item که متعلق به Catalog است میشود تا نام محصول را بخواند
۲. دادههای استاتیک مشترک (Shared Static Data): مثال کلاسیک: جدول country_codes که همه سرویسها از آن میخوانند . سه راهحل وجود دارد:
- تکرار این جدول در هر سرویس (ریسک ناسازگاری دارد)
- تبدیل این داده به Configuration File یا Enum در کد (توصیه اصلی نویسنده)
- ساختن یک سرویس مستقل فقط برای این دادهها (معمولاً اضافهکاری است)
۳. جداول مشترک (Shared Tables): مثال: جدول line_item که هم Catalog نام آلبوم را در آن ذخیره میکند و هم Warehouse تعداد موجودی را .
مرحلهبندی تجزیه (Staging the Break)
نویسنده یک توصیه حیاتی دارد که اکثر تیمها نادیده میگیرند :
ابتدا Schema را جدا کنید، بعد کد را.
یعنی سه مرحله وجود دارد:
- کد یکجا، Schema یکجا (وضعیت اولیه)
- کد یکجا، Schema جدا ← در اینجا خطاها را میگیرید و Roll Back آسان است
- کد جدا، Schema جدا (حالت نهایی ریزسرویس)
مرزهای تراکنشی (Transactional Boundaries): سختترین چالش
وقتی Schema را تقسیم میکنید، تراکنش ACID را از دست میدهید :
- تلاش مجدد (Try Again Later): عملیات ناموفق را در یک صف قرار دهید و بعداً دوباره تلاش کنید. این در واقع Eventual Consistency است.
- لغو کل عملیات (Abort + Compensating Transaction): یک تراکنش جبرانی برای Rollback اجرا کنید. اما اگر خود این تراکنش جبرانی هم شکست بخورد، پیچیدگی به شدت بالا میرود .
- تراکنش توزیعشده (Distributed Transaction با Two-Phase Commit): مکانیزمی که یک Transaction Manager مرکزی همه شرکتکنندگان را هماهنگ میکند. نویسنده هشدار میدهد که این الگوریتمها بسیار سختاند، به Contention و Lock منجر میشوند، و Scale کردن سیستم را دشوار میکنند .
توصیه نهایی نویسنده: هر بار که با عملیاتی مواجه شدید که نیاز به تراکنش دارد، بپرسید: «آیا واقعاً باید همزمان اتفاق بیفتد؟» اگر Eventual Consistency کافی است، از آن استفاده کنید .
۱. تحلیل انتقادی/فنی
تله Two-Phase Commit در .NET: در اکوسیستم .NET،
System.Transactions.TransactionScopeامکان اجرای Distributed Transaction را فراهم میکند. اما تجربه عملی نشان میدهد که در معماری ریزسرویس، هر بار که یک توسعهدهنده بهTransactionScopeدر یک فراخوانی بینسرویسی متوسل میشود، در واقع یک Coupling عمیق و شکننده ایجاد کرده است. راهحل عملی در .NET: Outbox Pattern با استفاده از کتابخانههایی مانند MassTransit یا NServiceBus، که عملیات نوشتن در DB و انتشار پیام را اتمیک میکند بدون نیاز به Distributed Transaction.ابزار SchemaSpy: نویسنده از SchemaSpy برای مصورسازی وابستگیهای جداول یاد میکند. در پروژههای .NET با EF Core، ابزار EF Core Power Tools و همچنین بررسی دستی Migrationها میتواند این تصویر را بدهد. اما مهمتر از ابزار، این اصل است: هر Foreign Key بین جداول دو Bounded Context مختلف، یک زنگ خطر است که باید مورد بررسی قرار گیرد.
فصل ششم: استقرار (Deployment)
استقرار یک مونولیت فرآیندی نسبتاً ساده است، اما ریزسرویسها به دلیل وابستگیهای متقابل، داستان کاملاً متفاوتی دارند . اگر رویکرد درستی برای استقرار انتخاب نکنید، این حوزه میتواند زندگی را برایتان دشوار کند.
یکپارچهسازی پیوسته (Continuous Integration - CI)
نویسنده ابتدا مفهوم CI را مرور میکند، زیرا چارچوب ذهنی تمام تصمیمهای بعدی را شکل میدهد .
جز هامبل سه سوال ساده برای سنجش اینکه آیا واقعاً CI را انجام میدهید، ارائه میدهد :
- آیا روزانه حداقل یک بار به Mainline چکاین میکنید؟ اگر کد شما مکرراً با بقیه ادغام نشود، یکپارچهسازی آینده سختتر میشود.
- آیا یک مجموعه تست برای اعتبارسنجی تغییرات دارید؟ بدون تست، فقط میدانید که کد از نظر نحوی درست است، نه اینکه رفتار سیستم را نشکسته باشید.
- وقتی Build خراب میشود، آیا اولین اولویت تیم رفع آن است؟ یک Build قرمز یعنی آخرین تغییر به احتمال زیاد به درستی ادغام نشده؛ نباید اجازه داد تغییرات جدید روی آن انباشته شوند .
نگاشت CI به ریزسرویسها (Mapping CI to Microservices)
این بخش یکی از مهمترین تصمیمهای معماری فنی است. نویسنده سه گزینه را بررسی میکند :
| گزینه | توضیح | مشکل اصلی |
|---|---|---|
| یک Repository، یک Build برای همه سرویسها | سادهترین مدل، تمام کد در یک جا | تغییر یک خط در یک سرویس، Build همه را تریگر میکند؛ نمیدانید کدام Artifact باید Deploy شود |
| یک Repository، چند Build جداگانه | هر بخشی از Source Tree به یک Build مجزا Map میشود | چکاین همزمان برای چند سرویس را آسان میکند و وسوسه Coupling را بالا میبرد |
| یک Repository و یک Build به ازای هر سرویس ← توصیه نویسنده | هر میکروسرویس Repository و Pipeline مستقل دارد | مدیریت تغییرات Cross-Repository کمی پیچیدهتر است، اما قابل حل است |
نویسنده صراحتاً میگوید: مالکیت سرویس یعنی مالکیت Repository و Build . هر تیم که سرویس را میسازد، باید Pipeline آن را هم کنترل کند.
Pipeline استقرار و تحویل پیوسته (Build Pipeline & CD)
نویسنده فلسفه Continuous Delivery را چنین تعریف میکند: هر چکاین را به عنوان یک Release Candidate در نظر بگیرید . یک Build Pipeline چندمرحلهای این ایده را پیادهسازی میکند:
1
Compile → Fast Tests → Slow Tests → UAT → Performance Testing → Production
یک Artifact واحد در ابتدای Pipeline ساخته میشود و در تمام مراحل بدون تغییر جابجا میشود .
استثنا در پروژههای جدید (Greenfield): در ابتدای پروژه، وقتی مرزهای سرویسها هنوز ناپایدار هستند، میتوان موقتاً همه سرویسها را در یک Build نگه داشت .
Artifactها: چه چیزی را Deploy میکنیم؟
نویسنده سه نوع Artifact را بررسی میکند:
۱. Artifactهای مختص پلتفرم (Platform-Specific Artifacts): مثل Ruby Gems، Java JARها یا Python Eggs. مشکل اینجاست که اگر یک تیم باید سرویسهایی با سه تکنولوژی متفاوت را Deploy کند، با سه مکانیزم استقرار متفاوت دست و پنجه نرم میکند .
۲. پکیجهای سیستمعامل (OS Artifacts): ساخت RPM برای RedHat یا Deb Package برای Ubuntu. مزیت بزرگ اینجاست که از دید استقرار، دیگر نگران تکنولوژی زیرین نیستید؛ فقط ابزار OS را اجرا میکنید .
۳. Image ماشین مجازی (Custom VM Image) ← رویکرد Netflix: به جای اینکه هر بار Dependency ها را نصب کنید، یک VM Image میسازید که Dependencyها در آن «Bake» شدهاند .
ابزار Packer از HashiCorp این فرآیند را ساده میکند: با یک فایل پیکربندی میتوانید Image برای VMware، AWS AMI، Vagrant و Rackspace به صورت همزمان بسازید .
Docker: بهترین ترکیب دو دنیا
نویسنده Docker را به عنوان یک «PaaS سبکوزن روی یک ماشین» توصیف میکند .
مزیت اصلی Docker نسبت به VM های کامل این است که Image ها بسیار سبکتر هستند و میتوان تعداد زیادی Container را روی یک Host اجرا کرد .
یک سرویس، یک Host (Single Service Per Host)
نویسنده این اصل را با قاطعیت توصیه میکند: از قرار دادن چندین سرویس روی یک Host پرهیز کنید . دلایل زیر این توصیه را پشتیبانی میکنند:
- اگر سرویس A تمام منابع CPU را مصرف کند، سرویس B که روی همان Host است آسیب میبیند، بدون اینکه خودش مشکلی داشته باشد.
- Deploy کردن یک سرویس بدون اثرگذاری بر سرویس دیگر پیچیدهتر میشود.
- مانیتورینگ و Logging جداسازی واضحتری نیاز دارند.
مدیریت پیکربندی (Service Configuration)
یک اشتباه رایج این است که به ازای هر محیط (Test، Staging، Production) یک Artifact مجزا بسازید که پیکربندی داخل آن Bake شده باشد
راهحل صحیح: یک Artifact بسازید، پیکربندی را جداگانه مدیریت کنید باید خارج از Artifact و ایدهآلاً از یک سیستم متمرکز مدیریت پیکربندی بارگذاری شوند.
Immutable Server: هرگز به Production دست نزنید
یکی از جذابترین مفاهیم فصل ششم، الگوی Immutable Server است .
این الگو مشکل Configuration Drift را حل میکند — وضعیتی که در آن پیکربندی واقعی سرور با آنچه در Source Control است تفاوت پیدا کرده .
اتوماسیون: قلب تپنده همه چیز
نویسنده یک مثال واقعی از RealEstate.com.au در استرالیا نقل میکند .
۱. تحلیل انتقادی/فنی
Docker در ۲۰۲۶ دیگر «نو» نیست: نویسنده کتاب را در دورانی نوشته که Docker تازه در حال ظهور بود. امروز در دنیای .NET، Kubernetes به همراه Helm Charts استاندارد de facto برای مدیریت Container ها در سطح Production است. اگر تیم شما کوچک است، Azure Container Apps یا AWS ECS Fargate میتوانند بسیاری از پیچیدگیهای Kubernetes را پنهان کنند.
پیکربندی در .NET: توصیه نویسنده برای جداسازی Configuration از Artifact در دنیای .NET با
appsettings.{Environment}.json+ Environment Variables + Azure App Configuration یا AWS Parameter Store پیادهسازی میشود. مهمترین نکته: هرگز Connection String یا Secret را داخلappsettings.jsonکه به Source Control میرود قرار ندهید. برای این کار از Azure Key Vault یا HashiCorp Vault استفاده کنید.
فصل هفتم: تست کردن (Testing)
دنیای تست خودکار پیشرفتهای چشمگیری داشته، اما چالشها هنگامی که عملکرد سیستم در یک معماری توزیعشده پراکنده میشود، متفاوت و پیچیدهتر میشوند . این فصل میخواهد ابزار ذهنی دقیقی برای پاسخ به یک سوال اساسی فراهم کند: چه نوع تستی، چقدر، و در کجا باید انجام شود؟
هرم تست (The Test Pyramid)
مایک کوهن مدلی به نام هرم تست معرفی میکند که سه سطح تست را از پایین به بالا نشان میدهد :
1
2
3
▲ End-to-End Tests ← کم، کند، اطمینان بالا
▲▲ Service Tests ← متوسط
▲▲▲ Unit Tests ← زیاد، سریع، ایزولاسیون بالا
هر چه به بالای هرم میروید، اطمینان به درستی کل سیستم بیشتر میشود، اما چرخه بازخورد (Feedback Cycle) کندتر میشود و پیدا کردن علت خرابی سختتر . هر چه به پایین هرم میآیید، تستها سریعتر، ایزولهتر و دقیقتر در نشان دادن مکان خرابی هستند.
Anti-Pattern هرم وارونه (Test Snow Cone): وقتی تعداد Unit Testها کم و تعداد End-to-End Testها زیاد باشد، یک هرم وارونه داریم . این الگو باعث میشود Build Pipeline بسیار کند شود، چرخه بازخورد طولانی گردد و تیم اغلب ندانند که دقیقاً کدام بخش از کد مشکل دارد.
سطوح سهگانه تست
۱. Unit Test: کوچکترین سطح تست است که معمولاً یک تابع یا متد را به صورت ایزوله بررسی میکند. هیچ سرویس خارجی، دیتابیس یا شبکهای درگیر نیست . هدف اصلی آنها این است که سریعترین بازخورد ممکن را به توسعهدهنده بدهند.
۲. Service Test: این تستها برای بررسی قابلیتهای یک سرویس به صورت مستقل طراحی شدهاند. برای دستیابی به این ایزولاسیون، تمام Collaboratorهای خارجی Stub میشوند .
Stub در مقابل Mock:
- Stub: پاسخ ثابت برمیگرداند. اهمیتی نمیدهد که چند بار فراخوانی شود.
- Mock: علاوه بر پاسخ دادن، بررسی میکند که آیا فراخوانی اصلاً صورت گرفته یا نه. اگر فراخوانی انتظار داشته انجام نشود، تست شکست میخورد .
نویسنده توصیه میکند در Service Testها بیشتر از Stub استفاده کنید، نه Mock، زیرا Mockهای بیش از حد تستها را شکننده میکنند .
۳. End-to-End Test: این تستها چندین سرویس را با هم Deploy میکنند و سپس سناریوهای کاربر واقعی را روی کل سیستم اجرا میکنند . میتوانند از طریق Browser Driver یا شبیهسازی API Call باشند. اطمینان بالایی میدهند اما با مشکلات جدی همراهاند.
مشکلات End-to-End Testing
نویسنده با صراحت میگوید که End-to-End Testها در معماری ریزسرویس معضلساز هستند :
- Flaky Tests (تستهای ناپایدار): هرچه تعداد Moving Partهای درگیر در تست بیشتر باشد، احتمال خرابی تصادفی بالاتر است — یک قطع موقت شبکه، یک سرویس که کند پاسخ میدهد، یا یک Race Condition. تیمهایی که تستهای ناپایدار دارند و آنها را دوباره اجرا میکنند به جای اینکه اصلاحشان کنند، در دام «عادیسازی انحراف» (Normalization of Deviance) میافتند .
- چرخه بازخورد طولانی: برخی تستها تا یک روز طول میکشند. نویسنده پروژهای را ذکر میکند که Regression Suite کامل آن شش هفته طول میکشید .
- Metaversion: وقتی End-to-End Testها وجود دارند، تیمها به تدریج شروع به فکر کردن درباره «نسخه کل سیستم» میکنند و همه سرویسها را باهم Deploy میکنند — دقیقاً همان چیزی که ریزسرویسها سعی داشتند از آن دور شوند .
- مالکیت مبهم: چه کسی این تستهای مشترک را مینویسد؟ اگر یک تیم متمرکز بنویسد، فاصله بین سازنده سرویس و تست آن بالا میرود؛ اگر همه بنویسند، یک هرجومرج بیپایان خواهیم داشت .
راهحل پیشنهادی نویسنده: به جای تست هر Story با یک End-to-End Test، روی تعداد بسیار کمی از Journey Testها (سفرهای کاربر) تمرکز کنید؛ برای مثال «ثبتنام مشتری جدید»، «خرید یک آلبوم» یا «بازگشت محصول» . در سیستمهای پیچیده هم از اعداد دو رقمی کوچک فراتر نروید.
Consumer-Driven Contracts (CDC): جواب اصلی
بهترین راهحل برای کاهش وابستگی به End-to-End Testها استفاده از قراردادهای مبتنی بر مصرفکننده است . منطق CDC چنین است:
به جای اینکه همه سرویسها را با هم Deploy کنیم تا ببینیم با هم کار میکنند، انتظارات مصرفکننده را به صورت تست کد میکنیم و آن تستها را مستقیماً روی Producer اجرا میکنیم.
برای مثال، هر دو سرویس Helpdesk و Web Shop از Customer Service استفاده میکنند. هر کدام مجموعهای از تستها مینویسند که انتظاراتشان از Customer Service را تعریف میکنند. این تستها در Pipeline Build خودِ Customer Service اجرا میشوند . اگر Customer Service تغییری بدهد که یکی از این CDCها را بشکند، Build فوراً قرمز میشود و دقیقاً مشخص است کدام Consumer آسیب دیده.
ابزار Pact
Pact ابزار Open Source است که ابتدا در RealEstate.com.au توسعه یافت و حالا از Ruby، JVM و .NET پشتیبانی میکند . فرآیند کار به این صورت است:
- Consumer انتظاراتش را با استفاده از Pact DSL تعریف میکند.
- Pact یک Pact Specification File به فرمت JSON میسازد.
- این فایل JSON توسط یک Pact Broker نگهداری میشود.
- Producer این فایل JSON را بارگذاری کرده و درخواستهای مشخصشده را به API خود میزند تا تأیید کند پاسخها منطبق با انتظارات Consumer هستند .
ظرافت کار اینجاست که فرمت JSON زبانآگنوستیک است: یک Consumer نوشتهشده در Ruby میتواند یک Producer نوشتهشده در Java را تست کند .
تست در Production: Blue-Green و Canary
نویسنده میپذیرد که هیچ مجموعه تستی نمیتواند تمام مشکلات را قبل از Production پیدا کند . به همین دلیل دو تکنیک استقرار را برای کاهش ریسک معرفی میکند:
Blue-Green Deployment: دو نسخه از سرویس به طور همزمان در Production وجود دارد، اما فقط یکی ترافیک واقعی را دریافت میکند . نسخه جدید Deploy میشود، Smoke Testها روی آن اجرا میشوند و تنها پس از موفقیت، Load Balancer به آن Switch میکند. اگر مشکلی پیش آمد، Rollback تنها یک تغییر در تنظیمات Load Balancer است.
Canary Releasing: به جای Switch ناگهانی، ترافیک Production به صورت تدریجی به نسخه جدید هدایت میشود — ابتدا ۱٪، سپس ۵٪، سپس ۲۰٪ .
MTBF در برابر MTTR
نویسنده با یک جمعبندی قدرتمند این فصل را به پایان میرساند نمیکنند. ترکیب Blue-Green Deployment + مانیتورینگ خوب + Rollback سریع اغلب ارزش بیشتری نسبت به افزودن صد تست End-to-End دارد.
۱. تحلیل انتقادی/فنی
Pact در .NET: کتابخانه
PactNetامروز به خوبی پشتیبانی میشود و باxUnitیکپارچه میشود. توصیه عملی این است که Pact Broker را به عنوان بخشی از Infrastructure CI/CD سازمان راهاندازی کنید — میتوانید از نسخه Self-Hosted آن استفاده کنید. نکته مهم: PDCها باید توسط تیم Consumer نوشته و نگهداری شوند، نه تیم Producer. هر بار که تیم Consumer نیاز جدیدی دارد، یک CDC جدید مینویسد و در Pact Broker Publish میکند — این جریان طبیعی تکامل API است.Canary Releasing در Azure: در Azure میتوانید با استفاده از Azure Traffic Manager یا Application Gateway و همچنین قابلیت Deployment Slots در Azure App Service، یک Canary Release ساده پیادهسازی کنید. برای سطح پیشرفتهتر، Azure Container Apps از Traffic Splitting به صورت توکار پشتیبانی میکند و میتوانید ۱۰٪ ترافیک را به Revision جدید هدایت کنید.
فصل هشتم: مانیتورینگ (Monitoring)
تجزیه سیستم به ریزسرویسهای متعدد مزایای زیادی دارد، اما همین کار مانیتورینگ در محیط Production را به مراتب پیچیدهتر میکند . وقتی وبسایت کند میشود، دیگر «مقصر مشخص» وجود ندارد؛ چندین سرویس، چندین شبکه، و چندین Host باید همزمان بررسی شوند.
مسیر تکامل: از یک Host تا چند سرویس
نویسنده با یک مثال ساده شروع میکند و پیچیدگی را به تدریج افزایش میدهد:
مرحله ۱ — یک سرویس، یک Host: در این حالت، مانیتورینگ سه لایه اصلی دارد :
- لایه Host: CPU، RAM و استفاده از دیسک — ابزارهایی مثل Nagios میتوانند هشدار بدهند.
- لایه Logها: برای یک Host کافی است مستقیماً SSH بزنید و لاگها را با grep بررسی کنید.
- لایه Application: حداقل Response Time را از طریق لاگهای Web Server رصد کنید.
مرحله ۲ — یک سرویس، چند Host (پشت Load Balancer): حالا سوال تشخیصی مهمتر میشود: آیا مشکل CPU روی همه Hostها وجود دارد (مشکل از سرویس)، یا فقط روی یک Host (مشکل از آن ماشین خاص)؟ برای این کار باید Metricها را هم به صورت تجمیعی ببینید و هم بتوانید روی یک Host خاص Drill-down کنید.
مرحله ۳ — چند سرویس، چند Host: اینجاست که تخصص واقعی لازم است. SSH-Multiplexing کافی نیست، و هیچ صفحهنمایشی آنقدر بزرگ نیست که همه Terminal ها را نشان دهد . باید به سمت جمعآوری متمرکز داده حرکت کنید.
تجمیع Log: ELK Stack
نویسنده ابزار Logstash + Kibana + Elasticsearch را به عنوان راهحل متمرکزسازی لاگها معرفی میکند :
- Logstash: لاگها را از فرمتهای مختلف میخواند و به سیستمهای پاییندستی ارسال میکند.
- Elasticsearch: موتور ذخیرهسازی و جستجوی لاگها است.
- Kibana: رابط بصری است که میتوانید با Query Syntax در لاگها جستجو کنید، بازه زمانی محدود کنید، یا نمودار تعداد خطاها را در طول زمان ببینید.
قانون طلایی: تمام سرویسها باید لاگهایشان را در یک فرمت استاندارد بنویسند . اگر یک سرویس از فیلد ResponseTime و سرویس دیگر از RspTimeSecs استفاده کند، تجمیع عملاً ناممکن میشود.
ردیابی Metricها: Graphite
برای Metricهای عددی (مثل CPU، Response Time یا تعداد سفارشها)، نویسنده Graphite را توصیه میکند . ویژگی هوشمندانه Graphite در مدیریت حجم داده است: دادههای قدیمیتر خلاصهتر میشوند. برای مثال:
- ۱۰ ثانیه یکبار برای ۱۰ دقیقه اخیر
- ۱ دقیقه یکبار برای ۲۴ ساعت اخیر
- ۳۰ دقیقه یکبار برای چند سال اخیر
این رویکرد بینشِ تاریخی بلندمدت را با هزینه ذخیرهسازی معقول ترکیب میکند و برای برنامهریزی ظرفیت (Capacity Planning) بسیار ارزشمند است.
Metricهای سطح سرویس (Service Metrics)
نویسنده پیشنهاد میدهد که سرویسها علاوه بر Metricهای سیستمی، Metricهای تجاری هم در معرض نمایش بگذارند . دلایل:
- ۸۰٪ از Feature های نرمافزار هرگز استفاده نمیشوند — Metricها به ما میگویند کدام Feature ارزش نگهداری دارد.
- وقتی نسخه جدید را Deploy میکنید و جستجو بر اساس ژانر ناگهان ۲ برابر میشود، چگونه میدانید که این یک باگ است یا رفتار مورد انتظار؟
- دادهای که امروز جمع نکنید، فردا که نیازش دارید دیگر وجود ندارد.
مانیتورینگ معنایی (Semantic Monitoring)
صرف نگاه کردن به CPU و Response Time، به معنای واقعی کلمه به ما نمیگوید «آیا سیستم کار میکند؟» . نویسنده یک رویکرد عملی را از پروژهای در سال ۲۰۰۵ نقل میکند:
تیم میخواست مطمئن شود که قیمتگذاری پرتفولیو در کمتر از ۱۰ ثانیه پس از رویداد بازار انجام میشود. Nagios هر چند دقیقه یکبار یک رویداد جعلی (Synthetic Transaction) وارد صف میکرد. اگر نتیجهای در بازه زمانی مشخص نمیآمد، Nagios هشدار میداد .
این تکنیک Semantic Monitoring نام دارد: به جای اینکه بپرسید «CPU چقدر است؟»، میپرسید «آیا سیستم کارِ اصلیاش را درست انجام میدهد؟» نکته مهم: این رویکرد، Metricهای سطح پایین را جایگزین نمیکند، بلکه مکمل آنهاست.
پیادهسازی آسان: نویسنده اشاره میکند که چون تستهای End-to-End از فصل قبل برای همین منظور طراحی شدهاند، کافی است یک زیرمجموعه از آنها را به صورت دورهای روی محیط Production اجرا کنید
Correlation ID: ردیابی زنجیره فراخوانی
این مفهوم یکی از مهمترین بخشهای فصل هشتم است. تصور کنید یک کاربر ثبتنام میکند؛ پشت صحنه، Payment Service، Postal Service و Email Service صدا زده میشوند . اگر Payment Service خطا بدهد، چطور میفهمید که این خطا ناشی از کدام درخواست اولیه بوده؟
راهحل — Correlation ID: هنگامی که اولین فراخوانی وارد سیستم میشود، یک GUID تولید میشود. این GUID در تمام فراخوانیهای بعدی به عنوان Header ارسال میشود و در تمام لاگها ثبت میشود :
1
2
3
4
5
15-02-2014 16:01:01 Web-Frontend INFO abc-123 Register
15-02-2014 16:01:02 RegisterService INFO abc-123 RegisterCustomer
15-02-2014 16:01:03 PostalSystem INFO abc-123 SendWelcomePack
15-02-2014 16:01:03 EmailSystem INFO abc-123 SendWelcomeEmail
15-02-2014 16:01:03 PaymentGateway ERROR abc-123 ValidatePayment
با داشتن abc-123 میتوانید کل زنجیره فراخوانی را در Kibana در چند ثانیه بازسازی کنید .
هشدار مهم: بزرگترین مشکل Correlation ID این است که معمولاً وقتی کسی به آن نیاز دارد، دیگر دیر شده است . Retrofit کردن آن در سیستم موجود بسیار دشوار است، زیرا باید به صورت استانداردشده در همه سرویسها پیادهسازی شود. توصیه نویسنده: همین حالا شروع کنید، نه وقتی که اولین بحران Production دارید.
Cascading Failure و مانیتورینگ نقاط یکپارچهسازی
یک سناریوی خطرناک: اتصال شبکه بین وبسایت و Catalog Service قطع میشود. هر دو سرویس به تنهایی سالم به نظر میرسند، اما نمیتوانند با هم ارتباط برقرار کنند . اگر تنها Health سرویسهای مجزا را مانیتور کنید، هرگز این مشکل را نمیبینید.
راهحل: هر سرویس باید وضعیت سلامت Dependency های پاییندستیاش را هم در معرض نمایش بگذارد :
- Response Time فراخوانیهای Downstream
- نرخ خطا در ارتباط با سرویسهای پاییندستی
- وضعیت Circuit Breaker (که در فصل ۱۱ بیشتر توضیح داده میشود)
یکپارچهسازی Metricهای تجاری و عملیاتی
نویسنده یک مشکل سازمانی رایج را مطرح میکند: معمولاً Metricهای تجاری (مثل تعداد سفارشها) در سیستمهای Analytics ذخیره میشوند که فقط بخشی از سازمان به آنها دسترسی دارد، در حالی که Metricهای عملیاتی (مثل CPU) در ابزارهای Ops هستند .
نویسنده پیشنهاد میدهد این دو را در یک سیستم واحد ترکیب کنید، زیرا در دنیایی که چندین بار در روز Deploy میکنید، باید فوراً بفهمید که آیا Revenue پس از آخرین Release تغییر کرده است این رویکرد را ممکن میکنند.
خلاصه توصیههای عملی
نویسنده فصل را با یک Checklist جمعبندی میکند:
برای هر سرویس به صورت مجزا:
- حداقل Response Time فراخوانیهای ورودی را رصد کنید
- نرخ خطا و سپس Metricهای Application-level را اضافه کنید
- وضعیت تمام Dependencyهای پاییندستی را در معرض نمایش بگذارید
- لاگها را در یک مکان استاندارد و با فرمت یکسان بنویسید
- Correlation ID را پیادهسازی کنید
برای کل سیستم:
- Metricهای سطح Host را با Metricهای Application تجمیع کنید
- یک ابزار قابل Query برای لاگهای متمرکز داشته باشید
- ابزار ذخیره Metric باید Drill-down تا سطح Host را پشتیبانی کند
- دادههای Metric را به اندازه کافی برای تشخیص Trend نگه دارید
۱. تحلیل انتقادی/فنی
در دنیای .NET امروز: برای Correlation ID از
IHttpContextAccessorوActivity.Current.Idیا کتابخانهOpenTelemetryاستفاده کنید. OpenTelemetry استاندارد صنعتی شده و ازTraceId/SpanIdبه صورت خودکار در تمام فراخوانیهای HTTP و gRPC استفاده میکند. برای Log Aggregation در Azure، Application Insights جایگزین بلوغیافتهای برای ELK Stack است و Correlation ID را به صورت توکار پشتیبانی میکند. Semantic Monitoring با استفاده از Azure Monitor و Custom Metrics بسیار عملیتر از راهحلهای سال ۲۰۱۴ میشود.هشدار معماری: نویسنده در این فصل اشاره میکند که مانیتورینگ یکی از حوزههایی است که باید استاندارد باشد . اگر تیمهای مختلف ابزارهای مختلف استفاده کنند، دیدِ یکپارچه از سیستم غیرممکن میشود. این توصیه در .NET به معنای استفاده از یک Shared NuGet Package برای پیادهسازی Logging و Tracing است، نه اینکه هر تیم کتابخانه خودش را بنویسد.
فصل نهم: امنیت (Security)
امنیت در معماری ریزسرویسها پیچیدگیهای منحصربهفردی دارد؛ ما هم باید از دیدگاه کاربران انسانی به آن بپردازیم و هم از دیدگاه ارتباط سرویسها با یکدیگر .
احراز هویت و مجوزدهی (Authentication & Authorization)
نویسنده ابتدا مفاهیم پایه را روشن میکند :
- Authentication (احراز هویت): تأیید اینکه یک موجودیت (Principal) واقعاً همان کسی است که ادعا میکند.
- Authorization (مجوزدهی): تعیین اینکه آن موجودیت پس از شناسایی، چه کارهایی مجاز به انجام است.
در یک برنامه مونولیت، این دو مکانیزم معمولاً توکار هستند (مثل User Management داخلی Django). در سیستمهای توزیعشده، نمیتوان از کاربران خواست برای هر سرویس جداگانه وارد شوند؛ هدف داشتن یک هویت واحد است که یک بار احراز شود .
Single Sign-On (SSO): یک بار لاگین، همه جا مجاز
رویکرد استاندارد برای این مشکل، استفاده از SSO است. دو پیادهسازی رایج وجود دارند :
| ویژگی | SAML | OpenID Connect |
|---|---|---|
| پروتکل پایه | SOAP | REST / OAuth 2.0 |
| سطح پیچیدگی | بالا | پایینتر |
| Identity Providerهای موجود | بسیار زیاد (شامل Active Directory) | کم (Google، OpenAM، Gluu) |
| وضعیت فعلی | استاندارد Enterprise | آیندهنگرانهتر |
فرآیند کار SSO ساده است: کاربر میخواهد به یک سرویس دسترسی داشته باشد → به Identity Provider هدایت میشود → پس از تأیید هویت، Service Provider اجازه دسترسی میدهد .
SSO Gateway — یک Proxy مرکزی: به جای اینکه هر ریزسرویس خودش با Identity Provider ارتباط برقرار کند، یک Gateway این کار را برای همه انجام میدهد . این Gateway:
- Redirect احراز هویت را مدیریت میکند.
- اطلاعات Principal (مثل نقشها) را به صورت HTTP Header به سرویسهای پاییندستی میفرستد.
- ابزار Shibboleth در ترکیب با Apache میتواند این کار را انجام دهد.
هشدار مهم: Gateway نباید تنها لایه امنیتی باشد. اتکای کامل به آن یعنی تکیه به یک Single Point of Failure. دفاع در عمق یعنی همه لایهها با هم کار کنند .
مجوزدهی دقیقدانه (Fine-Grained Authorization)
Gateway میتواند دسترسی کلی را کنترل کند (مثلاً «فقط کارمندان وارد شوند»)، اما منطق دقیقتر باید درون خود سرویس باشد .
یک Anti-Pattern رایج: برخی سازمانها نقشهایی مثل CALLCENTER_50_DOLLAR_REFUND را در Directory Service تعریف میکنند — یعنی رفتار داخلی یک سرویس را در یک سیستم خارجی مدل میکنند . این رویکرد:
- نگهداری را کابوس میکند.
- Lifecycle مستقل سرویس را از بین میبرد.
- زمانی که منطق سرویس تغییر کند، باید Directory Service هم تغییر کند.
رویکرد صحیح: نقشهای coarse-grained در Directory Service (مثل CALLCENTER و CALLCENTER_TEAMLEADER) و منطق دقیقتر درون سرویس .
احراز هویت سرویسبهسرویس
این بخش از مهمترین بخشهای فصل نهم است. نویسنده پنج رویکرد را بررسی میکند :
۱. اعتماد ضمنی درون Perimeter (Allow Everything Inside): رایجترین رویکردی که میبینید، اما نویسنده صراحتاً میگوید اغلب یک انتخاب آگاهانه نیست، بلکه ناشی از عدم آگاهی از ریسک است . اگر مهاجم به شبکه نفوذ کند، هیچ محافظتی در برابر حملات Man-in-the-Middle وجود ندارد.
۲. HTTPS Basic Authentication: ارسال نام کاربری و رمز عبور در HTTP Header. ساده و همهجا پشتیبانی میشود، اما حتماً باید روی HTTPS باشد، نه HTTP ساده . مشکل اصلی: مدیریت Certificate در مقیاس بزرگ و اینکه سرور فقط میداند این اعتبارنامه ارسال شده، اما نمیداند از کجا آمده.
۳. SAML یا OpenID Connect: استفاده از همان زیرساخت SSO برای ارتباط سرویسها. هر سرویس یک Service Account مجزا دارد. نویسنده توصیه میکند هر ریزسرویس اعتبارنامههای مجزا داشته باشد تا در صورت Compromise شدن یکی، Revoke کردن آسان باشد .
۴. Client Certificates (TLS Mutual Authentication): هر سرویس یک Certificate X.509 دارد که هویت آن را اثبات میکند. قویترین ضمانت را میدهد، اما بار عملیاتی مدیریت Certificate بسیار سنگین است . نویسنده آن را برای حالتی توصیه میکند که دادهها بسیار حساس هستند یا از شبکههایی عبور میکنند که کاملاً تحت کنترل نیستند.
۵. HMAC (Hash-Based Message Authentication Code): روشی که Amazon S3 از آن استفاده میکند. Body درخواست + یک کلید خصوصی مشترک با هم Hash میشوند و Hash در کنار درخواست ارسال میشود . مزایا:
- کلید خصوصی هرگز در درخواست ارسال نمیشود.
- اگر مهاجم محتوای درخواست را تغییر دهد، Hash دیگر مطابقت ندارد.
- Cacheپذیر است (برخلاف HTTPS).
مشکل: یک Pattern است، نه یک استاندارد — پیادهسازیهای متفاوتی وجود دارد. نویسنده توصیه میکند JWT (JSON Web Tokens) را بررسی کنید که همین رویکرد را با استانداردسازی بیشتر پیادهسازی میکند .
۶. API Keys: تمام API های عمومی مثل Twitter، Google و AWS از این روش استفاده میکنند. API Key به شناسایی فراخوانیکننده کمک میکند و امکان Rate Limiting را فراهم میکند . رویکرد Gateway در این فضا بسیار محبوب است، زیرا API Key از نظر برنامهنویسی بسیار سادهتر از یک SAML Handshake است.
مشکل Deputy گیج (The Confused Deputy Problem)
این یکی از ظریفترین آسیبپذیریهای معماری توزیعشده است . سناریو:
کاربر وارد Online Shop میشود و روی «مشاهده وضعیت سفارش» کلیک میکند. Online Shop به Order Service و Shipping Service فراخوانی میکند. حالا سوال: آیا این سرویسها باید هر درخواستی از Online Shop را بپذیرند؟
اگر تنها بر اساس هویت Online Shop (که یک سرویس داخلی مورد اعتماد است) تصمیم بگیرند، یک کاربر مخرب میتواند Online Shop را فریب دهد تا اطلاعات سفارشهای سایر مشتریان را برگرداند .
نویسنده سه رویکرد را برای مقابله مطرح میکند :
- اعتماد ضمنی: Online Shop خودش بررسی کند که آیا سفارش متعلق به این کاربر است.
- تأیید هویت فراخوانیکننده: سرویسهای پاییندستی هویت Online Shop را تأیید کنند (اما این کافی نیست).
- ارسال اعتبارنامه Principal اصلی: Online Shop در درخواست به سرویسها مشخص کند که از طرف کاربر X این درخواست را میفرستد.
نویسنده صادقانه اعتراف میکند که پاسخ سادهای وجود ندارد، زیرا این مشکل ذاتاً پیچیده است .
امنیت دادههای ساکن (Securing Data at Rest)
نویسنده سه اصل کلیدی را مطرح میکند:
۱. از پیادهسازی خودتان استفاده نکنید: بزرگترین اشتباه، تلاش برای نوشتن الگوریتم رمزنگاری دستساز است . از پیادهسازیهای استاندارد و بررسیشده مثل AES-128 یا AES-256 استفاده کنید. هر دو Runtime های .NET و JVM پیادهسازیهای توکار دارند. برای رمز عبور، از Salted Password Hashing استفاده کنید.
۲. ماجرای کلیدها (Key Management): اگر دادهها را رمزگذاری کنید و کلید را در همان دیتابیس نگه دارید، هیچ امنیتی اضافه نشده ! کلیدها باید در یک Key Vault جداگانه نگهداری شوند. ابزارهایی مانند HashiCorp Vault یا SQL Server Transparent Data Encryption میتوانند کمک کنند — اما حتماً بررسی کنید که Key Management آنها چطور پیادهسازی شده.
۳. اصل Datensparsamkeit — داده کمتر، خطر کمتر: یک عبارت آلمانی به معنای «صرفهجویی در داده» . اگر دادهای را ذخیره نکنید:
- کسی نمیتواند آن را بدزدد.
- هیچ نهادی نمیتواند آن را از شما بخواهد.
مثال عملی: به جای ذخیره آدرس IP کامل، آخرین اکتت را با x جایگزین کنید. به جای ذخیره سن، تاریخ تولد، و جنسیت، فقط بازه سنی و کد پستی کافی است .
دفاع در عمق: لایههای دیگر
نویسنده فصل را با بررسی لایههای اضافی امنیت جمعبندی میکند :
- Firewall: IPTables روی هر Host + Firewall لایه Perimeter. چند Firewall بهتر از یکی است.
- IDS/IPS: سیستمهای تشخیص/جلوگیری از نفوذ که رفتار غیرعادی را رصد میکنند. برای شروع، IDS (حالت Passive) قبل از IPS (حالت Active) توصیه میشود.
- Network Segregation: قرار دادن سرویسهای مختلف در Subnet های مجزا. AWS VPC و VPC Peering این قابلیت را به خوبی پشتیبانی میکند .
- OS Security: اجرای سرویسها با حداقل Permission، Patch منظم سیستمعامل، و استفاده از ماژولهایی مثل AppArmour (پیشفرض Ubuntu) یا SELinux (پیشفرض RedHat) .
قانون طلایی نویسنده:
اگر هیچ چیز دیگری از این فصل یاد نگرفتید، این را به خاطر بسپارید: رمزنگاری دستساز ننویسید. حتی یک متخصص رمزنگاری هم ممکن است اشتباه کند .
۱. تحلیل انتقادی/فنی
JWT در .NET: امروز
System.IdentityModel.Tokens.Jwtو کتابخانهMicrosoft.AspNetCore.Authentication.JwtBearerپیادهسازیهای توکار و امنی دارند. اما هشداری که نویسنده میدهد همچنان کاملاً معتبر است: یک همکار آنها با فراموش کردن یکbooleanچک ساده، کل سیستم احراز هویت را ناامن کرد . همیشه از کتابخانههای ثابتشده استفاده کنید و مقدارalgدر JWT Header را اعتبارسنجی کنید.Key Management در Azure: نویسنده Key Vault را به عنوان الگوی صحیح معرفی میکند. Azure Key Vault دقیقاً این مشکل را حل میکند. در .NET میتوانید از
Azure.Extensions.AspNetCore.Configuration.Secretsاستفاده کنید تا Secret ها را به صورت شفاف در Configuration Pipeline بارگذاری کنید — بدون اینکه هیچ Secret ای در کد یاappsettings.jsonوجود داشته باشد.
فصل دهم: قانون کانوی و طراحی سیستم
این فصل از نظر نویسنده یکی از مهمترین فصلهای کتاب است، زیرا نشان میدهد که چرا معماری نرمافزار جدا از ساختار سازمانی وجود ندارد . میتوانید بهترین تصمیمات فنی را بگیرید، اما اگر ساختار تیمتان با آنها همسو نباشد، در نهایت سیستم خودش را با سازمان تنظیم میکند — نه با طراحی ایدهآل شما.
قانون کانوی چیست؟
ملوین کانوی در سال ۱۹۶۸ در مقالهای نوشت :
«هر سازمانی که سیستمی طراحی کند، ناگزیر سیستمی تولید میکند که ساختارش نسخهای از ساختار ارتباطی همان سازمان است.»
اریک ریموند این را به شکل طنزآمیزتری خلاصه کرد: «اگر چهار تیم روی یک کامپایلر کار کنند، یک کامپایلر چهارمرحلهای به دست میآورید» .
این قانون ساده به نظر میرسد، اما پیامدهایش عمیق هستند.
شواهد: این فقط یک نظریه نیست
نویسنده چند مطالعه را به عنوان پشتوانه ذکر میکند :
مطالعه Harvard Business School: محققان مکCormack، Rusnak و Baldwin سیستمهای نرمافزاری ساختهشده توسط سازمانهای «تنگبسته» (مثل شرکتهای تجاری که تیمهایشان یکجا هستند) را با سازمانهای «باز» (مثل جوامع Open Source توزیعشده) مقایسه کردند. نتیجه شگفتانگیز بود: سازمانهای توزیعشدهتر، سیستمهای مدولارتر و کمتر Coupled ساختند .
مطالعه Microsoft و Windows Vista: محققان عوامل مختلفی را برای پیشبینی میزان Bug-prone بودن یک کامپوننت در Vista بررسی کردند. معیارهای متداول کیفیت کد مثل پیچیدگی چرخهای (Cyclomatic Complexity) در مقایسه با معیارهای ساختار سازمانی آماری ضعیفتر بودند. به عبارت دیگر، با نگاه به نمودار سازمانی Microsoft، میشد بهتر از با نگاه به کد پیشبینی کرد که کدام بخش از Vista بیشتر Bug خواهد داشت .
Amazon و Netflix: طراحی معکوس
Amazon و Netflix نمونههای بارز سازمانهایی هستند که آگاهانه از قانون کانوی استفاده کردند :
- Amazon Two-Pizza Teams را تعریف کرد: هیچ تیمی نباید آنقدر بزرگ باشد که نتوان آن را با دو پیتزا سیر کرد.
- این تیمهای کوچک مالکیت کامل Lifecycle سرویسهایشان را داشتند؛ از ساخت تا Deploy تا نگهداری.
- این نیاز به استقلال تیمها، دلیل اصلی ساختهشدن Amazon Web Services بود — Amazon احتیاج داشت ابزاری بسازد که تیمهایش بتوانند بدون وابستگی به یکدیگر خودشان را تأمین کنند .
Netflix از این تجربه یاد گرفت و از همان ابتدا ساختار سازمانی را طوری طراحی کرد که معماری مطلوب سیستم را به وجود بیاورد — نه برعکس .
چه میتوانیم بکنیم؟
همراستاسازی تیمها با مسیرهای ارتباطی: اگر یک سرویس توسط دو تیم در دو شهر مختلف نگهداری شود، هزینه هماهنگی برای هر تغییر بالا میرود .
توصیه عملی: مرزهای جغرافیایی میتوانند راهنمای خوبی برای جداسازی سرویسها باشند. اگر شرکت شما دفتر جدیدی در کشور دیگری باز میکند، این سوال را مطرح کنید: چه بخشهایی از سیستم میتوانند به تیم جدید واگذار شوند؟
مالکیت سرویس (Service Ownership)
مالکیت سرویس یعنی یک تیم مسئول تمام جنبههای آن سرویس است: از تعریف نیازمندی تا ساخت، Deploy، و نگهداری . این مدل چندین مزیت ایجاد میکند:
- تیم انگیزه دارد که سرویس را قابلDeploy بسازد، چون مجبور است خودش آن را Deploy کند.
- تصمیمگیری نزدیک به کسانی است که اطلاعات کافی دارند.
- احساس مسئولیت جایگزین «پرت کردن کار به آن طرف دیوار» میشود.
نویسنده میگوید: «توسعهدهندگان زیادی دیدهام که پس از تحویل کد برای مرحله تست یا Deploy، فکر میکنند کارشان تمام شده» . در مدل ریزسرویس با مالکیت کامل، این ذهنیت باید از بین برود.
چرا سرویسهای اشتراکی (Shared Services) ایجاد میشوند؟
نویسنده سه دلیل اصلی را شناسایی میکند :
۱. تقسیم سرویس خیلی سخت است: اگر هزینه جداسازی زیاد باشد، تیمها ترجیح میدهند یک سرویس را با هم نگه دارند. راهحل: به توصیههای فصل پنجم مراجعه کنید و هزینه جداسازی را کاهش دهید .
۲. تیمهای Feature-Based (قابلیتمحور): در این مدل، یک تیم کوچک یک ویژگی جدید را از ابتدا تا انتها پیادهسازی میکند — حتی اگر این یعنی تغییر در چند سرویس مختلف. هدف معقول است: اطمینان از اینکه کار «به هم وصل» میشود و از چالشهای هماهنگی چند تیم جلوگیری میشود .
اما مشکل اینجاست: وقتی همه میتوانند هر چیزی را تغییر دهند، Custodian مشخصی برای هر سرویس وجود ندارد. کدبیس به تدریج از انسجام خارج میشود و هیچکس احساس مسئولیت نمیکند .
۳. Bottleneck تحویل (Delivery Bottleneck): تصور کنید تیم Catalog Service نیمیشان با آنفلوآنزا هستند. تیم وبسایت و تیم موبایل هر دو به تغییر در Catalog Service نیاز دارند. چه میشود؟ نویسنده سه گزینه را پیشنهاد میدهد :
- صبر کنید (و ارزیابی کنید که این تأخیر چقدر مهم است).
- نفر موقت به تیم Catalog اضافه کنید.
- Catalog Service را به دو سرویس تفکیک کنید — اگر کار آینده هم زیاد است، این منطقیترین گزینه است.
مدل Open Source داخلی (Internal Open Source)
اگر همه راهحلهای بالا ممکن نبود و یک سرویس ناگزیر اشتراکی شد، نویسنده مدل Open Source داخلی را توصیه میکند :
درست مانند یک پروژه Open Source موفق:
- یک گروه کوچک Core Committer وجود دارند که مالک و نگهبان کد هستند.
- تیمهای خارجی میتوانند Pull Request ارسال کنند.
- Core Committer ها وظیفه بررسی کیفیت، سازگاری با اصول کدبیس و تأیید تغییرات را دارند .
این مدل نیازمند سرمایهگذاری در ابزار است: Version Control با قابلیت Pull Request، سیستم Code Review، و یک Build/Deploy Pipeline که دیگران بتوانند راحت از آن استفاده کنند .
هشدار: این مدل روی سرویسهایی که هنوز در حال تثبیت هستند، کار نمیکند. Core Committer ها هنوز نمیدانند «خوب» چه شکلی است، و بنابراین نمیتوانند یک Pull Request خوب را از بد تشخیص بدهند مناسب است.
همراستاسازی تیمها با Bounded Contexts
نویسنده توصیه میکند تیمها را دقیقاً روی Bounded Context های کسبوکار تنظیم کنید :
- اعضای تیم راحتتر مفاهیم حوزهای را درک میکنند، چون مرتبطاند.
- سرویسهای داخل یک Bounded Context بیشتر با هم ارتباط دارند، پس هماهنگی Release آسانتر است.
- ارتباط تیم با ذینفعان کسبوکار بهتر میشود، چون هر دو درباره همان حوزه صحبت میکنند.
Case Study: RealEstate.com.au (REA)
نویسنده ساختار سازمانی REA را به عنوان نمونه موفق ارائه میدهد :
- کسبوکار به Line of Business (LOB) های مجزا تقسیم شده: مسکونی، تجاری، بینالملل.
- هر LOB تیمهای (Squad) مختص به خود دارد که Lifecycle کامل سرویسهایشان را مالکیت میکنند.
- درون یک LOB: سرویسها میتوانند به هر شکلی با هم ارتباط برقرار کنند (Sync یا Async).
- بین LOB ها: تمام ارتباطات اجباراً Async و Batch هستند .
این قانون ساده چند اثر قدرتمند دارد: هر LOB میتواند سرویسهایش را هر وقت خواست Down کند بدون اینکه LOB های دیگر تأثیر ببینند. ارتباط Coarse-grained بین LOB ها با ارتباط Coarse-grained بین بخشهای کسبوکار مطابقت دارد. نتیجه: از چند سرویس در آغاز به صدها سرویس رسیدند، با بیشتر سرویس از نفر .
قانون کانوی معکوس
نویسنده همچنین نشان میدهد که این رابطه دوطرفه است: طراحی سیستم میتواند ساختار سازمانی را هم تغییر دهد .
مثال واقعی: یک شرکت چاپ بزرگ در دوران اولیه اینترنت، سیستم وبسایتش را با سه بخش طراحی کرد: Input (ورود محتوا)، Core (پردازش)، Output (نمایش). سالها بعد، وقتی آن کسبوکار به شدت رشد کرد، نویسنده متوجه شد که ساختار سازمانی IT به تدریج همان سه بخش را پیدا کرده بود — نه اینکه ساختار سازمانی طراحی سیستم را تعیین کرده باشد، بلکه سیستم مسیر رشد سازمان را شکل داده بود .
۱. تحلیل انتقادی/فنی
قانون کانوی و تیمهای .NET در ایران: یک Anti-Pattern بسیار رایج این است که تیمها بر اساس لایه فنی تقسیم میشوند: «تیم Backend»، «تیم Frontend»، «تیم DBA». این دقیقاً همان چیزی است که نویسنده هشدار میدهد — این تقسیمبندی ناگزیر منجر به سیستمی میشود که بر اساس لایههای فنی جداسازی شده، نه Bounded Context. اگر میخواهید معماری ریزسرویس واقعی داشته باشید، تیمها باید عمودی (cross-functional) باشند: هر تیم شامل Backend، Frontend و DevOps است و یک حوزه کسبوکار را کامل میپوشاند.
مدل Squad/Tribe از Spotify: نویسنده به صراحت از این مدل یاد نمیکند، اما REA Case Study که توصیف میکند دقیقاً پیشنویس همان ایدهای است که Spotify چند سال بعد به عنوان Squad/Tribe/Chapter/Guild رسمیسازی کرد. اگر میخواهید این ساختار را در سازمانتان پیاده کنید، مطالعه مقاله اصلی Spotify Engineering Culture توصیه میشود.
فصل یازدهم: ریزسرویسها در مقیاس بزرگ (Microservices at Scale)
این فصل از نظر نویسنده، یکی از عملیترین فصلهای کتاب است. وقتی تعداد سرویسها از چند عدد به صدها سرویس میرسد، قوانین بازی کاملاً تغییر میکند . نویسنده با یک جمله محوری شروع میکند: شکست اجتنابناپذیر است — هنر طراحی این است که با آن کنار بیایید، نه اینکه سعی کنید جلویش را بگیرید.
شکست همهجا هست (Failure Is Everywhere)
نویسنده از یک تجربه مستقیم در Google یاد میکند : در Google، هارددیسکهای سرورها با ولکرو نصب میشدند — نه پیچ. دلیل؟ هارددیسکها آنقدر زیاد خراب میشدند که پیچکردن اتلاف وقت بود. مهندسان Google نه تلاش میکردند جلوی خرابی را بگیرند، بلکه برای خرابی طراحی کرده بودند.
این یک تغییر ذهنی اساسی است: سازمانهایی که انرژی میگذارند تا از شکست جلوگیری کنند، اما به بازیابی از شکست فکر نمیکنند، در معرض خطر جدیتری قرار دارند .
چقدر مقاومت لازم دارید؟ (How Much Is Too Much?)
قبل از هر چیز، باید بدانید سیستمتان چه سطحی از خرابی را باید تحمل کند. نویسنده سه معیار اصلی را معرفی میکند :
| معیار | پرسش اصلی | مثال |
|---|---|---|
| Response Time / Latency | هر عملیات چقدر باید طول بکشد؟ | «90th-percentile پاسخ زیر ۲ ثانیه برای ۲۰۰ اتصال همزمان» |
| Availability | آیا سرویس میتواند Down باشد؟ | «سرویس پرداخت باید ۲۴/۷ در دسترس باشد» |
| Durability of Data | چقدر از دست دادن داده قابل قبول است؟ | «لاگهای کاربری یک سال، تراکنشهای مالی چند سال» |
نکته مهم: این الزامات از سرویسی به سرویس دیگر متفاوتند. یک سیستم Autoscaling برای یک گزارش ماهانه که دو روز قطعی هم قابل قبول است، کاملاً Overkill است . ابتدا الزامات Cross-Functional کلی را تعریف کنید و سپس به ازای سرویسهای خاص آنها را Override کنید.
کاهش تدریجی قابلیتها (Degrading Functionality)
نویسنده یک اصل کلیدی را مطرح میکند: وقتی یک سرویس پاییندستی خراب است، نباید کل سیستم متوقف شود .
سناریو: یک صفحه فروشگاه آنلاین از چند سرویس اطلاعات میگیرد — جزئیات آلبوم، قیمت و موجودی، سبد خرید. اگر سرویس سبد خرید Down باشد، چه اتفاقی میافتد؟
- رویکرد غلط: کل صفحه نمایش داده نشود.
- رویکرد صحیح: صفحه نمایش داده شود، اما سبد خرید جای خود را به یک پیام «به زودی برمیگردیم» یا یک شماره تلفن بدهد .
نکته مهم نویسنده: تصمیم در این باره فنی نیست، بلکه تجاری است. مثلاً شاید کسبوکار ترجیح دهد سایت کاملاً بسته شود تا مشتری بدون سبد خرید نباشد. این یک تصمیم استراتژیک است که تیم فنی به تنهایی نباید بگیرد .
قانون طلایی: برای هر سرویس مصرفکنندهای که به چند سرویس پاییندستی وابسته است، این سوال را مطرح کنید: «اگر این سرویس Down شود، چه میکنیم؟» و پاسخ را مستند کنید.
داستان واقعی: شکست آبشاری (Cascading Failure)
نویسنده یک تجربه اولشخص جذاب تعریف میکند :
او روی یک وبسایت آگهی آنلاین کار میکرد که در ساعات اوج در حدود ۶٫۰۰۰ تا ۷٫۰۰۰ درخواست در ثانیه پردازش میکرد. یک روز، درست قبل از اوج ظهر، سیستم شروع به کندی کرد و بعد به طور کامل از کار افتاد. CPU تمام Node های اپلیکیشن به ۱۰۰٪ رسیده بود.
علت اصلی چه بود؟ یکی از سیستمهای Legacy پاییندستی که آگهیهای قدیمی را سرویس میداد، خیلی کند شروع به پاسخدادن کرد (نه اینکه قطع شود — کند شد). و این دقیقاً بدترین حالت ممکن است :
«یک سیستم که کاملاً قطع است، خیلی زودتر شناسایی میشود. اما یک سیستم که فقط کند است، باعث میشود همه منتظر بمانند — و این منتظر ماندن سیستم را از پا درمیآورد.»
سه اشتباه در طراحی آن سیستم شناسایی شد :
- Timeout برای Worker Thread های Connection Pool غیرفعال بود (به عنوان پیشفرض).
- یک Connection Pool مشترک برای همه سرویسهای پاییندستی وجود داشت — پس یک سرویس کند توانست تمام Worker Thread ها را مصرف کند.
- با اینکه سرویس آسیبدیده وضعیت بدی داشت، ترافیک به آن ادامه یافت و باعث شد نتواند ریکاوری کند.
این سرویس کند تنها ۵٪ مشتریان را پوشش میداد و درصد ناچیزی از درآمد را تولید میکرد — اما توانست کل سیستم را Down کند . راهحل سه Pattern بود که در بخش بعدی توضیح داده میشود.
اقدامات امنیتی معماری (Architectural Safety Measures)
نویسنده سه Pattern اساسی برای مقابله با این نوع خرابی معرفی میکند که باید استانداردسازی شوند — یعنی همه سرویسها باید از آنها استفاده کنند، چون یک سرویس «بد» میتواند کل سیستم را خراب کند :
۱. Timeout (محدودیت زمانی)
سادهترین Pattern، اما اغلب نادیده گرفته میشود :
- Timeout خیلی کوتاه: یک فراخوانی که ممکن بود موفق شود، به عنوان شکست تلقی میشود.
- Timeout خیلی طولانی: سیستم کند میشود و Thread ها هدر میروند.
- بدون Timeout: یک سرویس کند میتواند کل سیستم را Down کند.
توصیه عملی: یک Timeout پیشفرض معقول برای همه فراخوانیهای خارج از Process تعریف کنید. رفتار واقعی را Monitor کنید و بر اساس دادههای واقعی تنظیم نمایید .
۲. Circuit Breaker (قطعکننده مدار)
این Pattern از کتاب Release It! نوشته Michael Nygard الهام گرفته شده و دقیقاً مثل قطعکننده برق خانه عمل میکند :
فرآیند کار به چهار مرحله تقسیم میشود:
- مرحله بسته (Closed): فراخوانیها عادی هستند.
- آستانه خطا: تعداد مشخصی از فراخوانیها Timeout یا خطای
5XXبرمیگردانند. - مرحله باز (Open): Circuit Breaker میپرد. تمام فراخوانیهای بعدی فوری با خطا برمیگردند — بدون اینکه اصلاً به سرویس پاییندستی فرستاده شوند.
- مرحله نیمهباز (Half-Open): بعد از یک بازه زمانی، چند فراخوانی آزمایشی ارسال میشود. اگر سالم برگشتند، Circuit Breaker Reset میشود .
مزایای مضاعف:
- سیستم فراخوانیکننده از کندی سرویس پاییندستی محافظت میشود.
- سرویس پاییندستی هم نفس میکشد و فرصت ریکاوری دارد، چون ترافیک بیشتری دریافت نمیکند.
- میتوان به صورت دستی Circuit Breaker را پرید تا یک سرویس را در حین نگهداری Offline کرد .
ابزارهای موجود:
- Hystrix (Netflix) — برای JVM
- Polly — برای .NET
- circuitbreaker mixin — برای Ruby
۳. Bulkhead (دیواره ضربهگیر)
این Pattern هم از کتاب Release It! میآید و از مهندسی کشتی الهام گرفته است : در کشتی، بخشهای مختلف با دیوارههای آببند از هم جدا هستند. اگر یک بخش آب بگیرد، بقیه کشتی سالم میمانند.
در معماری نرمافزار، این مفهوم به این شکل پیاده میشود :
اشتباه: یک Connection Pool مشترک برای همه سرویسهای پاییندستی. درست: یک Connection Pool مجزا برای هر سرویس پاییندستی.
نتیجه: اگر سرویس Legacy Ads شروع به کند شدن کند، فقط Connection Pool مربوط به آن اشباع میشود. سرویسهای دیگر همچنان Worker Thread دارند و به کار ادامه میدهند .
نکته: Hystrix در JVM قابلیت Load Shedding هم دارد: اگر Connection Pool یک سرویس پر شد، درخواستهای جدید فوری رد میشوند تا منابع بیشتر آزاد بماند. گاهی رد کردن یک درخواست بهتر از قبول آن و کند شدن کل سیستم است .
اولویتبندی سه Pattern:
نویسنده صراحتاً میگوید: Bulkhead مهمترین است. Timeout و Circuit Breaker منابع را بعد از اینکه محدود شدند آزاد میکنند، اما Bulkhead از محدود شدن آنها از اول جلوگیری میکند .
سازمان ضدشکست (The Antifragile Organization)
ناسیم طالب در کتاب Antifragile از چیزهایی صحبت میکند که نهتنها در برابر اختلال مقاوم هستند، بلکه از آن بهره میبرند . Netflix این مفهوم را در عملیاتش پیاده کرده:
Chaos Monkey: برنامهای که در ساعات کاری، ماشینهای تصادفی را خاموش میکند . هدف: اگر توسعهدهندگان بدانند که این اتفاق میافتد، مجبور میشوند سیستمهایشان را از ابتدا Fault-Tolerant بسازند.
Simian Army: مجموعهای از «رباتهای شکست» :
- Chaos Gorilla: کل یک Availability Zone را خاموش میکند (معادل یک Data Center).
- Latency Monkey: اتصال شبکه را کند شبیهسازی میکند.
Google هم DiRT (Disaster Recovery Test) سالانه دارد که حوادثی مثل زلزله را شبیهسازی میکند .
نکته فرهنگی: Netflix علاوه بر ابزارها، یک فرهنگ بدون سرزنش (Blameless Culture) دارد. وقتی شکست اتفاق میافتد، هدف یادگیری است، نه پیدا کردن مقصر . هر توسعهدهنده مسئول عملیات تولید سرویسش هم هست — این باعث میشود هیچکس بتواند بگوید «مشکل Production نگرانی من نیست».
۱. تحلیل انتقادی/فنی
Polly در .NET: کتابخانه Polly دقیقاً هر سه Pattern را به صورت یکجا پیاده میکند: Timeout، Circuit Breaker، و Bulkhead (از طریق
BulkheadPolicy). در .NET 8 باHttpClientFactoryمیتوانید این Policy ها را به صورت Declarative روی هر Named HTTP Client اعمال کنید، بدون اینکه کد Business Logic آلوده شود. این ترکیب دقیقاً همان چیزی است که نویسنده توصیه میکند.هشدار درباره Default Timeout در HttpClient: یک Anti-Pattern بسیار رایج در .NET این است که مقدار پیشفرض
HttpClient.Timeout(یعنی ۱۰۰ ثانیه!) به همان حال باقی میماند. این دقیقاً همان اشتباهی است که نویسنده در داستان Connection Pool بیان کرد. در تمام پروژههای Production، این مقدار را صریحاً تنظیم کنید.
ادامه فصل یازدهم: ریزسرویسها در مقیاس بزرگ
این بخش به بقیه مباحث فنی مقیاسپذیری میپردازد: از Idempotency و Scaling Databases تا Caching، CAP Theorem و Service Discovery .
عملیات Idempotent: کلید ریکاوری ایمن
عملیات Idempotent عملیاتی است که هر چند بار هم اجرا شود، نتیجه یکسانی دارد . این ویژگی در سیستمهای توزیعشده اهمیت حیاتی دارد، زیرا وقتی پیامی ارسال میشود و مطمئن نیستیم دریافت شده یا نه، میتوانیم آن را دوباره بفرستیم — بدون اینکه نگران عوارض جانبی باشیم.
نویسنده یک مثال ساده ارائه میدهد .
نکته مهم نویسنده: Idempotent بودن لزوماً به معنای «هیچ تغییر جانبی رخ نمیدهد» نیست. Log کردن درخواست یا ثبت Metrics هنوز مجاز است؛ آنچه مهم است این است که عملیات تجاری اصلی بیاثر باشد، نه کل State سیستم .
مقیاسپذیری (Scaling): چهار رویکرد اصلی
نویسنده چهار تکنیک اصلی Scaling را شرح میدهد :
۱. بزرگتر شو (Go Bigger — Vertical Scaling): خرید سرور قویتر با CPU سریعتر و IO بهتر. سریعترین راهحل است اما گرانترین نیز هست — یک سرور بزرگ معمولاً گرانتر از دو سرور کوچک با توان ترکیبی مشابه است . همچنین این روش به تنهایی Resiliency نمیدهد: اگر تنها یک سرور دارید و خراب شود، خراب ماندید.
۲. تقسیم بار (Splitting Workloads): برای مثال، یک Accounts Service که هم ثبت تراکنشهای مالی انجام میدهد هم گزارش تولید میکند، میتواند به دو سرویس جداگانه تبدیل شود: سرویس مالی حساس (Critical) و سرویس گزارشگیری غیرحساس (Non-critical) . نتیجه: بار از سرویس اصلی برداشته میشود و هر سرویس را میتوان با سطح مناسب Resiliency Deploy کرد.
۳. توزیع ریسک (Spreading Your Risk): اجرای سرویسهای مختلف روی Host های مختلف تنها اول راه است .
۴. Autoscaling: با داشتن Provisioning خودکار و Deploy خودکار، میتوانید Scaling را هم خودکار کنید. دو نوع Autoscaling وجود دارد :
- Reactive: در واکنش به افزایش Load یا خرابی Instance، نمونه جدید راهاندازی میشود.
- Predictive: بر اساس الگوهای تاریخی. مثلاً اگر میدانید اوج بار روزانه از ساعت ۹ تا ۵ است، قبل از آن Scale Up و بعد از آن Scale Down کنید.
توصیه عملی نویسنده: Autoscaling را ابتدا برای مدیریت خرابی (نه Load) تنظیم کنید تا داده کافی جمعآوری شود. وقتی میخواهید برای Load هم Scale کنید، در Scale Down عجله نکنید — داشتن منابع اضافه بهتر از کمبود آنهاست .
مقیاسپذیری دیتابیس (Scaling Databases)
تفاوت حیاتی: Availability vs. Durability: دو مفهوم را نباید با هم قاطی کرد :
- Durability (پایداری داده): آیا داده گم میشود؟
- Availability (در دسترس بودن سرویس): آیا دیتابیس پاسخ میدهد؟
میتوان دادهای داشت که گم نمیشود اما دیتابیس برای خواندنش در دسترس نباشد .
Scaling برای خواندن (Read Replicas): اکثر سرویسها بیشتر میخوانند تا مینویسند. در یک Catalog Service، به ازای هر Write ممکن است ۱۰۰ Read داشته باشیم .
Scaling برای نوشتن (Sharding): این روش پیچیدهتر است. دادهها بر اساس یک Hashing Function روی چند نود توزیع میشوند (مثلاً مشتریان A-M روی Node 1، N-Z روی Node 2) . جستجوی یک رکورد ساده است، اما Query هایی که باید چند Shard را بپیمایند دشوارتر میشوند. Cassandra این را خوب مدیریت میکند.
CQRS (Command-Query Responsibility Segregation): الگویی که در آن Command (تغییر حالت) و Query (خواندن داده) به دو سیستم کاملاً جداگانه تقسیم میشوند . مزایا:
- مدل داخلی Command و Query میتوانند کاملاً متفاوت باشند.
- از Storage های مختلف برای هر بخش میتوان استفاده کرد (مثلاً یک Graph DB برای Query و یک Event Store برای Command).
- به صورت بالقوه با Event Sourcing ترکیب میشود.
هشدار نویسنده: CQRS یک تغییر ذهنی جدی از مدل CRUD معمول است. تیمهای باتجربه هم در پیادهسازی درست آن مشکل داشتهاند . قبل از استفاده مطمئن شوید که واقعاً به آن نیاز دارید.
Caching: کجا و چگونه؟
کشینگ رایجترین ابزار بهینهسازی Performance است. در معماری ریزسرویس، جاهای بیشتری برای کشگذاشتن وجود دارد — و این یعنی پیچیدگی بیشتر :
| نوع Cache | توضیح | مثال |
|---|---|---|
| Client-Side | Client نتیجه را نگه میدارد و خودش تصمیم میگیرد چه وقت Refresh کند | کتابخانه HTTP Client |
| Proxy | یک Proxy بین Client و Server نتایج را Cache میکند | Varnish، Squid، CDN |
| Server-Side | سرور خودش نتایج را Cache میکند | Redis، Memcached |
HTTP Cache Controls: نویسنده ابزارهای توکار HTTP را برجسته میکند :
- Cache-Control: مشخص میکند چند ثانیه Response قابل Cache است.
- Expires: تاریخ انقضای دقیق Resource را مشخص میکند.
- ETag: یک هش از محتوای Resource است. Client در درخواست بعدی با
If-None-Matchاین هش را میفرستد. اگر تغییری نبوده، سرور304 Not Modifiedبرمیگرداند — بدون انتقال محتوا .
خطر Cache Poisoning — یک داستان واقعی: نویسنده یک حادثه تولیدی جدی را شرح میدهد : در یک پروژه، یک باگ کوچک باعث شد برخی صفحات Header Cache-Control درستی نداشته باشند و به جای آن Header Expires: Never از سرور Legacy آنها دریافت کنند. نتیجه: آن صفحات برای ماهها در Cache مرورگرهای کاربران ماندند. حتی پس از Fix کردن باگ، هیچ راهی برای Invalidate کردن Cache مرورگر کاربر نبود — مجبور شدند URL صفحات را تغییر دهند.
درس: Cache در چندین لایه اتفاق میافتد — سرور شما، Reverse Proxy، CDN، ISP، و مرورگر کاربر. هر تغییری در منطق Cache میتواند تبعات جدی داشته باشد .
قانون طلایی: Cache را ساده نگه دارید. هرچه Cache بیشتری بین کاربر و منبع داده اصلی داشته باشید، Freshness داده دشوارتر میشود .
قضیه CAP: ریاضیات توزیعشدگی
قضیه CAP توسط Eric Brewer بیان شد و بعداً اثبات ریاضی دقیقی پیدا کرد: در یک سیستم توزیعشده، هنگام بروز Partition (قطع ارتباط بین نودها)، فقط میتوانید دو مورد از سه مورد زیر را حفظ کنید :
| ویژگی | تعریف |
|---|---|
| Consistency | همه نودها داده یکسانی میبینند |
| Availability | هر درخواستی پاسخ دریافت میکند |
| Partition Tolerance | سیستم حتی وقتی ارتباط بین نودها قطع است، کار میکند |
نویسنده با یک مثال این را شفاف میکند : یک سرویس Inventory روی دو Data Center با Replication دوطرفه. اگر ارتباط شبکه بین دو DC قطع شود:
- AP (Availability + Partition Tolerance): هر دو DC به درخواستها پاسخ میدهند، اما ممکن است دادههای کهنه بدهند. این یعنی Eventual Consistency.
- CP (Consistency + Partition Tolerance): برای حفظ Consistency، درخواستها رد میشوند تا Partition برطرف شود. این یعنی از دسترس خارج شدن .
- CA (Consistency + Availability): تنها در سیستمهایی ممکن است که اصلاً روی شبکه کار نمیکنند — یعنی در سیستمهای توزیعشده وجود ندارد.
نکته ظریف نویسنده: یک سیستم واحد نیازی نیست کلاً AP یا کلاً CP باشد . مثلاً:
- Catalog Service میتواند AP باشد — یک رکورد کهنه برای چند دقیقه مشکل جدی ایجاد نمیکند.
- Inventory Service شاید CP باشد — نمیخواهیم محصولی که موجود نیست به مشتری بفروشیم.
- Points Balance Service میتواند ترکیبی باشد: نمایش موجودی AP است، اما برداشت از موجودی باید CP باشد.
Cassandra این انعطاف را در سطح هر عملیات میدهد: میتوانید مشخص کنید آیا برای پاسخ صبر کنید تا تمام Replica ها تأیید کنند، یا تنها یک Quorum کافی است .
قانون طلایی نویسنده:
«هیچکس قضیه CAP را شکست نداده. آنچه برخی سیستمها انجام میدهند این است که بخشی از Capability هایشان CP و بخشی AP است.»
Service Discovery: کجا همه چیز هست؟
در یک سیستم با صدها ریزسرویس که مداوم Deploy میشوند و IP هایشان تغییر میکند، چطور سرویسها یکدیگر را پیدا میکنند؟
نویسنده چند رویکرد را بررسی میکند:
۱. DNS: سادهترین رویکرد. یک نام DNS مثل accounts.musiccorp.com به IP یک Load Balancer اشاره میکند که پشتش چند Instance دارد . مشکل: DNS Caching باعث میشود تغییرات IP دیر به دیر propagate شوند. در محیطهای Dynamic که Instance ها مداوم عوض میشوند، DNS خودش میتواند Bottleneck شود.
۲. Consul: یک Service Registry توزیعشده که از الگوریتم Raft برای Consistency قوی استفاده میکند. هر Instance وقتی بالا میآید خودش را ثبت میکند و وقتی پایین میآید حذف میشود. Consul علاوه بر Service Discovery، یک Key-Value Store توزیعشده هم هست که میتوان از آن برای Configuration Sharing استفاده کرد .
۳. Zookeeper: یک سرویس مشابه که در اکوسیستم Hadoop محبوب است. به عنوان یک منبع قابل اعتماد برای اطلاعات Configuration و Coordination بین سرویسها استفاده میشود .
۴. Eureka (Netflix): رویکرد Netflix متفاوت است. هر Instance اطلاعات سرویسهای دیگر را در حافظه خودش Cache میکند. حتی اگر Eureka Server موقتاً Down باشد، سرویسها همچنان میتوانند با هم ارتباط برقرار کنند . این برای یک سیستم در مقیاس Netflix که در هر لحظه هزاران Instance دارد، بسیار مهم است.
۱. تحلیل انتقادی/فنی
CAP در .NET و Azure: Azure Cosmos DB به صورت صریح به شما اجازه میدهد Consistency Level را تنظیم کنید: از
Strong(CP-like) تاEventual(AP-like) و چند حالت میانی. این دقیقاً همان انعطافی است که نویسنده به عنوان ایدهآل توصیف میکند — نه یک انتخاب باینری، بلکه یک Spectrum.Service Discovery در .NET: در دنیای .NET مدرن، Kubernetes به عنوان بستر اجرا بیشتر مسئله Service Discovery را حل میکند: هر Service یک
ClusterIPثابت دارد و DNS داخلی K8s آدرس آن را resolve میکند. اما Consul هنوز برای محیطهای Multi-Cloud یا سرویسهایی که خارج از K8s هستند ارزش دارد.
فصل دوازدهم: جمعبندی نهایی (Bringing It All Together)
این فصل آخر کتاب است. نویسنده در اینجا کل مسیر طیشده را به یک مجموعه اصل منسجم تبدیل میکند . هدف این است که بعد از ۱۱ فصل عمیق و فنی، یک نقشه راه ذهنی داشته باشید که در لحظات تصمیمگیری سریعاً قابل مراجعه باشد.
اصول هفتگانه ریزسرویسها
نویسنده هفت اصل محوری را استخراج میکند که در تصویر ۱۲-۱ کتاب به صورت شماتیک نشان داده شدهاند . این اصول با هم یک سیستم منسجم تشکیل میدهند — نادیده گرفتن هر کدام، اثر بقیه را تضعیف میکند.
۱. مدلسازی بر اساس مفاهیم کسبوکار
تجربه نشان داده که رابطهایی که حول Bounded Context های کسبوکار شکل میگیرند، پایدارتر از رابطهایی هستند که حول مفاهیم فنی ساخته میشوند . دامنهای که سیستم شما در آن فعالیت میکند را مدل کنید. Bounded Context های دامنه را شناسایی کنید تا مرزهای احتمالی سرویسها را پیدا کنید.
۲. فرهنگ اتوماسیون
ریزسرویسها پیچیدگی زیادی اضافه میکنند که بخش مهمی از آن از تعداد زیاد اجزای متحرک نشأت میگیرد. پذیرش فرهنگ اتوماسیون یکی از کلیدیترین راههای مقابله با این پیچیدگی است :
- Automated Testing: تضمین اینکه سرویسهایتان هنوز کار میکنند، در سیستمهای توزیعشده پیچیدهتر از مونولیت است.
- Uniform Deployment Pipeline: یک خط فرمان یکنواخت برای Deploy در همه محیطها.
- Immutable Servers: هرگز سرور زنده را دستی تغییر ندهید — همیشه یک Image جدید بسازید و Deploy کنید.
- Environment Definitions: به جای مستند کردن تفاوتهای محیطی، آنها را کد کنید .
۳. پنهان کردن جزئیات پیادهسازی
برای اینکه هر سرویس بتواند مستقل از بقیه تکامل پیدا کند، پنهان کردن جزئیات پیادهسازی حیاتی است :
- سرویسها دیتابیسشان را پنهان کنند تا به رایجترین نوع Coupling در معماریهای سرویسمحور مبتلا نشوید.
- برای گزارشگیری از Data Pump یا Event Data Pump استفاده کنید، نه از دسترسی مستقیم به دیتابیس.
- تا جای ممکن، از API های Technology-Agnostic استفاده کنید تا آزادی انتخاب Stack فنی را داشته باشید.
- استفاده از REST این جداسازی را به صورت ساختاری اجبار میکند .
۴. استقرار مستقل (Independently Deployable)
هیچ اصلی در کتاب به اندازه این اصل تکرار نشده است :
- همیشه تلاش کنید سرویسها به تنهایی قابل Deploy باشند.
- حتی وقتی Breaking Change اجتنابناپذیر است، از Versioned Endpoint ها به صورت همزمان پشتیبانی کنید تا مصرفکنندگان به تدریج مهاجرت کنند.
- از Blue-Green یا Canary Release برای جدا کردن Deploy از Release استفاده کنید.
- Consumer-Driven Contracts را اعمال کنید تا Breaking Change ها قبل از رسیدن به Production کشف شوند .
قانون طلایی نویسنده: باید هنجار (Norm) باشد، نه استثنا، که بتوانید یک سرویس را تغییر دهید و بدون Deploy کردن هیچ سرویس دیگری آن را به Production برسانید .
۵. جداسازی خرابیها (Isolate Failure)
معماری ریزسرویس میتواند از مونولیت مقاومتر باشد — اما تنها در صورتی که برای خرابی برنامهریزی کرده باشیم :
- فراخوانی شبکه را مثل فراخوانی محلی ندانید. این پنهان کردن حالتهای مختلف خطاست.
- Timeout مناسب تنظیم کنید.
- Bulkhead و Circuit Breaker را به کار بگیرید تا تبعات یک کامپوننت خراب را محدود کنید.
- درک کنید که تأثیر روی کاربر چه خواهد بود اگر فقط یک بخش از سیستم خراب باشد .
- بدانید که در صورت Network Partition، انتخاب بین Availability و Consistency کدام است.
۶. غیرمتمرکزسازی همه چیز (Decentralize All the Things)
برای بهرهمندی کامل از Autonomy که ریزسرویسها ممکن میکنند، تصمیمگیری و کنترل باید به تیمهای مالک سرویس تفویض شود :
- Self-Service را در همه جا بپذیرید: Deploy در هر زمان، توسعه و تست آسان.
- تیمها سرویسهایشان را مالکیت کنند و مسئول تغییرات باشند — ایدهآل آن است که آنها خودشان تصمیم بگیرند چه وقت Release کنند.
- از Choreography به جای Orchestration استفاده کنید تا از مرکزی شدن منطق تجاری جلوگیری شود.
- ساختار تیم را با قانون کانوی همراستا کنید — سازمان را طوری تنظیم کنید که معماری دلخواهتان از آن ظاهر شود .
۷. قابلیت مشاهده بالا (Highly Observable)
در یک سیستم توزیعشده، نمیتوان به مشاهده رفتار یک Instance منفرد تکیه کرد . به یک دید جامع نیاز دارید:
- Semantic Monitoring: به جای فقط چک کردن «آیا CPU بالاست؟»، از Synthetic Transaction استفاده کنید تا رفتار واقعی کاربر را شبیهسازی کنید.
- Aggregate Logs: تمام لاگها باید در یک مکان مرکزی جمع شوند.
- Correlation IDs: برای Trace کردن یک فراخوانی در کل زنجیره سرویسها .
- Aggregate Stats: معیارها باید هم در سطح سرویس و هم در سطح سیستم قابل مشاهده باشند.
چه زمانی نباید از ریزسرویس استفاده کرد؟
نویسنده صادقانه یک بخش مجزا به این سوال اختصاص میدهد :
۱. وقتی دامنه را نمیشناسید: هر چه دامنه کمتر شناخته شده باشد، پیدا کردن Bounded Context درست سختتر است. مرز سرویس اشتباه یعنی تغییرات زیاد بین سرویسها — که بسیار گران است . توصیه: ابتدا مونولیت بسازید، دامنه را یاد بگیرید، سپس جدا کنید.
۲. پروژههای Greenfield (از صفر): جدا کردن چیزی که هنوز وجود ندارد بسیار دشوارتر از جدا کردن چیزی است که دارید. باز هم اول مونولیت .
۳. وقتی تیم آمادگی ندارد: چالشهای ریزسرویسها با مقیاس بدتر میشوند. اگر همه چیز را دستی انجام میدهید، با ۲ سرویس شاید قابل مدیریت باشد، اما با ۵ یا ۱۰ سرویس چطور؟
سیستمهای خودتوصیفگر (Self-Describing Systems)
وقتی صدها سرویس دارید، نه تنها برنامهها باید بدانند کجا همه چیز هست، بلکه انسانها هم باید بفهمند . نویسنده دو ابزار را معرفی میکند:
- Swagger: یک استاندارد برای مستندسازی REST API که از طریق Annotation در کد تولید میشود و همیشه بهروز است.
- HAL (Hypertext Application Language): یک فرمت که به یک Endpoint اجازه میدهد قابلیتهایش را به صورت Link خودش اعلام کند — مشتری میتواند API را به صورت پویا کشف کند .
Humane Registry: مارتین فاولر این مفهوم را مطرح کرد: یک مکان مرکزی که انسانها اطلاعات سرویسها را ثبت میکنند — حتی یک Wiki ساده .
کلام پایانی نویسنده
نویسنده با یک پیام صادقانه کتاب را تمام میکند :
«ریزسرویسها یک سفر هستند، نه یک مقصد. به صورت تدریجی پیش بروید. سیستم را تکه به تکه جدا کنید و در مسیر یاد بگیرید. و به این نظم عادت کنید — توانایی مداوم برای تغییر و تکامل سیستمهایتان، مهمترین درسی است که من میتوانستم به شما بیاموزم. تغییر اجتنابناپذیر است. آن را بپذیرید.»
۱. تحلیل انتقادی/فنی: جمعبندی کل کتاب
پس از دوازده فصل، اگر بخواهیم یک Anti-Pattern اصلی را نام ببریم که نویسنده بیش از هر چیز در برابرش هشدار داده، این است:
شروع با ریزسرویس — بدون شناخت دامنه، بدون اتوماسیون، بدون آمادگی تیم.
نویسنده بارها به یک توالی صحیح اشاره میکند :
- مونولیت مدولار بساز — مرزهای تمیز درون یک Process
- دامنه را بشناس — Bounded Context ها را در عمل آزمایش کن
- ابزار اتوماسیون را آماده کن — Deploy Pipeline، Monitoring، Logging
- اولین سرویس را جدا کن — کمریسکترین، با بیشترین اطلاعات
- یاد بگیر و تکرار کن
