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

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

Проблема:

Есть один инстанс приложения, использующего веб-сокеты, например, мессенджера. Хотим добиться горизонтального масштабирования, добавив несколько инстансов приложения + балансировщик нагрузки.

Решение:

Использовать Sticky session load balancing, который заключается в том, что запросы от одного клиента будут приходить на один и тот же сервер. В контексте веб-сокетов это особенно важно, поскольку часто требуется, чтобы запросы от клиента приходили на тот же бэкенд, с которым изначально было установлено соединение. Например, это полезно, если у нас есть локальный кеш, в котором хранятся клиентские данные, и мы хотим чтобы после разрыва соединения, следующий запрос на установку соединения пришел на тот же бэкенд.

Принцип работы следующий: балансировщик нагрузки смотрит на какие-то параметры запроса, которые будут постоянны для одного клиента/одной сессии и по ним определяет, на какой сервер переадресовать запрос. Например, можно использовать хеш ip адреса клиента.
👍29🔥5
Паттерны обработки ошибок в Apache Kafka

Проблема:

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

Решение:

Ответ зависит от специфики приложения: проигнорировать это сообщение, остановить чтение, перебросить в error-топик и тд. В статье описываются популярные паттерны обработки ошибок в Apache Kafka вместе с вариантами использования.
🔥15👍4
Виды индексов в БД

Проблема:

Когда таблица увеличивается, длительность seq scan (последовательного чтения строк) растет линейно по отношению к размеру таблицы. Когда размер доходит до нескольких миллионов, простые запросы, например, поиск по идентификатору, начинают работать несколько секунд.

Решение:

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

1. B-Tree

B-Tree - наиболее распространенный и универсальный тип индекса. Представляет собой сбалансированное дерево поиска, которое позволяет быстро производить поиск по равенству, ренжу, префиксу строки.

2. Hash

Hash-индексы работают на основе хеш-таблицы. Позволяют производить быстрый поиск по равенству.

3. GIN

GIN (Generalized Inverted Index) представляет собой инвертированный индекс, который строится по столбцу с типом ts_vector для проведения эффективного полнотекстового поиска. Также (в Postgres) для GIN существуют operator classes, как, например, gin_trgm_ops, который позволяет эффективно искать по текстовому столбцу по запросам … where column like ‘%query%’ с помощью разбиения текста на триграммы.
🔥24👍9
Индексирование больших таблиц

Проблема:

Есть большая таблица, хотим накатить на нее индекс. Обычный create index блокирует таблицу на время создания индекса. В случае таблиц с сотнями миллионами строк длительность создания индекса может достигать часа. В это время все пишущие запросы будут падать с lock timeout из-за невозможности взятия блокировки => сервис начнет пятисотить => фактически получаем даунтайм.

Решение:

Использовать конструкцию create index concurrently. Она позволяет создать индекс, не блокируя пишущие запросы, однако создание индекса в таком режиме занимает существенно больше времени.

Пример: хотим накатить следующий индекс

create index idx__big_table__column
on big_table(column)


Для этого перед деплоем приложения руками делаем

create index concurrently idx__big_table__column
on big_table(column)


И в файле миграции пишем

create index if not exist idx__big_table__column
on big_table(column)


Смысл накатывания индекса руками в том, что инструменты для миграции схемы БД обычно применяют миграции синхронно, что при деплое приложения заставило бы ждать существенное время.
👍57🔥16
Оптимистические блокировки в СУБД

Проблема:

В БД есть обновляемая сущность. Период между чтением сущности и записью может быть большим. При этом хотим, чтобы если пользователь отправляет запрос на обновление, он не перезаписывал изменения другого пользователя. То есть хотим избежать ситуаций

user1: read
user2: read
user1: write
user2: write (перезаписали обновления user1)


Решение:

Использовать оптимистические блокировки. Они заключаются в том, что перед обновлением мы проверяем, что на руках имеем актуальную версию сущности. Реализовать это можно так:

1. Добавляем в сущность БД колонку revision с дефолтом 0
2. Навешиваем триггер, чтобы при каждом апдейте колонка инкрементилась
3. При обновлении проверяем версию

Запрос на обновление будет выглядеть так:

update entity
set …
where id = <entity_id>
and revision = <revision>
returning *;


Далее если returning * ничего не вернул, значит мы имеем неактуальное состояние. Если вернул, то мы имели актуальное состояние и успешно сделали апдейт.
👍32🔥9🙏4
Оптимизация производительности приложений

Качественный обзорный доклад про роль, общие принципы профилирования при разработке ПО, а также про отличия оптимизаций в “зеленой”, “желтой” и “красной” зонах с множеством примеров.
🔥10👍4
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