Microservices Thoughts – Telegram
Microservices Thoughts
7.75K subscribers
32 photos
55 links
Вопросы и авторские статьи по микросервисам, архитектуре, БД

Сотрудничество: t.me/qsqnk
Download Telegram
Deadline propagation

Проблема:

Есть бизнес-операция, задействующая несколько сервисов, с некоторым SLA времени ответа. В случае отказа одного из сервисов, мы не хотим заставлять пользователя ждать и нагружать остальные сервисы, а просто выводим понятное сообщение об ошибке.

Решение:

Использовать технику deadline propagation. Она заключается в том, что на стороне клиента генерируется дедлайн, до которого нужно исполнить операцию, и отправляется вместе с запросом серверу. В случае межсервисных взаимодействий дедлайн пересылается от сервиса к сервису, что позволяет избежать ситуаций, когда изначальный запрос уже отменился, но у нас остались подвешенные межсервисные запросы.
👍31🔥5
Consistent hashing

Проблема:

Есть шардированное хранилище. При записи объекта по ключу определяем, в какое хранилище писать с помощью hash(key) % n, где n - число серверов. Вдруг место заканчивается и нужно добавить новый шард, для этого мы должны перехешировать все ключи и перераспределить данные между шардами, что долго и дорого.

Решение:

Использовать консистентное хеширование. Это метод заключается в следующем: представим что наши сервера и ключи располагаются на окружности:

1. Назначаем каждому шарду псевдорандомное число-угол из [0, 360), которое определяет место на окружности

2. При записи объекта по ключу определяем его угол как hash(key) % 360

3. От полученного значения идем по окружности против часовой стрелки и находим ближайший шард, куда и записываем значение

Всё это нужно для того, чтобы при добавлении нового шарда B, который попал между A и C, нам достаточно было перехешировать объекты лишь находящиеся между A и B, а не все. Иначе говоря, добавление или удаление нового шарда требует перехеширований в среднем n/m объектов вместо n, где n - общее число объектов, m - число шардов.
👍47🔥16🤔4
CQRS + Event Sourcing

CQRS - паттерн, призванный разделить commands (модифицирующие запросы) и queries (запросы на чтение). Обычно в таком случае для записи и чтения используются разные БД, более подходящие под конкретные нужды.

Event Sourcing - паттерн, при котором состояние сущности представляет собой последовательность некоторых событий, например:

1. entity created
2. name changed
3. size changed


В докладе на реальном примере рассказывается, почему CQRS и Event Sourcing хорошо сочетаются, а также преимущества и недостатки подобной архитектуры.
🔥20👍8
Distributed tracing

Проблема:

Начинает медленно работать операция, охватывающая несколько сервисов. Хочется локализовать проблему и найти конкретное место, которое тормозит.

Решение:

Использовать distributed tracing, основными сущностями которого являются:

Trace - полный путь запроса через систему, который включает в себя операции, которые прошел запрос от начала до конца.

Span - непрерывный сегмент работы, выполняемый в рамках трейса. Span может быть, например, вызовом API или исполнением запроса к базе данных. Каждый span имеет имя, начальное время, продолжительность и дополнительные метаданные. Они выстраиваются в иерархию parent-chlid, например, если один сервис для выполнения запроса должен вызвать другой (на графике сервис A вызывает B, который вызывает C и т.д).

Таким образом, по спенам можно легко понять, что является узким местом, и исправить это.
👍25🔥6🤔1💅1
Bloom Filter

Проблема:

Есть высоконагруженная база данных. В случае гарантированного отсутствия запрашиваемого элемента в БД хотим не делать бессмысленное (и дорогое) IO с диском, а просто отдать ответ, что элемента нет.

Решение:

Использовать фильтр Блума - структуру данных, которая по входной строке умеет давать два ответа:
- Элемента точно нет в множетсве
- Элемент возможно содержится в множетсве

Принцип работы следующий:

Заводится битовый массив длины N. Выбираются несколько фиксированных хеш-функций, отдающих значения из [0, N-1]. В примере на картинке их 3.

Добавление элемента в множество: для каждой хеш функции, считаем хеш по входной строке и выставляем этот бит в массиве в единичку.

Проверка наличия: если хотя бы для одной хеш-функции ее результат по входной строке в битовом массиве выставлен в 0, то такой строки гарантированно нет (в противном случае при добавлении этой строки, там бы была единичка). Однако, если все единички, то что-то утверждать мы не можем, поскольку единичка могла “прийти” от другой строки.
👍35🔥12
Почему батчевые update/delete не безопасны

Конкурентно исполняющиеся стейтменты вида

