بخش اول : آشنایی با مفهوم میکروسرویس
بخش دوم : ویژگی های اصلی یک میکروسرویس
بخش سوم : تحلیل یک پروژه کوچک بر اساس میکروسرویس ها
بخش چهارم : شروع پیاده سازی یک پروژه فروشگاهی
پیشنیاز 1 بخش پنجم : آموزش Node و Typescript برای تولید api
پیشنیاز 2 بخش پنجم : آموزش داکر و مفاهیم اولیه
بخش پنجم : پیاده سازی یک میکروسرویس برای نمایش کالاها
پیشنیاز 1 بخش ششم : آشنایی با Asp.net core 6
بخش ششم : پیاده سازی یک میکروسرویس برای کار با سبد خرید (همین مقاله)
بخش هفتم : پیاده سازی یک میکروسرویس برای محاسبه قیمت و تخفیفات
بخش هشتم: پیاده سازی یک میکروسرویس برای ثبت سفارش
برای دیدن ویدیوهای من در مورد برنامه نویسی عضو این کانال شوید :
https://t.me/mediapub_channel
در این نوشته میخواهیم سرویسی برای ایجاد و مدیریت سبد خرید کاربر طراحی کنیم. یعنی با توجه به لیست کالاهایی که در میکروسرویس مربوط به نمایش کالا (این مقاله) طراحی کردیم، کاربر میتواند در UI کالایی را انتخاب کرده و به سبد خرید خود اضافه کند. همچنین کاربر باید بتواند لیست سبد خرید خود را در هر زمان ببینید و یا آیتمی از آن را حذف کند. پس در این میکروسرویس حداقل به سه سرویس اضافه کردن کالا به سبدخرید، حذف کالا از سبد خرید و لیست کالاهای سبد خرید نیاز داریم.
همانطور که در تصویر میبینید یک فلش آبی رنگ از سمت چپ و یک فلش زرد رنگ از سمت راست کادر قرمز خارج شده اند. این دو فلش بیانگر ارتباطات داخلی بین میکروسرویس هاست که در بخش های بعدی به آن می پردازیم. همینقدر بدانید که به کمک gRPC یک ارتباط داخلی شبیه به ریکوئست/ریسپانس برقرار میکنیم. علت اینکار مشخص کردن قیمت و تخفیف کالاهای موجود در سبد است (متد Update سبد که در ادامه این متد را بدون ارتباط gRPC پیاده سازی میکنیم، فعلا از ورودی مقادیر را میگیرد) که با این روش، اطلاعات را از میکروسرویس Discount.Api دریافت میکنیم.
به کمک RabbitMQ اطلاعات سبد را به سفارش تبدیل میکنیم و به تعبیری سبد را نهایی میکنیم. اینکار را اصطلاحا Checkout میگویند و وظیفه ثبت این سفارش به عهده میکروسرویس Ordering.Api است.
انتظار من این است که خواننده بعد از مطالعه این نوشته بتواند موارد زیر را به راحتی تجزیه تحلیل کنید :
صرف نظر از نحوه ذخیره اطلاعات سبد، نگهداری اطلاعات سبد خرید هر کاربر و تفکیک سبدها از هم، بر اساس شناسه کاربر یا نام کاربری صورت میگیرد. ما برای ذخیره سازی از Redis استفاده خواهیم کرد. ردیس یک دیتابیس Nosql و بر اساس key/value است. یعنی میتوانیم آیتم هایی به شکل کلید/مقدار داشته باشیم. مثلا پنج کاربر زیر با سبدهایشان مشخص شده اند.
ما شناسه کالا را به شکل لیست جیسونی در Value قرار داده ایم و با این اطلاعات به راحتی میتوانیم سبد خرید هر کاربر را در هر لحظه داشته باشیم. (کاربر با شناسه 1500 هیچ کالایی در سبد خود ندارد، احتمالا قبلا داشته و حذف کرده و این نتیجه یعنی ما در متد حذف، ردیف مربوط به کاربر در دیتابیس Redis را حذف نکرده ایم!)
ردیس یک دیتابیس فوق العاده سریع است، چون به شکل sync کار میکند و از حافظه Ram برای ذخیره سازی استفاده میکند. ردیس دیتاتایپ های مختلفی را ساپورت میکند ومیتواند دیتا را در رم و دیسک ذخیره کند.(بر اساس آنچه در کانفیگیوریشن تعریف میکنیم) یعنی بعد از ری استارت شدن سرور هم میتوانیم دیتاها را نگه داریم و داشته باشیم. همچنین redis بسیاری از امکانات enterprise را دارد مثل sharding, Clustering, Sentinel, replication
عیوب ردیس در اشغال رم است و همچنین عدم پشتیبانی از کوئریهای پیچیده مثل حالت دیتابیس های ریلیشنال.
اگر ترنزکشن شما به خطا بخورد هیچ خطای از سمت ردیس برگردانده نمیشود.
برای نوشتن api ها از Asp Core 6 استفاده خواهیم کرد اما قبل از آن به کمک داکر، ایمیج Redis را به سیستممان منتقل کرده و Container آن را میسازیم تا در برنامه از آن استفاده کنیم. اگر درمورد داکر اطلاعات کمی داریم پیشنهاد میکنم این مقاله را بخوانید.
Docker pull redis
دقت کنید تمامی این دستورات و تنظیمات در سایت داکر نوشته شده است. کافیست docker redis را سرچ کنید.
حالا با دستور زیر این ایمیج را به container تبدیل میکینیم.
docker run -p 6379:6379 --name basketredis -d redis
ما پورت داخلی ردیس را به بیرون مپ کردیم. همچنین نام basketredis را برای این کانتینر انتخاب کردیم و از -d برای detach کردن اجرای ایمیج استفاده کردیم(در بک گراند اجرا شود و ترمینال به محض Enter زدن آزاد شود) در پایان هم نام ایمیجی که از آن قرار است کانتینر بسازیم را نوشته ایم.
با اجرای دستور docker ps میبینیم که کانتینر جدیدی درست شده است. فرض کنید بقیه کانتینر ها را که در مقاله قبلی ساخته بودیم پاک کردیم.
با دستور زیر از اجرای ردیس مطمئن میشویم و لاگ این کانتینر را میبینیم.
میتوانیم صحت اجرای redis را با ورود به ترمنیال داخلی کانتینر بررسی کنیم.
در خط اول وارد ترمینال لینوکسی کانتینر شدیم.
سپس redis-cli را اجرا کردیم که محیط کنسولی برای کار با ردیس است.
با دستور ping از حاضر جوابی Redis مطمئن شدیم و به ما pong برگرداند.
در نهایت با دستور set یک کلید به نام name ساختیم و به آن مقدار morteza را نسبت دادیم. همانطور که میبینید اضافه کردن داده به سادگی با دستور set انجام میشود
به کمک get میتوانیم مقدار یک کلید را بگیریم. Get name یعنی مقداری که به کلید nameمرتبط است برگردان.
با exit ِاول از redis-cli خارج شدیم و با exit ِدوم از ترمنیال لینوکسی داخلی کانتینر بیرون آمدیم.
میتوانید دیتابیس خود را به کمک اکستنشنی از ویژوال استودیو که در این مقاله توضیح داده ام ببینید.
بعد از فشردن دکمه کانکت در سمت چپ صفحه میتوانید دیتابیس ردیس را انتخاب کنید و کانکشن را باز کنید و دیتای داخل این دیتابیس را ببینید
اگر با asp.core آشنایی ندارید پیشنهاد میکنم این ویدیوی آموزشی را ببینید.(در یوتیوب به زبان فارسی)
ابتدا هدف ما این است که سرویسی بنویسیم که شناسه کاربر را بدهیم و اطلاعات سبد کاربر را دریافت کنیم. برای کار با دات نت کور بهترین IDE برنامهی Visual Studio است. اما چون ممکن است مشتاق نصب آن به لحاظ فضای دیسک و حافظه نباشید از همان Visual Code استفاده خواهیم کرد.
dotnet --version
3. با دستور زیر یک پروژه webApi به نام Basket.Api بسازیدو بطور خودکار فولدر آن نیز ساخته میشود. دقت کنید دستور را جایی بنویسید که فولدر Basket.Api در کنار Catalog.Api ساخته شود.
4. پروژه را با دستور dotnet run اجرا میکنیم.
روت(آدرس) weatherforecast یک api برای مثال است که در تمپلیت پیش فرض دات نت وجود دارد و خروجی زیر را برمیگرداند:
در تصویر زیر کد مربوط به این api را میبینید.
توضیح مختصر اینکه در فولدر کنترلر فایل هایی با قاعده نامگذاری مثل آنچه میبینید درست میکنیم. مثلا چون در این میکروسرویس قرار است با Basket کار کنیم فایل BasketController را میسازیم. در این فایل که کنترلر نام دارد متدهایی میسازیم که وظایف مختلف دارند. هر کدام از این متدها به یک روت (Route) مپ میشوند که بصورت پیش فرض هم نام متد هستند.
5. میخواهیم متدهایی با وظایف زیر بنویسیم
اگر قرار بود تمامی قواعد Clean Architecture را رعایت کنیم و Repository Pattern در پروژه مان پیاده سازی شود نیاز به مقالات بیشتر و توضیحات کامل تر بود اما در این نوشته از این جزئیات صرف نظر میکنیم و بدون رعایت لایه بندی، در کنترلر متدها را پیاده سازی میکنیم. (برای مطالعه اطلاعات بیشتر این مقاله را ببینید).
این نکته را هم در نظر بگیرید که هدف از معماری ها و پترن ها چیست؟ آیا در یک میکروسرویس ساده که وظیفه محدودی برای آن تعریف شده و قرار است تا ابد همین وظیفه محدود را به عهده داشته باشد و شاید تعداد خطوط کد آن از 100 فراتر نرود، نیاز به رعایت قواعد و قوانین دست و پا گیر است؟
6. یک مدل برای سبد تعریف میکنیم و در فولدر Models قرار میدهیم.
7. کتابخانه دسترسی به Redis را در دات نت نصب میکنیم.
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
همانطور که میبینید بعد از اجرای دستور فوق، خط مشخص شده به فایل csproj اضافه میشود.
تنظیمات استفاده از ردیس به شکل زیر است. در فایل program.cs (برای نسخه 6 دات نت) و یا Startup.cs (برای نسخه های قدیمی تر) به شکل زیر نحوه اتصال به ردیس را کانفیگ میکنیم.
8. با ملاحظاتی که در ادامه به آن اشاره خواهم کرد، کنترلر مربوط به وظایف Basket را به شکل زیر ایجاد میکنیم.
دقت کنید ما اطلاعات کالا را برای ثبت در سبد از بیرون به عنوان ورودی میگیریم. یعنی مثلا کاربری با یوزرنیم mortezadalil سبدی با 5 کالا دارد که شناسه های کالا به شکل 110و112و122و134و115 است. در مدل BasketItem ما پراپرتی قیمت و تعداد داریم. پس این دو مقدار هم به ازای هر کالا از ورودی میگیریم اما طبیعتا مقدار قیمت برای ما سندیت ندارد. قیمت بر اساس آنچه در رابط کاربری به کاربر نمایش داده میشود برای ما ارسال میشود و چون این مقدار توسط کاربر قابل دستکاری است (از طریق Postman یا حتا تغییر Dom در رابط کاربری) پس تصمیم درست این است که این مقدار را بی اهمیت تلقی کنیم و بر اساس شناسه کالا و تعداد آن که از UI به عنوان ورودی دریافت میکنیم یک درخواست جدید بسازیم و به میکروسرویس مرتبط با قیمت و تخفیف یعنی Discount.Api ارسال کنیم و به تعبیری قیمت واقعی محصول را از سرور استعلام کنیم و بر اساس قیمت درست و تخفیف احتمالی سبد را در Redis به روز رسانی کنیم. اینکار را در آینده به کمک gRPC و ارتباط برقرارکردن با میکروسرویس مختص اینکار، انجام خواهیم داد. (این مقاله)
همچنین ما متد پیاده سازی نشده ای به نام Checkout داریم که برای قطعی کردن سبد به منظور خرید استفاده میشود. این متد با میکروسرویس Order.Api ارتباط برقرار میکند و سفارش را ثبت میکند. پیاده سازی این بخش را در آینده انجام خواهیم داد و از RabbitMQ استفاده خواهیم کرد. (این مقاله)
9. با دستور dotnet run پروژه را اجرا میکنیم یا میتوانیم F5 را در Visual Code فشار دهیم. دقت کنید برای اجرا به کمک F5 بهتر است که ویژوال کد داخل پروژه دات نت اجرا شود. یعنی ما در پنجره explorer مربوط به visual code محتویات فولدر Basket.Api را ببینیم و نه همه ی میکروسرویس ها.
بهتر است هر میکروسرویس فولدر vscode. مربوط به خودش را داشته باشد. ( با فشردن F5 از شما میپرسد که اجرای پروژه به چه شکل باشد. روشی که انتخاب میکنید باعث ایجاد فولدر .vscode میشود.)
10. حالا با پستمن متدهایی که نوشتیم را تست میکنیم
هنوز سبدی برای کاربر mortezadalil ساخته نشده است. پس خالی یا null برگردانده میشود.
حالا سبد این کاربر را با آنچه در پستمن تعریف کرده ایم پر میکنیم.
به کمک برک پوینت میتوانیم مقادیر را ببینیم و رصد کنیم
همانطور که قبلا گفته شد برای متد آپدیت شناسه کالا(یا کالاها) ،تعدادشان و نام کاربر اهمیت دارد. قیمت و نام کالا هیچ استفاده ای نمیشود. درست است که فعلا همین مقادیر ورودی را در Redis به نام این کاربر ذخیره میکنیم اما در مقالات بعدی قرار است قیمت به کمک میکروسرویس Discount.Api به شکل مطمئن دریافت و در ردیس ذخیره شود. همچنین نام کالا هم به همین شکل قابل بررسی و به روزرسانی است. دلیل ذخیره سازی قیمت و نام کالا در سبد به این منظور است که برای هر فرد زمانی که سبد وی فراخوانی میشود هر بار نیاز به استعلام نام و قیمت از Discount.Api نباشد.
با اینکار ما در کوئری گرفتن از دیتابیس صرفه جویی میکنیم و از طرفی آنچه در سبد داریم نیز معتبر است.
1. قبل از هر چیز یک فایل به نام .dockerignore درست میکنیم و فولدر bin و obj را در آن به شکل زیر تعریف میکنیم.
**/bin/ **/obj/
اگر اینکار را نکنیم بعد از ایجاد dockerfile و هنگام اجرای دستور docker build ایمیج داکر ایجاد نمیشود و به خطا برخورد خواهیم کرد.
2. حالا فایل داکر را به شکل زیر درست میکنیم.(این داکر فایل یک مشکل دارد که در ادامه آن را تصحیح میکنیم)
3. دستور زیر را برای ایجاد ایمیج از روی فایل داکر فوق مینویسیم
docker build . -t basketapi:0.0.1
docker run -d -p 4600:80 --name basketapi basketapi:0.0.1
فرض ما این است که دستور dotnet basket.api.dll به شکل production و روی پورت 80 اجرا میشود و ما 80 داخلی را به 4600 بیرونی مپ میکنیم.
اما پس از اجرا میبینیم که روی localhost:4600 ریسپانس نمیگیریم.
برای پیدا کردن مشکل ابتدا لاگ را بررسی میکنیم
docker logs basketapi
اگر مشکلی وجود نداشت و روی پورت 80 اجرا شده بود حالا بررسی میکنیم آیا میکروسرویس درون خود container به درستی اجرا شده و میتوان api ها را صدا کرد.
با دستور ls بررسی میکنیم که فایل ها درست در این داکر وجود داشته باشند.
ظاهر تا اینجا مشکلی نیست. برای تست api باید برنامه curl را در محیط لینوکسی کانتینر داشته باشیم که نداریم!
پس به کمک دستورات apt-get update ابتدا پکیج ها را آبدیت و سپس به کمک ap-get install curlپکیج مورد نظر یعنی curlرا از اینترنت میگیریم
حالا از پستمن curl ریکوئست را میگیریم.
کرل به شکل زیر است:
curl --location --request GET 'http://localhost:4600/basket/GetBasketItems/mortezadalil'
طبیعتا جواب نمیگیریم چون در داخل لینوکس با پورت خارجی کرل را صدا زدیم.
با پورت 80 اینکار را میکنیم:
curl --location --request GET 'http://localhost:80/basket/GetBasketItems/mortezadalil'
اینبار خطا گرفتیم یعنی آدرس و پورت درست است و به هر دلیلی Internal Server Error گرفتیم. میخواهید دلیلش را بدانید؟ باید از لاگ دات نت استفاده کنیم. یک تب جدید ترمینال درست میکنیم و لاگ این کانتینر را رصد میکنیم.
docker logs --tail 1 basketapi
یعنی یک خطای آخر یا یک خط آخر را برگردان.
در متن خطا مشخص است که ارتباط برقرار شده ولی چون نمیتواند با Redis کانکشن برقرار کند خطا میدهد و خطای 500 به این دلیل بود.
برگردیم به مشکل اصلی ، ما داخل کانتینر، پروژه را به شکل اجرا شده داریم و پورت 80 داخلی درست کار میکند. اما به پورت بیرونی 4600 متصل نشده است که از بیرون به آن دسترسی داشته باشیم.
پورت اجرایی داخلی را با اضافه کردن خط زیر در داکر فایل میتوان تغییر داد.
ENV ASPNETCORE_URLS=http://+:5000
داکر فایل به شکل زیر خواهد شد:
دقت کنید این عمل با روش های دیگر مثل تغییر در program.cs و appsettings.json نیز ممکن است ولی چه بهتر که برنامه مان را درگیر این کارها نکنیم!
کانتینر و ایمیج خود را پاک میکنیم. (به کمک دستور یا docker desktop قابل انجام است در مورد دستورات حذف ایمیج یا کانتینر در این مقاله صحبت کردیم)
انیمیشن زیر حذف با docker desktop را نشان میدهد.
توجه داشته باشید که داکر یک ابزار است که توسط ترمینال همه جور آپشنی را در اختیار شما قرار میدهد. داکر دسکتاپ یک رابط کاربری نسبتا خوب برای داکر است که شما را از درگیری با دستورات ترمینال تا اندازه ای دوره میکند. درست است که سفارش شده از دستورات خود داکر استفاده کنید تا دستورات را فراموش نکنید اما مطلع باشید که تسلط خداگونه به داکر و کوبرنتیز و فهمیدن جزئیات آنها هیچ ارتباطی به مهارت پیاده سازی بیزنس های پیچیده برنامه نویسی ندارد. به همین دلیل خیلی وقتتان را با خواندن مطالبی در مورد ابزارها (مثل Git و Docker و ... ) تلف نکنید دستورات اینها در حد Regex بیخود و فراموش شدنی است. قابلیت ها را پیدا کنید ولی حفظ نکنید. حین انجام پروژه با این ابزار کار کنید، سرچ کنید و مشکلتان را حل کنید. همینقدر کافیست. از داکر دسکتاپ هم به اندازه استفاده کنید :)
حالا پروژه را دوباره بیلد و اجرا میکنیم.
مشکل بر طرف خواهد شد و در پستمن خطای 500 مربوط به عدم برقراری ارتباط با redisرا میبینیم که از طریق docker logs جزئیات این خطا قابل دسترسی است.
اما چطور این خطای کانکشن را برطرف کنیم؟ مقاله قبل را خوانده اید؟ دو نکته در آن مقاله در همین زمینه گفته شد.
این دو مورد را در مقاله قبل تست کردیم و شما هم میتوانید دستورات ایجاد کانتینر را به همراه نام network برای Redis و basketApi باز نویسی کنید. (برای اینکار هر دو کانتینر را پاک کنید).
راه حل بهتر برای این مشکلات همانطور که در مقاله قبل به آن اشاره شد استفاده از docker-compose است. بدون هیچ توضیح اضافی داکر کامپوزی که در مقاله قبلی تهیه کردیم را کامل میکنیم. دقت میکنیم که appsettings را به شکل یک فایل خارجی به کمک volume تعریف میکنیم که تغییر و ری استارت کانتینر باعث اعمال تغییرات appsettings در پروژه شود.
نکته اول:
در کانفیگ داکر کامپوز سعی کنید آدرس ها را نسبی ثبت کنید. مثلا خط زیر برای مشخص کردن مسیر volume باید اصلاح شود:
./ecommerceMicroservice/Basket.Api/appsettings.json:/app/appsettings.json
به شکل زیر باید تبیدل شود:
./Basket.Api/appsettings.json:/app/appsettings.json
چون این ایمیج ها برای هر سیستمی باید قابلیت بیلد شدن داشته باشند. وقتی مسیر روی یک سیستم وجود نداشته باشد بیلد شدن با مشکل روبرو خواهد شد. فایل docker-compose محل اصلی برای اجرای دستورات داکر است و ریشه محسوب میشود و مسیر ها باید به نسبت این فایل تعیین شود.
نکته دوم :
دقت کنید که هر بار appsettings را در سیستم خودتان در محلی که آدرس آن را به docker-compose داده اید تغییر دهید باید داکرکامپوز را یکبار ری استارت کنید و بهتر است از دستور زیر برای اینکار استفاده کنید
docker-compose -f .\docker-compose.yml -f .\docker-compose.override.yml up -d --force-recreate
نکته سوم:
اگر اطلاعات مهمی مثل پسورد یا private key در appsettings دارید بهتر است در همان حالت لوکال از آن استفاده کنید.برای حالت پروداکشن به یکی از روش های زیر عمل کنید.
روش 1: برای پروداکشن از appsettings مربوط به پروداکشن استفاده کنید که تنظیمات آن در دات نت وجود دارد.(این مقاله را ببینید)
در مقاله فوق توضیح داده شده که چطور چندین پروفایل در دات نت داشته باشیم (مثلا برای development, production, staging) . طبیعتا اگر چند پروفایل دارید حتما در dockerfile باید ASPNETCORE_ENVIRONMENT را مشخص کنید.
روش 2 : اگر حال استفاده از دو apsettings ندارید میتوانید در کد به کمک دستورات مرتبط با Environment ابتدا مطلع شوید کجا هستید و سپس تصمیم بگیرید از appsetting استفاده کنید یا از variable ها. مثلا در program.cs به شکل زیر میتوان وضعیت جاری پروژه را فهمید و تصمیم گرفت.
در محل های دیگر نیز میتوان از دستور زیر در سی شارپ استفاده کرد
Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")
روش 3: این روش پیشنهادی و بهینه بدون نیاز به چند appsettings است. در حالت لوکال env ها را به روش زیر تعریف میکنیم. مثلا برای کانکشن ردیس به شکل زیر تعریف میکنیم و در powershell به همین شکل مینویسیم. این دستور در لینوکس متفاوت است. (بعدا این متغیرها را در فایل داکر یا داکر کامپوز به طریقی که در روش 1 گفته شد باید تعریف کنید و سپس ایمیج بسازید)
$env:REDIS_CONNECTION = 'redisdb:6379'
در appsetting کلیدهای مربوط به environment variable ها را(که قبلا در سیستم تعریف کرده اید) به عنوان value قراردهید
در کد ابتدا با متد GetValue از کلاس Configuration این Valueها را بیرون بکشید. حالا این Value برای Environment Variable کلید محسوب میشوند. با دستور GetEvironmentVariable که در روش 2 گفتیم مقدار آن را بیرون بکشید. در تصویر زیر استفاده از این روش را در program.cs برای معرفی آدرس کانکشن ردیس میبینیم.
امیدوارم این مقاله گره ای از مشکلات شما را باز کرده باشد. سعی کردم تمامی مشکلاتی که حین تولید میکروسرویس با دات نت به آن برخورد میکنید ضمن شرح مشکل، پاسخ دهم. در مقالات بعدی دو میکروسرویس باقیمانده را طراحی و پیاده سازی خواهیم کرد. همچنین به نحوه ارتباط بین میکروسرویس ها از طریق gRPC و RabbitMQ نیز خواهیم پرداخت.
کدهای مربوط به این بخش را در این ریپازیتوری از گیتهاب ببنید (کامیت پایانی تا اینجا "پایان میکروسرویس سبد خرید")