میکروسرویسها چی هستن؟
با یه سرچ ساده تو اینترنت، میشه اطلاعات خیلی زیادی رو راجع به میکروسرویسها پیدا کرد، اما من براشون یه تعریف ساده دارم: واضحترین نقطه مقابل معماریهای یکپارچه یا monolith.
مونولیتها وقتی ایجاد میشن که ما تمام اجزای برناممون رو یکجا تو یه ساختار بزرگ قرار بدیم و اون رو یکجا دیپلوی کنیم. مونولیتها تاریخچهی نسبتا طولانیای دارن، از زمان فریمورکهایی مثل جنگو، ریلز و پیاچپی.
ولی بیاین همینجا یک چیز رو مطمئن بشیم، اون اینکه مونولیتها و میکروسرویسها تنها آپشنهای ما برای طراحی سیستم نیستن!
اما اگر آدمی باشید که اصولا دنبال ترندها میره، پس حتما حداقل یکبار یه سیستم مونولیت رو ساختید (حالا یا با آگاهی، یا فقط چون فریمورکی که ازش استفاده میکردید شما رو مجبور به رعایتش کرده) و بعد به مشکلاتی که این نوع معماری با خودش به همراه داره برخوردید. بعد شنیدید که معماری میکروسرویس هم هست و تلاش کردید تا معماری رو تغییر بدید و همهچیز رو تبدیل به میکروسرویسها کنید.
اما حقیقت اینه که نباید همیشه دنبال ترندها رفت. بین این دو معماری (مونولیت و میکروسرویس) نقاط زیادی وجود داره که احتمالا فقط یکی از این نقاط برای شما کارامده. یه رویکرد درست از جایی شروع میشه که شما اول فکر کنید کجا interfaceهای برنامتون رو قرار بدید.
جعبه و پیکان یا Box and Arrows
به اینترفیسها یا Interfaces میشه به چشم پل ارتباطی بین ماژولها یا Modules نگاه کرد. یه ماژول، مجموعهای از کدهای به هم مرتبطه. حالا تو بحث طراحی سیستم، ما در مورد باکسها و فلشها، جعبه و پیکان یا Boxes and Arrows صحبت میکنم. ماژولها همون باکسها و اینترفیسها همون فلشها هستن.
سوال عمیقتر اینه که، این جعبهها یا باکسها چقدر باید بزرگ باشن؟ چقدر اصلا قرار توشون جا بگیره؟ چطور میشه تصمیم گرفت که باید یه جعبه رو به تیکههای کوچکتر تقسیم کرد؟ بهترین راه ارتباطی برای این باکسها چیه؟
برای حل تکتک این سوالات، رویکردهای مختلفی وجود دارن. واقعهیت اینه که کسی نمیدونه بهترین راهکار چیه و واقعیت اینه که این سوالات، در واقع سخترین سوالات تو مهندسی و طراحی نرمافزار هستن!
تو دهههای مختلف و با گذر زمان، ما از انواع مختلفی از این جعبهها عبور کردیم. از دورهای گذشتیم که برای کدهامون لازم بود آدرس خط ها رو بدیم و روی کارتها پانچ کنیم. ولی ازشون گذشتیم چون بهمون امکان ساختار بندی رو نمیدادن.
بعد اومدیم و فانکشنها رو اضافه کردیم، باکسهای خیلی کوچولو با اینترفیسهاشون یا همون پارامترها و مقادیر بازگشتیشون.
بسته به اینکه وارد چه شاخهای از برنامهنویسی بشیم، با باکسهای مختلفی مثل فانکشنهای تودرتو، پروتوتایپها یا Prototypes، کتابخونهها، شیگرایی، coroutineها، پراسسها، تردها و غیره مواجه میشیم. و همهی اینا باکسهای ما هستن! وقتی بتونیم این باکسها رو از هم جدا کنیم، لازم میشه که تو قدم بعد با پیکانها یا همون Arrowها اینها رو به هم متصل کنیم. برای اینکار، ما APIها، Socketها، RPCها، فایلسیستم، دیتابیس و غیره رو داریم.
تا حالا تلاش کردید که این جعبهها رو برای یه سیستم عامل یونیکسی طراحی کنید؟ به نظرمم هیچوقت این کار رو نکنید! بذارید اینطوری بگم: یهسری فانکشن توی تردها، تردها توی پراسسها، پراسسها توی کانتینرها، کانتینرها توی یوزر اسپیسها، توی کرنل، توی ماشینمجازی که داره توی یه رک توی یه دیتابیس اجرا میشه و…
هر کدوم از این جعبهها تو هر کدوم از لایههای خودشون، به نوعی از مابقی لایهها جدا و به شکلی بهشون متصل هستن. واقعیت اینه که نمیشه یه طراحی دقیقی از این سیستم رو تو یه فضای دو بعدی ساخت، بدون اینکه خطوط اتصالیشون از روی هم (خیلی بینظم) عبور نکنن!
تمام اینها طی دههها تکامل پیدا کردن. اما خب، واقعیت مسئله: یه کثافتکاریه به تمام معنا!
اما به جای اینکه سعی کنم بگم چه چیزهایی تو این مدتزمان کقیف شدن، بیاین با هم به این بپردازیم که اساسا برنامهنویسها میخواستن به چه چیزهایی برسن؟
در راه ماژولار کردن (همهچیز)
نهایت اونچه که بشه از سیستمهای ماژولار بدست آورد ایناست:
- هر تیکه از کد رو از تیکههای دیگه جدا کرد،
- اون تیکهها هر جا که نیاز بود به هم، با اینترفیسهای لازم، به هم وصل کرد،
- مطمئن باشیم اگر تیکه رو تغییر میدیم، به تیکههای دیگه آسیبی وارد نمیشه، یا اونها هم اونطور که باید تغییر میکنن.
صنعت کامپیوتر حقیقتا زمان زیادی رو صرف میکنم تا اکتشاف کنه، شاید یکی از دلایلی که این صنعت استرس زیادی رو توی خیلیها ایجاد میکنه همین باشه، اینکه میخواد بهترین روشهای ماژولاریتی رو پیدا کنه، در عین حال هم تلاش کنه که توسعهی نرمافزار تا جایی که ممکنه بدون درد باقی بمونه!
ولی متاسفانه، خیلی خلاصه، موفق نبوده!
در واقع اولین مشکل و بزرگترین مشکل تو دنیای کامپیوتر، ایزولیشن یا Isolation جعبههای ما از همدیگست.
ایزوله کردن مسئلهی بزرگیه، همینطور مشکلی بزرگ! چه تلاشهایی که روز و شب برای بهینهسازیش اتفاق نمیفته، اما باز هم، ما همچنان از حملاتی میشنویم که سیستمهایی رو از پا درآوردن، یا هکهایی که باعث شدن اطلاعاتی به سرقت برن.
هر تکنولوژی جدیدی (مثل داکر فرضا) که به وجود میاد، چنین پروسهای رو طی میکنه:
- یه ایدهی جدیده، ما اینبار میتونیم این مشکل رو برای همیشه برطرف کنیم!
- (کاربرها شکایت میکنن که این سیستم حتی از قبلی هم کندتر و کار و زمان بیشتری میبره که کانفیگ بشه)
- مشکلات و باگهای بزرگ اولیه پیدا میشن و برطرف میشن
- استفاده ازش سراسری میشه
- مشکلات جدیدتری پیدا میشن و فیکس میشن
- به نقطهای میرسیم که دیگه نمیدونیم چطور باید فیکس کنیم
- امیدمون رو از دست میدیم که اساسا ایزولیشن کردن با این روش حتی فکر درستی بوده!
- حتی نمیتونیم بیخیال این تکنولوژی بشیم چون آدمهای زیادی دارن ازش استفاده میکنن
- داستان رو از اول تکرار کنیم!
برای مثال، متخصصین امنیت حتی شک دارن که تکنولوژیهای زیر نزدیک به امن هستن:
- ایزولیشن پراسسها یا Process Isolation و حفاظت از حافظه در یک سیستم یونیکس،
- به اشتراکگذاری درست دسترسیها بین پراسسهای سیستمعامل، وقتی که اجرای از راه دور کد یا Remote Code Execution اجازه داده شده باشه،
- فیلتر کردن syscallها برای ایزوله کردن یه پراسس،
- چند پراسس نا امن همزمان از Hyperthread در CPU استفاده کنن،
- ایزوله کردن حافظه بین چند ماشین مجازی که همزمان دارن از یک هستهی CPU استفاده میکنن.
راستش، تا جایی که من میدونم، بهترین نوع ایزولیشن در حال حاضر، با توجه به اینکه تخصصم وب هست، سَندباکس کروم یا Chrome Sandbox هست (اگر شما مورد دیگهای میشناسید توی کامنتها بنویسید)، در حقیقت تولید کنندههای مرورگرهای وب همشون از ابزاری مثل این استفاده میکنن.
ماشین مجازی برای همهچیز!
ولی صبر کنید! راهاندازی یه ماشین مجازی برای هر ماژول واقعا دردسر بزرگیه، تازه، اون ماژول ابعادش چقدره؟!
مدتها پیش، وقتی جاوا برای اولینبار عرضه شد، ایده این بود که بشه به هر خط از هر تابع تو هر شئ، مجوزهای دسترسی رو اعمال کرد، حتی بین اشیا موجود در یک باینری، که درواقع باعث بشه که این بار از روی CPU برداشته بشه. اما واقعیت چی شد؟ هیچکس دیگه حتی یادشم نمیاد که اساسا همچینچیزی ممکن بوده، و خب اگر مارکتینگ رو کنار بذاریم و از Cloud Functionها بگذیرم، هیچکس حتی فکر هم نمیکنه که باید این کار رو انجام داد.
هیچکدوم از روشهای مرسوم و شناختهشدهی ایزولیشن بینقص کار نمیکنن، ولی هرکدومشون تا حدودی از پس این کار برمیان. بهترین آپشنی که در حال حاضر میشناسیم برای این موضوع، ماشینهای مجازی هستن که تامین کنندههای اینترنت (فقط خوباشون) به ما ارائه میدن!
بیاین همچنان تجسم کنیم، با در نظر نگرفتن شواهد، که اکثر سیستمها انقدر در هم گره خورده هستن که یه هکر با تجربه میتونه بین همهی ماژولها نفوذ کنه. برای مثال، اگر یه کسی یه کتابخونه رو به برنامهی شما اضافه کنه (چیزی که ما هر روز توی انپیام میبینیم) عملا میتونه کل سیستم رو دستش بگیره.
به همین شکل، اگر برنامهی ما دسترسی نوشتن روی دیتابیس داشته باشه، هکرها میتونن تقریبا کاری کنن که هرچیزی هر جای دیتابیس نوشته بشه. یا اگه بتونه به شبکهای متصل بشه، احتمالا اونا هم میتونن به همهجای اون شبکه وصل بشن…
شاید این موارد بالا کمی تخیلی به نظر برسن، اما در نظر گرفتنشون میتونه کمک کنه که جلوی Over-complication توی برنامهی ما گرفته بشه.
در هر حال، بیاین بهمچنان به فرضمون ادامه بدیم. این طوری میشه دو تا مرز رو برای ماژولهامون در نظر بگیریم:
- مرز اطمینان، فضایی که ماژولها توش به هم اطمینان دارن و میتونن مطمئن باشن که خرابکاری توشون صورت نمیگیره و
- مرز بدون اطمینان، یا فضایی که ماژولها به هم اطمینانی ندارن، پس باید از هم جدا بشن.
دقت کنید که اینجا یه موضوع خیلی عجیب امنیتی مطرح نشده، خیلی از نرمافزارهای جدید دنیا یه جورایی با همین قائده جلو اومدن. مثلا گوگل کروم کدهای جاوااسکریپت رو تو یه حالت سندباکس اجرا میکنه، چون صفحههای وب خطرناک هستن.
اکثر سیستمها عامل با برنامههای داخلی خودشنو یا Native Apps به شکل یه پراسس خالی اجرا میکنن که همشون به یه شکل به فایلسیستم، شبکه و غیره دسترسی دارن، چون یه زمانی فکر میکردیم که میشه بهشون اعتماد کنیم! از همینجا هم ویروسها به وجود اومدن!
متخصصها امروزه دیگه به سیستمعاملهای چند-کاربری یونیکسی اعتمادی ندارن، چون معلوم شده که Process Isolation اونطورها هم که فکر میکردن قدرتمند نبوده، حتی سرویسهای کلادی که میگیریم هم بای دیفالت، دستور sudo رو بدون نیاز به پسورد اجرا میکنن، چون در نهایت مشخص شده که چه با root ایزوله کنیم چه بدون روت، فرقی به حالمون نمیکنه، خب دیگه چرا اصلا از اول خودمون رو اذیت کنیم؟!
(حالا اصلا چرا دیگه مجبور میکنیم کاربر sudo رو تایپ کنه؟!)
راه دوری نمیرم، فکر کنید که چقدر همین ایدهی بهم چسبوندن dependencyها، یا همون Peer Dependencies توی NodeJS و NPM، راه رو برای حملات زنجیرهای یا Chain Attackها به سرویسهای مختلف درست کرده!
حتی تو داکر و کوبرنتیس، که چندتا کانتینر مختلف رو روی هم اجرا میکنن، چون فرض اول اینه که میشه بهشون اعتماد کرد! یادمه یکبار روی سرور یکی از مشتریامون که لازم بود داکر نصب بشه، من بعد از یه مدت، یه رشد خیلی زیادی رو توی مصرف منابع (بیشتر سیپییو) دیدم، کلی گشتم و متوجه شدم که یکی از کانتینرها مصرف عجیبی پیدا کرده. رفتم گشتم و خلاصه بعد از چند دقیقه متوجه شدم که اون کانتینر تو آخرین آپدیتش به ویروسی مبتلا شده که بعد از گذشت چند روز از نصبش، کدی رو فعال میکنه تا از روی سرور بتونه کریپتو ماین کنه! (اشتباه اول من: بهش اعتماد کرده بودم).
مرز بین ماژولها یا مرز بین سرویسها
اگه اینهمه لایههای ایزوله ضعیف هستن، چرا اصلا داریم باهاشون کار میکنیم؟!
تاریخ کم و بیش نشون داده، ما میتونیم سادگی کدها و روابط بین اینترفیسها بهینه کنیم، بدون اینکه آسیب زیادی به امنیت بزنیم، اگه خیلی از این لایههای اضافی رو حذف کنیم. ولی بیخیال تاریخ، لازم بود همهی این موارد ایزوله کردن رو بگم، تا در نهایت به یه چیز برسم: ما هیچوقت مرز بین ماژولها رو، به دلایل امنیتی، تعیین نمیکنیم!، بلکه دقیقا پیرو قانون کانوی سیستمهامون رو مطابق روابط داخلی سازمانمون طراحی میکنیم!
درواقع، مهندسین ماژولها رو بر اساس تقسیم کار بین تیم طراحی میکنن، و در نهایت باعث میشن که ماژولها مشابه روابط بین تیمی، با هم ارتباط برقرار کنن (منطقی هم هست! نمونهی کوچکش، تقسیم تسکها توی یه تیم فنی!)
حالا این وسط، کاری که این مرزها انجام نمیدن، اینه که حجم لازم برای Deploy کردن سیستم رو مشخص نمیکنن! بیاین به سیستمعاملها به عنوان یه مثال نگاه کنیم:
- سیستمعامل Chrome OS، چندین برنامهنویس داره، اما کاربرهاش (که خودم یکیشون بودم) یه بروزرسانی رو دریافت میکنن که شامل تمام قسمتهای تست شدهی سیستم (به نوعی پایدار) هست، که مکاواس، آیاواس و اندروید هم همین روال رو دنبال میکنن،
- دبیان (لینوکس) هم چندین هزار برنامهنویس داره، اما کاربرهاش پکیجهای تکی رو روی سیستمشون نصب میکنن، بدون اینکه مشکلی روی نسخههای مختلفش داشته باشن!
ما خیلی وقتها، عدم محبوبیت لینوکس روی دسکتاپ رو، به پای شرایط سیستمهای متنباز میذاریم که در برابرشون پول شرکتهای گنده وجود داره، اما به نظر من این بیشتر برمیگرده به مدل پیادهسازی (Deployment) سیستمعاملهای متن باز!
هر دوتا سیستم (دبیان و کروم) ماژولها (پکیجها)ی زیادی دارن که توسط برنامهنویسهای زیادی، که خودشون تو تیمهای مختلف دستهبندی شدن، ساخته میشن، و هردو سیستمعامل، روابطی رو بین ماژولهاشون دارن. راستش حتی احساس میکنم که اگه نمودار جعبه و پیکان این دو سیستمعامل رو ترسیم کنیم، احتمالا با شکلهایی یکسان مواجه بشیم!
حالا! قسمت جالب موضوع، اگه این دوتا سیستمعامل، قرار بود نرمافزارهای بکاند یه سرویس کلاد باشن، ما به احتمال زیاد با دو ساختار Monolith و Micro-service مواجه میشدیم! 🤯 که این دقیقا برمیگرده به نوع پیادهسازیشون! یکیشون چندین سرویس مختلف رو داره، و اونیکی، یه سرویس گنده برای دیپلوی!
یعنی دو پیادهسازی متفاوت برای یک معماری! یعنی مرز بین ماژولها با مرز بین سرویسها زمین تا آسمون فرقشونه!
مرز بین سرویسها چی میشه؟
بیاین یه بار دیگه اهداف از مرزبندی بین سرویسها رو مشخص کنیم:
- ایزولهکردن، که دلیل اولش، حفظ امنیت سیستمه.
- روابط، که مطابق قانون کانوی، مرز بین ماژولها در تلاشه تا روابط بین تیم رو بازسازی کنه، درحالی که کاری به مرز بین سرویسها نداره.
- همخونی یا Compatibility بین سیستمی، خصوصا اگر از ساختاری مثل NodeJS استفاده میشه که با یه آپدیت ممکنه خیلی چیزها بهم بریزن.
- قابلیت آپگرید و دانگرید و مقیاسپذیری، که دقیقا همونچیزهایی هستن که مرز بین سرویسها رو مشخص میکنن.
حالا اگه بخوایم مرز بین سرویسها رو مشخص کنیم، میتونیم این سوالات رو از خودمون بپرسیم:
چقدر قرار هست که بالا اومدن سیستم Monolithما طول بکشه؟
که این میتونه باعث بشه، آپگرید کردن (خصوصا با اینترنت ایران) تبدیل به یه عذاب بزرگ بشه.
آیا مدل دیتابیس بین همهی سرویسها، یا بهتر بگم Schema Version درستی در حال استفاده شدنه؟
که بخاطرش گاهی شاید مجبور بشیم همهی سیستمها رو متوقف یا Pause کنیم تا مدل بین همه یکسان بشه.
آیا CIها هی در حال Fail شدن هستن؟
که مشخصه یه ایراد بزرگ توی کد وجود داره! اینجا اصلا سرویسهای کوچتر ساختن برای حل تستها جواب نمیده، چون اصل ماجرا ایراد داره!
آیا قسمتهای مختلف قراره از نظر مقیاس، متفاوت رشد کنن؟
که اینجا لازم میشه یه ساختار Load Balancing درست طراحی شده باشه، چون اینطوری خود Load Balancer میتونه اجزا رو (مثلا وقتی تسکهای Memory-Heavy و تسکهای CPU-Heavy داریم) اونطور که باید بالانس کنه. (این ویژگی اصلی Load Balancerهاست)
آیا ریکوئستهای زیاد و سنگینی، به صورت سری و نه موازی، اجرا خواهند شد؟
که مثلا اینجا میشه از یه حرکت معمول تو معماری میکروسرویس استفاده کرد، که ریکوئستها رو تو Message Queueها بذاریم و یه سری Worker Instance رو تعریف کنیم تا اونها رو پراسس کنن. که خود این میتونه باعث بروز یه مشکل بزرگ بشه (Queue Explosion) که البته دیزاینهای بهتری برای حلش طراحی شدن.
واقعیت اینه که اکثر موارد بالا، واقعیاتی هستن که بخاطرشون باید بین سرویسها مرزبندی کرد. حتی میشه بر مبنای اونا، تیمهای مختلفی رو ساخت و تسکهای محصول رو طبقهبندی کرد! حتی میشه، با یکم تغییر، یه همچین ساختار میکروسرویسی رو، تبدیل به یه ساختار منولیت کرد و آپدیتها رو عرضه!
یادمون نره، آیاواس، کروماواس و عیره منولیتهای بزرگی هستن، که تیم ما هم صد در صد از تیم اونا کوچکتره، برای همین حقیقتا نیاز نیست که برای یه نرمافزار کوچک، چندین سرویس مختلف طراحی بشه، در حالی که میشه همهرو یکجا درآورد.
اصولا باید معماری رو طوری ساخت که سادهترین حالت ممکن برای تیم باشن، تا زمانی که واقعا (و جدا میگم، واقعا) مجبور بشیم اونها رو به سرویسهای ریزتر تقسیم کنیم
ممنونم که این مطلب رو خوندید. این مطلب، بخشی از تجربیات من تو طراحی سیستمی GraphCMS بود که برای نوشتنش هم از چند مطلب دیگه کمک گرفتم، اگر خوشتون اومد، خوشحال میشم که به اشتراک بذاریدش. ممنونم.