ساخت میکروسرویس با nameko در پایتون

امیرحسین بیگدلو 12 ماه قبل

الگوی معماری میکروسرویس یک سبک معماری است که استفاده از آن به دلیل انعطاف پذیری بالا در حال افزایش است. استفاده از تکنولوژی هایی مانند Kubernetes و docker میتوانید به سادگی برنامه خودتان را با معماری میکروسرویس اجرا کنید.

 

به طور خلاصه، سبک معماری میکروسرویس رویکردی برای توسعه یک برنامه کاربردی واحد به عنوان مجموعه‌ای از سرویس های کوچک است که هر سرویس به طور جداگانه اجرا می‌شوند. این سرویس ها بر اساس قابلیت های تجاری ساخته شده اند و به طور اتوماتیک مستقر میشوند.

 

به عبارت دیگر، یک برنامه کاربردی که از معماری میکروسرویس پیروی می کند از چندین سرویس مستقل و پویا تشکیل شده است که با استفاده از یک پروتکل ارتباطی با یکدیگر ارتباط برقرار می کنند. استفاده از HTTP (و REST) معمول است، اما همانطور که خواهیم دید، می‌توانیم از انواع دیگر پروتکل‌های ارتباطی مانند RPC روی AMQP استفاده کنیم.

 

یکی دیگر از جنبه های میکروسرویس ها این است که هیچ قانونی در مورد اینکه کدام فناوری باید در هر سرویس استفاده شود وجود ندارد. شما باید بتوانید سرویسی را با هر نرم افزاری بنویسید که بتواند با سرویس های دیگر ارتباط برقرار کند. هر سرویس مدیریت چرخه حیات خود را نیز دارد. همه اینها به این معنی است که در یک شرکت، این امکان وجود دارد که تیم هایی روی خدمات جداگانه، با فناوری های مختلف و حتی روش های مدیریتی مختلف کار کنند. هر تیم به قابلیت‌های تجاری اهمیت می‌دهد و به ایجاد سازمانی چابک‌تر کمک می‌کند.

 

دوره پیشنهادی: دوره آموزش الگوریتم‌نویسی در پایتون

 

 #  میکروسرویس با پایتون

با در نظر گرفتن این مفاهیم، در این مقاله بر روی ساخت یک برنامه Microservices با استفاده از Python تمرکز خواهیم کرد. برای آن، از Nameko، یک فریمورک میکروسرویس پایتون استفاده می کنیم. این فریمورک دارای RPC روی AMQP داخلی است که به شما امکان می دهد به راحتی بین سرویس های خود ارتباط برقرار کنید. همچنین دارای یک رابط ساده برای HTTP است که در این آموزش از آن استفاده خواهیم کرد. با این حال، برای نوشتن Microservice هایی که یک نقطه پایانی HTTP را در معرض دید قرار می دهند، توصیه می شود از چارچوب دیگری مانند Flask استفاده کنید. برای فراخوانی متدهای Nameko از طریق RPC با استفاده از Flask، می‌توانید از flask_nameko استفاده کنید، یک ابزار که فقط برای تعامل Flask با Nameko ساخته شده است.

 

 

 +  آماده سازی محیط توسعه

برای شروع از یک مثال ساده استفاده خواهیم کرد و سپس آن را گسترش میدهیم. در این مقاله ما از پایتون 3 و داکر استفاده خواهیم کرد. پس مطمئن شوید که این دو را نصب دارید. ابتدا یک محیط مجازی با پایتون ایجاد کرده و با دستور pip install nameko فریمورک nameko را نصب کنید.

 

برای اجرای Nameko به واسطه پیام RabbitMQ نیاز داریم. ربیت ام کیو مسئول ارتباط بین سرویس های Nameko ما خواهد بود. با این حال، نگران نباشید، زیرا نیازی به نصب یک وابستگی دیگر روی دستگاه خود ندارید. با Docker، ما به سادگی می توانیم یک ایمیج از پیش پیکربندی شده برای rabbitmq را دانلود کنیم، آن را اجرا کنیم، و پس از پایان کار، به سادگی کانتینر را متوقف کنیم.

 

