Квест → Как хакнуть форму
Прошли: 77
Использование правильных шаблонов/паттернов для общения между микросервисами может помочь масштабировать ваше приложение и решить наиболее распространенные системные проблемы. Мы начали с прямых HTTP-вызовов для всех взаимодействий, но решили перейти к событийной системе. Эта система изменила наше мышление о взаимодействии между службами, навязала масштабируемые паттерны и увеличила отказоустойчивость.
Мы перешли к использованию событий вместо традиционного HTTP-взаимодействия по нескольким причинам.
Во-первых, это вынудило к разделеню сервисов. Из нашего опыта работы с HTTP, один сервис может делать вызовы к каждому сервису, который ему необходим, и это означало, что этому сервису нужна клиентская библиотека для каждого сервиса, к которому он обращается. Клиентская библиотека обеспечивала бы то, что ошибки не будут останавливать или блокировать функциональность и будут соответствовать каждой службе.
Так как мы разрослись до 20+ сервисов, разработка и поддержка клиентских библиотек стала долгим и трудоёмким процессом. Для новых сервисов, которые заменяли устаревшую функциональность, требовалось обновление всех зависимостей. Это сделало процесс разработки и выкладки более длительным и более подверженным ошибкам из-за множества задействованных частей.
Во-вторых, преимущество использования событий заключается в том, что сервисам больше не нужно организовывать функциональность, удаляя прямые вызовы к клиентам. Сервисы могут появляться и удаляться без надобности обновлять клиентские библиотеки или добавления нового HTTP-вызова. Мы можем быстро развернуть прототип приложения, которое слушает события, не беспокоясь о том, что оно приведёт к падению всей системы.
В-третьих, это изменение позволило нам реализовать глобальные паттерны. Мы добавили ограничения скорости и таймауты для каждого воркера, без необходимости их реализации в каждой из наших различных клиентских библиотек (GitHub, AWS, внутренних сервисах и т. д.). Мы также можем с легкостью реализовать паттерн Автоматический выключатель, отрезав слушателя события, пока он не восстановит свою работу. Нужно изменить только сам воркер, а не все приложения/сервисы, которые вызывают его.
И, наконец, нам не нужно держать открытым http-соединение для длительных обработок (которое может быть потеряно или ограничено в связи с большим количеством открытых сокетов и т. д.).
Есть два различных паттерна, которые составляют нашу систему, управляемую событиями: события и задачи.
События — это уведомления, которые сообщают подписанным приложениям, что что-то произошло. Приложения подписываются на определенные события и реагируют на них, создавая задачи для самих себя. События никогда не должны изменять состояние напрямую.
Задачи — это действия, которые изменяют состояние. Единственное, что может создать задачу для данного приложения - само приложение. Таким образом, приложения не могут напрямую изменить состояние друг друга.
Строгие соглашения по именованию помогают нам поддерживать согласованность и четкость, когда дело доходит до именования события и задачи. Задачи начинаем с названия приложения, чтобы быть уверенными, что они обрабатываются только этим приложением. Далее идет модель, чье состояние должно быть изменено задачей, и заканчивается описательным глаголом настоящего времени. Пример задачи будет api.user.authorize
. Опираясь на соглашение, мы знаем, что эта задача обрабатывается сервисом API
, и он хочет выполнить авторизацию
на объекте пользователь
.
События не имеют имени приложения, потому что на них могут быть подписаны несколько приложений. Они начинаются с имени модели, а заканчиваются глаголом прошедшего времени, который описывает то, что произошло. Примером события будет user.authorized
.
Наше приложение, разбитое на задачи и события, заставило нас изменить способ мышления. Раньше, если мы хотели отправить электронное письмо после того, как мы получили платеж, мы бы просто добавили вызов SendGrid в наш платежный сервис. Просто и понятно.
Но теперь с нашей новой событийной системой, наш платежный сервис генерирует событие org.payment.processed
. Наш сервис электронной почты, Pheidi, забирает это событие и создает задачу pheidi.email.send
. Теперь мы должны думать в терминах реакций вместо команд. Если нужны дополнительные данные, которые не были предоставлены в событии (зарегистрированное имя на кредитной карте, например), мы все еще используем http-вызовы в нашу биллинговую службу.
Однако, есть и некоторые минусы, которые появляются вместе с плюсами событийно-ориентированного подхода. Поскольку вы явно не вызываете сервис, вы не можете знать наверняка какой именно будет ответ на выброшенное событие. Это затрудняет отладку, потому что система сложнее и труднее для понимания.
Мы используем RabbitMQ в качестве системы обмена сообщениями. Он отвечает за доставку событий до сервисов, которые слушают их. Задачи также проходят через RabbitMQ, таким образом балансируя нагрузку между несколькими экземплярами приложения. Мы выбрали RabbitMQ, потому что он был прост в установке и разворачивании и имеет много клиентский библиотек, готовых к использованию.
Мы создали единый шаблон воркера для взаимодействия с RabbitMQ. Вот некоторые паттерны, которые мы используем для обработки наших очередей.
В самом начале мы добавили экспоненциальную отмену для задач. Если задание выбросило ошибку, которая допускает повторную попытку, оно будет повторно запущено после задержки. Каждое задание начинает с минимальной задержкой по времени, которая удваивается, пока не достигнет заданного максимального предела (или до бесконечности, если предел не определен).
Изначально, мы хотели, чтобы количество повторных попыток выполнить задание было бесконечным, думая, что если что-то «зависнет», то наши системы оповещения будут в агонии и кто-нибудь из нас пойдёт спасать возникшую ситуацию. Поначалу это работало хорошо, но как только мы добавили больше заданий, количество «застрявших» в очереди выросло по разным причинам.
Для борьбы с растущими очередями, мы добавили максимальное количество повторов для каждой очереди. Если задание повторяется заданное число раз, мы прекращаем его попытки и запускаем функцию восстановления. Функция восстановления логирует ошибку в базе данных. Теперь наши системы оповещения срабатывают на функции восстановления, что позволит нам заняться исправлением проблемы, вместо поднятия нашей очереди. Мы обнаружили, что лучше потерпеть неудачу быстро и показать ошибку пользователям, вместо того, чтобы заставлять их ждать долгое время, чтобы что-то произошло.
Prefetch — это важный параметр, который выставляется на канал RabbitMQ. Без этого ваш воркер будет принимать все доступные задания в очереди. Например, если ваше приложение испытало всплеск нагрузки и в очередь упало 10 000 заданий, все 10 000 заданий будут направлены воркеру и станут храниться в памяти, что, как правило, приводит к краху. Prefetch ограничивает количество заданий, которое ваш воркер будет держать в памяти. Этот пост в блоге RabbitMQ помог нам определить лучший способ задать prefetch.
Для реализации событий и задач, мы используем следующие конструкции в RabbitMQ. Для задач используем одну очередь и API sendToQueue
. Поскольку задачи могут быть использованы только для одного приложения, мы не создаем для них обмен. Для событий немного сложнее. Приложение, выбрасывающее событие, создает веер обменов и каждый подписчик будет создавать и привязывать очередь к этому обмену. Это позволяет любому приложению получить любое событие, не затрагивая другие приложения.
Одна вещь, которая помогла нам отладить и обеспечить самонаблюдение нашей системы событий — идентификаторы транзакции (TID). Каждое задание, которое мы посылаем в RabbitMQ, начинается с TID. Если это задание было результатом события или задачи, то он использует тот же TID. Если задание не создаётся из события или задачи, мы создаём новый TID. Это помогает нам отслеживать, какие события запускают какие задачи.
Наша событийно-управляемая система ускорила нашу разработку, сделала нас более устойчивыми к сбоям и улучшила отзывчивость нашего продукта для наших пользователей. Мы надеемся, что эти техники помогут вырасти и вашему продукту.