Термин «микросервисы» сегодня у всех на слуху – внезапно это стало очень модно, и многие компании объявляют переход на этот архитектурный паттерн даже толком не разобравшись в нём. Впрочем, обсуждение полезности микросервисов оставим за пределами этой статьи.
Традиционно перед коллекцией микросервисов предлагается дополнительный слой – так называемый API gateway, который решает сразу несколько проблем (они будут перечислены позже). На момент написания этой статьи open source реализаций таких gateway почти нет, поэтому я решил написать свой на PHP с использованием микрофреймворка Lumen (часть Laravel).
В этой статье я покажу насколько это простая задача для современного PHP!
Если говорить совсем коротко, то API gateway – это умный proxy-сервер между пользователями и любым количеством сервисов (API), отсюда и название.
Необходимость в этом слое появляется сразу же при переходе на паттерн микросервисов:
Преимуществ больше – это просто те, что пришли на ум за 10-20 секунд.
Nginx выпустили неплохую бесплатную электронную книгу посвященную микросервисам и API gateway – советую почитать всем, кому интересен этот паттерн.
Как я уже сказал выше, вариантов очень мало, да и те появились сравнительно недавно. Многих возможностей в них пока нет.
С выходом версии 7 PHP стал высокопроизводительным, а с появлением фреймфорков вроде Laravel и Symfony – PHP доказал миру, что может быть красивым и функциональным. Lumen, являясь «очищенной» быстрой версией Laravel здесь идеально подходит, ведь нам не нужны будут сессии, шаблоны и прочие возможности full stack приложений.
Кроме того, у меня просто больше опыта с PHP и Lumen, а разворачивая полученное приложение через Docker – будущим пользователям вообще будет не важен язык, на котором оно написано. Это просто слой, который выполняет свою роль!
Мною предлагается следующая архитектура и соответствующая ей терминология. В коде буду придерживаться этих терминов, чтобы не запутаться:

