در این مقاله قصد دارم درباره پروژهای که در درس معماری نرمافزار دانشگاه شهید بهشتی انجام دادم توضیحاتی رو خدمتتون ارائه بدم. هدف این مقاله آشنایی با یکی از معماریهای مطرح در حوزه مهندسی نرمافزار هستش و میخواهیم این معماری را به همراه کانتینرها (Container) بررسی کرده و نهایتاً یک پیادهسازی از این معماری داشته باشیم.
در قدم اول یک معرفی کوتاهی درباره معماری مایکروسرویسها خواهم داشت (درباره مفاهیم و معرفی کامل کانتینرها در این مقاله صحبت کردهام). سپس موضوعات مطرح و مرتبط با این دو معماری را بررسی و دستهبندی خواهم کرد (همچنین در هر دستهبندی، بهترین گزینه را معرفی میکنم). و در آخر به صورت مختصر درباره یک سیستم دمو که با معماری مایکروسرویسی به صورت داکرایز شده (dockerized) پیادهسازی کردهام توضیحاتی را ارائه میدهم (لینک گیتهاب پروژه را در انتهای این مقاله در اختیار علاقهمندان قرار خواهم داد).
ما همیشه اسم معماری مایکروسرویسها را کنار معماری یکپارچه (monolithic) شنیدهایم. این دو معماری به نوعی دو دیدگاه مختلف از معماری نرمافزار را نشان میدهند که تقریبا در تقابل و تضاد با یکدیگر میباشند. معماری یکپارچه، یک رویکرد سنتی برای تولید سیستمهای نرمافزاری میباشد و اکثر بخشهای نرمافزار در یکجا متمرکز هستند. در تصویر زیر میتوانید یک نمونه ساده از این معماری را مشاهده کنید.
همانطور که در تصویر بالا مشاهده میکنید بیشتر بخشهای این سیستم مانند امنیت (احرازهویت، اعطای دسترسی و...)، لایه Presentation، لایه منطقی و کسبوکاری، بخش پیامرسانی (Notification module)، بخش ارتباط با repository (پایگاهدادهها و external storage ها) و... در یکجا و در یک codebase واحد متمرکز شدهاند. علاوه بر مشکلات متعددی مانند SPOF (Single point of failure)، تاثیر بهسزای یک تغییر کوچک در کل سیستم، قابلیت اطمینان (Reliability) و... که این معماری به همراه دارد، مشکل مقیاسپذیر نبودن این معماری برای پروژههای بزرگ (Enterprise Systems یا سیستمهای نرمافزاری مقیاس وسیع) نیز یکی دیگر از دغدغههای اصلی سازمانها و شرکتهای IT میباشد.
بنابراین به دلیل معایب بسیاری که معماری یکپارچه به همراه داشت، مهندسان و معماران نرمافزار بیش از پیش به سمت معماریهای توزیع شده (مانند معماری سرویسگرا یا مایکروسرویسی که در اینجا فقط به معماری مایکروسرویسها میپردازیم) متمایل شدند. در معماری مایکروسرویس، بخشهای مختلف یک سیستم نرمافزاری به صورت مجزا طراحی، توسعه و در قالب سرویسهای کوچک مستقر میشوند و نهایتاً با ارتباط با یکدیگر یک کل واحد را تشکیل میدهند.
معماری مایکروسرویس ویژگیها و خصوصیات زیادی از جمله مولفهای بودن سرویسها (ویژگی plug & play)، کسبوکار محور بودن هر مایکروسرویس، محصولگرا بودن (هر مایکروسرویس یک محصول محسوب میشود و نه یک پروژه)، حاکیمت غیرمتمرکز، خودشمول بودن (یعنی هر مایکروسرویس به صورت مجزا دادههایش را مدیریت میکند و حافظه، مخزن و یا دیتابیس مخصوص خود را دارد) و... دارد. البته قابل ذکر هست که برخی از این ویژگیها الزاماً یک مزیت نمیباشد و گاهی ممکن است یک عیب نیز محسوب شود (مثلا ویژگی خودشمول بودن در سناریوهایی که تمامی مایکروسرویسها در یک شبکه وجود دارند بیمعنی است زیرا که وجود یک پایگاهداده یا یک schema به ازای هر مایکروسرویس، بسیار هزینهبر میباشد).
همانطور که در تصویر ۳ مشاهده میکنید، پس از تجزیه صحیح بخشهای یک سیستم monolithic میتوان یک سیستم با معماری مایکروسرویس را تولید کرد. نکته قابل ذکر درباره معماری مایکروسرویس این است که با وجود مزیتهای زیاد این معماری نسبت به معماری یکپارچه، همچنان نمیتوان با قطعیت بیان کرد که این معماری در آینده نیز همچنان به عنوان یک معماری قالب و مطرح استفاده خواهد شد. به عنوان مثال یکی از چالشهای مهمی که در معماری مایکروسرویس همچنان وجود دارد این است که نمیتوان یک رویکرد (یا چندین رویکرد مشخص) برای خطایابی و ردگیری خطای سیستم مایکروسرویسی مطرح کرد (رویکردها و راهحلهای مختلفی برای این مسئله وجود دارند ولی در هر use case نحوه حل این مسئله متفاوت میباشد.). در سیستم مایکروسرویسی که توسعه دادهام یک راهحل ساده برای حل این مسئله پیادهسازی کردهام.
در این بخش به بررسی و دستهبندی موضوعات مختلف معماری مایکروسرویس و کانتینرها (از جمله زبانها، تکنولوژیها، ابزارها، فریمورکها و...) پرداختهام. اصولاً در شکلگیری هر معماری، مباحث و موضوعات مختلفی دخیل میباشند. به همین دلیل در این بخش سعی میکنیم بیشتر موضوعات و مباحثی که مربوط به معماری مایکروسرویسها و کانتینرهاست را مطرح کنیم.
مهمترین و حیاتیترین کار برای ساختن هر برنامه نرمافزاری، تنظیم و پیکربندی پایه و اساس صحیح آن است که این کار توسط سیستم عامل باید انجام شود. اصولاً سیستم عاملهای مختلفی برای اجرای برنامههای سروری (میزبانی سرور) وجود دارند ولی پرکاربردترین آنها سیستم عاملهای لینوکسی میباشند. لینوکس یک سیستم عامل است که برای ساخت و توسعه برنامه نرمافزاری استفاده میشود و یک محیط مستقل برای اجرا ارائه میدهد. همچنین لینوکس امکان هماهنگی و پیکربندی Custom سرویسها و فرآیندهای کوچک و بزرگ مانند شبکه، امنیت، ذخیرهسازی و... را فراهم میکند. نکته مثبت دیگری که سیستم عاملهای لینوکسی دارند، پشتیبانی خوب آنها از Container ها می باشد که همین مزیت بسیار خوبی برای مایکروسرویسها محسوب میشود (در دستهبندی Containerization بیشتر درباره این موضوع صحبت میکنیم).
اما سوالی که باقی میماند این است که کدام توزیع از سیستم عامل لینوکس برای زیرساخت معماری مایکروسرویس مناسب میباشد. ممکن است در ابتدا به نظر برسد که Red Hat یا Ubuntu گزینههای منطقی باشند، اما نکتهای که باید در نظر گرفت این است که هر دو، سیستم عاملهایی با فیچرهای کامل هستند و دارای عملکردهای زیادی میباشند که نیازی به تمامی آنها نداریم. زیرا علاوه بر ناکارآمد بودن (عملکرد غیرضروری دیسک و حافظه مصرفی بالا که ممکن است بر عملکرد سیستم نیز تأثیر بگذارد)، اجرای یک سیستم عامل با فیچرهای غیرضروری، امکان و فرصت حمله هکرها را بیشتر میکند (مثلا باز بودن پورتهای زیاد دسترسیهای بیشتر باعث ایجاد فرصتهای بیشتری برای هک شدن ایجاد میکند).
در سطوح مختلفی کسبوکاری انتخاب سیستمعامل مناسب بسیار حائز اهمیت میباشد به همین دلیل میتوان انتخاب زیرساخت مناسب را مهمترین گام توسعه هر سیستمی معرفی کرد. برای این پروژه از سیستمعامل CentOS استفاده کردهام زیرا علاوه بر اینکه برای سرورها optimize شدهاند، میتوان آن را با حداقل منابع سختافزاری اجرا کرد.
معماری مایکروسرویس یک مزیت بسیار مهم را ارائه میدهد و آن این است که اجازه میدهد از تکنولوژیها و زبانهای مختلف برای ساخت سرویسهای متنوع از یک سیستم نرمافزاری استفاده شود. بنابراین، توسعه دهندگان در انتخاب زبان و تکنولوژی که می خواهند با آن برای ساخت مایکروسرویسها کار کنند، آزادند. نکتهای که باید در این دستهبندی در نظر گرفت، انتخاب زبان و تکنولوژی برحسب نوع کسبوکار میباشد؛ یعنی اینکه کدام زبان برنامهنویسی و کدام تکنولوژی باید برای توسعه انتخاب شود کاملاً به نیاز عملکرد تجاری بستگی دارد.
زبانها و تکنولوژیها و فریمورکهای مختلفی برای پیادهسازی مایکروسرویسها وجود دارند. به عنوان مثال چند مورد از محبوبترین این تکنولوژیها عبارتند از: Spring Boot (جاوا)، Flask (پایتون)، Express JS (جاوااسکریپت - Node JS) و Elixir (بر روی Erlang virtual machine اجرا میشود). در این پروژه از فریمورک Spring boot، فریمورک Angular، ابزارهای Consul، Splunk و RabbitMQ استفاده شده است.
کانتینرسازی (Containerization) شامل توسعه یک نرمافزار با تمام وابستگیها و فایلهای پیکربندی میباشد که برای اجرای آن در یک محیط پردازشی متفاوت از طریق ماشین توسعهدهنده به سرورهای مختلف (که ممکن است سیستمعامل متفاوتی داشته باشند) مورد نیاز است. از کانتینرسازی برای ساخت، انتقال و اجرای هر برنامه نرمافزاری در تمامی محیطهای مختلف (عملیاتی، تست، توسعه و...) کار میکند. همچنین کانتینرها، جایگزینی سبک وزن برای ماشینهای مجازی میباشند و انتخاب بهتری برای اجرای برنامهها محسوب میشوند. به عبارتی دیگر، استفاده از کانتینرها به این معنی است که پردازش ارزش آفرین (برنامههای نرمافزاری توسعه داده شده) بیشتر ادامه پیدا کند و پردازشهای غیرضروری (مصرف بیهوده منابع توسط برنامههای غیرضروری VM) حذف شوند.
یکی از بهترین تطابقاتی که در حوزه معماری مایکروسرویسها مشاهده کرد، تطبیق مایکروسرویسها با تکنولوژی Docker میباشد. دو مورد از پرکاربردترین تکنولوژیهای کانتینریسازی عبارتند از Docker، Containerd. از این دو تکنولوژی برای ایجاد کانتینر برنامهها و سرویسهای نرمافزاری استفاده میشود.
مایکروسرویسها و API ها ارتباط بسیار نزدیکی بهم دارند و هر مایکروسرویس از طریق API با دیگر مایکروسرویسها و سرویسهای خارجی ارتباط برقرار میکند. بنابراین یکی از چالشهای مطرح در این حوزه مدیریت این API های مختلف و متنوع میباشد. به کمک API Management علاوه انتشار مدیریت شده API های مایکروسرویسها میتوان چندین API مربوط به مایکروسرویس ها را ترکیب کرد و سپس به عنوان یک API واحد منتشر کرد، روی API های مایکروسرویسها محدودیتهای امنیتی و دسترسی قرار داد (سیاستگذاری روی API ها) و سرویسهای قدیمی را به عنوان API ها مدرن در معرض دید قرار داد.
ابزارهای متعددی برای مدیریت API منتشر شدهاند که صرفاً برخی از آنها را در این بخش معرفی میکنیم. Kong Gateway، Tyk و WSO2 API Microgateway برخی از ابزارهای متنباز برای مدیریت API (از دید پیادهسازی API Gateway) میباشند. برای سیستم پیادهسازی شده به واسطه فریمورک Spring یک Reverse Proxy توسعه دادم که به صورت محدود برخی از قابلیتهای یک API Gateway در دنیای صنعت را دارد (Gateway توسعه داده شده فاقد قابلیتهای امنیتی، Rate Limit، سیاستگذاری و... میباشد).
برنامههای نرمافزاری که به واسطه معماری مایکروسرویسها ساخته شدهاند، سیستمی هستند که در آن هر مایکروسرویس مستقل با یکدیگر به صورت همزمان (Synchronously) و ناهمزمان (Asynchronously) ارتباط برقرار میکنند. برای انجام ارتباط بین مایکروسرویسها، از ابزارهای پیامرسانی (یا انواع صفهای پیامرسانی) استفاده میشود. دو مورد از پرکاربردترین این ابزارهای پیامرسانی عبارتند از:
همانطور که قبلتر صحبت کردیم بهترین تکنولوژی برای اجرای مایکروسرویسها در محیطهای مختلف، Container ها میباشند. روی هر سرور ممکن است یک یا دهها مورد کانتینر اجرا شوند. کانتینرها به مجموعهای از سرویسهای پشتیبانی مانند API کنترلی و همچنین اقدامات امنیتی نیاز دارند. جابجایی همه این کانتینرها روی سرور و خاموش کردن آنها، اطمینان از اتصال صحیح آنها، ردیابی آنها و...، اقداماتی پیچیده هستند که نیاز به ابزارهای زمانبندی کانتینرها (یا همان ابزارهای همنواسازی) دارند.
ابزارهای همنواسازی مختلفی وجود دارند، اما برای بسیاری از سازمانهای IT، انتخاب یکی از دو گزینه Docker Swarm یا Kubernetes مطرح میباشد. اولین تکنولوژی توسط شرکت Docker تولید شده و ممکن است فکر کنیم که بهترین انتخاب در همنواسازی است. با این حال Kubernetes بسیار محبوبتر میباشد و به نظر میرسد که اکوسیستم بزرگتری از فروشندگانی که با آن درگیر شدهاند، ساخته است. به عنوان مثال، دو فریمورک کاربردی محبوب، Red Hat’s OpenShift و Apprenda، هر دو از Kubernetes برای هماهنگ کردن استقرار چارچوب خود استفاده میکنند.
یکی از بخشها و حوزههای مهم در سیستمهای مایکروسرویسی مربوط به نظارت و مانیتورینگ این سرویسهای ریزدانه میشود. بسیار مهم است که ببینیم دقیقاً در هر زمان چه چیزی در یک سیستم در حال انجام است و این موضوع در یک توپولوژیهای توزیعشده بسیار مهمتر میباشد. متأسفانه نظارت در مایکروسرویسها و درکل سیستمهای توزیعشده دشواریها و چالشهای خاص خود را دارد. اکثر سیستمهای نظارتی سنتی برای محیطهای استاتیک با تعداد گرههای کم طراحی شدهاند. هیچ کدام از این سیستمها در یک محیط میکروسرویس مناسب نمیباشند. بسیاری از سیستمهای مانیتورینگ سنتی برای رسیدگی بهتر به نیازمندیهای نظارتی در مایکروسرویسها اصلاح شدهاند، اما همچنان در محیطهای پیچیده دنیای واقعی شکست میخورند. خوشبختانه یک ابزار متنباز به نام Prometheus وجود دارد که توسط یکی از مهندسان گوگل ساخته شده است. ما با جزئیات بیشتر درباره Prometheus و Grafana صحبت خواهیم کرد و هریک را به تفصیل بررسی میکنیم.
بحث مهم دیگر لاگهایی میباشد که سیستمهای توزیعشده تولید میکنند. اصولا به واسطه لاگهای سیستم میتوان مشکلات و خطاهای سیستم را ردگیری کرد و نظارت بهتری روی سیستم داشت. ولی خب در مایکروسرویسها، سیستم مدیریت لاگها نیز با چالشهای متعددی روبهرو میباشند (به عنوان مثال مختلف بودن تکنولوژی پیادهسازی و نحوه کاربرد هر سرویس، منجر به تولید لاگهای مختلف و متنوعی میشود که مدیریت آنها و سپس tracing سیستم را با دشواریهایی روبهور میکند). همچنین دو ابزار مطرح در این حوزه وجود دارند که یکی از آنها متنباز میباشد. اولین ابزار Splunk میباشد و نسخه trial دارد. ابزار دیگر ELK (یا Elasticsearch) میباشد که متنباز است. Splunk نسبتاً سیستمی قدیمیتر و جا افتادهتر میباشد ولی برخی مزیتهای ELK مانند فراگیر و رایگان بودن را ندارد.
اصولاً به دلیل زیاد بودن گرهها و سرویسها در معماری مایکروسرویس، فازهای ساخت، سرهمبندی و استقرار هر یک از آنها به صورت جداگانه کار زمانگیر و هزینهبری میباشد. به همین دلیل توسعه دهندگان نیاز به استفاده از ابزارهای ساخت مانند maven، Gradle و ابزارهای سرهمبندی و یکپارچهسازی (version control هایی مانند git) و ابزارهای استقرار مانند Jenkins دارند. به عبارت سادهتر در این معماری استفاده از CI/CD یک نیاز مبرم میباشد و پیکربندی و استقرار خودکار را تسهیل میکند.
همانطور که در بخشهای قبلی مطرح شد، تکنولوژی داکر یکی از گزینههای پرکاربرد برای استقرار مایکروسرویسها میباشد. به همین دلیل در توسعه این سیستم از این تکنولوژی استفاده کردم. همچنین برای این دموی ساده امکان بهرهبرداری از تمامی تکنولوژیها و ابزارهای مطرح در بخش قبل وجود نداشت.
در تصویر زیر میتوانید یک شمای کلی از سیستم طراحی شده مشاهده کنید. تمامی مولفههای این سیستم (یا به عبارتی تمامی مایکروسرویسها و تکنولوژیها) به صورت داکرایز شده مستقر شدهاند و امکان اجرای تمامی این مولفهها در هر سیستمعاملی که داکر روی آن نصب باشد وجود دارد.
سیستم توسعه داده شده یک سیستم نرمافزاری سادهی مبتنی بر وب برای تخصیص مجموعهای از محصولات به فروشندگان مختلف میباشد. این سیستم شامل ۱+۳ سرویس درشت دانه میباشد؛ یعنی هر سرویس خود شامل چندین مایکروسرویس است. این سرویسها عبارتند از:
علاوه بر تکنولوژیها و ابزارهای مطرح شده، از تکنولوژی Consul برای معرفی مایکروسرویسها به یکدیگر استفاده میشود (علاوه بر ویژگی Service Discovery از Consul برای ذخیرهسازی پیکربندیهای سیستمهای توزیع شده نیز میتوان استفاده کرد. اما فیچر اصلی این تکنولوژی، Service Discovery است که کاری شبیه به DNS را برای مایکروسرویسها انجام میدهد). همچنین برای ایجاد یک abstraction حول مایکروسرویسها از یک API Gateway استفاده شده (برای دریافت اطاعات بیشتر درباره این موضوع میتوانید مقاله API Gateway را بخوانید).
مطابق تصویر زیر اولین Tab بیانگر سرویس اول میباشد که در آن میتوان از بین فروشندگانی که ثبتنام نکردهاند، فروشندگانی را در این سیستم ثبت کرد.
دومین Tab در تصویر ۶ مربوط به سرویس دوم (یعنی ثبت محصول در سیستم میباشد). ستون سمت چپ مربوط به محصولاتی است که از MS3 گرفته شده است.
سومین Tab نیز برای سرویس سوم میباشد که ۲ مایکروسرویس MS1 و MS2 را درگیر میکند و برای ثبت یک محصول ثبتشده برای یک فروشنده ثبتنام کرده در سیستم، پیادهسازی شده است (در تصویر ۷ میتوانید این سرویس را مشاهده کنید).
و نهایتاً آخرین Tab مربوط به سرویس چهارم میباشد که در آن صفحه میتوانید جریان درخواستهای ارسال شده به این سیستم مایکروسرویسی را به همراه جزئیات هرکدام مشاهده کنید (هر جریان شامل یک شناسه منحصر به فرد، زمان ورود درخواست، زمان پاسخ، نام درخواست و جزئیات مربوط به مایکروسرویسهایی که این درخواست از آنها عبور کرده است، میباشد).
برای رسیدن از لاگهای خام به لاگهای Correlate شده از کوئری زیر (این کوئری با زبان SPL نوشته شده است)، استفاده کردهام. این کوئری دربازههای یک دقیقه لاگهای خام دریافتی را براساس شناسه هر درخواست (UID) یکپارچه میکند و خروجی این یکپارچهسازی لاگها را میتوانید در تصویر ۸ مشاهده کنید.
index="wild_index" _index_earliest="-1m" _index_latest=now()
| sort 0 -time
| eval nodeInfo = json_object("time", time, "project", project, "methodName", methodName, "flowStart", flowStart, "flowEnd", flowEnd, "start", start, "end", end, "status", status, "data", data, "url", url)
| transaction uid
| eval flowInfo = json_object("uid", uid, "nodes", nodeInfo)
| eval flowInfo = replace(flowInfo, "\\\\(.)", "\1")
| eval flowInfo = replace(flowInfo, "\"\{", "{")
| eval flowInfo = replace(flowInfo, "\}\"", "}")
| table flowInfo
| collect index="correlate_index"
میتوانید به Source Code، فایلهای Dockerfile، فایل docker-compose و... از طریق این آدرس github دسترسی داشته باشید.
این مقاله، بخشی از تمرینهای درس معماری نرمافزار در دانشگاه شهید بهشتی میباشد