ارتباط میکرو سرویس ها با rabbitmq

 

با دستور زیر یک کانتینر rabbitmq ایجاد کنید(ممکن است نیاز به دسترسی sudo داشته باشید):

docker run -p 5672:5672 --hostname nameko-rabbitmq rabbitmq:3

 

این دستور یک کانتینر rabbitmq در پورت 5762 ایجاد میکند.

 

 

 +  ساخت یک سرویس ساده

برای شروع یک فایل به نام hello.py ایجاد کرده و کد زیر را در آن قرار دهید:

from nameko.rpc import rpc


class GreetingService:
    name = "greeting_service"

    @rpc
    def hello(self, name):
        return "Hello, {}!".format(name)

 

سرویس های nameko کلاس هستند. این کلاس‌ها نقاط ورودی را نشان می‌دهند که به عنوان extension یا افزونه پیاده‌سازی می‌شوند. افزونه‌های داخلی(built-in) شامل توانایی ایجاد نقاط ورودی است که نشان‌دهنده روش‌های RPC، شنوندگان رویداد، نقاط پایانی HTTP یا تایمرها هستند. همچنین افزونه‌های انجمنی(community) وجود دارند که می‌توان از آنها برای تعامل با پایگاه داده PostgreSQL، Redis و غیره استفاده کرد. همچنین می‌توانید افزونه های خودتان را بنویسید.

 

حالا بیایید کدمان را اجرا کنیم. اگر rabbitmq را با پورت پیشفرض اجرا کرده باشید میتوانید با دستور زیر برنامه را اجرا کنید:

nameko run hello

 

با این دستور به طور اتوماتیک rabbitmq را پیدا کرده و به آن متصل خواهد شد. برای تست سرویس، در ترمینالی دیگر دستور nameko shell را اجرا کنید. این دستور یک شل تعاملی ایجاد می کند که به همان RabbitMQ متصل می شود. نکته مهم این است که با استفاده از RPC بر روی AMQP، فریمورک Nameko کشف خودکار سرویس را پیاده سازی می کند. هنگام فراخوانی یک متد RPC، ابزار nameko سعی می کند سرویس در حال اجرا مربوطه را پیدا کند.

 

کشف خودکار سرویس ها با nameko

 

هنگام اجرای شل Nameko، یک آبجکت خاص به نام n به فضای نام اضافه می شود. این آبجکت اجازه می دهد تا رویدادها را ارسال و تماس های RPC را انجام دهید. برای برقراری تماس RPC با سرویس ما، این دستور را اجرا کنید:

>>> n.rpc.greetingservice.hello(name='world')
'Hello, world!'

 

دوره پیشنهادی: دوره آموزش GraphQL در پایتون

 

 +  تماس های همزمان

این کلاس‌های سرویس در لحظه فراخوانی ایجاد می‌شوند و پس از تکمیل فراخوانی از بین می‌روند. بنابراین، آنها باید ذاتا بدون حالت(stateless) باشند، به این معنی که شما نباید سعی کنید هیچ حالتی را در آبجکت یا کلاس بین فراخوانی ها حفظ کنید. با این فرض که همه سرویس‌ها بدون وضعیت هستند، Nameko می‌تواند با استفاده از eventlet، از همزمانی استفاده کند. سرویس‌های نمونه‌سازی‌شده «کارگر» نامیده می‌شوند و می‌توان حداکثری برای تعداد کارگران مشخص کرد.

 

برای تأیید همزمانی Nameko در عمل، قبل از بازگرداندن پاسخ، کد منبع را با افزودن یک sleep تغییر دهید:

from time import sleep

from nameko.rpc import rpc


class GreetingService:
    name = "greeting_service"

    @rpc
    def hello(self, name):
        sleep(5)
        return "Hello, {}!".format(name)

 

در حال حاضر انتظار می رود که زمان پاسخ از یک فراخوانی باید حدود 5 ثانیه طول بکشد. با این حال، هنگامی که آن را از شل nameko اجرا می کنیم، رفتار کد زیر چگونه خواهد بود؟