update entity set … where id in (1, 2)


могут приводить к дедлокам.

Почему так происходит? Чтобы произвести обновление или удаление, обычно производится следующий набор операций:

1. Выборка записей, подходящих под условие where
2. Блокировка записей
3. Повторная проверка условия + исполнение самого update/delete

И поскольку порядок выборки записей никак не регламентирован, на втором шаге может возникнуть дедлок:

tx1: lock(1) _____ lock(2) _____
tx2: _____ lock(2) ______ lock(1)


Наиболее простым решением будет явное взятие блокировок в нужном порядке перед апдейтом:

begin;
select * from entity where id in (1, 2) order by id for update;
update entity set … where id in (1, 2);
commit;
👍50🔥15🤯1
Sharing data between services

Проблема:

Чтобы сервису X выполнить запрос, ему нужно получить данные из сервиса Y, который нестабильно/долго отвечает.

Решение:

Сделать в сервисе X локальную копию требуемых данных, и обновлять их по событиям из сервиса Y.

Плюсы:
- уменьшается связность между сервисами, поскольку в случае отказа сервиса Y, сервис X продолжит работать

Минусы:
- сервис Y обязан публиковать события о своих обновлениях
- “лаг репликации” - в локальном view какое-то время могут быть неактуальные данные
👍34🔥5
Conflict-free replicated data type

Проблема:

Есть некоторые данные, которые реплицируются на несколько узлов. Например, в рамках коллаборативного инструмента, в котором изменения от одного участника должны увидеть остальные. При этом, если два пользователя модифицируют одну и ту же часть данных, будут возникать конфликты, и их нужно решать.

Решение:

CRDT - структура данных, реплицированная на несколько узлов, обладающая следующими свойствами:

- приложение может обновлять реплику без координации с другими репликами
- в случае конфликтов структура данных гарантированно умеет их разрешать
- в конечном счете все реплики будут согласованы

Разделяют operation-based и state-based типы: в одном случае рассылаются лишь обновления структуры данных, в другом - цельное состояние структуры.

Рассмотрим operation-based.

Принцип работы следующий:

- один из узлов хочет сделать обновление
- генирируется функция update, которая принимает на вход некоторое состояние структуры данных, и возвращает новое состояние
- функция применяется локально - на текущую реплику
- функция отсылается на остальные реплики, которые тоже применяют эту операцию

Ограничения на функцию update:

- либо гарантировать порядок доставки update-ов, либо гарантировать, что update_1(update_2(state)) = update_2(update_1(state)), иначе говоря, функции update_1 и update_2 должны коммутировать
- если update может быть доставлен несколько раз, то он должен быть идемпотентным, то есть update(state) = update(update(state))

Собственно, именно подобные ограничения и вызывают сложности. По многим типовым структурам данных написаны пейперы, про json, например, это.
👍13🔥5
Saga Orchestration

Saga - паттерн, позволяющий проводить “eventually-атомарные” распределенные транзакции. То есть, в конце концов части транзакции либо выполнятся на всех сервисах, либо нигде.

Saga Choreography - разновидность, где нет централизованного компонента управления, и сервисы координируют работу друг друга с помощью событий.

Saga Orchestration - наоборот, координирует выполнение транзакции с помощью одного централизованного компонента. Обычно это реализуется так:

- В окестратор прилетает запрос на исполнение транзакции

- В базе оркестратора создается стейт-машина, описывающая какие шаги были выполнены

- После выполнения очередного шага, записываем результат в базу

- Если какой-то шаг упал, запускаем компенсационную цепочку, чтобы отменить уже выполненную часть транзакции

- Если отмена невозможна, загорается мониторинг, и человек вручную разбирается

Из плюсов:

- Простота такого подхода сильно выше нежели у хореографии

- Хорошо подходит для сложных сценариев

- Сервисы могут совсем не знать друг про друга

Минусы:

- Оркестратор является единой точкой отказа
🔥28👍7
Reverse Proxy

Тип прокси-сервера, который стоит перед группой серверов, и обеспечивает, что все запросы, адресованные этим серверам, проходят через него.

Возможные применения:

- Балансировка нагрузки: прокси будет равномерно распределять нагрузку на стоящие за ним серверы

- Rate-limiting: например, ограничения общего числа запросов в секунду (1k RPS) и ограничение числа запросов от одного ip адреса (20 RPS)

- Кеширование контента: например, чтобы напрямую клиенту отдавать какой-то статический контент, не совершая запрос к серверу

- Производить TLS шифрование/дешифрование, снимая нагрузку с целевых серверов

