Оливет Битти, глава технического департамента британского необанка Monzo, рассказывает о построенной его командой архитектуре бэкенд-систем банка.
Бэкенд в Monzo строился с нуля. Из первичных требований была отмечена необходимость для системы быть доступной 24 х 7 и масштабируемой до миллионов клиентов по всему миру — это привело к тому, что с самого начала система разрабатывалась как коллекция распределенных микросервисов. Для стартапа на ранней стадии этот подход необычен; как правило, такие компании выбирают уже знакомую централизованную архитектуру с привычными фреймворками и реляционными базами данных. Однако в Monzo этот вариант не рассматривался, так как несет за собой привычные «детские болезни» — единую точку отказа, необходимость в периодическом отключении для проведения техобслуживания и плохую масштабируемость.
На старте создания собственной платформы у Monzo было всего три разработчика, и приходилось быть прагматичными
Крупные компании, такие как Amazon, Netflix и Twitter, показали, что единые монолитные базы данных не масштабируются на большое количество пользователей и разработчиков — а если банк планирует выходить на множество различных рынков с уникальными требованиями на каждом, то ему просто придется держать много команд, работающих над разными частями продукта. Всем этим группам придется самостоятельно контролировать свой процесс разработки, вывода новых версий и масштабирования без необходимости координации усилий с другими командами. Следовательно, процесс разработки должен быть распределенным и декомпозированным, как и продукт.
На старте создания собственной платформы у Monzo было всего три разработчика, и приходилось быть прагматичными — были выбраны уже знакомые технологии, которые вели в нужном направлении, и разработка велась с учетом того, что программисты в будущем всегда смогут кардинально изменить тот или иной модуль.
При запуске бета-тестирования бэкенд Monzo включал в себя около 100 микросервисов. Сейчас их около 150, и команда разработки выросла соответственно. Так как получение банковской лицензии было уже почти решенным делом, команда пересмотрела некоторые архитектурные подходы и пришла к необходимости сфокусировать работы на следующих направлениях:
- Управление кластером.
- Многоязычные сервисы.
- RPC-транспорт, обеспечивающий связь компонентов.
- Асинхронный обмен сообщениями.
Давайте разберем каждое из этих направлений подробнее.
Управление кластером
Команде Monzo был необходим эффективный и автоматизированный способ управлять большим количеством серверов, распределять между ними нагрузку и реагировать на их выход из строя.
- Зачем это нужно?
Если вам необходимо построить отказоустойчивую гибко масштабируемую систему, то у вас должна быть возможность добавлять и удалять новые сервера: по мере того как система развивается, сервера выходят из строя или меняются требования пользователей. Запускать по одному сервису на хосте — трата ресурсов (ведь сервису могут быть не нужны все ресурсы хоста), а традиционный подход ручного распределения сервисов по хостам слишком сложно масштабируется.
- Какое решение применили?
Контейнеризация — это комбинация технологий, применяемых в ядре Linux
В Monzo используют планировщики для кластеров, которые разворачивают приложения на серверах в соответствии с ресурсными требованиями, масштабируют их при необходимости и заменяют сервера при выходе из строя. В этом помогает контейнеризация, и в частности Docker. Контейнеризация — это комбинация технологий, применяемых в ядре Linux, объединенная в образ, что позволяет отделить приложение от операционной системы и изолировать от других приложений на том же сервере. Таким образом, системе управления кластером приходится иметь дело исключительно с контейнерами.
В Monzo используют планировщик Kubernetes, запускаемый на серверах с CoreOS. Этот планировщик тесно интегрируется с Docker и известен тем, что используется в эксплуатации Google.
- Что это дало?
Кроме повышения надежности Kubernetes позволил Monzo снизить расходы на инфраструктуру — теперь она обходится банку всего в четверть от привычных затрат. К примеру, если раньше для выпуска на production использовалось несколько дорогих производительных серверов с CI-системой Jenkins, то теперь Kubernetes использует для работы свободные ресурсы на уже имеющейся инфраструктуре — практически бесплатно. Подобный подход позволяет быть уверенным в том, что ресурсоемкие, но низкоприоритетные задачи (такие, как формирование отчетов) не повлияют на производительность критически важных сервисов, используемых клиентом.
Многоязычные сервисы
Основным языком разработки микросервисов в Monzo был и остается Go, но в общую систему бесшовно включаются модули на других языках.
- Зачем это нужно?
Использование языка Go позволяет создавать сервера, обеспечивающие параллельную работу с низкими задержками, но ни одна экосистема языка не содержит всех инструментов, необходимых для создания банка. В Monzo осознали важность использования большого количества инструментов, как коммерческих, так и с открытым исходным кодом, а также предстоящую эволюцию архитектуры платформы по мере изменений в технологиях.
В Monzo выделили повторно используемый код в отдельные сервисы
Обычно фрагменты кода, которые используют несколько приложений, принято объединять в библиотеки, но этот подход не годится в многоязычной системе — если бы пришлось реплицировать и поддерживать в актуальном состоянии тысячи и сотни тысяч строк повторно используемого кода каждый раз, когда к платформе подключается сервис на новом языке, все это довольно быстро стало бы неуправляемым.
- Какое решение применили?
В Monzo выделили повторно используемый код в отдельные сервисы. Чтобы заблокировать данные для проведения в них изменений, сервис фиксирует их в распределенном хранилище etcd с помощью чистого RPC. Все многоязычные сервисы, использующие общую инфраструктуру (включая базы данных и очереди сообщений), реализуются подобным образом.
- Что это дало?
Переход на подобные сервисы позволил уменьшить количество кода для каждого языка — нужен только клиент для вызовов RPC. Этот подход уже сейчас позволяет Monzo писать сервисы на Java, Python и Scala.
RPC-транспорт
Когда большое количество сервисов распределено между серверами, датацентрами и даже континентами, успех системы зависит от объединяющего компоненты слоя удаленного вызова процедур (RPC, remote procedure calling), который может обеспечить работоспособность в случае выхода из строя отдельных компонентов, снизить задержки при передаче данных и помочь в понимании поведения системы при работе.
- Зачем это нужно?
В идеале запросы к элементам системы производятся через уже имеющиеся соединения
С учетом того что разработка сервисов ведется на множестве языков, протокол для удаленного вызова процедур должен их все поддерживать. Победителем стал HTTP — у каждого языка есть под него стандартные библиотеки.
Однако для того чтобы создать надежную мультисервисную платформу, к слою RPС пришлось добавить следующие элементы:
- Балансировка нагрузки. Большинство HTTP-библиотек поддерживают балансировку кругового обслуживания (round-robin) на базе DNS, но это не самый разумный вариант. В идеале балансировка нагрузки должна выбирать необходимый хост, основываясь одновременно и на минимизации отказов, и на времени отклика — таким образом даже при потере части серверов и наличии медленных реплик система останется работоспособной и быстрой.
- Автоматические повторы запроса. В распределенных системах сбои неизбежны, и если вызываемый сервис не может обслужить идемпотентный (когда при повторном исполнении результат будет таким же, что и при начальном) запрос, тот может быть безболезненно перенаправлен на другую реплику сервиса, тем самым поддерживая работоспособность системы в целом даже при выходе из строя отдельных компонент.
- Пул соединений. Открытие нового соединения на каждый запрос очень плохо отражается на времени отклика системы. В идеале запросы к элементам системы производятся через уже имеющиеся соединения.
- Маршрутизация. Полезно иметь возможность управлять поведением RPC-системы во время работы, к примеру при запуске новой версии сервиса можно направить на него только часть трафика. Это позволяет производить внутреннее и внешнее тестирование — вместо того чтобы полностью реализовывать тестовое окружение, можно вывести в тестовый режим лишь некоторые сервисы для некоторых пользователей.
- Какое решение применили?
Одна из наиболее сложных RPC-систем — это Finagle, она построена на модульной архитектуре и обладает практически всей необходимой функциональностью. На ее основе создан linkerd, который и используют в Monzo. Вместо того чтобы связываться с другими частями системы напрямую, каждый компонент платформы Monzo общается с локальной копией linkerd, который направляет запрос к нужному узлу, используя балансировку нагрузки Power of Two Choices + Peak EWMA. Если идемпотентные запросы не могут быть выполнены, они автоматически возобновляются. Вся логика взаимодействия между сервисами вынесена в RPC-слой, что позволяет писать сервисы на любых языках без необходимости поддержки RPC-библиотек.
- Что это дало?
Это сэкономило время на удаленный запуск процедур и повысило надежность RPC. Он развернут как набор демонов в Kubernetes, так что сервисы всегда обращаются к нему на локальном хосте, который пробрасывает запрос дальше. В тех случаях, когда реплики обоих сервисов, связь между которыми обеспечивает RPC, находятся физически на одном сервисе, запрос даже не уходит в сеть.
Асинхронный обмен сообщениями
Чтобы бэкенд был производительным и надежным, применяются очереди сообщений для планирования фонового выполнения задач. Эта механика должна гарантировать возможность постановки задания в очередь и удаления из нее, а также то, что данные в очереди никогда не будут потеряны.
- Зачем это нужно?
Большая часть задач в бэкенде Monzo выполняется асинхронно. К примеру, даже когда обработка платежа занимает меньше секунды, бэкенд в течение десятка миллисекунд сообщает платежной системе об одобрении или отклонении платежа с карты Monzo. Работы по обработке данных мерчанта, рассылке push-нотификаций и даже включение транзакции в поток данных пользователя происходят асинхронно.
Кроме асинхронности, эти шаги не должны быть пропущены. Даже если произойдет ошибка, в ходе которой будет невозможно автоматически восстановить состояние системы, должна быть возможность исправить данные и возобновить процесс. Это определяет следующие требования к асинхронной архитектуре:
- Высокая доступность. Паблишеры (модули, публикующие сообщения) должны иметь возможность работать в режиме «выстрелил-забыл», вне зависимости от отказов серверов или состояния модулей-слушателей.
- Масштабируемость. В Monzo хотели иметь возможность расширять очередь сообщений без прерывания работы сервиса и без добавления нового «железа» — очередь сама по себе должна быть горизонтально масштабируемой.
- Персистентность. Если узел очереди сообщений выйдет из строя, данные не должны быть потеряны. Точно так же если выйдет из строя модуль-слушатель, у очереди должна быть возможность повторной доставки сообщения сервису.
- Воспроизводимость. Полезна возможность воспроизвести поток сообщений с какой-то исторической точки с тем, чтобы новые процессы могли обрабатывать исторические данные (к примеру, обучать нейросети, анализировать ретро-выборки, etc).
- В основном разовая доставка. Все сообщения должны достигнуть слушателей. Хотя, как правило, невозможно доставить сообщение только один раз, в нормальном режиме работы очередь не должна производить множественную доставку.
- Какое решение применили?
В качестве платформы обмена сообщениями между сервисами в Monzo выбрали Apache Kafka. Ее реплицируемый модульный дизайн позволяет обрабатывать выход узлов из строя и масштабировать платформу по мере необходимости без прерывания обслуживания. Дизайн слушателей в этой платформе тоже интересен: в то время как большинство других платформ держат отдельную очередь для каждого сервиса, в слушатели проходят «курсором» по общему журналу сообщений.
- Что это дало?
Это позволило удешевить систему паблишеров / слушателей, уменьшить их требовательность к ресурсам. Так как сообщения сохраняются в журнале вне зависимости от событий, их всегда можно воспроизвести для определенных сервисов.
В завершение хочется обратить внимание на то, что три важнейших компонента, обеспечивающих связность и цельность бэкэнда Monzo — система управления кластером Kubernetes, система удаленного вызова процедур linkerd и система асинхронного обмена сообщениями Kafka — это бесплатно распространяемые системы с открытым исходным кодом. Может быть, архитектурные перемены в банке — это не так уж и дорого?