res = []
for i in range(5):
    hello_res = n.rpc.greeting_service.hello.call_async(name=str(i))
    res.append(hello_res)

for hello_res in res:
    print(hello_res.result())

 

Nameko یک متد غیر مسدود کننده call_async برای هر نقطه ورودی RPC ارائه می‌کند و یک آبجکت پاسخ پراکسی را برمی‌گرداند که سپس می‌توان نتیجه آن را جستجو کرد. متد result، زمانی که در پراکسی پاسخ فراخوانی شود، تا زمانی که پاسخ برگردانده شود، مسدود خواهد شد.

 

همانطور که انتظار می رفت، این مثال فقط در حدود پنج ثانیه اجرا می شود. هر کارگر در انتظار پایان تماس خواب مسدود می شود، اما این کار مانع از شروع کار کارگر دیگر نمی شود.

 

همانطور که قبلا توضیح داده شد، Nameko هنگامی که یک متد فراخوانی می شود، کارگران را ایجاد می کند. حداکثر تعداد کارگران قابل تنظیم است. به طور پیش‌فرض، این عدد روی 10 تنظیم شده است. این روش 20 بار متد hello را فراخوانی می کند که اکنون ده ثانیه طول می کشد تا اجرا شود:

> >> res = []
> >> for i in range(20):
...     hello_res = n.rpc.greeting_service.hello.call_async(name=str(i))
...     res.append(hello_res)
> >> for hellores in res:
...     print(hello_res.result())
Hello, 0!
Hello, 1!
Hello, 2!
Hello, 3!
Hello, 4!
Hello, 5!
Hello, 6!
Hello, 7!
Hello, 8!
Hello, 9!
Hello, 10!
Hello, 11!
Hello, 12!
Hello, 13!
Hello, 14!
Hello, 15!
Hello, 16!
Hello, 17!
Hello, 18!
Hello, 19!

 

حال، فرض کنید که تعداد زیادی (بیش از 10) کاربر همزمان آن متد hello را فراخوانی کنند. برخی از کاربران بیش از پنج ثانیه برای پاسخ منتظر خواهند ماند. یک راه حل برای این مشکل افزایش تعداد کارها با نادیده گرفتن تنظیمات پیش فرض با استفاده از مثلاً یک فایل پیکربندی است. با این حال، اگر سرور شما به دلیل اینکه متد فراخوانی شده قرار است کوئری سنگینی روی دیتابیس انجام دهد، افزایش تعداد کارگران می تواند باعث افزایش بیشتر زمان پاسخ شود.

 

 

 +  بزرگ کردن مقیاس برنامه

راه حل بهتر استفاده از قابلیت های میکروسرویس Nameko است. تا به حال، ما فقط از یک سرور (کامپیوتر شما) استفاده کرده ایم که یک نمونه از RabbitMQ و یک نمونه از سرویس را اجرا می کرد. در یک محیط واقعی، می‌خواهید به‌طور اتوماتیک تعداد گره‌هایی را که سرویسی را اجرا می‌کنند، افزایش دهید. همچنین اگر می‌خواهید کارگزار پیام شما قابل اعتمادتر باشد، می‌توانید یک خوشه RabbitMQ ایجاد کنید.

 

برای شبیه‌سازی مقیاس‌بندی سرویس، می‌توانیم به سادگی ترمینال دیگری را باز کنیم و با استفاده از nameko run hello، سرویس را مانند قبل اجرا کنیم. این دستور یک نمونه سرویس دیگر را با پتانسیل اداره ده کارگر دیگر آغاز می کند. اکنون، دوباره آن قطعه را با range(20) اجرا کنید. اکنون باید دوباره پنج ثانیه طول بکشد تا اجرا شود. هنگامی که بیش از یک نمونه سرویس در حال اجرا باشد، Nameko درخواست‌های RPC را در میان نمونه‌های موجود بررسی می‌کند.

 