Без reverse-proxy пришлось бы дублировать всю эту логику на каждом сервере, а также раскрывать внутреннюю структуру сети, чтобы клиенты могли делать запросы напрямую целевым серверам.
👍30🔥4💅3
Asynchronous Request-Reply pattern

Проблема:

На бэкенде есть асинхронное апи запуска какой-то долгой операции, а клиент хочет получить результат этой операции. Делать апи синхронным - не вариант с точки зрения архитектуры.

Решение:

Использовать поллинг на клиенте:

- Клиент идет в апи запуска операции

- Бэкенд отдает operationId

- Клиент раз в какое-то время идет в эндпоинт проверки статуса операции по operationId

- Когда операция завершится, бэкенд отдает uri, по которому можно найти результат операции (либо клиент сам знает, куда сходить)

Это один из самых простых вариантов решения проблемы на чистом http в случае если, например, в проект не хочется затаскивать вебсокеты.
👍33🔥3💅3
Поддерживаете ли вы порядок отправки сообщений из outbox таблицы?

Если да, то будет классно, если в комментах напишите, как у вас это реализовано
Anonymous Poll
40%
Да
60%
Нет
🔥6🤔4💅3
Soft-delete pattern

Паттерн удаления, при котором происходит не удаление записи из БД, а проставление метки deleted_at = now() (либо status = ‘INACTIVE’):

delete from entity
where id = 1;

Заменяется на

update entity
set deleted_at = now()
where id = 1;


Обычно этот паттерн позиционируется как способ “более безопасного” удаления, тк мы можем легко восстановить записи, но зачастую на практике такого применения он не находит.

Более реальные юзкейсы:

- Отмена операции удаления: например, в UI после совершения удаления в течение 5 секунд можно отменить операцию. В случае c soft delete - это будет просто изменением статуса

- Историчность какой либо сущности: вместо update делаем soft delete старой версии и создание новой

Недостатки:

- Большинство запросов обычно оперирует с активными сущностями, поэтому придется во всех них добавлять условие на deleted_at is null

- Сложнее поддерживать согласованность данных. В случае дефолтного удаления мы можем ее гарантировать с помощью внешних ключей. В случае мягкого удаления у нас могут возникать ситуации, когда ACTIVE сущность ссылается на INACTIVE. Это решается либо более сложными внешними ключами, включающими статус, либо гарантиями со стороны приложения
👍35🔥4💅3🤔1
Keyset pagination

Проблема:

Пагинация через order by + limit + offset

select *
from events
order by id desc
limit 10 offset 1000;


Начинает работать медленно при большом оффсете, поскольку мы будем по-честному проходить по этой тысяче записей, прежде чем выбрать 10, которые нам нужны.

Решение:

Использовать Keyset pagination, смысл которой заключается в том, что offset заменяется на условие

id < <последний id предыдущей страницы>

Рассмотрим на примере:

Первая страница:
select *
from events
order by id desc
limit 10
Допустим, последний id был 253, тогда вторая страница:
select *
from events
where id < 253
order by id desc
limit 10
Третья страница:
select *
from events
where id < 243
order by id desc
limit 10

И так далее.

Запрос, сформированный таким образом, позволяет базе данных с помощью индекса на id сразу найти первую запись, где id < <последний id предыдущей страницы> и далее просто пройтись по индексу, взяв первые 10 записей.
🔥40👍11🤯8🤔1💅1
Structured logging

Проблема:

По логам в виде plain текста тяжело осуществлять поиск, фильтрацию.

Решение:

Использовать идею Structured logging:

Вместо того, чтобы записывать лог в виде plain текста

[info] [Friday, 20-Jan-23 11:17:55 UTC] The application has started.


Будем записывать его в виде структуры определенного формата

{
"timestamp": "Friday, 20-Jan-23 11:17:55 UTC",
"level": "info",
"message": "The application has started."
}


Это позволит складывать логи в какую-нибудь БД, например, Elasticsearch, индексировать и эффективно производить поиск и фильтрацию.
🔥26💅6👍4🤯3🤔1
Durable executions

Представим, что есть некоторая задача

workflow {
firstCall()
sleep(1 hour)
secondCall()
sleep(1 hour)
thirdCall()
}


Если мы ее запустим, то нужно, чтобы воркер, который ее будет исполнять, был жив минимум два часа подряд. Если по-середине ожидания что-то пойдет не так, то весь прогресс потеряется.

Именно на этом примере можно описать концепцию Durable executions:

После того, как мы сделали firstCall(), мы не будем ждать, а сделаем следующее:

- Сохраним в состояние задачи, что мы уже сделали firstCall()
- Зашедулим ее на +1 час

