Книга «Распределенные системы. Паттерны проектирования»
Современный мир попросту немыслим без использования распределенных систем. Даже у простейшего мобильного приложения есть API, через который оно подключается к облачному хранилищу. Однако проектирование распределенных систем до сих пор остается искусством, а не точной наукой. Необходимость подвести под нее серьезный базис назрела давно, и, если вы хотите обрести уверенность в создании, поддержке и эксплуатации распределенных систем — начните с этой книги!
Брендан Бёрнс, авторитетнейший специалист по облачным технологиям и Kubernetes, излагает в этой небольшой работе абсолютный минимум, необходимый для правильного проектирования распределенных систем. Эта книга описывает неустаревающие паттерны проектирования распределенных систем. Она поможет вам не только создавать такие системы с нуля, но и эффективно переоборудовать уже имеющиеся.
Отрывок. Паттерн Decorator. Преобразование запроса или ответа
FaaS идеально подходит в том случае, когда нужны простые функции, которые обрабатывают входные данные, а затем передают их другим сервисам. Такого рода паттерн может использоваться для расширения или декорирования HTTP-запросов, передаваемых или принимаемых другим сервисом. Данный паттерн схематически изображен на рис. 8.1.
К слову, в языках программирования существует несколько аналогий данному паттерну. В частности, в Python есть декораторы функций, которые функционально похожи на декораторы запросов или ответов. Поскольку декорирующие преобразования не хранят состояния и часто добавляются постфактум по мере развития сервиса, они идеально подходят для реализации в виде FaaS. Кроме того, легковесность FaaS означает, что можно экспериментировать с разными декораторами до тех пор, пока не найдется тот, который теснее интегрируется в реализацию сервиса.
Добавление значений по умолчанию во входные параметры HTTP RESTful API-запросов выгодно демонстрирует преимущества паттерна Decorator. Во многих API-запросах есть поля, которые необходимо заполнять разумными значениями, если они не были указаны вызывающей стороной. К примеру, вы хотите, чтобы по умолчанию поле хранило значение true. Этого трудно добиться с помощью классического JSON, поскольку в нем значение пустого поля по умолчанию равно null, что обычно интерпретируется как false. Чтобы решить эту проблему, можно добавить логику подстановки значений по умолчанию либо перед API-сервером, либо в коде самого приложения (например, if (field == null) field = true). Однако оба этих подхода неоптимальны, поскольку механизм подстановки значений по умолчанию концептуально независим от обработки запроса. Вместо них мы можем использовать FaaS-паттерн Decorator, преобразующий запрос на пути между пользователем и реализацией сервиса.
Учитывая сказанное ранее в разделе об одноузловых паттернах, вам, возможно, стало интересно, почему мы не оформили сервис подстановки значений по умолчанию в виде контейнера-адаптера. Такой подход имеет смысл, но он также означает, что масштабирование сервиса подстановки значений по умолчанию и масштабирование самого API-сервиса становятся зависимы друг от друга. Подстановка значений по умолчанию — вычислительно легкая операция, и для нее, скорее всего, не понадобится много экземпляров сервиса.
В примерах данной главы мы будем использовать FaaS-фреймворк kubeless (https://github.com/kubeless/kubeless). Kubeless разворачивается поверх сервиса оркестратора контейнеров Kubernetes. Если вы уже подготовили Kubernetes-кластер, то приступайте к установке Kubeless, который можно загрузить с соответствующего сайта (https://github.com/kubeless/kubeless/releases). Как только у вас появился исполняемый файл kubeless, установить его в кластер можно командой kubeless install.
Kubeless устанавливается как сторонняя API-надстройка Kubernetes. А это значит, что после установки его можно будет использовать в рамках инструмента командной строки kubectl. К примеру, развернутые в кластере функции можно будет увидеть, выполнив команду kubectl get functions. На данный момент в вашем кластере не развернуто ни одной функции.
Практикум. Подстановка значений по умолчанию до обработки запроса
Продемонстрировать полезность паттерна Decorator в FaaS можно на примере подстановки значений по умолчанию в RESTful-вызов для параметров, значения которых не были заданы пользователем. С помощью FaaS это делается довольно просто. Функция подстановки значений по умолчанию написана на языке Python:
# Простая функция-обработчик, подставляющая значения
# по умолчанию
def handler(context):
# Получаем входное значение
obj = context.json
# Если поле "name" отсутствует, инициализировать его
# случайной строкой
if obj.get("name", None) is None:
obj["name"] = random_name()
# Если отсутствует поле 'color', установить его
# значение в 'blue'
if obj.get("color", None) is None:
obj["color"] = "blue"
# Выполнить API-вызов с учетом значений параметров
# по умолчанию
# и вернуть результат
return call_my_api(obj)
Сохраните эту функцию в файл под названием defaults.py. Не забудьте заменить вызов call_my_api вызовом нужного вам API. Эту функцию подстановки значений по умолчанию можно зарегистрировать в качестве kubeless-функции следующей командой:
kubeless function deploy add-defaults \
--runtime python27 \
--handler defaults.handler \
--from-file defaults.py \
--trigger-http
Чтобы ее протестировать, можно использовать инструмент kubeless:
kubeless function call add-defaults --data '{"name": "foo"}'
Паттерн Decorator показывает, насколько просто адаптировать и расширять существующие API дополнительными возможностями вроде валидации или подстановки значений по умолчанию.
Обработка событий
Большинство систем являются запросно-ориентированными — они обрабатывают непрерывные потоки пользовательских и API-запросов. Несмотря на это, существует довольно много событийно-ориентированных систем. Различие между запросом и событием, как мне кажется, кроется в понятии сессии. Запросы представляют собой части более крупного процесса взаимодействия (сессии). В общем случае каждый пользовательский запрос есть часть процесса взаимодействия с веб-приложением либо API в целом. События видятся мне более «одноразовыми», асинхронными по своей природе. События важны и должны соответствующим образом обрабатываться, но они оказываются вырваны из основного контекста взаимодействия и ответ на них приходит лишь спустя некоторое время. Примером события может служить подписка пользователя на некоторый сервис, что вызовет отправку приветственного письма; загрузка файла в общую папку, что приведет к отправке уведомлений всем пользователям данной папки; или даже подготовка компьютера к перезагрузке, что приведет к уведомлению оператора или автоматизированной системы о том, что необходимо предпринять соответствующие действия.
Поскольку эти события в значительной степени независимы и не имеют внутреннего состояния, а частота их весьма изменчива, они идеально подходят для работы в событийно-ориентированных FaaS-архитектурах. Их часто разворачивают рядом с «боевым» сервером приложений для обеспечения дополнительных возможностей или для фоновой обработки данных в ответ на возникающие события. Кроме того, поскольку к сервису постоянно добавляются новые типы обрабатываемых событий, простота развертывания функций делает их подходящими для реализации обработчиков событий. А так как каждое событие концептуально независимо от остальных, вынужденное ослабление связей внутри системы, построенной на основе функций, позволяет снизить ее концептуальную сложность, позволяя разработчику сосредоточиться на шагах, необходимых для обработки только одного конкретного типа событий.
Конкретный пример интеграции событийно-ориентированного компонента к существующему сервису — реализация двухфакторной аутентификации. В данном случае событием будет вход пользователя в систему. Сервис может генерировать для этого действия событие и передавать его функции-обработчику. Обработчик на основе переданного кода и контактных данных пользователя отправит ему аутентификационный код в виде текстового сообщения.
Практикум. Реализация двухфакторной аутентификации
Двухфакторная аутентификация указывает, что для входа в систему пользователю надо что-то, что он знает (например, пароль), и что-то, что он имеет (например, номер телефона). Двухфакторная аутентификация намного лучше просто пароля, поскольку злоумышленнику для получения доступа придется украсть и ваш пароль, и номер вашего телефона.
При планировании реализации двухфакторной аутентификации нужно обработать запрос на генерацию случайного кода, зарегистрировать его в службе входа в систему и отправить сообщение пользователю. Можно добавить код, реализующий эту функциональность, непосредственно в саму службу входа в систему. Это усложняет систему, делает ее более монолитной. Отправка сообщения должна выполняться одновременно с кодом, генерирующим веб-страницу входа в систему, что может привнести определенную задержку. Эта задержка ухудшает качество взаимодействия пользователя с системой.
Лучше будет создать FaaS-сервис, который бы асинхронно генерировал случайное число, регистрировал его в службе входа в систему и отправлял на телефон пользователя. Таким образом, сервер входа в систему может просто выполнить асинхронный запрос к FaaS-сервису, который параллельно выполнит относительно медленную задачу регистрации и отправки кода.
Для того чтобы увидеть, как это работает, рассмотрим следующий код:
def two_factor(context):
# Сгенерировать случайный шестизначный код
code = random.randint(1 00000, 9 99999)
# Зарегистрировать код в службе входа в систему
user = context.json["user"]
register_code_with_login_service(user, code)
# Для отправки сообщения воспользуемся библиотекой Twillio
account = "my-account-sid"
token = "my-token"
client = twilio.rest.Client(account, token)
user_number = context.json["phoneNumber"]
msg = "Здравствуйте, {}, ваш код аутентификации:
{}.".format(user, code)
message = client.api.account.messages.create(to=user_number,
from_="+1 20652 51212",
body=msg)
return {"status": "ok"}
Затем зарегистрируем FaaS в kubeless:
kubeless function deploy add-two-factor \
--runtime python27 \
--handler two_factor.two_factor \
--from-file two_factor.py \
--trigger-http
Экземпляр этой функции может асинхронно порождаться из клиентского кода на JavaScript после ввода пользователем правильного пароля. Веб-интерфейс может немедленно отобразить страницу для ввода кода, а пользователь, как только получит код, может сообщить его службе входа в систему, в которой этот код уже зарегистрирован.
Итак, подход FaaS существенно облегчил разработку простого, асинхронного, событийно-ориентированного сервиса, который инициируется при входе пользователя в систему.
Событийные конвейеры
Существует ряд приложений, которые, по сути, проще рассматривать как конвейер слабо связанных событий. Конвейеры событий часто напоминают старые добрые блок-схемы. Их можно представить в виде ориентированного графа синхронизации связанных событий. В рамках паттерна Event Pipeline узлы соответствуют функциям, а дуги, их соединяющие, — HTTP-запросам или другого рода сетевым вызовам.
Между элементами контейнера, как правило, нет общего состояния, но может быть общий контекст или другая точка отсчета, на основе которой будет выполняться поиск в хранилище.
Какова же разница между таким конвейером и микросервисной архитектурой? Есть два важных различия. Первое и самое главное различие между сервисами-функциями и постоянно работающими сервисами состоит в том, что событийные конвейеры, по сути, управляются событиями. Микросервисная архитектура же, напротив, подразумевает набор постоянно работающих сервисов. Кроме того, событийные конвейеры могут быть асинхронными и связывать разнообразные события. Сложно представить, как можно интегрировать одобрение заявки в системе Jira в микросервисное приложение. В то же время нетрудно представить, как оно интегрируется в событийный конвейер.
В качестве примера рассмотрим конвейер, в котором исходным событием будет загрузка кода в систему контроля версий. Это событие вызывает пересборку кода. Сборка может занять несколько минут, после чего создается событие, инициирующее функцию тестирования собранного приложения. В зависимости от успешности сборки функция тестирования предпринимает разные действия. Если сборка прошла успешно, создается заявка, которая должна быть одобрена человеком, чтобы новая версия приложения вошла в эксплуатацию. Закрытие заявки служит сигналом к вводу новой версии в эксплуатацию. Если сборка завершилась неудачно, в Jira делается заявка об обнаруженной ошибке, а конвейер завершает работу.
Практикум. Реализация конвейера для регистрации нового пользователя
Рассмотрим задачу реализации последовательности действий для регистрации нового пользователя. При создании новой учетной записи всегда выполняется целый ряд действий, например отправка приветственного электронного письма. Есть также ряд действий, которые могут выполняться не каждый раз, например подписка на e-mail-рассылку о новых версиях продукта (также известную как спам).
Один из подходов подразумевает создание монолитного сервиса создания новых учетных записей. При таком подходе одна команда разработчиков несет ответственность за весь сервис, который к тому же развертывается как единое целое. Это затрудняет проведение экспериментов и внесение изменений в процесс взаимодействия пользователя с приложением.
Рассмотрим реализацию входа пользователя в систему как событийный конвейер из нескольких FaaS-сервисов. При таком разделении функция создания пользователя понятия не имеет, что происходит во время входа пользователя в систему. У нее есть два списка:
- список необходимых действий (например, отправка приветственного электронного письма);
- список необязательных действий (например, подписка на рассылку).
Каждое из этих действий также реализуется в виде FaaS, а список действий есть не что иное, как список HTTP-функций обратного вызова. Стало быть, функция создания пользователя имеет следующий вид:
def create_user(context):
# Безусловный вызов всех необходимых обработчиков
for key, value in required.items():
call_function(value.webhook, context.json)
# Необязательные обработчики выполняются
# при соблюдении определенных условий
for key, value in optional.items():
if context.json.get(key, None) is not None:
call_function(value.webhook, context.json)
Каждый из обработчиков теперь также можно реализовать по принципу FaaS:
def email_user(context):
# Получить имя пользователя
user = context.json['username']
msg = 'Здравствуйте, {}, спасибо, что воспользовались нашим
замечательным сервисом!".format(user)
send_email(msg, contex.json['email])
def subscribe_user(context):
# Получить имя пользователя
email = context.json['email']
subscribe_user(email)
Декомпозированный таким образом FaaS-сервис становится значительно проще, содержит меньше строк кода и сосредоточен на реализации одной конкретной функции. Микросервисный подход упрощает написание кода, но может привести к сложностям при развертывании и управлении тремя разными микросервисами. Здесь подход FaaS проявляет себя во всей красе, поскольку в результате его использования становится очень просто управлять небольшими фрагментами кода. Визуализация процесса создания пользователя в виде событийного конвейера позволяет также в общих чертах понять, что именно происходит во время входа пользователя в систему, просто проследив изменение контекста от функции к функции в рамках конвейера.
» Более подробно с книгой можно ознакомиться на сайте издательства
» Оглаление
» Отрывок
Для Хаброжителей скидка 20% по купону — Design patterns
По факту оплаты бумажной версии книги на e-mail высылается электронная версия книги.