Nameko به گونه ای ساخته شده است که به طور قوی فراخوانی متدها را در یک خوشه مدیریت کند. برای آزمایش آن، کد را اجرا کنید و قبل از اتمام آن، به یکی از ترمینال‌هایی که سرویس Nameko را اجرا می‌کند بروید و دوبار Ctrl+C را فشار دهید. این کار میزبان را بدون انتظار برای پایان کار کارگران خاموش می کند. Nameko تماس‌ها را مجدداً به یک نمونه سرویس موجود دیگر اختصاص می‌دهد.

 

در عمل، همانطور که بعداً خواهیم کرد، از Docker برای کانتینری کردن خدمات خود و یک ابزار هماهنگ‌سازی مانند Kubernetes برای مدیریت گره‌های در حال اجرا سرویس و سایر وابستگی‌ها، مانند واسطه پیام، استفاده می‌کنید. اگر به درستی انجام شود، با Kubernetes، به طور موثر برنامه خود را در یک سیستم توزیع شده قوی تبدیل خواهید کرد. همچنین، Kubernetes اجازه استقرار بدون توقف را می دهد. بنابراین، استقرار یک نسخه جدید از یک سرویس تأثیری بر در دسترس بودن سیستم شما نخواهد داشت.

 

ساخت سرویس‌ها با در نظر گرفتن برخی سازگاری‌های قبلی بسیار مهم است، زیرا در یک محیط واقعی ممکن است چندین نسخه مختلف از همان سرویس به طور همزمان در حال اجرا باشند، به خصوص در حین استقرار. اگر از Kubernetes استفاده می‌کنید، در حین استقرار، تنها زمانی تمام کانتینرهای نسخه قدیمی را از بین می‌برد که کانتینرهای جدید در حال اجرا به اندازه کافی وجود داشته باشد.

 

برای Nameko، اجرای همزمان چندین نسخه مختلف از یک سرویس مشکلی ندارد. از آنجایی که فراخوانی ها را به صورت دور برگشتی توزیع می کند، ممکن است فراخوانی از طریق نسخه های قدیمی یا جدید انجام شود. برای آزمایش آن، یک ترمینال را با سرویس ما در حال اجرا نسخه قدیمی نگه دارید و ماژول سرویس را به شکل زیر ویرایش کنید:

from time import sleep
from nameko.rpc import rpc

class GreetingService:
    name = "greeting_service"

    @rpc
    def hello(self, name):
        sleep(5)
        return "Hello, {}! (version 2)".format(name)

 

اگر آن سرویس را از ترمینال دیگری اجرا کنید، دو نسخه را همزمان اجرا خواهید کرد. اکنون، قطعه آزمایشی ما را دوباره اجرا کنید و هر دو نسخه را مشاهده خواهید کرد:

> >> res = []
> >> for i in range(5):
...     hello_res = n.rpc.greeting_service.hello.call_async(name=str(i))
...     res.append(hello_res)
> >> for hellores in res:
...     print(hello_res.result())
Hello, 0!
Hello, 1! (version 2)
Hello, 2!
Hello, 3! (version 2)
Hello, 4!

 

 

 +  کار با چند نمونه

اکنون می دانیم که چگونه به طور موثر با Nameko کار کنیم و مقیاس بندی چگونه کار می کند. بیایید یک قدم جلوتر برویم و از ابزار بیشتری از اکوسیستم Docker استفاده کنیم: docker-compose. اگر در حال استقرار روی یک سرور واحد هستید، این کار می‌کند، که قطعا ایده‌آل نیست زیرا از بسیاری از مزایای معماری Microservices استفاده نمی‌کنید. مجدداً، اگر می خواهید زیرساخت مناسب تری داشته باشید، ممکن است از یک ابزار ارکستراسیون مانند Kubernetes برای مدیریت یک سیستم توزیع شده از کانتینرها استفاده کنید. بنابراین، docker-compose را نصب کنید.

 

باز هم، تنها کاری که ما باید انجام دهیم این است که یک نمونه RabbitMQ را مستقر کنیم و Nameko بقیه موارد را بر عهده خواهد داشت، با توجه به اینکه همه سرویس ها می توانند به آن نمونه RabbitMQ دسترسی داشته باشند. کد منبع کامل این مثال در این مخزن GitHub موجود است.

 