Спустя час какой-то другой воркер сможет взять эту задачу и продолжить с прыдыдущего “чекпоинта”. То есть мы сохраняем прогресс по определенным частям задачи, что позволяет переживать отказы воркеров и позволяет исполнять задачу по частям разными воркерами.

И сейчас существует довольно много Workflow engines, которые строятся на этой концепции.
👍28🔥8💅6
Temporal

Temporal - оркестрационный движок, который координирует работу распределенных воркеров, сохраняет промежуточные результаты, делает ретраи и т.д.

Temporal cluster состоит из следующих компонентов:

- Frontend gateway: авторизация, rate limit

- History subsystem: хранит состояние задач в БД

- Matching subsystem: управляет очередьми задач

- Worker Service: исполняет внутренние (не пользовательские) фоновые процессы

❗️Пользовательские workflow исполняются не temporal кластером, а внешними пользовательскими воркерами. Temporal лишь координирует их работу.

Возможности:

- Обработка распределенных транзакций
- Запуск задач по крону
- Исполнение стейт машин
- И еще некоторые вещи

Как описываются задачи:

Activity - некоторое атомарное действие, например, вызов API или запрос в базу. Это действие должно быть идемпотентным, чтобы, например, в случае ретраев из-за сетевых проблем не сделать некоторое действие дважды.

Workflow состоят из вызовов activity и некоторой дополнительной логики. И как раз в местах вызова activity сохраняются промежуточные результаты workflow.

Всё это описывается с помощью SDK на стороне приложения, на текущий момент существуют реализации для Go, Java, PHP, Python, Typenoscript, .NET. Посмотреть пример описания workflow можно тут.
👍14🔥8💅5
Caching patterns

При работе с кешом возникает вопрос “в какой момент нужно синхронизировать данные из бд и кеша?”. Рассмотрим три часто встречающихся паттерна:

1. Read-aside caching

Наиболее простой и часто используемый паттерн.

Как происходит чтение:
- Пробуем достать данные из кеша
- Если не получилось, идем в БД, складываем в кеш

Как происходит запись:
- Просто пишем в БД

2. Write-aside caching

Паттерн с чтением аналогичным предыдущему варианту, но при записи сразу обновляем и кеш, что позволяет в кеше иметь актуальные данные и в теории увеличить hit rate ценой того, что в кеш могут попадать данные, которые не нужны для чтения.

Как происходит чтение:
- Пробуем достать данные из кеша
- Если не получилось, идем в БД, складываем в кеш

Как происходит запись:
- Пишем в БД
- Пишем в кеш

3. Full caching

Кеш (обычно по крону) сам себя обновляет, подгружая все необходимые данные из БД. Подходит для случаев, когда хотим добиться ~100% hit rate, и данные целиком влезают в кеш.

Как происходит чтение:
- Просто читаем из кеша

Как происходит запись:
- Просто пишем в БД
👍35🔥7💅32
Пара подходов к описанию деревьев в реляционной БД

Зачастую требуется хранить какие-то иерархичные данные, например, подобие файловой системы или какую-то организационную структуру.

1. id + parent_id

create table folders
(
id bigserial not null,
parent_id bigint null references folders (id),
data jsonb not null
);


Самый простой подход, при котором храним идентификатор родительской сущности (либо null, если сущность и так корневая).

Плюсы:
- Простота модели
- Простая вставка
- Простой перенос поддерева

Минусы:
- “Дерево” может стать не деревом - МД не запрещает циклические ссылки
- Рекурсивные запросы, чтобы доставать поддерево по id корня

2. id, path + parent_id, parent_path

Помимо id добавляется path - путь по айдишникам до текущей сущности. Это нам позволяет сделать более интересные констрейнты и гарантировать, что мы имеем дело реально с деревом.

create table folders
(
id bigserial not null,
path varchar not null,
parent_id bigint null,
parent_path varchar null,
data jsonb not null

constraint ck__folders__path
check (path = coalesce(parent_path, '/') || id || '/'),

-- нужен для FK
constraint uc__folders__path__id
unique (path, id),

constraint fk__folders__parent_id__parent_path
foreign key (parent_path, parent_id) references folders (path, id)
match full
);


Здесь мы гарантируем, что
1) path корневой папки - это /<id>/
2) path некорневой папки - это <parent_path><id>/

Что позволяет обеспечить отсутствие циклов

Плюсы:
- Гарантия отсутствия циклов
- Простой запрос поддерева - where path like ‘/1/2/3/%’

Минусы:
- Сложность модели
- Более сложная вставка
- Сложный перенос поддерева
👍34🔥33🤔21