Готовим простой блог на микросервисах, пишем свой микрофреймворк на php и запускаем все на Docker с примерами
А что если я скажу вам, что новый продукт можно сразу начинать писать на микросервисной архитектуре, а не заниматься распилом монолита? Это вообще нормально? Удобно? Хотите узнать ответ?
Задача: необходимо написать за выходные (время ограниченно 10-15 часами) сферический блог на микросервисах, на php, не используя никаких фреймворков. Можно пользоваться здравым смыслом. А еще забудем о том что такое фронтенд и вспомним что мы жить не можем без виртуализации. Выберем Docker. Интересно? Вперед под кат.

Микросервисы
Если вам интересен микросервисный подход, но вы не знаете с чего начать, начните с книги «Building Microservices» Сэма Ньюмена. Постараюсь немного описать основные моменты данного подхода, если у вас будут какие-то дополнения, пишите пожалуйста в комментариях. И вообще по любому поводу пишите, я не претендую на истинность какого-либо из описанных ниже подходов, особенно в Вашем конкретном случае.
Будем рассматривать все на примере вышеупомянутого блога. Безусловно, это задача ради задачи, но хочется отметить что даже в таком варианте, это работать будет и будет работать неплохо (быстро и без проблем).
Суть микросервисов легко понять в сравнении с монолитной архитектурой. Как у нас выглядит обычный движок для блога? Грубо говоря это просто одно приложение. Работа с статьями, комментариями, страницами, пользователями и прочими функциональными единицами заключена в едином пакете исходного кода, который не делится никак.
Где все связи между компонентами — это вызовы внутри кода, какие-то отношения между классами, паттерны и т.п. или даже просто говнокод, если нельзя отделить одно от другого.
Как будет выглядеть наш блог? Да примерно также, если честно.
Единственное отличие, что квадратики с компонентами — это больше не компоненты заключенные в код одного приложения, а стрелочки — это больше не системные вызовы классов внутри этого кода. Теперь — это отдельные компоненты, а стрелочки — обычные запросы по http.
Зачем это нужно? Сразу определимся, что наверное, это нужно не всем. Это должно быть очень удобно, если вы — достаточно крупная компания, способная выделить по команде разработки на каждый сервис. Думаю, даже средним компаниям, если выделить по человеку на каждый сервис будет тоже неплохо. Впрочем, даже если ты один на всю компанию, ты сможешь найти в микросервисах что-то интересное.
Насколько большим должен быть сервис? Границы провести сложно, ошибка будет стоить вам дорого, но, если вкратце, то сервис это некая единица вашей системы, которую вы можете полностью переписать за короткое время. Эмпирически пусть за неделю вы или ваша команда должны справится с сервисом. Основная идея тут — сервисы должны быть небольшие. Они не должны превращаться в кучу монолитов.
Итак, позитивные вещи, которые я смог выделить для себя, в целом все они проходят под одним трендом: Невероятное удобство для разработки:
Что должно уметь наше приложение? Так то не очень много.
Четыре страницы:
Docker
Все, не будем больше о теории, давайте пилить приложение. Оно у нас будет на докере. Настолько распределенное приложение разрабатывать на одной машине без виртуализации почти невозможно. Описание работы докера будет представлено обрывками, поскольку выходит за тему данной статьи. Предполагается, что вы о нем что-то знаете.
Кстати, вот ссылка на репу, из которой вы можете скачать и запустить блог, посмотреть что-то по коду ниже. https://github.com/gregory-vc/blog
Сколько будет контейнеров в нашем простейшем блоге? Контейнер, это кстати по сути виртуализация отдельного сервера, который общается по сети с другими контейнерами, правда если проводить жесткую аналогию контейнер=сервер, от некоторых контейнеров нужно будет отказаться, но тем не менее. Для простейшей реализации блога на микросервисах я насчитал 24 контейнера. Давайте посмотрим.
Зачем на по две копии некоторых сервисов? Потому что с одной будет не интересно и не понятно.
Файл docker-compose, который развернет все это одной командой выглядит вот так:
https://github.com/gregory-vc/blog/blob/master/host/docker-compose.yml
Из самого интересного рассмотрим настройки php контейнера нашего шлюза.
Раздел описания контейнера links, это по сути просто редактирование /etc/hosts/
Где по обозначенному хосту мы просто имеем доступ к другому контейнеру через внутреннюю сеть докера.
А раздел environment — это просто обозначение переменных, которые мы сможем достать с вами внутри приложения через getenv(). Сделано так, чтобы файл docker-compose был единой точкой входа для настройки всего приложения.
В то время как структура наших сервисов выглядит как просто директории лежащие рядом,
Но, на самом деле, при запуске докера хостов, каждая из этих директорий оказывается внутри отдельного изолированного соответствующего контейнера. Делается это как-то так:
То есть, хоть сейчас они и рядом, при запуске не будет возможности из одного сервиса проинклудить класс другого сервиса или что-то типа того. Рядом они исключительно из удобства, в реальной жизни они должны быть каждый в своем репозитории, вообще не касаясь друг друга.
Сервис Gate
Этот сервис будет являтся точкой входа на наш блог, именно он будет рендерить шаблоны, отображать результат и дергать нужные ему сервисы. Кстати существуют разные подходы, например можно отказаться от единой точки входа и реализовать все на фронтенде. То есть браузер сам будет ходить в нужные сервисы и собирать результат прямо в браузере. Что сказать, все зависит от вашего конкретного случая и там и там есть свои плюсы и минусы.
Итак, у нас есть php и больше ничего. Хотя, давайте возьмем хотя бы composer, куда без него. Создадим еще две директории, одну с нашим микрофреймворком, который мы сейчас напишем, вторую для public скриптов, js, и прочих ресурсов.
В composer просто укажем, откуда осуществлять autoload, чтобы нам самим с этим не заморачиваться, и подключим сгенеренный autoload в public/index.php
Так, что-то у нас уже есть, давайте определимся что нам вообще еще будет надо?
Напишем вот такое хранилище объектов, чтобы не создавать их где попало, а иметь возможность получить доступ (inject) к уже созданным в любой точке приложения со всем нужными зависимостями (dependency). Мы не будем развлекаться с Reflection и прочими интересными штуками, время у нас жестко ограничено.
В Di, используя это хранилище просто добавляем все объекты что нам нужны.
В паблике стартуем Di, получаем роутер, регистрируем все урлы что нам пригодятся, получаем приложение и стартуем его.
В приложении получаем request, маппим в роутере существующий экшен существующего контроллера по этому реквесту, одновременно еще записываем в request все пост или гет переменные, что нам пришли.
Выполняем метод контроллера, получаем response, рендерим response и показываем результат нашей работы, все.
Каркас есть, теперь нам надо работать с сервисами, создаем директорию с сервисами, создаем класс каждого сервиса, описываем точки доступа к каждому из сервисов. Наследуем их от основного класса сервисов, где реализуем варианты запросов.
Там внутри при запросе выбираем рандомны коннектор из предоставляемых сервисом, как-то так
Делаем запрос, из контроллера и рендерим, вот так:
Нам нужно отрендерить, но как? Шаблонизаторов у нас нет. Писать свой? Ну нет. Просто используем php.
Черезвычайно мощный шаблонизатор размером с 4 строчки.
Сервис post и comment
Что дальше? Теперь мы можем делать запросы и рендерить результат, теперь нам нужно написать сервисы отдающие ответ. Все просто копируем наш новый движок в другие сервисы, меняем урлы и пишем работу с моделями и бд, вместо удаленных сервисов.
Итак? Если честно, это почти все, что нам нужно, не считая авторизации.
Мы можем делать запросы gate на любой сервис, с любого другого сервиса на любой другой.
Сервис авторизации
Схема проста: мы имеем юзеров и их доступы на сервере авторизации, мы делаем из шлюза запрос на авторизацию, генерируем токен, возвращаем его шлюзу и еще юзера, кладем юзера и токен в сессию и все. Незабываем посылать токен вместе с запросом на добавление поста, потому что что? Правильно, сервис постов пойдет в сервис авторизации и спросит, а правда ли что этот токен хорош? В зависимости от результата генерим разные эксепшены.
Результат
Вообще, можете скачать и развернуть его одной командой, напомню репозиторий: https://github.com/gregory-vc/blog
Где подходило по смыслу — я вывел для наглядности какой именно нодой был сгенерен тот или иной блок.
Еще меня впечатлило время генерации странички. Это 5-9 мс для странички с постом и несколькими комментариями (!). Да, все это необъективно, да, все это попугаи, да, микросервисы тут не при чем, да, смотря с чем сравнивать. Но. Тот же ларавель генерит свою страничку, вообще без запросов и данных, просто приветствие, за 90 мс, на моей же машине. Это в 10-20 раз дольше.
Я понимаю, что там происходит куда больше всего, не сравнить, но тем не менее, попытаюсь выразить мысль: для конкретно текущей задачи отдельного изолированного микросервиса всего этого и не надо. Для сервиса комментов я выкинул класс работы с сервисами по сети. Для сервиса шлюза я выкинул класс работы с базой. Для каждого конретного сервиса я собрал лишь то, что ему надо. А правильном сервису надо совсем чуть-чуть 🙂
А главное это невероятный потенциал для масштабирования этого блога под просто невероятные нагрузки. Никто не помешает например потом взять и переписать сервис комментариев на Go.
Проблемы
Сетевые накладные расходы
Не зная о том как работает другой сервис, мы можем вполне попасть в ситуацию, когда, он не то что работает плохо, и портит нам все, он еще и использует наш сервис (!) чтобы нам же отдать наши результаты.
Как использовать PHP для создания микросервиса?
В этой статье рассказывается, как использовать PHP для построения архитектуры микросервисов. Так как PHP идет в ногу со временем, он способен поддерживать микросервисные архитектуры для больших систем.
Проблема сервитизации
При использовании traditional framework (laravel, yii, symfony) для реализации микросервисов на Php эффекта очень мало.
В режиме разработки fpm, поскольку резидентная память не может быть предоставлена, каждый запрос должен начинаться с нуля, начиная с загрузки процесса, чтобы потом выйти из него, добавляя много бесполезных накладных расходов.
Кроме того, соединение с базой данных не может быть использовано повторно и не защищается, потому что fpm является процессно-ориентированным, и количество процессов fpm также определяет и число параллельных процессов. Таковы проблемы, с которыми мы столкнулись при обычной разработке fpm.
Он недостаточно дружелюбен к микросервисным инструментам, таким как docker, и полагается на nginx для предоставления услуг.
Таким образом, вот причины, по которым Java сейчас более популярна как интернет-платформа по сравнению с PHP. Помимо PHP non-memory resident, существует множество других проблем, требующих решения.
Теперь давайте посмотрим, как Swoft имплементирует микросервис.
Что такое Swoft?
Swoft — это фреймворк корутин для микросервисов PHP, основанный на расширении Swoole. Как и Go, Swoft имеет встроенный веб-сервер и общий клиент для корутин, и является резидентным в памяти, независимым от традиционного PHP-FPM.
Здесь есть Go-подобные языковые операции, гибкие аннотации, аналогичные фреймворку Spring Cloud, мощный контейнер для внедрения глобальных зависимостей, комплексное управление сервисами, гибкое и мощное AOP (Aspect Oriented Programming), стандартная реализация спецификации PSR (PHP Standards Recommendations) и так далее.
Swoft Github
Что нам нужно для создания микросервиса?
Высокопроизводительный фреймворк для приложений
Регистрация и обнаружение услуг
Автоматическое переключение услуг
Да, в Swoft все уже готово
Высокая производительность Swoft
Вы можете представить, какие преимущества дает нам резидентная память.
Фреймворк инициализируется только один раз; мы можем сосредоточиться на обработке запросов, потому что фреймворк инициализируется в памяти только один раз при запуске для резидентной памяти
Мультиплексирование соединений; если мы не используем пул соединений, тогда к чему приводит создание соединений для каждого запроса, этого не могут понять некоторые инженеры. Это приводит к чрезмерному использованию ресурсов бэкенда. Для некоторых основных сервисов, таких как Redis, базы данных, соединения оказываются дорогостоящим расходом ресурсов.
Итак, есть ли подходящее решение? Ответ — да, и многие используют фреймворк под названием Swoft. Swoft — это RPC (Remote Procedure Call)-фреймворк с функциональностью Service Governance. Swoft — это первый фулл-стек PHP-фреймворк, резидентный в памяти, корутинный, базирующийся на основной концепции Spring Boot согласно которой соглашение важнее чем конфигурация.
Swoft обеспечивает более элегантный способ использования RPC-сервисов, таких как Dubbo, имеет отличную производительность, схожую с Golang. Вот результат стресс-теста производительности Swoft на моем PC.
Скорость обработки данных в стресс-тесте ab очень впечатляет. С процессором i7 generation 8 и памятью 16GB на 100000 запросов уходит всего 5s. Такого быстродействия практически невозможно достичь в режиме разработки fpm.
Данного теста также достаточно, чтобы продемонстрировать высокую производительность и стабильность Swoft.
В процессе управления микросервисами часто требуется регистрация сервисов, инициированных на сторонних кластерах, таких как consul/etcd. В этой главе для осуществления регистрации и обнаружения сервисов используется компонент swoft-consul в составе фреймворка Swoft.
В базовом режиме автоматический выключатель гарантирует, что поставщик не будет вызван, когда он находится в разомкнутом состоянии, однако нам также необходим дополнительный метод сброса автоматического выключателя после возобновления обслуживания поставщиком.
Одно из возможных решений заключается в том, что автоматический выключатель периодически определяет, возобновлено ли обслуживание. Как только оно возобновляется, он устанавливается в закрытое положение. При повторной попытке состояние становится полуоткрытым.
Ограничение потока, автоматический выключатель, понижение уровня обслуживания Это можно подчеркивать неоднократно, потому что они действительно важны. Когда услуга не работает, она должна быть прекращена. Ограничение потока — это инструмент самозащиты. Если нет механизма самозащиты и соединения принимаются независимо от их количества, то при очень большом трафике фронтенд обязательно зависнет, а бэкенд не сможет обработать все соединения.
Ограничение потока — это ограничение числа одновременных запросов и количества запросов при доступе к дефицитным ресурсам, таким как товары на флэш-распродаже, чтобы эффективно срезать пиковые нагрузки и сгладить кривую потока.
Цель ограничения потока — ограничить скорость одновременного доступа и одновременных запросов, или ограничить скорость запроса в пределах временного окна для защиты системы. Как только предел скорости достигнут или превышен, запросы могут быть отклонены или поставлены в очередь.
Ограничение потока Swoft не только ограничивает контроллеры, оно также ограничивает методы в любом бине и контролирует скорость доступа к методам. Ниже приводится пример с подробным объяснением.
Прежде чем перейти к обсуждению центра конфигурации, давайте поговорим о конфигурационном файле. Он нам хорошо знаком. С его помощью мы можем динамически изменять программу. Вот чья-то цитата об этом:
Динамическая регулировка полета системы во время выполнения
Для автономной версии мы называем его конфигурацией (файлом); для распределенной кластерной системы мы называем его центром конфигурации (системой);
В этой главе в качестве примера используется Apollo для извлечения сервисов конфигурации и безопасного перезапуска из удаленного центра конфигурации. Если вы не знакомы с Apollo, вы можете сначала посмотреть на компонент Apollo расширения Swoft и прочитать официальную документацию Apollo.
В этой главе в качестве примера используется Apollo в Swoft. При изменении конфигурации Apollo перезапустите службу ( http-server / rpc-server / ws-server ). Ниже приведен пример агента:
Выше приведен обычный способ настройки Apollo, в дополнение к этому методу Swoft-Apollo предоставляет больше способов использования.
Заключение
В данный момент наш простой фреймворк микросервисов был построен. Если использовать традиционный PHP-фреймворк, этого достичь очень сложно. Но с помощью Swoft все гораздо проще.
Всех желающих приглашаем на двухдневный онлайн-интенсив «Пишем микросервисный бэкенд на PHP». На занятии:
— Познакомимся с понятием контейнеризации на примере Docker и принципами в работе с контейнерами;
— Сложим понимание термина микросервис и построим инфраструктуру для приложения, состоящего из двух микросервисов;
— Узнаем как работают Nginx и PHP-FPM в связке и внедрим их в созданные микросервисы.
>> РЕГИСТРАЦИЯ
Как мы пишем микросервисы и почему не делаем этого быстро
Истории по распиливанию монолита часто похожи одна на другую. Был у команды здоровенный неповоротливый монолит, решили его распилить на россыпь правильных и шустреньких микросервисов, все стало круто. Отличаются истории лишь степенью ужаса “до”, радости “после” и рядом вторичных характеристик.
У нас в RBK.money тоже микросервисы. Но пришли мы к ним немного не так, как большинство. У нас все было даже хуже монолита — у нас на старте просто все было хреново.
Под катом о том, как мы, собственно, и строили микросервисы, почему OpenSource — это не только здорово в принципе, но еще и работает как мотивационная составляющая писать хороший код.
Так вот, всё было хреново. Настолько, что фиксить это не имело никакого смысла, зато имело смысл договориться больше никогда не вспоминать об этом ужасе и просто написать все с нуля. И сразу на микросервисах. На самом первом этапе разработки мы сразу взяли за правило постоянно держать в голове тот факт, что однажды мы захотим заопенсорсить все это добро или его часть. В истории коммитов ведь сохраняется все, включая ники разрабов, поэтому просто садимся и сразу стараемся писать всё так, чтобы потом не было стыдно за свой код перед сообществом. Никому ведь не хочется краснеть за свой код или архитектуру проекта, так себе история.
Быстро vs хорошо
В идеальном мире всегда хочется писать код быстро и писать его хорошо. Ну это как “Лучше быть богатым и здоровым, чем бедным и больным”. Поэтому микросервисы стали отличным выходом из ситуации. Процесс написания кода был построен от бизнес-задач. Допустим, бизнесу нужна функциональность, которая будет учитывать средства на счетах контрагентов при платежах. Эта функциональность превращается в микросервис под кодовым именем Accounter, который и занимается учетом средств. С остальными микросервисами такая же история.
Главным тут было сделать так, чтобы каждая бизнес-функциональность была настолько конкретизирована, чтобы её мог писать один человек. Это сильно зависит и от самих задач, поступающих в работу, и от того, как техдир или проджект транслирует это команде. У нас получилось так сделать, это даёт сразу пару хороших весомых плюсов.
Во-первых, это обеспечивает большую параллелизацию разработки. У нас на старте было человек 10, и мы умудрялись писать одновременно большое количество кода (и писать хорошо). Во-вторых, это дарит вам возможность полноценной ротации. А вот это уже немного важнее, чем кажется на первый взгляд.
Очень часто человек начинает говнокодить не потому, что он за этим и устраивался к вам работать, а потому, что у него банально глаз замыливается и скучно становится. Если один человек постоянно сидит на одном и том же микросервисе, он может начать генерить говнокод. И это не столько вопрос профессионализма, сколько вопрос времени. Месяцев через 7-8 человек устанет поддерживать один и тот же микросервис, будет смотреть по сторонам — а там жизнь, после зимы пришла весна, у коллег какая-то движуха, опять новый айфон вышел, а ты все сидишь на одном и том же микросервисе. Вот так рождается монолит с единой точкой отказа в виде этого уставшего человека с мешками под глазами.
Или вообще человек начинает думать, что тут все только на нем и держится. Будет стараться сделать себя незаменимым, окружив свою работу кучей “тайных знаний” и странных процедур. У меня здесь в начале пути были ситуации, когда легаси было настолько диким, что без этих самых знаний разобраться было невозможно. Например, надо было запустить один сервис. Как себе обычно представляешь такое:
Это классический пример сложности ради сложности и “Без меня тут ничего не работает”. На самом деле, без подобного всё работает. Только быстрее и лучше. Избавиться от этого можно ротацией — в идеале когда человек пишет один микросервис примерно пару спринтов, а потом уходит делать другую задачу. Этим же мы поддерживаем и постоянный обмен знаниями в команде.
Код от протокола
Если взять любое ТЗ от бизнеса, хорошенько перевести на человеческий, отряхнуть от шелухи и выпарить — получится протокол, язык, по которому и будет общаться машина. То есть мы берем бизнес-задачу, понимаем для себя, что именно и как мы будем делать, и превращаем это в спецификацию на thrift или swagger (микросервисы внутри общаются по thrift). Первый шаг — всё подробно описать: что будет делать микросервис, какие типы данных принимать, чем отвечать, какие будут структуры и прочее. Этот протокол проходит первое ревью у тех, кто точно представляет себе, как все работает (де-факто — архитекторы). Срабатывает как фильтр грубой очистки, через который какое-то откровенное фуфло не пройдет даже на уровне концепции.
Как только появляется протокол, можно садиться писать код. И если протокол ревьюится вполне себе универсальными людьми, то сам код — в команде конкретных людей. Мы пишем на трёх языках — JS, Java, Erlang. Главное — никого не торопить ни с ревью, ни с написанием кода. Да, бизнесу всегда и везде надо быстро и круто. Но я как техдир редко тороплю ребят, потому что понимаю, что они хотят сделать хорошо. В итоге получается ситуация, что я часто бываю взбодрен бизнес-заказчиками за сроки. Зато практически не приходится краснеть за качество.
Мы поспешили лишь однажды, когда наложился джекпот — суперзаказчик и крайне срочные дедлайны, как раз создавали наш Wallet. Тогда да, мы чууууток спустили рукава и сделали все быстрее, чем планировали (и хуже, чем хотели, да). В идеале все задумывалось как кучка аккуратных микросервисов. Получился такой себе кусочек монолита. Плюсы ситуации в том, что мы еще разок для себя поняли, что спешить не надо. А сам сервис уже потихоньку растаскиваем на отдельные микросервисы, как и хотели.
Микросервисов в RBK Money штук 50, их пишут около 20 человек. Внутри везде thrift, для разрабов это довольно сложный протокол, дебажить сложно, документацию писать тоже сложно. И если бы я выпустил thrift наружу в чистом виде, меня бы стали называть нехорошими словами. Поэтому мы не стали ничего придумывать — наружу у нас бодро торчит рестовый JSON, простой и понятный, плюс OpenAPI. Чтобы иметь возможность принимать эти запросы снаружи, их надо валидировать, авторизовывать, а потом другими микросервисами уже запускать внутрь платформы. И всё это дело мы тоже написали в качестве самостоятельного микросервиса, который:
Удобно ли писать платёжную систему на микросервисах? Безусловно — тут тебе и параллелизация работы, и поддержание интереса сотрудников, и отсутствие единой точки отказа. Сломался какой-то один микросервис или вдруг ушел человек, который еще вчера его делал — не проблема, можно быстро починить что-то, а на место пилота посадить на время нового спринта нового человека.
Но есть мнение, что если один человек сидит и тщательно пилит конкретный микросервис довольно долго, то он точно делает это хорошо. Раз уж мы заговорили об этом — напишите, пожалуйста, в комментариях, какой подход вам ближе. А главное — почему.