بیایید یک برنامه سفر ساده برای آزمایش قابلیت های Nameko بسازیم. این برنامه امکان ثبت فرودگاه ها و سفرها را فراهم می کند. هر فرودگاه عنوان نام airport ذخیره می شود و هر trip شناسه id فرودگاه های مبدا و مقصد را ذخیره می کند. معماری سیستم ما به شکل زیر است:

 

مثال از یک میکروسرویس

 

در حالت ایده آل، هر میکروسرویس باید نمونه پایگاه داده خود را داشته باشد. با این حال، برای سادگی، من یک پایگاه داده Redis را برای هر دو میکروسرویس Trips و Airports ایجاد کرده ام تا بتوانم آن را به اشتراک بگذارم. میکروسرویس Gateway درخواست‌های HTTP را از طریق یک API ساده REST دریافت می‌کند و از RPC برای برقراری ارتباط با فرودگاه‌ها و سفرها استفاده می‌کند.

 

بیایید با میکروسرویس Gateway شروع کنیم. ساختار آن ساده است و باید برای هر کسی که از چارچوبی مانند Flask آمده است بسیار آشنا باشد. ما اساساً دو نقطه پایانی تعریف می کنیم که هر کدام به هر دو روش GET و POST اجازه می دهند:

import json

from nameko.rpc import RpcProxy
from nameko.web.handlers import http


class GatewayService:
    name = 'gateway'

    airports_rpc = RpcProxy('airports_service')
    trips_rpc = RpcProxy('trips_service')

    @http('GET', '/airport/<string:airport_id>')
    def get_airport(self, request, airport_id):
        airport = self.airports_rpc.get(airport_id)
        return json.dumps({'airport': airport})

    @http('POST', '/airport')
    def post_airport(self, request):
        data = json.loads(request.get_data(as_text=True))
        airport_id = self.airports_rpc.create(data['airport'])

        return airport_id

    @http('GET', '/trip/<string:trip_id>')
    def get_trip(self, request, trip_id):
        trip = self.trips_rpc.get(trip_id)
        return json.dumps({'trip': trip})

    @http('POST', '/trip')
    def post_trip(self, request):
        data = json.loads(request.get_data(as_text=True))
        trip_id = self.trips_rpc.create(data['airport_from'], data['airport_to'])

        return trip_id

 

بیایید اکنون نگاهی به سرویس Airport بیندازیم. همانطور که انتظار می رفت، دو متد RPC را نشان می دهد. متد get به سادگی پایگاه داده Redis را پرس و جو می کند و فرودگاه را برای id داده شده برمی گرداند. متد create یک id تصادفی ایجاد می کند، اطلاعات فرودگاه را ذخیره می کند و id را برمی گرداند:

import uuid

from nameko.rpc import rpc
from nameko_redis import Redis


class AirportsService:
    name = "airports_service"

    redis = Redis('development')

    @rpc
    def get(self, airport_id):
        airport = self.redis.get(airport_id)
        return airport

    @rpc
    def create(self, airport):
        airport_id = uuid.uuid4().hex
        self.redis.set(airport_id, airport)
        return airport_id

 

توجه کنید که چگونه از افزونه nameko_redis استفاده می کنیم. به لیست افزونه های انجمن نگاهی بیندازید. برنامه های افزودنی به گونه ای اجرا می شوند که از تزریق وابستگی استفاده می کنند. Nameko از راه اندازی شی برنامه افزودنی واقعی که هر کارگر استفاده می کند مراقبت می کند.

 

تفاوت چندانی بین سرویس های Airports و Trips وجود ندارد. میکروسرویس Trips به این شکل است:

import uuid

from nameko.rpc import rpc
from nameko_redis import Redis


class AirportsService:
    name = "trips_service"

    redis = Redis('development')

    @rpc
    def get(self, trip_id):
        trip = self.redis.get(trip_id)
        return trip

    @rpc
    def create(self, airport_from_id, airport_to_id):
        trip_id = uuid.uuid4().hex
        self.redis.set(trip_id, {
            "from": airport_from_id,
            "to": airport_to_id
        })
        return trip_id

 