Само приложение решил назвать Vrata, потому что «врата» на русском это почти «gateway», а ещё миру не хватает приложений с русскими названиями )
Непосредственно за «вратами» находится количество N микросервисов – API сервисов, способных отвечать на web-запросы. У каждого сервиса может быть любое количество экземпляров, поэтому API gateway будет выбирать конкретный экземпляр через так называемый реестр сервисов.
Каждый сервис предлагает какое-то количество ресурсов (на языке REST), а у каждого ресурса может быть несколько возможных действий. Достаточно простая и логичная структура для любого опытного в REST программиста.
Ещё не приступив к коду, можно сразу определить некоторые требования к будущему приложению:
Скажу сразу, что большая часть пунктов уже работает, а реализовать их было очень просто. Ведь правду говорят – мы живем в лучшую для программиста эпоху!
В этом направлении почти не пришлось работать – достаточно было поправить Laravel Passport под Lumen и мы получили поддержку всех современных OAuth2 фич, включая JWT. Мой маленький пакет-порт опубликован на GitHub/Packagist и кто-то его уже устанавливает.
Все низлежащие маршруты с микросервисов импортируются в Vrata из конфигурационного файла в формате JSON. В момент запуска в service provider происходит добавление этих маршрутов:
А тем временем в базе маршрутов:
Теперь каждому публичному (и разрешенному в конфигах) маршруту с микросервисов соответствует маршрут на API gateway. Кроме того, добавлены также синтетические или объединенные запросы, которые существуют только на этом шлюзе. Все запросы уходят в один и тот же контроллер:
Вот так контроллер обрабатывает любой GET-запрос:
В качестве HTTP-клиента выбран Guzzle, который прекрасно справляется с async-запросами, а также имеет готовые средства для integration-тестирования.
Уже работают сложные, составные запросы – это когда одному маршруту на шлюзе соответствует любое количество маршрутов на разных микросервисах. Вот рабочий пример:
Как видим, сложные маршруты уже доступны и обладают неплохим набором фич – можно выделать критически важные из них, можно делать параллельные запросы, можно использовать ответ одного сервиса в запросе к другому и так далее. Помимо всего прочего, на выходе прекрасная производительность – всего 56 миллисекунд на получение суммарного ответа (загрузка Lumen и три фоновых запроса, все микросервисы с базами данных).
Это пока самая слабая часть – реализован только один очень простой метод: DNS. Несмотря на всю его примитивность, он отлично работает в среде вроде Docker Cloud или AWS, где сам провайдер наблюдает за группой сервисов и динамически редактирует DNS-запись.
В настоящий момент Vrata просто берет hostname сервиса, не вникая – облако это или один физический компьютер. Самым популярным реестром на сегодня, пожалуй, является Consul, и именно его стоит добавить следующим.
Суть работы реестра очень проста – надо хранить таблицу живых и мертвых экземпляров сервиса, выдавая адреса конкретных экземпляров когда надо. AWS и Docker Cloud (и многие другие) умеют это делать за вас, предоставляя вам один «волшебный» hostname, который всегда работает.
Говоря о микросервисах просто нельзя не упомянуть Docker – одну из самых «горячих» технологий последних пары лет. Микросервисы, как правило, тестируются и деплоятся именно как образы Docker – это стало стандартной практикой, поэтому мы быстро подготовили публичный образ в Docker Hub.
Одна команда, введённая в терминале любой OS X, Windows или Linux машины, и у вас работает мой шлюз Vrata:
Всю конфигурацию можно передать в переменных окружения в формате JSON.
Приложение (шлюз) уже используется на практике в компании, где я работаю. Весь код в репозитории на GitHub. Если кто-либо хочет поучаствовать в разработке – милости просим :)
Так как составные запросы как по идее, так и по реализации очень напоминают продвигаемый Facebook формат запросов GraphQL (в противовес REST), то одна из приоритетных будущих фич – поддержка GraphQL-запросов.
Традиционно перед коллекцией микросервисов предлагается дополнительный слой – так называемый API gateway, который решает сразу несколько проблем (они будут перечислены позже). На момент написания этой статьи open source реализаций таких gateway почти нет, поэтому я решил написать свой на PHP с использованием микрофреймворка Lumen (часть Laravel).
В этой статье я покажу насколько это простая задача для современного PHP!
Что такое API gateway?
Если говорить совсем коротко, то API gateway – это умный proxy-сервер между пользователями и любым количеством сервисов (API), отсюда и название.
Необходимость в этом слое появляется сразу же при переходе на паттерн микросервисов:
- Единый адрес намного удобнее сотни (у Netflix их более 600) индивидуальных адресов API;
- Логично проверять данные пользователя (token) в едином месте, на «входе»;
- Удобно реализовывать ограничения на количество запросов в едином месте;
- Вся система становится более гибкой – можно менять внутреннюю структуру хоть каждый день. Поддержка старых версий API становится тривиальным делом;
- Можно кешировать или мутировать ответы;
- Для удобства пользователя (или разработчиков front end) можно объединять ответы от разных сервисов. Facebook давно предлагает такую возможность.
Преимуществ больше – это просто те, что пришли на ум за 10-20 секунд.
Nginx выпустили неплохую бесплатную электронную книгу посвященную микросервисам и API gateway – советую почитать всем, кому интересен этот паттерн.
Существующие варианты
- API Umbrella, Lua;
- Kong, Lua;
- AWS API Gateway – платный сервис от Amazon.
Как я уже сказал выше, вариантов очень мало, да и те появились сравнительно недавно. Многих возможностей в них пока нет.
Почему PHP и Lumen?
С выходом версии 7 PHP стал высокопроизводительным, а с появлением фреймфорков вроде Laravel и Symfony – PHP доказал миру, что может быть красивым и функциональным. Lumen, являясь «очищенной» быстрой версией Laravel здесь идеально подходит, ведь нам не нужны будут сессии, шаблоны и прочие возможности full stack приложений.
Кроме того, у меня просто больше опыта с PHP и Lumen, а разворачивая полученное приложение через Docker – будущим пользователям вообще будет не важен язык, на котором оно написано. Это просто слой, который выполняет свою роль!
Выбранная терминология
Мною предлагается следующая архитектура и соответствующая ей терминология. В коде буду придерживаться этих терминов, чтобы не запутаться:

Само приложение решил назвать Vrata, потому что «врата» на русском это почти «gateway», а ещё миру не хватает приложений с русскими названиями )
Непосредственно за «вратами» находится количество N микросервисов – API сервисов, способных отвечать на web-запросы. У каждого сервиса может быть любое количество экземпляров, поэтому API gateway будет выбирать конкретный экземпляр через так называемый реестр сервисов.
Каждый сервис предлагает какое-то количество ресурсов (на языке REST), а у каждого ресурса может быть несколько возможных действий. Достаточно простая и логичная структура для любого опытного в REST программиста.
Требования к Vrata
Ещё не приступив к коду, можно сразу определить некоторые требования к будущему приложению:
- Шлюз должен масштабироваться горизонтально, потому что на дворе 2016 год и все хотят масштабировать горизонтально. Следовательно – никакого состояния приложения не должно быть;
- Шлюз должен уметь объединять запросы и вызывать микросервисы асинхронно;
- Шлюз должен уметь ограничивать количество запросов в промежуток времени;
- Шлюз должен уметь проверять достоверность токена аутентификации. Традиционно предлагается, что API gateway выполняет аутентификацию, а скрытые под ним микросервисы выполняют авторизацию на свои ресурсы;
- Шлюз должен уметь автоматически импортировать доступные ресурсы с микросервисов. Для начала выберем формат Swagger, как самый популярный в мире на сегодня;
- Шлюз должен уметь менять (мутировать) ответы микросервисов;
- И напоследок: шлюз должен прекрасно запускаться напрямую из образа Docker и конфигурироваться через переменные окружения. Мы не хотим никаких дополнительных репозиториев, скриптов деплоя и так далее!
Скажу сразу, что большая часть пунктов уже работает, а реализовать их было очень просто. Ведь правду говорят – мы живем в лучшую для программиста эпоху!
Реализация
Аутентификация
В этом направлении почти не пришлось работать – достаточно было поправить Laravel Passport под Lumen и мы получили поддержку всех современных OAuth2 фич, включая JWT. Мой маленький пакет-порт опубликован на GitHub/Packagist и кто-то его уже устанавливает.
Маршруты и контроллер
Все низлежащие маршруты с микросервисов импортируются в Vrata из конфигурационного файла в формате JSON. В момент запуска в service provider происходит добавление этих маршрутов:
// Получаем синглетный класс – база данных всех маршрутов $registry = $this->app->make(RouteRegistry::class); // Передаем наш Lumen контейнер этой базе, чтобы она могла зарегистрировать маршруты $registry->bind(app());
А тем временем в базе маршрутов:
/** * @param Application $app */ public function bind(Application $app) { // Очень просто - маршрут за маршрутом добавляем в Lumen // Все запросы пойдут в один и тот же служебный контроллер // Добавляем middleware для аутентификации OAuth2, а также своего дополнительного помощника $this->getRoutes()->each(function ($route) use ($app) { $method = strtolower($route->getMethod()); $app->{$method}($route->getPath(), [ 'uses' => 'App\Http\Controllers\GatewayController@' . $method, 'middleware' => [ 'auth', 'helper:' . $route->getId() ] ]); }); }
Теперь каждому публичному (и разрешенному в конфигах) маршруту с микросервисов соответствует маршрут на API gateway. Кроме того, добавлены также синтетические или объединенные запросы, которые существуют только на этом шлюзе. Все запросы уходят в один и тот же контроллер:
Вот так контроллер обрабатывает любой GET-запрос:
/** * @param Request $request * @param RestClient $client * @return Response */ public function get(Request $request, RestClient $client) { // Это наша баночка с параметрами, подробнее - позже $parametersJar = $request->getRouteParams(); // Соберем финальный ответ из N ответов микросервисов $output = $this->actions->reduce(function($carry, $batch) use (&$parametersJar, $client) { // Соберем N ответов полученных асинхронно $responses = $client->asyncRequest($batch, $parametersJar); // Добавим необходимые новые параметры в баночку параметров $parametersJar = array_merge($parametersJar, $responses->exportParameters()); // Склеим с текущим состоянием - делаем array reduce return array_merge($carry, $responses->getResponses()->toArray()); }, []); // Отдаем ответ классу форматирования. Сейчас это только JSON return $this->presenter->format($this->rearrangeKeys($output), 200); }
В качестве HTTP-клиента выбран Guzzle, который прекрасно справляется с async-запросами, а также имеет готовые средства для integration-тестирования.
Составные запросы
Уже работают сложные, составные запросы – это когда одному маршруту на шлюзе соответствует любое количество маршрутов на разных микросервисах. Вот рабочий пример:
// Boolean-флаг, обозначающий сложный маршрут 'aggregate' => true, 'method' => 'GET', // Любой путь на наш вкус, параметры из него сразу попадут в "jar" 'path' => '/v2/devices/{mac}/extended', // Массив с низлежащими маршрутами 'actions' => [ 'device' => [ // Имя микросервиса из реестра сервисов 'service' => 'core', 'method' => 'GET', 'path' => 'devices/{mac}', // Компоненты с одинаковым порядком будут запущены параллельно 'sequence' => 0, // Если в составе есть критичные компоненты и они недоступны - весь маршрут недоступен 'critical' => true ], 'ping' => [ 'service' => 'history', // Вывод никак не участвует в нашем финальном ответе 'output_key' => false, 'method' => 'POST', 'path' => 'ping/{mac}', 'sequence' => 0, 'critical' => false ], 'settings' => [ 'service' => 'core', // Вставляем вывод под альтернативным JSON-ключом 'output_key' => 'network.settings', 'method' => 'GET', // Используем параметр, добытый ранее в пункте 'device' 'path' => 'networks/{device%network_id}', 'sequence' => 1, 'critical' => false ] ]
Как видим, сложные маршруты уже доступны и обладают неплохим набором фич – можно выделать критически важные из них, можно делать параллельные запросы, можно использовать ответ одного сервиса в запросе к другому и так далее. Помимо всего прочего, на выходе прекрасная производительность – всего 56 миллисекунд на получение суммарного ответа (загрузка Lumen и три фоновых запроса, все микросервисы с базами данных).
Реестр сервисов
Это пока самая слабая часть – реализован только один очень простой метод: DNS. Несмотря на всю его примитивность, он отлично работает в среде вроде Docker Cloud или AWS, где сам провайдер наблюдает за группой сервисов и динамически редактирует DNS-запись.
В настоящий момент Vrata просто берет hostname сервиса, не вникая – облако это или один физический компьютер. Самым популярным реестром на сегодня, пожалуй, является Consul, и именно его стоит добавить следующим.
Суть работы реестра очень проста – надо хранить таблицу живых и мертвых экземпляров сервиса, выдавая адреса конкретных экземпляров когда надо. AWS и Docker Cloud (и многие другие) умеют это делать за вас, предоставляя вам один «волшебный» hostname, который всегда работает.
Образ Docker
Говоря о микросервисах просто нельзя не упомянуть Docker – одну из самых «горячих» технологий последних пары лет. Микросервисы, как правило, тестируются и деплоятся именно как образы Docker – это стало стандартной практикой, поэтому мы быстро подготовили публичный образ в Docker Hub.
Одна команда, введённая в терминале любой OS X, Windows или Linux машины, и у вас работает мой шлюз Vrata:
$ docker run -d -e GATEWAY_SERVICES=... -e GATEWAY_GLOBAL=... -e GATEWAY_ROUTES=... pwred/vrata
Всю конфигурацию можно передать в переменных окружения в формате JSON.
Послесловие
Приложение (шлюз) уже используется на практике в компании, где я работаю. Весь код в репозитории на GitHub. Если кто-либо хочет поучаствовать в разработке – милости просим :)
Так как составные запросы как по идее, так и по реализации очень напоминают продвигаемый Facebook формат запросов GraphQL (в противовес REST), то одна из приоритетных будущих фич – поддержка GraphQL-запросов.
