⚡Взаимно-рекурсивные внешние ключи
Самый частый юзкейс:
- Есть главная сущность
- Есть дочерние сущности
- Главная сущность ссылается на конкретную дочернюю
Пример: у вопроса может быть несколько ответов, но лишь один корректный:
Как тут могут помочь 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
Привет! Внезапно осознал, что канал уже почти как год ведется под некоторой пеленой анонимности, пора исправлять
Меня зовут Александр Федькин, мне 21, сейчас руковожу командой бэкендеров в Яндексе
Мы разрабатываем:
- No-Code платформу для автоматизации клиентского сервиса: сейчас в ней можно в UI настраивать чат-ботов и пайплайны обработки пользовательских обращений, а также привязывать их запуск к некоторым событиям
- Инфраструктуру для чатовой поддержки различных продуктов: она позволяет соединить клиента, чат-бота и оператора поддержки, а также общаться им друг с другом надежно и быстро
До этого успел окончить программную инженерию матмеха СПбГУ и поучиться в питерском CSC до его закрытия
В канал обычно пишу про какие-то занятные вещи с работы либо когда прочитал/нашел что-то интересное
—
Для новичков: топ постов за прошедший год:
- Каскадное удаление за один запрос
- Индексирование больших таблиц
- Consistent hashing
- Почему батчевые update/delete не безопасны
- Почему Redis быстрый, несмотря на однопоточность
Меня зовут Александр Федькин, мне 21, сейчас руковожу командой бэкендеров в Яндексе
Мы разрабатываем:
- No-Code платформу для автоматизации клиентского сервиса: сейчас в ней можно в UI настраивать чат-ботов и пайплайны обработки пользовательских обращений, а также привязывать их запуск к некоторым событиям
- Инфраструктуру для чатовой поддержки различных продуктов: она позволяет соединить клиента, чат-бота и оператора поддержки, а также общаться им друг с другом надежно и быстро
До этого успел окончить программную инженерию матмеха СПбГУ и поучиться в питерском CSC до его закрытия
В канал обычно пишу про какие-то занятные вещи с работы либо когда прочитал/нашел что-то интересное
—
Для новичков: топ постов за прошедший год:
- Каскадное удаление за один запрос
- Индексирование больших таблиц
- Consistent hashing
- Почему батчевые update/delete не безопасны
- Почему Redis быстрый, несмотря на однопоточность
👍97🔥41🤯32💅9🤔2 2
Microservices Thoughts pinned «Привет! Внезапно осознал, что канал уже почти как год ведется под некоторой пеленой анонимности, пора исправлять Меня зовут Александр Федькин, мне 21, сейчас руковожу командой бэкендеров в Яндексе Мы разрабатываем: - No-Code платформу для автоматизации…»
⚡Монотонность и PostgreSQL
Когда возникает потребность в монотонно возрастающих колонках обычно используются bigserial, timestamp и т.п.
На первый взгляд все хорошо, но рассмотрим такую ситуацию:
Для примера возьмем bigserial (bigint, который берет значения из определенного сиквенса)
Получается ситуация, что после шага 5 у нас коммитится сущность с id=2, и только после шага 6 коммитится с id=1. Иными словами, для внешнего наблюдателя айдишники будут добавляться не в монотонном порядке
Последствия: например, фоновые выгрузки данных, которые с пагинацией вычитывают данные, сохраняя последний обработанный id. Может произойти ситуация, что сохранили last_processed_id=5, и после этого добавляется сущность с id=4, которую мы благополучно пропустим
Как с таким можно бороться:
1. Ограничивать временной промежуток
При выгрузках делать условие на
2. Брать айдишники не из секвенса, а из таблицы
И получать айдишник как
В таком случае постгрес с помощью блокировок при update-ах гарантирует строгую монотонность появления новых айдишников, но очевидно в таком случае эта таблица становится боттлнеком для всего, что ее задействует
3. Использование transaction id
Основная суть:
1) В сущности добавляем колонку transaction_id - xid8 транзакции, которая последняя ее проапдейтила. PG гарантирует, что он строго возрастает (подобно значениям из сиквенса)
2) В выгрузках делаем условие
Такой подход при отсутствии долгих транзакций в БД позволяет получить одновременно и “монотонность” и почти реалтаймовость. Однако требует дополнительных усилий с добавлением transaction_id, навешеванием триггера, который будет ее обновлять, и написанием довольно странных запросов с pg_current_snapshot()
Когда возникает потребность в монотонно возрастающих колонках обычно используются bigserial, timestamp и т.п.
На первый взгляд все хорошо, но рассмотрим такую ситуацию:
Для примера возьмем bigserial (bigint, который берет значения из определенного сиквенса)
1. tx1: begin
2. tx1: save(entity) // взяли id=1 из сиквенса
3. tx2: begin
4. tx2: save(entity) // взяли id=2 из сиквенса
5. tx2: commit
6. tx1: commit
Получается ситуация, что после шага 5 у нас коммитится сущность с id=2, и только после шага 6 коммитится с id=1. Иными словами, для внешнего наблюдателя айдишники будут добавляться не в монотонном порядке
Последствия: например, фоновые выгрузки данных, которые с пагинацией вычитывают данные, сохраняя последний обработанный id. Может произойти ситуация, что сохранили last_processed_id=5, и после этого добавляется сущность с id=4, которую мы благополучно пропустим
Как с таким можно бороться:
1. Ограничивать временной промежуток
При выгрузках делать условие на
created_time < now() - interval ’n seconds’, у которого цель — гарантировать, что в данном промежутке появление новых айдишников/таймстемпов будет выглядить монотонно. Подразумевается, что за n секунд все “старые” транзакции закоммитятся. Вполне рабочий способ, если реалтаймовость необязательна2. Брать айдишники не из секвенса, а из таблицы
create table id (
value bigint not null
);
И получать айдишник как
update id set value = value + 1 returning *;
В таком случае постгрес с помощью блокировок при update-ах гарантирует строгую монотонность появления новых айдишников, но очевидно в таком случае эта таблица становится боттлнеком для всего, что ее задействует
3. Использование transaction id
Основная суть:
1) В сущности добавляем колонку transaction_id - xid8 транзакции, которая последняя ее проапдейтила. PG гарантирует, что он строго возрастает (подобно значениям из сиквенса)
2) В выгрузках делаем условие
transaction_id < pg_snapshot_xmin(pg_current_snapshot()). Оно гарантирует, что в выбранных сущностях не появится изменений с меньшим transaction_id (т.е. которые логически произошли раньше)Такой подход при отсутствии долгих транзакций в БД позволяет получить одновременно и “монотонность” и почти реалтаймовость. Однако требует дополнительных усилий с добавлением transaction_id, навешеванием триггера, который будет ее обновлять, и написанием довольно странных запросов с pg_current_snapshot()
👍55🔥11💅11 5
В процессе разработки зачастую возникает множество проблем, которые могут затормозить проект и снизить качество итогового продукта. Эти проблемы не ограничиваются только написанием кода: проблема может возникнуть на любом этапе, начиная с распределения задач в команде и заканчивая сниженным качеством итогового продукта вследствие неоптимального процесса тестирования
Часто можно встретиться со следующим:
- Проблемы с коммуникацией: участники команды не понимают к кому обращаться по определенным вопросам, не понимают, как эскалировать в случае возникновения разногласий. Это может приводить к тому, что все пишут друг другу по личкам, теряется контекст, и в итоге возникает огромный оверхед на коммуникацию, который отвлекает от работы
- Непонятное разграничение зон ответственности: роли и обязанности не определены чётко, задачи могут пересекаться или оставаться не выполненными. Это создаёт хаос, приводит к перекосу нагрузки между участниками команды и также является причиной проблем с коммуникацией
- Разрозненные правила разработки: отсутствие стандартизированных процессов и инструментов затрудняет работу и тормозит развитие проектов, поскольку каждая команда вынуждена изобретать собственные методы и подходы
- Качество и безопасность: недостаток тестирования, использования инструментов статического анализа кода и общепринятых стандартов качества создают риски, которых можно было легко избежать, а также тормозят разработку, вызывая необходимость постоянно чинить баги
Одним из решений, которое помогает минимизировать описанные выше проблемы, являются sensible defaults — внутренние стандарты команды/подразделения, описывающие кто чем занимается, как надо тестировать, каким правилам должна соответствовать архитектура проекта и так далее. Заранее установленные стандарты делают процесс разработки более предсказуемым и позволяют заранее иметь ответы на большинство возникающих вопросов
Часто можно встретиться со следующим:
- Проблемы с коммуникацией: участники команды не понимают к кому обращаться по определенным вопросам, не понимают, как эскалировать в случае возникновения разногласий. Это может приводить к тому, что все пишут друг другу по личкам, теряется контекст, и в итоге возникает огромный оверхед на коммуникацию, который отвлекает от работы
- Непонятное разграничение зон ответственности: роли и обязанности не определены чётко, задачи могут пересекаться или оставаться не выполненными. Это создаёт хаос, приводит к перекосу нагрузки между участниками команды и также является причиной проблем с коммуникацией
- Разрозненные правила разработки: отсутствие стандартизированных процессов и инструментов затрудняет работу и тормозит развитие проектов, поскольку каждая команда вынуждена изобретать собственные методы и подходы
- Качество и безопасность: недостаток тестирования, использования инструментов статического анализа кода и общепринятых стандартов качества создают риски, которых можно было легко избежать, а также тормозят разработку, вызывая необходимость постоянно чинить баги
Одним из решений, которое помогает минимизировать описанные выше проблемы, являются sensible defaults — внутренние стандарты команды/подразделения, описывающие кто чем занимается, как надо тестировать, каким правилам должна соответствовать архитектура проекта и так далее. Заранее установленные стандарты делают процесс разработки более предсказуемым и позволяют заранее иметь ответы на большинство возникающих вопросов
Telegram
StringConcat - разработка без боли и сожалений
Самое ценное что я получил за 5 лет в Thoughtworks — это внутренние стандарты разработки. Называются они Sensible Defaults.
Стандарты описывают, как должен выглядеть любой проект TW и кто чем должен заниматься:
PM — служить команде разработки и устранять…
Стандарты описывают, как должен выглядеть любой проект TW и кто чем должен заниматься:
PM — служить команде разработки и устранять…
👍17🔥8 1
⚡Entity state transfer
В микросервисной архитектуре часто возникает задача, чтобы один микросервис реагировал на изменения сущностей другого микросервиса. Для примера возьмем следующую задачу:
1. Есть ticket service, хранящий обращения пользователей. Тикет задается тремя полями: id — идентификатор обращения, status — текущий статус OPEN / CLOSED и assignee — текущий оператор, обрабатыващий тикет
2. Есть chat service, который связывает обращения пользователей с оператором, ботом и т.д. Он должен как-то реагировать на изменения тикета
Возникает вопрос — как передавать изменения стейта
1. Синхронный подход
При изменении тикета ticket service синхронно дергает ручку в chat service, которая обработает изменения. Самый простой в реализации подход, но возникает проблема с high-coupling — в случае недоступности chat service будет недоступен и ticket service. Однако они должны уметь существовать в отдельности друг от друга, и доступность одного сервиса не должна влиять на доступность другого
2.1. Асинхронный подход: id
Ticket service отправляет эвент, говорящий, что “что-то изменилось у тикета с определенным id”. Далее chat service, получая этот эвент, синхронно идет в ticket service и получает актуальный стейт. Такой подход хорош тем, что он охватывает сразу все изменения тикета, а также позволяет особо не думать про порядок эвентов — тк мы все равно сами ходим за актуальным стейтом. Из минусов — повышенная нагрузка на чтение на ticket service
2.2. Асинхронный подход: only change
Ticket service отправляет эвент, говорящий, что конкретно изменилось. Например status: OPEN -> CLOSED. Такой подход хорош тем, что отправляются лишь минимальный необходимый набор данных, но здесь уже нужно думать про порядок эвентов — потому что может быть важно обрабатывать изменения status и изменения assignee ровно в том порядке, в котором они произошли в тикетной системе
2.3. Асинхронный подход: snapshot
Ticket service отправляет эвент со снапшотом текущего тикета. Из плюсов — не нужно синхронно ходить за стейтом тикета, он уже полностью есть в эвенте; особо не нужно думать про порядок — можно хранить timestamp последнего обработанного снапшота, и если приходит более ранний, то просто игнорируем. Из минусов — хранение в брокере большего объема информации
В микросервисной архитектуре часто возникает задача, чтобы один микросервис реагировал на изменения сущностей другого микросервиса. Для примера возьмем следующую задачу:
1. Есть ticket service, хранящий обращения пользователей. Тикет задается тремя полями: id — идентификатор обращения, status — текущий статус OPEN / CLOSED и assignee — текущий оператор, обрабатыващий тикет
{
“id”: 123,
“status”: “OPEN”,
“assignee”: null
}2. Есть chat service, который связывает обращения пользователей с оператором, ботом и т.д. Он должен как-то реагировать на изменения тикета
Возникает вопрос — как передавать изменения стейта
1. Синхронный подход
При изменении тикета ticket service синхронно дергает ручку в chat service, которая обработает изменения. Самый простой в реализации подход, но возникает проблема с high-coupling — в случае недоступности chat service будет недоступен и ticket service. Однако они должны уметь существовать в отдельности друг от друга, и доступность одного сервиса не должна влиять на доступность другого
2.1. Асинхронный подход: id
{
“id”: 123
}Ticket service отправляет эвент, говорящий, что “что-то изменилось у тикета с определенным id”. Далее chat service, получая этот эвент, синхронно идет в ticket service и получает актуальный стейт. Такой подход хорош тем, что он охватывает сразу все изменения тикета, а также позволяет особо не думать про порядок эвентов — тк мы все равно сами ходим за актуальным стейтом. Из минусов — повышенная нагрузка на чтение на ticket service
2.2. Асинхронный подход: only change
{
“id”: 123,
“status”: {
“old”: “OPEN”,
“new”: “CLOSED”
}
}Ticket service отправляет эвент, говорящий, что конкретно изменилось. Например status: OPEN -> CLOSED. Такой подход хорош тем, что отправляются лишь минимальный необходимый набор данных, но здесь уже нужно думать про порядок эвентов — потому что может быть важно обрабатывать изменения status и изменения assignee ровно в том порядке, в котором они произошли в тикетной системе
2.3. Асинхронный подход: snapshot
{
“id”: 123”,
“status”: “CLOSED”,
“assignee”: “operator_login”,
“snapshotTimestamp”: “2024-11-12T13:14:15Z”
}Ticket service отправляет эвент со снапшотом текущего тикета. Из плюсов — не нужно синхронно ходить за стейтом тикета, он уже полностью есть в эвенте; особо не нужно думать про порядок — можно хранить timestamp последнего обработанного снапшота, и если приходит более ранний, то просто игнорируем. Из минусов — хранение в брокере большего объема информации
👍90🔥16 4✍3💅3
⚡Application-level sharding
Метод распределения данных между несколькими серверами БД, где логика распределения хранится на уровне приложения. При этом само хранилище может ничего не знать про шардирование
Логика работы:
1. Выбираем ключ шардирования
2. Определяем правила раутинга — как по ключу шардирования понять, на какой шард нужно отправить запрос
3. Приложению нужно исполнить некоторый запрос в БД
4. По запросу определяем, на какой(-ие) шард(-ы) его нужно отправить
Самое сложное — правильно выбрать ключ шардирования и правила раутинга, потому что
1. Важно, чтобы было по минимуму читающих запросов, задействующих >1 шардов
2. Важно, чтобы не было изменяющих запросов, задействующий >1 шардов. Поскольку в таком случае у нас теряется транзакционность, и нужно решать проблему распределенных транзакций
3. Менять ключ шардирования и правила раутинга очень больно
Ключом может быть, например, id главной сущности в системе, к которой привязываются все остальные
Определив ключ шардирования нужно определить правила раутинга. Здесь можно выделить
1. Stateless подход — грубо говоря, когда правила раутинга задаются чистой функцией, не зависящей от состояния системы. Например, выбор шарда определяется как hash(entityId) % n, где n - фиксированное число шардов
2. Stateful подход — есть некоторое изменяемое хранилище метаданных, которое определяет, куда раутить запросы по определенным ключам. Например, таблица с динамически расширяющимися диапазонами: по entityId от 0 до 9999 идем в шард 1, по entityId от 10000 до 19999 идем в шард 2, и тд. Правила могут динамически добавлятся, что позволяет управлять нагрузкой, если к примеру мощности шардов не одинаковые
Метод распределения данных между несколькими серверами БД, где логика распределения хранится на уровне приложения. При этом само хранилище может ничего не знать про шардирование
Логика работы:
1. Выбираем ключ шардирования
2. Определяем правила раутинга — как по ключу шардирования понять, на какой шард нужно отправить запрос
3. Приложению нужно исполнить некоторый запрос в БД
4. По запросу определяем, на какой(-ие) шард(-ы) его нужно отправить
Самое сложное — правильно выбрать ключ шардирования и правила раутинга, потому что
1. Важно, чтобы было по минимуму читающих запросов, задействующих >1 шардов
2. Важно, чтобы не было изменяющих запросов, задействующий >1 шардов. Поскольку в таком случае у нас теряется транзакционность, и нужно решать проблему распределенных транзакций
3. Менять ключ шардирования и правила раутинга очень больно
Ключом может быть, например, id главной сущности в системе, к которой привязываются все остальные
Определив ключ шардирования нужно определить правила раутинга. Здесь можно выделить
1. Stateless подход — грубо говоря, когда правила раутинга задаются чистой функцией, не зависящей от состояния системы. Например, выбор шарда определяется как hash(entityId) % n, где n - фиксированное число шардов
2. Stateful подход — есть некоторое изменяемое хранилище метаданных, которое определяет, куда раутить запросы по определенным ключам. Например, таблица с динамически расширяющимися диапазонами: по entityId от 0 до 9999 идем в шард 1, по entityId от 10000 до 19999 идем в шард 2, и тд. Правила могут динамически добавлятся, что позволяет управлять нагрузкой, если к примеру мощности шардов не одинаковые
👍41💅8🤯1 1
⚡Вертикальное партицирование
В отличие от горизонтального партицирования / шардирования таблица разделяется не по строкам, а по столбцам
Например, из
Получается
MVCC работает так, что при апдейте строки в таблице создается ее полностью новая версия, даже если поменялась всего одна колонка. Поэтому если таблица большая и часто апдейтится, имеет смысл задуматься о переносе частоизменяющихся столбцов в отдельную таблицу
Но стоит быть аккуратным, если есть запросы, задействующие одновременно и status, и assignee. Поскольку пока это хранится в одной таблице, можно сделать многоколоночный индекс на (status, assignee) и быстрым индекс сканом выполнять запрос. Если таблица поделится, то такое уже станет невозможным: нужно будет либо 1) поискать по таблице ticket по условию на status, а затем поджойнить с ticket_assignee, либо 2) поискать по ticket_assignee по условию assignee, а затем поджойнить с ticket. Если фильтрация по каждому условию по отдельности возвращает много строк, то запрос начнет работать сильно медленнее
В отличие от горизонтального партицирования / шардирования таблица разделяется не по строкам, а по столбцам
Например, из
create table ticket
(
id bigserial primary key not null,
status varchar not null,
assignee varchar null
);
Получается
create table ticket
(
id bigserial primary key not null,
status varchar not null,
);
create table ticket_assignee
(
ticket_id bigserial not null,
assignee varchar null
);
MVCC работает так, что при апдейте строки в таблице создается ее полностью новая версия, даже если поменялась всего одна колонка. Поэтому если таблица большая и часто апдейтится, имеет смысл задуматься о переносе частоизменяющихся столбцов в отдельную таблицу
Но стоит быть аккуратным, если есть запросы, задействующие одновременно и status, и assignee. Поскольку пока это хранится в одной таблице, можно сделать многоколоночный индекс на (status, assignee) и быстрым индекс сканом выполнять запрос. Если таблица поделится, то такое уже станет невозможным: нужно будет либо 1) поискать по таблице ticket по условию на status, а затем поджойнить с ticket_assignee, либо 2) поискать по ticket_assignee по условию assignee, а затем поджойнить с ticket. Если фильтрация по каждому условию по отдельности возвращает много строк, то запрос начнет работать сильно медленнее
👍32✍10🔥3💅2 2
⚡n/2 + 1
Кворум — подмножество нод в распределенной системе
Quorum-based Consistency — модель, использумая для обеспечения Consistency из CAP-теоремы “Every read receives the most recent write or an error”, т.е. гарантирует, что не возникнет ситуаций, что один клиент получил успешное подтверждение о записи, а второй клиент эту запись некоторое время не видит. Обеспечивается за счет того, что для подтверждения операции записи/чтения нужно подтверждение от некоторого количества остальных узлов
Обычно разделяют кворум на запись и кворум на чтение
Пусть общее кол-во узлов в системе = N
Размер кворума на чтение = R
Размер кворума на запись = W
Возникает вопрос, как выбрать R и W, чтобы обеспечить consistency. Как минимум нужно, чтобы R + W > N, поскольку это обеспечивает пересечение множеств узлов на запись и на чтение — это нам гарантирует, что при чтении, мы обязательно увидим узел, на который успешно произошла запись
Однако вариаций, как обеспечить R + W > N довольно много, рассмотрим некоторые из них
1) R = 1, W = N
При записи на какой-либо из узлов, он ждет подтверждения от всех остальных узлов
Читаем всегда с одного узла
Поскольку каждая операция записи гарантированно распространится на все узлы, то и чтение с одного узла тоже будет консистентным. Однако в случае недоступности любого из узлов, весь кластер становится недоступным на запись
2) R = N, W = 1
Записываем изменения только на один узел
При чтении опрашиваем все узлы, чтобы определить где самые актуальные данные
И здесь в случае недоступности одного из узлов, весь кластер становится недоступным на чтение, т.к. мы уже не сможем гарантировать, что write-узел будет попадать в кворум на чтение
3) R = N / 2 + 1, W = N / 2 + 1
Записываем изменения на большинство узлов
При чтении опрашиваем большинство узлов
Здесь мы по прежнему сохраняем условие, что кворумы на запись и чтение пересекаются, однако мы можем пережить отказы N - (N / 2 + 1) узлов, поскольку в случае отказа кворумы могут просто перегруппироваться
—
Пример:
Узлы {A, B, C}, N = 3
Кворум на чтение: {A, B}, R = 2
Кворум на запись: {B, C}, W = 2
Узел B отказывает, кворумы просто перераспределяются
Кворум на чтение: {A, C}
Кворум на запись: {A, C}
Таким образом, n/2 + 1 используется как компромисс между availability и consistency, поскольку позволяет сохранять consistency, но также и переживать отказы некоторых узлов
Кворум — подмножество нод в распределенной системе
Quorum-based Consistency — модель, использумая для обеспечения Consistency из CAP-теоремы “Every read receives the most recent write or an error”, т.е. гарантирует, что не возникнет ситуаций, что один клиент получил успешное подтверждение о записи, а второй клиент эту запись некоторое время не видит. Обеспечивается за счет того, что для подтверждения операции записи/чтения нужно подтверждение от некоторого количества остальных узлов
Обычно разделяют кворум на запись и кворум на чтение
Пусть общее кол-во узлов в системе = N
Размер кворума на чтение = R
Размер кворума на запись = W
Возникает вопрос, как выбрать R и W, чтобы обеспечить consistency. Как минимум нужно, чтобы R + W > N, поскольку это обеспечивает пересечение множеств узлов на запись и на чтение — это нам гарантирует, что при чтении, мы обязательно увидим узел, на который успешно произошла запись
Однако вариаций, как обеспечить R + W > N довольно много, рассмотрим некоторые из них
1) R = 1, W = N
При записи на какой-либо из узлов, он ждет подтверждения от всех остальных узлов
Читаем всегда с одного узла
Поскольку каждая операция записи гарантированно распространится на все узлы, то и чтение с одного узла тоже будет консистентным. Однако в случае недоступности любого из узлов, весь кластер становится недоступным на запись
2) R = N, W = 1
Записываем изменения только на один узел
При чтении опрашиваем все узлы, чтобы определить где самые актуальные данные
И здесь в случае недоступности одного из узлов, весь кластер становится недоступным на чтение, т.к. мы уже не сможем гарантировать, что write-узел будет попадать в кворум на чтение
3) R = N / 2 + 1, W = N / 2 + 1
Записываем изменения на большинство узлов
При чтении опрашиваем большинство узлов
Здесь мы по прежнему сохраняем условие, что кворумы на запись и чтение пересекаются, однако мы можем пережить отказы N - (N / 2 + 1) узлов, поскольку в случае отказа кворумы могут просто перегруппироваться
—
Пример:
Узлы {A, B, C}, N = 3
Кворум на чтение: {A, B}, R = 2
Кворум на запись: {B, C}, W = 2
Узел B отказывает, кворумы просто перераспределяются
Кворум на чтение: {A, C}
Кворум на запись: {A, C}
Таким образом, n/2 + 1 используется как компромисс между availability и consistency, поскольку позволяет сохранять consistency, но также и переживать отказы некоторых узлов
👍52🤯6💅4 1
⚡Transactional inbox
С помощью transactional outbox мы умеем обеспечивать надежную доставку сообщений до брокера
Но теперь возникает другая проблема — консюмеру нужно ровно один раз обработать сообщение
В случае наивного решения
Может случиться ситуация, что databaseTx закоммитилась, но message.commit() не отработал. Это приведет к тому, что при следующем чтении мы обработаем сообщение еще раз
И здесь нам поможет transactional inbox, у которого я выделяю два вида
1) По-прежнему сначала обрабатываем сообщение, потом коммитим. Но добавляем дедупликацию
В таком случае даже если databaseTx закоммитилась, но message.commit() не отработал, то при повторном чтении мы увидим сохраненный ключ сообщения, и сразу его закоммитим
2) Сохраняем сообщение в таблицу, и фоновые воркеры достают сообщения из таблицы и обрабатывают
Несмотря на то, что такой подход решает ту же проблему, еще и при этом добавляет latency, у него есть весомый плюс — консюмер теперь может балансировать нагрузку на себя
Причем это работает в обе стороны:
1) Например, если сообщения в нас отправляют по http со слишком высоким рейтом, то мы просто сохраняем их в таблицу и процессим с доступной нам скоростью
2) И наоборот: если сообщения мы сами читаем из топика, но у топика слишком мало партиций, и существующие консюмеры не успевают обрабатывать приходящие сообщения, то можно также их просто сохранить в таблицу, и далее нужным количеством воркеров разгребать эту таблицу
С помощью transactional outbox мы умеем обеспечивать надежную доставку сообщений до брокера
Но теперь возникает другая проблема — консюмеру нужно ровно один раз обработать сообщение
В случае наивного решения
processMessage() {
databaseTx {
…
}
message.commit()
}Может случиться ситуация, что databaseTx закоммитилась, но message.commit() не отработал. Это приведет к тому, что при следующем чтении мы обработаем сообщение еще раз
И здесь нам поможет transactional inbox, у которого я выделяю два вида
1) По-прежнему сначала обрабатываем сообщение, потом коммитим. Но добавляем дедупликацию
processMessage() {
databaseTx {
if (!tryInsert(msgKey)) {
message.commit()
return
}
…
}
message.commit()
}В таком случае даже если databaseTx закоммитилась, но message.commit() не отработал, то при повторном чтении мы увидим сохраненный ключ сообщения, и сразу его закоммитим
2) Сохраняем сообщение в таблицу, и фоновые воркеры достают сообщения из таблицы и обрабатывают
processMessage() {
databaseTx {
tryInsert(message)
}
message.commit()
}Несмотря на то, что такой подход решает ту же проблему, еще и при этом добавляет latency, у него есть весомый плюс — консюмер теперь может балансировать нагрузку на себя
Причем это работает в обе стороны:
1) Например, если сообщения в нас отправляют по http со слишком высоким рейтом, то мы просто сохраняем их в таблицу и процессим с доступной нам скоростью
2) И наоборот: если сообщения мы сами читаем из топика, но у топика слишком мало партиций, и существующие консюмеры не успевают обрабатывать приходящие сообщения, то можно также их просто сохранить в таблицу, и далее нужным количеством воркеров разгребать эту таблицу
👍47💅1
⚡Postgres и oversized-атрибуты
В Postgres страница — это базовая единица ввода-вывода данных, хранимых на диске (нельзя прочитать с диска меньше чем одну страницу). По умолчанию размер страницы составляет 8кб
Также Postgres не позволяет хранить одну запись на нескольких страницах. Однако, как известно существует множество типов данных, значения которых могут превышать 8кб (например, varchar). Возникает вопрос — как их уместить в одну страницу
TOAST (The Oversized-Attribute Storage Technique) — техника, когда мы храним значение аттрибута не напрямую в странице рядом с остальными аттрибутами, а перемещаем значения в отдельную toast-таблицу. В основной таблице храним указатель на запись в toast-таблице. Все это скрыто от пользователя, что позволяет не задумываясь хранить большие тексты/жсоны прямо в реляционной таблице (не делайте так), но в то же время имеет несколько неочевидных недостатков:
1. Идентификатор записи в toast-таблице имеет тип oid, который имеет 2^32 уникальных значений, что не так много. В случае когда все идентификаторы “потратятся”, и захочется сделать insert в основную таблицу, подразумевающий вставку в toast, то вставка не сработает
2. На toast-таблицу по-прежнему распространяются правила MVCC — если поредактировать колонку в основной таблице, значение которой лежит в toast, то старое значение в toast-таблице не удалится, а просто пометится удаленным. Физическое удаление и освобождение памяти произойдет после vacuum. Это может приводить к раздуванию toast-таблицы
—
Поэтому если у вас есть потребность хранить большие тексты/жсоны, и у сервиса высокая нагрузка/большой поток данных, то имеет смысл воспользоваться специализированными инструментами. Например, S3 — сам документ храним в S3, а в реляционной таблице храним просто s3_object_id. Получается тоже своего рода TOAST, но без вышеприведенных недостатков
Ставьте 👍 на этот пост, если нужен рассказ про S3
В Postgres страница — это базовая единица ввода-вывода данных, хранимых на диске (нельзя прочитать с диска меньше чем одну страницу). По умолчанию размер страницы составляет 8кб
Также Postgres не позволяет хранить одну запись на нескольких страницах. Однако, как известно существует множество типов данных, значения которых могут превышать 8кб (например, varchar). Возникает вопрос — как их уместить в одну страницу
TOAST (The Oversized-Attribute Storage Technique) — техника, когда мы храним значение аттрибута не напрямую в странице рядом с остальными аттрибутами, а перемещаем значения в отдельную toast-таблицу. В основной таблице храним указатель на запись в toast-таблице. Все это скрыто от пользователя, что позволяет не задумываясь хранить большие тексты/жсоны прямо в реляционной таблице (не делайте так), но в то же время имеет несколько неочевидных недостатков:
1. Идентификатор записи в toast-таблице имеет тип oid, который имеет 2^32 уникальных значений, что не так много. В случае когда все идентификаторы “потратятся”, и захочется сделать insert в основную таблицу, подразумевающий вставку в toast, то вставка не сработает
2. На toast-таблицу по-прежнему распространяются правила MVCC — если поредактировать колонку в основной таблице, значение которой лежит в toast, то старое значение в toast-таблице не удалится, а просто пометится удаленным. Физическое удаление и освобождение памяти произойдет после vacuum. Это может приводить к раздуванию toast-таблицы
—
Поэтому если у вас есть потребность хранить большие тексты/жсоны, и у сервиса высокая нагрузка/большой поток данных, то имеет смысл воспользоваться специализированными инструментами. Например, S3 — сам документ храним в S3, а в реляционной таблице храним просто s3_object_id. Получается тоже своего рода TOAST, но без вышеприведенных недостатков
Ставьте 👍 на этот пост, если нужен рассказ про S3
👍183🔥12💅4 2
⚡Heartbeat pattern
Предположим у нас есть простая очередь задач
Чтобы брать задачи из этой очереди и гарантировать эксклюзивность, можно использовать стандартный подход с
Окей, попробуем не брать блокировку, сделаем что-то типа такого
Казалось бы все ок, но что если воркер, взявший задачу, умрет перед execute task? Таска навечно повиснет в статусе running и никто не будет с ней ничего делать
—
Ровно эту проблему решают хартбиты:
1. Воркер раз в n секунд пингует базу, что позволяет нам убедиться что воркер жив и имеет конекшн до базы
2. Супервизор наблюдает за воркерами: если какой-то воркер не пинговал базу m секунд, то снимаем с него все задачи
В итоге задача снимется с умершего воркера, и ее возьмет любой другой свободный
Предположим у нас есть простая очередь задач
create table task
(
id bigint not null,
status varchar not null,
data jsonb not null
);
Чтобы брать задачи из этой очереди и гарантировать эксклюзивность, можно использовать стандартный подход с
select for update. Однако, это вынуждает нас держать блокировку и транзакцию на все время исполнения задачи. В итоге получаем long-running transactions, которые негативно влияют на перфоманс базыОкей, попробуем не брать блокировку, сделаем что-то типа такого
val task = tx {
get scheduled task with lock;
set task status running;
}
execute task;
set task status finished;Казалось бы все ок, но что если воркер, взявший задачу, умрет перед execute task? Таска навечно повиснет в статусе running и никто не будет с ней ничего делать
—
Ровно эту проблему решают хартбиты:
1. Воркер раз в n секунд пингует базу, что позволяет нам убедиться что воркер жив и имеет конекшн до базы
2. Супервизор наблюдает за воркерами: если какой-то воркер не пинговал базу m секунд, то снимаем с него все задачи
В итоге задача снимется с умершего воркера, и ее возьмет любой другой свободный
👍63🔥6💅6✍1🤔1 1
⚡Потери данных в Redis
В случаях, когда на присутствие элемента в редисе завязана какая-то бизнес логика, бОльшая чем просто ускорение доступа к данным, хорошо бы задуматься о гарантиях, которые редис предоставляет
Можно выделить два сценария потери успешно (со стороны клиента) записанных данных
1. Записали данные в RAM, но не записали на диск
С выключенным redis persistence может случиться крайне неприятная ситуация: данные льются на мастер, записываются в оперативку, реплицируются на слейв узлы. Но в один момент происходит отказ мастера. После рестарта мастер-узла он окажется пустым, т.к. ему не из чего будет восстанавливать данные. Более того, такое может случиться даже в sentinel конфигурации — мастер может упасть и рестартнуться настолько быстро, что sentinel это не задетектит и не переключит мастер
2. Записали данные на мастер, но не успели реплицировать
Предположим, у нас включен redis persistence и данные каким-то образом записываются и на диск. Здесь мы решаем вышеописанную проблему — мастер после рестарта просто восстановит данные с диска. Однако, есть другая проблема — асинхронная репликация. Мы можем записать данные на мастер, клиент получит ок с надеждой, что эти данные асинхронно долетят до реплик. Но здесь случается отказ мастера, данные не успевают дойти до реплик, и происходит переключение мастера. В итоге реплики так и не узнают про эту запись
—
Итого — первую проблему можем решать с помощью включения redis persistence. Вторую проблему не можем решать, так как редис поддерживает только асинхронную репликацию
Ставьте 👍 на этот пост, если нужен рассказ про варианты использования redis persistence
В случаях, когда на присутствие элемента в редисе завязана какая-то бизнес логика, бОльшая чем просто ускорение доступа к данным, хорошо бы задуматься о гарантиях, которые редис предоставляет
Можно выделить два сценария потери успешно (со стороны клиента) записанных данных
1. Записали данные в RAM, но не записали на диск
С выключенным redis persistence может случиться крайне неприятная ситуация: данные льются на мастер, записываются в оперативку, реплицируются на слейв узлы. Но в один момент происходит отказ мастера. После рестарта мастер-узла он окажется пустым, т.к. ему не из чего будет восстанавливать данные. Более того, такое может случиться даже в sentinel конфигурации — мастер может упасть и рестартнуться настолько быстро, что sentinel это не задетектит и не переключит мастер
2. Записали данные на мастер, но не успели реплицировать
Предположим, у нас включен redis persistence и данные каким-то образом записываются и на диск. Здесь мы решаем вышеописанную проблему — мастер после рестарта просто восстановит данные с диска. Однако, есть другая проблема — асинхронная репликация. Мы можем записать данные на мастер, клиент получит ок с надеждой, что эти данные асинхронно долетят до реплик. Но здесь случается отказ мастера, данные не успевают дойти до реплик, и происходит переключение мастера. В итоге реплики так и не узнают про эту запись
—
Итого — первую проблему можем решать с помощью включения redis persistence. Вторую проблему не можем решать, так как редис поддерживает только асинхронную репликацию
Ставьте 👍 на этот пост, если нужен рассказ про варианты использования redis persistence
👍201🔥10
⚡Redis persistence
В продолжение к предыдущему посту. В редисе есть две политики отлива данных на диск
1. RDB File (Redis Database File) — компактный снапшот-файл, хранящий всю информацию про ключи и значения на определенный момент времени
Поскольку снапшоты хранят весь снимок базы, брать их часто не очень целесообразно. Поэтому если страшно потерять данные, скажем, за последний час — то для восстановления данных после рестарта этот режим не очень подходит. Однако, этот режим удобно использовать для бэкапов: например, брать снапшот каждый час и отливать в S3
+: Компактный
+: Быстрое восстановление данных из снапшота
-: Не получится делать снимки часто, соотв-но режим не подходит, если хочется минимизировать риски потери данных
2. AOF (Append Only File) — лог write-операций. При рестарте редис просто проходится по логу и выполняет записанные операции
Когда мы делаем write-операцию, редис для быстроты делает запись только в оперативную память. Возникает вопрос — когда данные отливать на диск? Здесь есть несколько режимов:
2.1. appendfsync always: при write-операциях синхронно записываем данные и на диск. Риски потери данных минимальны, но и просадка в производительности на порядок (можно погуглить бенчмарки). Если вам нужен этот режим, вам точно нужен редис?
2.2. appendfsync no: запись на диск происходит, когда заполняется buffer memory. Периодичность отлива данных недетерменирована, поэтому не можем давать никаких гарантий
2.3. appendfsync everysec: бекграунд тредик каждую секунду сбрасывает данные на диск. Таким образом, операции записи по прежнему будут быстрыми (пишем только в ram), но будут возможны потери данных за последнюю секунду
+: Подходит, если нужно минимизировать риски потери данных
-: Менее компактный нежели RDB
-: Дольше восстановление данных нежели из RDB
Также никто не запрещает комбинировать эти подходы — RDB для бэкапов, а AOF для сохранности при рестартах
В продолжение к предыдущему посту. В редисе есть две политики отлива данных на диск
1. RDB File (Redis Database File) — компактный снапшот-файл, хранящий всю информацию про ключи и значения на определенный момент времени
Поскольку снапшоты хранят весь снимок базы, брать их часто не очень целесообразно. Поэтому если страшно потерять данные, скажем, за последний час — то для восстановления данных после рестарта этот режим не очень подходит. Однако, этот режим удобно использовать для бэкапов: например, брать снапшот каждый час и отливать в S3
+: Компактный
+: Быстрое восстановление данных из снапшота
-: Не получится делать снимки часто, соотв-но режим не подходит, если хочется минимизировать риски потери данных
2. AOF (Append Only File) — лог write-операций. При рестарте редис просто проходится по логу и выполняет записанные операции
Когда мы делаем write-операцию, редис для быстроты делает запись только в оперативную память. Возникает вопрос — когда данные отливать на диск? Здесь есть несколько режимов:
2.1. appendfsync always: при write-операциях синхронно записываем данные и на диск. Риски потери данных минимальны, но и просадка в производительности на порядок (можно погуглить бенчмарки). Если вам нужен этот режим, вам точно нужен редис?
2.2. appendfsync no: запись на диск происходит, когда заполняется buffer memory. Периодичность отлива данных недетерменирована, поэтому не можем давать никаких гарантий
2.3. appendfsync everysec: бекграунд тредик каждую секунду сбрасывает данные на диск. Таким образом, операции записи по прежнему будут быстрыми (пишем только в ram), но будут возможны потери данных за последнюю секунду
+: Подходит, если нужно минимизировать риски потери данных
-: Менее компактный нежели RDB
-: Дольше восстановление данных нежели из RDB
Также никто не запрещает комбинировать эти подходы — RDB для бэкапов, а AOF для сохранности при рестартах
Telegram
Microservices Thoughts
⚡Потери данных в Redis
В случаях, когда на присутствие элемента в редисе завязана какая-то бизнес логика, бОльшая чем просто ускорение доступа к данным, хорошо бы задуматься о гарантиях, которые редис предоставляет
Можно выделить два сценария потери успешно…
В случаях, когда на присутствие элемента в редисе завязана какая-то бизнес логика, бОльшая чем просто ускорение доступа к данным, хорошо бы задуматься о гарантиях, которые редис предоставляет
Можно выделить два сценария потери успешно…
👍40🔥15
⚡CQRS и check-then-act
В CQRS (Command and Query Responsibility Segregation) мы разделяем запросы на чтение (queries) и запросы на модификацию (commands). Зачастую это ведет к тому, что у нас есть две базы данных: куда пишем, и откуда читаем. Данные из write-базы асинхронно реплицируются на read-базу
Далее представим, что мы хотим сделать какую-то выборку, проверить ее на соответствие условиям (check), и далее сделать модифицирующий запрос (then act)
Для примера возьмем модель данных, которая уже была в постах выше - с ticket и assignee
Хотим назначить тикет на оператора, если на него сейчас назначено меньше 4х тикетов:
1. read-db: считаем кол-во тикетов
2. write-db: если count < 4, то назначаем новый тикет по assignee_id
Далее представим что приходят два конкурентных запроса
[rq1] read-db: считаем кол-во тикетов, count = 3
[rq2] read-db: считаем кол-во тикетов, count = 3
[rq1] write-db: назначаем новый тикет
[rq2] write-db: назначаем новый тикет
... Данные асинхронно реплицируются, count = 4 ...
... Данные асинхронно реплицируются, count = 5 ...
Итого получаем 5 назначенных тикетов, и мы нарушили инвариант
---
Решить эту проблему можно с помощью оптимистических блокировок. К сущности assignee добавляется поле update_id:
В таком случае назначение будет происходить не по assignee_id, а по паре (assignee_id, update_id)
[rq1] read-db: считаем кол-во тикетов, count = 3, update_id = 0
[rq2] read-db: считаем кол-во тикетов, count = 3, update_id = 0
[rq1] write-db: назначаем новый тикет по update_id = 0, получаем update_id = 1
[rq2] write-db: назначаем новый тикет по update_id = 0, ошибка, 0 != 1
... Данные асинхронно реплицируются, count = 4, update_id = 1 ...
Таким образом, когда [rq2] попытается назначить тикет, он увидит в write-db неактуальную версию и ничего не сделает/кинет ошибку, что сохранит нам инвариант
В CQRS (Command and Query Responsibility Segregation) мы разделяем запросы на чтение (queries) и запросы на модификацию (commands). Зачастую это ведет к тому, что у нас есть две базы данных: куда пишем, и откуда читаем. Данные из write-базы асинхронно реплицируются на read-базу
Далее представим, что мы хотим сделать какую-то выборку, проверить ее на соответствие условиям (check), и далее сделать модифицирующий запрос (then act)
Для примера возьмем модель данных, которая уже была в постах выше - с ticket и assignee
assignee(id);
ticket(id, assignee_id);
Хотим назначить тикет на оператора, если на него сейчас назначено меньше 4х тикетов:
1. read-db: считаем кол-во тикетов
2. write-db: если count < 4, то назначаем новый тикет по assignee_id
Далее представим что приходят два конкурентных запроса
[rq1] read-db: считаем кол-во тикетов, count = 3
[rq2] read-db: считаем кол-во тикетов, count = 3
[rq1] write-db: назначаем новый тикет
[rq2] write-db: назначаем новый тикет
... Данные асинхронно реплицируются, count = 4 ...
... Данные асинхронно реплицируются, count = 5 ...
Итого получаем 5 назначенных тикетов, и мы нарушили инвариант
---
Решить эту проблему можно с помощью оптимистических блокировок. К сущности assignee добавляется поле update_id:
assignee(id, update_id);
ticket(id, assignee_id);
В таком случае назначение будет происходить не по assignee_id, а по паре (assignee_id, update_id)
[rq1] read-db: считаем кол-во тикетов, count = 3, update_id = 0
[rq2] read-db: считаем кол-во тикетов, count = 3, update_id = 0
[rq1] write-db: назначаем новый тикет по update_id = 0, получаем update_id = 1
[rq2] write-db: назначаем новый тикет по update_id = 0, ошибка, 0 != 1
... Данные асинхронно реплицируются, count = 4, update_id = 1 ...
Таким образом, когда [rq2] попытается назначить тикет, он увидит в write-db неактуальную версию и ничего не сделает/кинет ошибку, что сохранит нам инвариант
👍50✍5💅5🔥2 1
⚡Geohash
Обычно координаты задаются парой чисел - долготой и широтой. Это позволяет описать любую точку на земле, однако работать с таким представлением весьма сложно (см. например формулу расстояния между двумя точками)
Другим представлением координат является geohash:
1. Плоскость земли делится на 32 участка
2. Каждому участку приписывается определенная буква/цифра из Base32
Мы научились одним символом делить пространство на 32 участка. Чтобы получить бОльшую точность, выбираем один из участков и делаем ту же процедуру
1. Плоскость участка делится на 32 под-участка
2. Каждому под-участку приписывается определенная буква/цифра из Base32
И так далее
---
Точность получается примерно следующая:
1 символ ~ 1000 км2
2 символа ~ 1000 км2 / 32
...
n+1 символов ~ 1000 км2 / 32^n
При n=6 точность будет будет около 1м2
---
Основные плюсы такого представления:
1. Не пара чисел, а одна строка - легче индексировать
2. Если пара точек имеет одинаковый префикс geohash-а, то они находятся в одном участке. Это позволяет очень быстро выполнять запросы на поиск ближайших точек с нужной точностью
3. Можно выбрать любую требуемую для конкретной задачи точность и экономить место на хранение, если скажем точности в 1м2 будет достаточно
---
Идея "Hierarchical Spatial Index", когда пространство делится на большие участки, большие участки делятся на участки поменьше и тд, весьма популярна и используется многими компаниями. Про подход Uber, где они делят пространство на шестиугольники можно почитать здесь
Обычно координаты задаются парой чисел - долготой и широтой. Это позволяет описать любую точку на земле, однако работать с таким представлением весьма сложно (см. например формулу расстояния между двумя точками)
Другим представлением координат является geohash:
1. Плоскость земли делится на 32 участка
2. Каждому участку приписывается определенная буква/цифра из Base32
Мы научились одним символом делить пространство на 32 участка. Чтобы получить бОльшую точность, выбираем один из участков и делаем ту же процедуру
1. Плоскость участка делится на 32 под-участка
2. Каждому под-участку приписывается определенная буква/цифра из Base32
И так далее
---
Точность получается примерно следующая:
1 символ ~ 1000 км2
2 символа ~ 1000 км2 / 32
...
n+1 символов ~ 1000 км2 / 32^n
При n=6 точность будет будет около 1м2
---
Основные плюсы такого представления:
1. Не пара чисел, а одна строка - легче индексировать
2. Если пара точек имеет одинаковый префикс geohash-а, то они находятся в одном участке. Это позволяет очень быстро выполнять запросы на поиск ближайших точек с нужной точностью
3. Можно выбрать любую требуемую для конкретной задачи точность и экономить место на хранение, если скажем точности в 1м2 будет достаточно
---
Идея "Hierarchical Spatial Index", когда пространство делится на большие участки, большие участки делятся на участки поменьше и тд, весьма популярна и используется многими компаниями. Про подход Uber, где они делят пространство на шестиугольники можно почитать здесь
👍88🔥25🤯4💅3 1