Dockerfile هر میکروسرویس نیز بسیار ساده است. تنها وابستگی نامکو است و در مورد سرویس های Airports و Trips، نیاز به نصب nameko-redis نیز وجود دارد. این وابستگی ها در requirements.txt در هر سرویس آورده شده است. Dockerfile برای سرویس Airports به صورت زیر است:

FROM python:3

RUN apt-get update && apt-get -y install netcat && apt-get clean

WORKDIR /app

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY config.yml ./
COPY run.sh ./
COPY airports.py ./

RUN chmod +x ./run.sh

CMD ["./run.sh"]

 

تنها تفاوت بین این داکر فایل و Dockerfile برای سایر سرویس‌ها، فایل منبع (در این مورد airports.py) است که باید بر اساس آن تغییر کند.

 

قطعه زیر محتوای run.sh را برای فرودگاه ها نشان می دهد. باز هم، برای سایر سرویس ها، فقط از aiports به gateway یا trips تغییر دهید:

#!/bin/bash

until nc -z ${RABBIT_HOST} ${RABBIT_PORT}; do
    echo "$(date) - waiting for rabbitmq..."
    sleep 1
done

until nc -z ${REDIS_HOST} ${REDIS_PORT}; do
    echo "$(date) - waiting for redis..."
    sleep 1
done

nameko run --config config.yml airports

 

سرویس های ما حالا آماده اجرا هستند:

docker-compose up

 

برای تست برنامه کدهای زیر را اجرا کنید:

$ curl -i -d "{\"airport\": \"first_airport\"}" localhost:8000/airport
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 32
Date: Sun, 27 May 2018 05:05:53 GMT

f2bddf0e506145f6ba0c28c247c54629

 

خط آخر id تولید شده برای فرودگاه ما است. برای آزمایش اینکه آیا کار می کند، اجرا کنید:

$curl localhost:8000/airport/f2bddf0e506145f6ba0c28c247c54629
{"airport": "first_airport"}

Great, now let’s add another airport:
$ curl -i -d "{\"airport\": \"second_airport\"}" localhost:8000/airport
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 32
Date: Sun, 27 May 2018 05:06:00 GMT

565000adcc774cfda8ca3a806baec6b5

 

اکنون دو فرودگاه داریم، این برای تشکیل یک سفر کافی است. بیایید اکنون یک سفر ایجاد کنیم:

$ curl -i -d "{\"airport_from\": \"f2bddf0e506145f6ba0c28c247c54629\", \"airport_to\": \"565000adcc774cfda8ca3a806baec6b5\"}" localhost:8000/trip
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 32
Date: Sun, 27 May 2018 05:09:10 GMT

34ca60df07bc42e88501178c0b6b95e4

 

همانند قبل، خط آخر مشخص کننده id سفر است. برای تست صحت اطلاعات کد زیر را اجرا کنید:

$ curl localhost:8000/trip/34ca60df07bc42e88501178c0b6b95e4
{"trip": "{'from': 'f2bddf0e506145f6ba0c28c247c54629', 'to': '565000adcc774cfda8ca3a806baec6b5'}"}

 

 

 #  جمع بندی

ما نحوه عملکرد Nameko را با ایجاد یک نمونه در حال اجرا محلی از RabbitMQ، اتصال به آن و انجام چندین آزمایش مشاهده کردیم. سپس دانش به دست آمده را برای ایجاد یک سیستم ساده با استفاده از معماری Microservices به کار بردیم.

 

برای رسیدگی به درخواست های HTTP ترجیحاً از چارچوب دیگری مانند Falcon یا Flask استفاده کنید. هر دو گزینه‌های عالی هستند و به راحتی می‌توان از آنها برای ایجاد سایر میکروسرویس‌های مبتنی بر HTTP استفاده کرد. Flask این مزیت را دارد که قبلاً یک افزونه برای تعامل با Nameko دارد، اما می‌توانید از nameko-proxy مستقیماً از هر چارچوبی استفاده کنید.

مطالب مشابه



مونگارد