⚡Saga Orchestration
Saga - паттерн, позволяющий проводить “eventually-атомарные” распределенные транзакции. То есть, в конце концов части транзакции либо выполнятся на всех сервисах, либо нигде.
Saga Choreography - разновидность, где нет централизованного компонента управления, и сервисы координируют работу друг друга с помощью событий.
Saga Orchestration - наоборот, координирует выполнение транзакции с помощью одного централизованного компонента. Обычно это реализуется так:
- В окестратор прилетает запрос на исполнение транзакции
- В базе оркестратора создается стейт-машина, описывающая какие шаги были выполнены
- После выполнения очередного шага, записываем результат в базу
- Если какой-то шаг упал, запускаем компенсационную цепочку, чтобы отменить уже выполненную часть транзакции
- Если отмена невозможна, загорается мониторинг, и человек вручную разбирается
Из плюсов:
- Простота такого подхода сильно выше нежели у хореографии
- Хорошо подходит для сложных сценариев
- Сервисы могут совсем не знать друг про друга
Минусы:
- Оркестратор является единой точкой отказа
Saga - паттерн, позволяющий проводить “eventually-атомарные” распределенные транзакции. То есть, в конце концов части транзакции либо выполнятся на всех сервисах, либо нигде.
Saga Choreography - разновидность, где нет централизованного компонента управления, и сервисы координируют работу друг друга с помощью событий.
Saga Orchestration - наоборот, координирует выполнение транзакции с помощью одного централизованного компонента. Обычно это реализуется так:
- В окестратор прилетает запрос на исполнение транзакции
- В базе оркестратора создается стейт-машина, описывающая какие шаги были выполнены
- После выполнения очередного шага, записываем результат в базу
- Если какой-то шаг упал, запускаем компенсационную цепочку, чтобы отменить уже выполненную часть транзакции
- Если отмена невозможна, загорается мониторинг, и человек вручную разбирается
Из плюсов:
- Простота такого подхода сильно выше нежели у хореографии
- Хорошо подходит для сложных сценариев
- Сервисы могут совсем не знать друг про друга
Минусы:
- Оркестратор является единой точкой отказа
🔥28👍7
⚡Reverse Proxy
Тип прокси-сервера, который стоит перед группой серверов, и обеспечивает, что все запросы, адресованные этим серверам, проходят через него.
Возможные применения:
- Балансировка нагрузки: прокси будет равномерно распределять нагрузку на стоящие за ним серверы
- Rate-limiting: например, ограничения общего числа запросов в секунду (1k RPS) и ограничение числа запросов от одного ip адреса (20 RPS)
- Кеширование контента: например, чтобы напрямую клиенту отдавать какой-то статический контент, не совершая запрос к серверу
- Производить TLS шифрование/дешифрование, снимая нагрузку с целевых серверов
Без reverse-proxy пришлось бы дублировать всю эту логику на каждом сервере, а также раскрывать внутреннюю структуру сети, чтобы клиенты могли делать запросы напрямую целевым серверам.
Тип прокси-сервера, который стоит перед группой серверов, и обеспечивает, что все запросы, адресованные этим серверам, проходят через него.
Возможные применения:
- Балансировка нагрузки: прокси будет равномерно распределять нагрузку на стоящие за ним серверы
- Rate-limiting: например, ограничения общего числа запросов в секунду (1k RPS) и ограничение числа запросов от одного ip адреса (20 RPS)
- Кеширование контента: например, чтобы напрямую клиенту отдавать какой-то статический контент, не совершая запрос к серверу
- Производить TLS шифрование/дешифрование, снимая нагрузку с целевых серверов
Без reverse-proxy пришлось бы дублировать всю эту логику на каждом сервере, а также раскрывать внутреннюю структуру сети, чтобы клиенты могли делать запросы напрямую целевым серверам.
👍30🔥4💅3
⚡Asynchronous Request-Reply pattern
❓Проблема:
На бэкенде есть асинхронное апи запуска какой-то долгой операции, а клиент хочет получить результат этой операции. Делать апи синхронным - не вариант с точки зрения архитектуры.
✅ Решение:
Использовать поллинг на клиенте:
- Клиент идет в апи запуска операции
- Бэкенд отдает operationId
- Клиент раз в какое-то время идет в эндпоинт проверки статуса операции по operationId
- Когда операция завершится, бэкенд отдает uri, по которому можно найти результат операции (либо клиент сам знает, куда сходить)
Это один из самых простых вариантов решения проблемы на чистом http в случае если, например, в проект не хочется затаскивать вебсокеты.
❓Проблема:
На бэкенде есть асинхронное апи запуска какой-то долгой операции, а клиент хочет получить результат этой операции. Делать апи синхронным - не вариант с точки зрения архитектуры.
✅ Решение:
Использовать поллинг на клиенте:
- Клиент идет в апи запуска операции
- Бэкенд отдает operationId
- Клиент раз в какое-то время идет в эндпоинт проверки статуса операции по operationId
- Когда операция завершится, бэкенд отдает uri, по которому можно найти результат операции (либо клиент сам знает, куда сходить)
Это один из самых простых вариантов решения проблемы на чистом http в случае если, например, в проект не хочется затаскивать вебсокеты.
👍33🔥3💅3
Поддерживаете ли вы порядок отправки сообщений из outbox таблицы?
Если да, то будет классно, если в комментах напишите, как у вас это реализовано
Если да, то будет классно, если в комментах напишите, как у вас это реализовано
Anonymous Poll
40%
Да
60%
Нет
🔥6🤔4💅3
⚡Soft-delete pattern
Паттерн удаления, при котором происходит не удаление записи из БД, а проставление метки
Заменяется на
Обычно этот паттерн позиционируется как способ “более безопасного” удаления, тк мы можем легко восстановить записи, но зачастую на практике такого применения он не находит.
Более реальные юзкейсы:
- Отмена операции удаления: например, в UI после совершения удаления в течение 5 секунд можно отменить операцию. В случае c soft delete - это будет просто изменением статуса
- Историчность какой либо сущности: вместо update делаем soft delete старой версии и создание новой
Недостатки:
- Большинство запросов обычно оперирует с активными сущностями, поэтому придется во всех них добавлять условие на
- Сложнее поддерживать согласованность данных. В случае дефолтного удаления мы можем ее гарантировать с помощью внешних ключей. В случае мягкого удаления у нас могут возникать ситуации, когда
Паттерн удаления, при котором происходит не удаление записи из БД, а проставление метки
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
❓Проблема:
Пагинация через
Начинает работать медленно при большом оффсете, поскольку мы будем по-честному проходить по этой тысяче записей, прежде чем выбрать 10, которые нам нужны.
✅ Решение:
Использовать Keyset pagination, смысл которой заключается в том, что
Рассмотрим на примере:
Первая страница:
И так далее.
Запрос, сформированный таким образом, позволяет базе данных с помощью индекса на id сразу найти первую запись, где
❓Проблема:
Пагинация через
order by + limit + offsetselect *
from events
order by id desc
limit 10 offset 1000;
Начинает работать медленно при большом оффсете, поскольку мы будем по-честному проходить по этой тысяче записей, прежде чем выбрать 10, которые нам нужны.
✅ Решение:
Использовать Keyset pagination, смысл которой заключается в том, что
offset заменяется на условие id < <последний id предыдущей страницы>Рассмотрим на примере:
Первая страница:
select *Допустим, последний id был 253, тогда вторая страница:
from events
order by id desc
limit 10
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 текста
Будем записывать его в виде структуры определенного формата
Это позволит складывать логи в какую-нибудь БД, например, Elasticsearch, индексировать и эффективно производить поиск и фильтрацию.
❓Проблема:
По логам в виде 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
Представим, что есть некоторая задача
Если мы ее запустим, то нужно, чтобы воркер, который ее будет исполнять, был жив минимум два часа подряд. Если по-середине ожидания что-то пойдет не так, то весь прогресс потеряется.
Именно на этом примере можно описать концепцию Durable executions:
После того, как мы сделали
- Сохраним в состояние задачи, что мы уже сделали
- Зашедулим ее на +1 час
Спустя час какой-то другой воркер сможет взять эту задачу и продолжить с прыдыдущего “чекпоинта”. То есть мы сохраняем прогресс по определенным частям задачи, что позволяет переживать отказы воркеров и позволяет исполнять задачу по частям разными воркерами.
И сейчас существует довольно много Workflow engines, которые строятся на этой концепции.
Представим, что есть некоторая задача
workflow {
firstCall()
sleep(1 hour)
secondCall()
sleep(1 hour)
thirdCall()
}Если мы ее запустим, то нужно, чтобы воркер, который ее будет исполнять, был жив минимум два часа подряд. Если по-середине ожидания что-то пойдет не так, то весь прогресс потеряется.
Именно на этом примере можно описать концепцию Durable executions:
После того, как мы сделали
firstCall(), мы не будем ждать, а сделаем следующее:- Сохраним в состояние задачи, что мы уже сделали
firstCall()- Зашедулим ее на +1 час
Спустя час какой-то другой воркер сможет взять эту задачу и продолжить с прыдыдущего “чекпоинта”. То есть мы сохраняем прогресс по определенным частям задачи, что позволяет переживать отказы воркеров и позволяет исполнять задачу по частям разными воркерами.
И сейчас существует довольно много Workflow engines, которые строятся на этой концепции.
GitHub
GitHub - meirwah/awesome-workflow-engines: A curated list of awesome open source workflow engines
A curated list of awesome open source workflow engines - meirwah/awesome-workflow-engines
👍28🔥8💅6
Ставьте 💅 на этот пост, если нужен рассказ про возможности Temporal
temporal.io
Durable Execution Solutions
Build invincible apps with Temporal's open source durable execution platform. Eliminate complexity and ship features faster. Talk to an expert today!
💅146✍1🤔1
⚡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 можно тут.
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, и данные целиком влезают в кеш.
Как происходит чтение:
- Просто читаем из кеша
Как происходит запись:
- Просто пишем в БД
При работе с кешом возникает вопрос “в какой момент нужно синхронизировать данные из бд и кеша?”. Рассмотрим три часто встречающихся паттерна:
1. Read-aside caching
Наиболее простой и часто используемый паттерн.
Как происходит чтение:
- Пробуем достать данные из кеша
- Если не получилось, идем в БД, складываем в кеш
Как происходит запись:
- Просто пишем в БД
2. Write-aside caching
Паттерн с чтением аналогичным предыдущему варианту, но при записи сразу обновляем и кеш, что позволяет в кеше иметь актуальные данные и в теории увеличить hit rate ценой того, что в кеш могут попадать данные, которые не нужны для чтения.
Как происходит чтение:
- Пробуем достать данные из кеша
- Если не получилось, идем в БД, складываем в кеш
Как происходит запись:
- Пишем в БД
- Пишем в кеш
3. Full caching
Кеш (обычно по крону) сам себя обновляет, подгружая все необходимые данные из БД. Подходит для случаев, когда хотим добиться ~100% hit rate, и данные целиком влезают в кеш.
Как происходит чтение:
- Просто читаем из кеша
Как происходит запись:
- Просто пишем в БД
👍35🔥7💅3 2
⚡Пара подходов к описанию деревьев в реляционной БД
Зачастую требуется хранить какие-то иерархичные данные, например, подобие файловой системы или какую-то организационную структуру.
1. id + parent_id
Самый простой подход, при котором храним идентификатор родительской сущности (либо null, если сущность и так корневая).
Плюсы:
- Простота модели
- Простая вставка
- Простой перенос поддерева
Минусы:
- “Дерево” может стать не деревом - МД не запрещает циклические ссылки
- Рекурсивные запросы, чтобы доставать поддерево по id корня
2. id, path + parent_id, parent_path
Помимо id добавляется path - путь по айдишникам до текущей сущности. Это нам позволяет сделать более интересные констрейнты и гарантировать, что мы имеем дело реально с деревом.
Здесь мы гарантируем, что
1) path корневой папки - это /<id>/
2) path некорневой папки - это <parent_path><id>/
Что позволяет обеспечить отсутствие циклов
Плюсы:
- Гарантия отсутствия циклов
- Простой запрос поддерева - where path like ‘/1/2/3/%’
Минусы:
- Сложность модели
- Более сложная вставка
- Сложный перенос поддерева
Зачастую требуется хранить какие-то иерархичные данные, например, подобие файловой системы или какую-то организационную структуру.
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🔥3 3🤔2 1
⚡Token bucket rate limiting
Один из алгоритмов для ограничения числа входящих запросов. На данный момент используется во многих системах, например, AWS.
Принцип работы:
Есть ограниченный набор токенов, наличие токена = право исполнить запрос.
Когда приходит входящий запрос, сначала проверяем, есть ли свободный токен, если есть - забираем его и исполняем запрос, если нет - отдаем 429.
И поскольку токены назад не возвращаются, их надо как-то восстанавливать. Этим занимается фоновый процесс, который по крону (обычно раз в секунду) восстанавливает число свободных токенов до максимума.
Один из алгоритмов для ограничения числа входящих запросов. На данный момент используется во многих системах, например, AWS.
Принцип работы:
Есть ограниченный набор токенов, наличие токена = право исполнить запрос.
Когда приходит входящий запрос, сначала проверяем, есть ли свободный токен, если есть - забираем его и исполняем запрос, если нет - отдаем 429.
И поскольку токены назад не возвращаются, их надо как-то восстанавливать. Этим занимается фоновый процесс, который по крону (обычно раз в секунду) восстанавливает число свободных токенов до максимума.
👍33🔥7 2💅1
🔥5 2 2✍1
⚡Continuous profiling
Continuous profiling - техника постоянного сбора данных о ресурсах, затрачиваемых приложением
Обычно профилирование применяется как ситуативная мера, чтобы здесь и сейчас подебагать перфоманс приложения. Но это не позволяет анализировать и разбирать инциденты, которые случились в прошлом
Постоянно профилирование позволяет строить графики и анализировать ретроспективные данные по затратам CPU, RAM и т.п. И одна из наиболее полезных вещей - построение flame graphs, которые позволяют посмотреть, на что именно приложение тратило память или процессор за определенный промежуток времени (например, во время какого-то инцидента)
Из популярных тулов для этих целей существует Grafana Pyroscope, который умеет запускаться как из приложения, так и отдельным агентом. Далее он собирает метрики и отливает их в графану, которая уже строит красивые графики
Continuous profiling - техника постоянного сбора данных о ресурсах, затрачиваемых приложением
Обычно профилирование применяется как ситуативная мера, чтобы здесь и сейчас подебагать перфоманс приложения. Но это не позволяет анализировать и разбирать инциденты, которые случились в прошлом
Постоянно профилирование позволяет строить графики и анализировать ретроспективные данные по затратам CPU, RAM и т.п. И одна из наиболее полезных вещей - построение flame graphs, которые позволяют посмотреть, на что именно приложение тратило память или процессор за определенный промежуток времени (например, во время какого-то инцидента)
Из популярных тулов для этих целей существует Grafana Pyroscope, который умеет запускаться как из приложения, так и отдельным агентом. Далее он собирает метрики и отливает их в графану, которая уже строит красивые графики
🔥17💅5👍4✍1 1 1
⚡Каскадное удаление за один запрос
Нет, речь не про on delete cascade
Во многих случаях мы не хотим использовать внешние ключи с каскадным удалением, как минимум потому что они существенно повышают цену ошибки - когда случайно удаляется одна сущность и каскадно пол базы
Но как тогда быть, если у нас достаточно глубокая иерархия сущностей, например
a <- b <- c
и хочется по a_id удалить все связанные b и c?
Наивное решение:
- поселектить из b по a_id
- удалить записи из c по полученным b_id
- удалить записи из b
- удалить записи из a
В итоге имеем 4 раудтрипа до базы и сложную логику, которая с ростом иерархии будет выглядить еще более громоздкой
Решение через CTE:
Почему это работает? CTE трактуется как единый стейтмент, соответственно все проверки констрейнтов будут осуществляться только после его полного выполнения
Это нам позволяет удалить все связанные с таблицей a сущности за один раундтрип до базы, и при необходимости легко масштабировать этот подход, просто добавляя новые строки в CTE, если иерархия вырастет
Ставьте 🔥 на этот пост, если нужен рассказ про еще один интересный вариант применения CTE в контексте внешних ключей
Нет, речь не про on delete cascade
Во многих случаях мы не хотим использовать внешние ключи с каскадным удалением, как минимум потому что они существенно повышают цену ошибки - когда случайно удаляется одна сущность и каскадно пол базы
Но как тогда быть, если у нас достаточно глубокая иерархия сущностей, например
a <- b <- c
и хочется по a_id удалить все связанные b и c?
create table a (
id bigserial not null primary key
);
create table b (
id bigserial not null primary key,
a_id bigint not null references a (id)
);
create table c (
id bigserial not null primary key,
b_id bigint not null references b (id)
);
Наивное решение:
- поселектить из b по a_id
- удалить записи из c по полученным b_id
- удалить записи из b
- удалить записи из a
В итоге имеем 4 раудтрипа до базы и сложную логику, которая с ростом иерархии будет выглядить еще более громоздкой
Решение через CTE:
with a_deleted as (
delete from a where id = <a_id> returning *
), b_deleted as (
delete from b where a_id in (select id from a_deleted) returning *
) delete from c where b_id in (select id from b_deleted)
Почему это работает? CTE трактуется как единый стейтмент, соответственно все проверки констрейнтов будут осуществляться только после его полного выполнения
Это нам позволяет удалить все связанные с таблицей a сущности за один раундтрип до базы, и при необходимости легко масштабировать этот подход, просто добавляя новые строки в CTE, если иерархия вырастет
Ставьте 🔥 на этот пост, если нужен рассказ про еще один интересный вариант применения CTE в контексте внешних ключей
🔥201👍3💅1
⚡Взаимно-рекурсивные внешние ключи
Самый частый юзкейс:
- Есть главная сущность
- Есть дочерние сущности
- Главная сущность ссылается на конкретную дочернюю
Пример: у вопроса может быть несколько ответов, но лишь один корректный:
Как тут могут помочь CTE:
Исходя из модели данных, при создании нового вопроса нам сразу же нужно создать для него правильный ответ. Если это делать последовательно, то мы получим конфликт, поскольку при создании одной сущности вторая должна быть уже создана, и наоборот
Но можно воспользовать тем, что CTE проверяет констрейнты лишь после полного своего выполнения и сделать вставку за один запрос:
Как вы считаете, стоит ли усложнять модель данных в БД взаимно-рекурсивными ключами, или подобные инварианты должны поддерживаться бизнес-логикой?
Самый частый юзкейс:
- Есть главная сущность
- Есть дочерние сущности
- Главная сущность ссылается на конкретную дочернюю
Пример: у вопроса может быть несколько ответов, но лишь один корректный:
create table questions (
id bigserial not null primary key,
correct_answer_id bigint not null,
question varchar not null
);
create table answers (
id bigserial not null primary key,
question_id bigint not null,
answer varchar not null
);
alter table questions
add foreign key (correct_answer_id) references answers (id);
alter table answers
add foreign key (question_id) references questions (id);
Как тут могут помочь CTE:
Исходя из модели данных, при создании нового вопроса нам сразу же нужно создать для него правильный ответ. Если это делать последовательно, то мы получим конфликт, поскольку при создании одной сущности вторая должна быть уже создана, и наоборот
Но можно воспользовать тем, что CTE проверяет констрейнты лишь после полного своего выполнения и сделать вставку за один запрос:
with question_id as (select nextval('questions_id_seq')),
answer_id as (
insert
into answers (answer, question_id)
values ('Some answer', (select * from question_id)) returning id)
insert
into questions (id, correct_answer_id, question)
values ((select * from question_id), (select * from answer_id), 'Some question’);Как вы считаете, стоит ли усложнять модель данных в БД взаимно-рекурсивными ключами, или подобные инварианты должны поддерживаться бизнес-логикой?
🔥21🤯10👍7💅3 3 3
⚡️Почему Redis быстрый, несмотря на однопоточность
Архитектура редиса устроена так, что все клиентские команды выполняются лишь одним тредом. Это позволяет избавиться от оверхеда на context switch и синхронизацию потоков, но встает вопрос о производительности
Если бы редис синхронно делал следующие шаги для каждого клиентского запроса:
- tcp handshake
- ожидание команд, отправка результатов
- tcp termination
то ни о какой производительности речи бы не шло, т.к. в случае долгого соединения наш тред был бы занят только им
Именно поэтому редис не блокируется на IO - т.е. во время ожидания позволяет потоку заниматься другими полезными вещами. Это является основным принципом обеспечения “параллельной” обработки запросов на одном потоке
Как оно устроено внутри:
epoll() - метод, предоставляемый линуксом, который позволяет проверить, что хотя бы один файловый дескриптор готов к IO. В нашем случае, например, если в установленное между клиентом и редис-сервером tcp соединение от клиента пришли какие-то данные
И далее на этом единственном треде крутится event loop с примерно такой логикой
Таким образом, мы заменили блокирующее ожидание новых данных на периодические проверки, между которыми можно заниматься полезной работой
Архитектура редиса устроена так, что все клиентские команды выполняются лишь одним тредом. Это позволяет избавиться от оверхеда на context switch и синхронизацию потоков, но встает вопрос о производительности
Если бы редис синхронно делал следующие шаги для каждого клиентского запроса:
- tcp handshake
- ожидание команд, отправка результатов
- tcp termination
то ни о какой производительности речи бы не шло, т.к. в случае долгого соединения наш тред был бы занят только им
Именно поэтому редис не блокируется на IO - т.е. во время ожидания позволяет потоку заниматься другими полезными вещами. Это является основным принципом обеспечения “параллельной” обработки запросов на одном потоке
Как оно устроено внутри:
epoll() - метод, предоставляемый линуксом, который позволяет проверить, что хотя бы один файловый дескриптор готов к IO. В нашем случае, например, если в установленное между клиентом и редис-сервером tcp соединение от клиента пришли какие-то данные
И далее на этом единственном треде крутится event loop с примерно такой логикой
while true:
if epoll():
do_something_useful()
Таким образом, мы заменили блокирующее ожидание новых данных на периодические проверки, между которыми можно заниматься полезной работой
👍60🤯15🔥4💅2✍1 1
⚡Синхронная реплика не всегда повторяет мастер
Допустим, у нас есть конфигурация мастер + синхронная реплика
Несмотря на то, что реплика синхронная, иногда могут возникать ситуации, что после успешного коммита на мастер мы получаем неактуальные данные с реплики
Почему такое может происходить? В postgres можно выбрать один из режимов синхронного коммита
Чем они отличаются:
off - не дожидаясь записи в WAL на мастере, говорим клиенту, что транзакция успешно завершилась
local - гарантируем запись в WAL на мастере
remote_write - гарантируем запись в WAL на мастере + доставку изменений до реплики
on - гарантируем запись в WAL на мастере + доставку и запись в WAL на реплике
remote_apply - гарантируем запись в WAL на мастере + коммит изменений на реплике
По умолчанию выбирается режим on, который позволяет закомитить транзакцию, не дождавшись применения изменений на реплике. И соотв-но какое-то время на реплике будут лежать неактуальные данные
Допустим, у нас есть конфигурация мастер + синхронная реплика
Несмотря на то, что реплика синхронная, иногда могут возникать ситуации, что после успешного коммита на мастер мы получаем неактуальные данные с реплики
Почему такое может происходить? В postgres можно выбрать один из режимов синхронного коммита
Чем они отличаются:
off - не дожидаясь записи в WAL на мастере, говорим клиенту, что транзакция успешно завершилась
local - гарантируем запись в WAL на мастере
remote_write - гарантируем запись в WAL на мастере + доставку изменений до реплики
on - гарантируем запись в WAL на мастере + доставку и запись в WAL на реплике
remote_apply - гарантируем запись в WAL на мастере + коммит изменений на реплике
По умолчанию выбирается режим on, который позволяет закомитить транзакцию, не дождавшись применения изменений на реплике. И соотв-но какое-то время на реплике будут лежать неактуальные данные
👍56💅7🔥3 3
⚡Частые архитектурные паттерны
При разработке небольших сервисов можно часто встретить повторяющиеся архитектурные паттерны. Мой личный топ 3:
1. Отсутствие архитектуры
Никаких правил нет. Обычно нет разграничения на модули, что приводит к тому, что всё доступно из всего. Например, эндпоинты могут напрямую вызывать бд и работать с энтитями. Несмотря на всю диковатость, такая “архитектура” вполне может подходить для простейших сервисов/прототипирования
2. Слоеная
Идет логическое разбиение на слои. Какого-то общепринятого разбиения нет, но обычно это что-то в духе:
web -> application -> domain -> DB
где web - контроллеры, application - бизнес логика, domain - модель данных + репозитории, DB - база данных
Также часто применяются прямые зависимости, т.е. высокоуровневые слои зависят от низкоуровневых, что противоречит dependency inversion principle в SOLID. Такая архитектура вполне жизнеспособна для сервисов до среднего размера с не очень сложной бизнес-логикой. Но стоит оговориться, что в команде должно быть сразу установлено, как именно делим на слои, могут ли быть “сквозные” вызовы (web -> domain) и т.п.
3. Гексагональная
Концептуально другой вид архитектуры с точки зрения инверсии зависимостей. В центре находится основная бизнес логика, в которой описываются интерфейсы:
1) которые бизнес логика предоставляет во вне (in-порты)
2) интерфейсы, которые необходимы для реализации бизнес логики (out-порты), например, интерфейс доступа к данным
И далее “по бокам” располагаются компоненты, которые
1) используют интерфейсы предоставляемые бизнес логикой, например, web-контроллеры
2) реализации интерфейсов (адаптеры), нужных для бизнес логики, например, реализация интерфейсов доступа к данным через postgres
При этом важно отметить, что именно “побочные” компоненты зависят от бизнес логики, а не наоборот. Это позволяет реализовывать основную логику приложения без использования каких-либо зависимостей (framework Agnostic). Такая архитектура хорошо подходит для относительно больших проектов со сложной логикой, но и накладывает некоторый оверхед
А какой вид архитектуры вы обычно используете в своих проектах и почему?
При разработке небольших сервисов можно часто встретить повторяющиеся архитектурные паттерны. Мой личный топ 3:
1. Отсутствие архитектуры
Никаких правил нет. Обычно нет разграничения на модули, что приводит к тому, что всё доступно из всего. Например, эндпоинты могут напрямую вызывать бд и работать с энтитями. Несмотря на всю диковатость, такая “архитектура” вполне может подходить для простейших сервисов/прототипирования
2. Слоеная
Идет логическое разбиение на слои. Какого-то общепринятого разбиения нет, но обычно это что-то в духе:
web -> application -> domain -> DB
где web - контроллеры, application - бизнес логика, domain - модель данных + репозитории, DB - база данных
Также часто применяются прямые зависимости, т.е. высокоуровневые слои зависят от низкоуровневых, что противоречит dependency inversion principle в SOLID. Такая архитектура вполне жизнеспособна для сервисов до среднего размера с не очень сложной бизнес-логикой. Но стоит оговориться, что в команде должно быть сразу установлено, как именно делим на слои, могут ли быть “сквозные” вызовы (web -> domain) и т.п.
3. Гексагональная
Концептуально другой вид архитектуры с точки зрения инверсии зависимостей. В центре находится основная бизнес логика, в которой описываются интерфейсы:
1) которые бизнес логика предоставляет во вне (in-порты)
2) интерфейсы, которые необходимы для реализации бизнес логики (out-порты), например, интерфейс доступа к данным
И далее “по бокам” располагаются компоненты, которые
1) используют интерфейсы предоставляемые бизнес логикой, например, web-контроллеры
2) реализации интерфейсов (адаптеры), нужных для бизнес логики, например, реализация интерфейсов доступа к данным через postgres
При этом важно отметить, что именно “побочные” компоненты зависят от бизнес логики, а не наоборот. Это позволяет реализовывать основную логику приложения без использования каких-либо зависимостей (framework Agnostic). Такая архитектура хорошо подходит для относительно больших проектов со сложной логикой, но и накладывает некоторый оверхед
А какой вид архитектуры вы обычно используете в своих проектах и почему?
👍38💅10🔥6 1