Введение в микросервисы Python с Nameko
Опубликовано: 2022-03-11Введение
Архитектурный шаблон микросервисов — это архитектурный стиль, популярность которого растет благодаря его гибкости и отказоустойчивости. Вместе с такими технологиями, как Kubernetes, загрузка приложения с использованием архитектуры микросервисов становится проще, чем когда-либо прежде.
Согласно классической статье из блога Мартина Фаулера, архитектурный стиль микросервисов можно резюмировать следующим образом:
Короче говоря, архитектурный стиль микросервисов — это подход к разработке отдельного приложения в виде набора небольших сервисов, каждый из которых работает в своем собственном процессе и взаимодействует с упрощенными механизмами, часто API ресурсов HTTP. Эти сервисы строятся с учетом бизнес-возможностей и могут быть независимо развернуты с помощью полностью автоматизированного механизма развертывания.
Другими словами, приложение с архитектурой микрослужб состоит из нескольких независимых и динамических служб, которые взаимодействуют друг с другом с помощью протокола связи. Обычно используется HTTP (и REST), но, как мы увидим, мы можем использовать другие типы протоколов связи, такие как RPC (удаленный вызов процедур) поверх AMQP (протокол расширенной очереди сообщений).
Шаблон микросервисов можно рассматривать как частный случай SOA (сервисно-ориентированной архитектуры). Однако в SOA принято использовать ESB (корпоративную служебную шину) для управления связью между службами. ESB обычно очень сложны и включают функциональные возможности для сложной маршрутизации сообщений и применения бизнес-правил. В микросервисах чаще используется альтернативный подход: «умные конечные точки и тупые каналы», что означает, что сами сервисы должны содержать всю бизнес-логику и сложность (высокая связность), но связь между сервисами должна быть максимально простой. возможно (высокая развязка), что означает, что сервису не обязательно нужно знать, какие другие сервисы будут с ним связываться. Это разделение задач, применяемое на архитектурном уровне.
Еще один аспект микросервисов заключается в том, что нет ограничений в отношении того, какие технологии следует использовать в каждом сервисе. Вы должны иметь возможность написать службу с любым программным стеком, который может взаимодействовать с другими службами. Каждая услуга также имеет собственное управление жизненным циклом. Все это означает, что в компании могут работать команды над отдельными сервисами, с разными технологиями и даже методологиями управления. Каждая команда будет заниматься бизнес-возможностями, помогая создать более гибкую организацию.
Микросервисы Python
Имея в виду эти концепции, в этой статье мы сосредоточимся на создании приложения для проверки концепции микросервисов с использованием Python. Для этого мы будем использовать Nameko, фреймворк микросервисов Python. Он имеет встроенный RPC через AMQP, что позволяет вам легко обмениваться данными между вашими службами. Он также имеет простой интерфейс для HTTP-запросов, который мы будем использовать в этом руководстве. Однако для написания микросервисов, предоставляющих конечную точку HTTP, рекомендуется использовать другую платформу, например Flask. Чтобы вызвать методы Nameko через RPC с помощью Flask, вы можете использовать flask_nameko — оболочку, созданную специально для взаимодействия Flask с Nameko.
Настройка базовой среды
Давайте начнем с самого простого примера, взятого с веб-сайта Nameko, и расширим его для наших целей. Во-первых, вам понадобится установленный Docker. В наших примерах мы будем использовать Python 3, поэтому убедитесь, что он у вас также установлен. Затем создайте python virtualenv и запустите $ pip install nameko
.
Для запуска Nameko нам понадобится брокер сообщений RabbitMQ. Он будет отвечать за связь между нашими службами Nameko. Однако не беспокойтесь, так как вам не нужно устанавливать еще одну зависимость на свой компьютер. С Docker мы можем просто загрузить предварительно настроенный образ, запустить его, а когда закончим, просто остановить контейнер. Никаких демонов, apt-get
или dnf install
.
Запустите контейнер RabbitMQ, запустив $ docker run -p 5672:5672 --hostname nameko-rabbitmq rabbitmq:3
(для этого вам может понадобиться sudo). Это запустит контейнер Docker с использованием самой последней версии 3 RabbitMQ и выставит его через порт по умолчанию 5672.
Привет, мир с микросервисами
Идем дальше и создаем файл hello.py
со следующим содержимым:
from nameko.rpc import rpc class GreetingService: name = "greeting_service" @rpc def hello(self, name): return "Hello, {}!".format(name)
Услуги Nameko — это классы. Эти классы предоставляют точки входа, реализованные в виде расширений. Встроенные расширения включают возможность создавать точки входа, представляющие методы RPC, прослушиватели событий, конечные точки HTTP или таймеры. Существуют также расширения сообщества, которые можно использовать для взаимодействия с базой данных PostgreSQL, Redis и т. д. Можно написать свои собственные расширения.
Давайте продолжим и запустим наш пример. Если вы запустили RabbitMQ на порту по умолчанию, просто запустите $ nameko run hello
. Он найдет RabbitMQ и автоматически подключится к нему. Затем, чтобы протестировать наш сервис, запустите $ nameko shell
в другом терминале. Это создаст интерактивную оболочку, которая будет подключаться к тому же экземпляру RabbitMQ. Самое замечательное то, что, используя RPC поверх AMQP, Nameko реализует автоматическое обнаружение служб. При вызове метода RPC nameko попытается найти соответствующий работающий сервис.
При запуске оболочки Nameko вы получите специальный объект с именем n
, добавленный в пространство имен. Этот объект позволяет отправлять события и выполнять вызовы RPC. Чтобы выполнить вызов RPC к нашему сервису, запустите:
> >> n.rpc.greetingservice.hello(name='world') 'Hello, world!'
Параллельные звонки
Эти классы обслуживания создаются в момент совершения вызова и уничтожаются после завершения вызова. Следовательно, они по своей сути должны быть без состояния, то есть вы не должны пытаться сохранять какое-либо состояние в объекте или классе между вызовами. Это означает, что сами сервисы не должны иметь состояния. Предполагая, что все службы не имеют состояния, Nameko может использовать параллелизм с помощью зеленых потоков событий. Созданные экземпляры сервисов называются «воркерами», и может быть настроено максимальное количество рабочих процессов, работающих одновременно.
Чтобы проверить параллелизм Nameko на практике, измените исходный код, добавив спящий режим к вызову процедуры перед возвратом ответа:
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)
Мы используем sleep
из модуля time
, который не поддерживает асинхронность. Однако при запуске наших сервисов с помощью nameko run
он автоматически исправит триггеры yield от блокирующих вызовов, таких как sleep(5)
.
Теперь ожидается, что время отклика от вызова процедуры должно занимать около 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
при вызове на прокси-сервере ответа будет заблокирован до тех пор, пока не будет возвращен ответ.
Как и ожидалось, этот пример выполняется всего за пять секунд. Каждый воркер будет заблокирован в ожидании завершения sleep
вызова, но это не помешает запуститься другому воркеру. Замените этот sleep
вызов, например, полезным блокирующим вызовом базы данных ввода-вывода, и вы получите чрезвычайно быстрый одновременный сервис.
Как объяснялось ранее, Nameko создает обработчиков при вызове метода. Максимальное количество рабочих настраивается. По умолчанию для этого числа установлено значение 10. Вы можете протестировать изменение range(5)
в приведенном выше фрагменте, например, на диапазон (20). Это вызовет метод hello
20 раз, что теперь должно занять десять секунд:
> >> 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 Microservices. До сих пор мы использовали только один сервер (ваш компьютер), на котором запущен один экземпляр RabbitMQ и один экземпляр службы. В производственной среде вы захотите произвольно увеличить количество узлов, на которых запущена служба, которая получает слишком много вызовов. Вы также можете создать кластер RabbitMQ, если хотите, чтобы ваш брокер сообщений был более надежным.
Чтобы имитировать масштабирование службы, мы можем просто открыть другой терминал и запустить службу, как и раньше, используя $ nameko run hello
. Это запустит еще один экземпляр службы с возможностью запуска еще десяти рабочих процессов. Теперь попробуйте снова запустить этот фрагмент с помощью range(20)
. Теперь для запуска снова потребуется пять секунд. Когда запущено более одного экземпляра службы, Nameko будет циклически распределять запросы RPC среди доступных экземпляров.
Nameko создан для надежной обработки вызовов этих методов в кластере. Чтобы проверить это, попробуйте запустить snipped и, прежде чем он завершится, перейдите к одному из терминалов, на которых работает служба 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. Это будет работать, если вы выполняете развертывание на одном сервере, что определенно не идеально, поскольку вы не будете использовать многие преимущества архитектуры микросервисов. Опять же, если вы хотите иметь более подходящую инфраструктуру, вы можете использовать инструмент оркестровки, такой как Kubernetes, для управления распределенной системой контейнеров. Итак, продолжайте и установите docker-compose.
Опять же, все, что нам нужно сделать, это развернуть экземпляр RabbitMQ, а Nameko позаботится обо всем остальном, учитывая, что все службы могут получить доступ к этому экземпляру RabbitMQ. Полный исходный код этого примера доступен в этом репозитории GitHub.
Давайте создадим простое приложение для путешествий, чтобы протестировать возможности Nameko. Это приложение позволяет регистрировать аэропорты и поездки. Каждый аэропорт просто хранится как название аэропорта, а поездка хранит идентификаторы для аэропортов отправления и назначения. Архитектура нашей системы выглядит следующим образом:
В идеале у каждой микрослужбы должен быть собственный экземпляр базы данных. Однако для простоты я создал единую базу данных Redis для микросервисов Trips и Airports. Микросервис шлюза будет получать HTTP-запросы через простой REST-подобный API и использовать 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
Давайте теперь посмотрим на службу аэропортов. Как и ожидалось, он предоставляет два метода RPC. Метод get
просто запросит базу данных Redis и вернет аэропорт для данного идентификатора. Метод create
сгенерирует случайный идентификатор, сохранит информацию об аэропорте и вернет идентификатор:
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
для каждого микросервиса также очень прост. Единственная зависимость — это nameko
, а в случае служб Airports и Trips также необходимо установить nameko-redis
. Эти зависимости указаны в requirements.txt
каждой службы. Dockerfile для службы аэропортов выглядит так:
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
позаботится о том, чтобы дождаться, пока RabbitMQ и, в случае служб Airports и Trips, база данных Redis будет готова. В следующем фрагменте показано содержимое 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
Эта последняя строка — сгенерированный идентификатор нашего аэропорта. Чтобы проверить, работает ли он, запустите:
$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
Как и прежде, эта последняя строка представляет идентификатор поездки. Проверим, правильно ли он вставлен:
$ curl localhost:8000/trip/34ca60df07bc42e88501178c0b6b95e4 {"trip": "{'from': 'f2bddf0e506145f6ba0c28c247c54629', 'to': '565000adcc774cfda8ca3a806baec6b5'}"}
Резюме
Мы увидели, как работает Nameko, создав локальный работающий экземпляр RabbitMQ, подключившись к нему и выполнив несколько тестов. Затем мы применили полученные знания для создания простой системы с использованием архитектуры микросервисов.
Несмотря на то, что наша система чрезвычайно проста, она очень близка к тому, как могло бы выглядеть готовое к работе развертывание. Для обработки HTTP-запросов лучше использовать другой фреймворк, такой как Falcon или Flask. Оба являются отличными вариантами и могут быть легко использованы для создания других микросервисов на основе HTTP, например, на случай, если вы захотите сломать службу шлюза. Преимущество Flask в том, что у него уже есть плагин для взаимодействия с Nameko, но вы можете использовать nameko-proxy напрямую из любого фреймворка.
Nameko также очень легко проверить. Мы не рассматриваем здесь тестирование для простоты, но рекомендуем ознакомиться с документацией по тестированию Nameko.
Со всеми движущимися частями архитектуры микросервисов вы хотите убедиться, что у вас есть надежная система ведения журнала. Чтобы создать его, см. Ведение журнала Python: подробное руководство, написанное коллегой по Toptaler и разработчиком Python: Сон Нгуен Ким.