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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1. Read-aside caching

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

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

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

2. Write-aside caching

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

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

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

3. Full caching

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

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

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

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

1. id + parent_id

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


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

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

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

2. id, path + parent_id, parent_path

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

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

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

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

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


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

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

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

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

Один из алгоритмов для ограничения числа входящих запросов. На данный момент используется во многих системах, например, AWS.

Принцип работы:

Есть ограниченный набор токенов, наличие токена = право исполнить запрос.

Когда приходит входящий запрос, сначала проверяем, есть ли свободный токен, если есть - забираем его и исполняем запрос, если нет - отдаем 429.

И поскольку токены назад не возвращаются, их надо как-то восстанавливать. Этим занимается фоновый процесс, который по крону (обычно раз в секунду) восстанавливает число свободных токенов до максимума.
👍33🔥72💅1
Матчится ли контент с названием канала?)
Anonymous Poll
60%
Да
40%
Нет
🔥5221
Continuous profiling

Continuous profiling - техника постоянного сбора данных о ресурсах, затрачиваемых приложением

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

Постоянно профилирование позволяет строить графики и анализировать ретроспективные данные по затратам CPU, RAM и т.п. И одна из наиболее полезных вещей - построение flame graphs, которые позволяют посмотреть, на что именно приложение тратило память или процессор за определенный промежуток времени (например, во время какого-то инцидента)

Из популярных тулов для этих целей существует Grafana Pyroscope, который умеет запускаться как из приложения, так и отдельным агентом. Далее он собирает метрики и отливает их в графану, которая уже строит красивые графики
🔥17💅5👍4111
Каскадное удаление за один запрос

Нет, речь не про 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
Взаимно-рекурсивные внешние ключи

Самый частый юзкейс:

- Есть главная сущность
- Есть дочерние сущности
- Главная сущность ссылается на конкретную дочернюю

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

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💅333
⚡️Почему Redis быстрый, несмотря на однопоточность

Архитектура редиса устроена так, что все клиентские команды выполняются лишь одним тредом. Это позволяет избавиться от оверхеда на context switch и синхронизацию потоков, но встает вопрос о производительности

Если бы редис синхронно делал следующие шаги для каждого клиентского запроса:

- tcp handshake
- ожидание команд, отправка результатов
- tcp termination

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

Именно поэтому редис не блокируется на IO - т.е. во время ожидания позволяет потоку заниматься другими полезными вещами. Это является основным принципом обеспечения “параллельной” обработки запросов на одном потоке

Как оно устроено внутри:

epoll() - метод, предоставляемый линуксом, который позволяет проверить, что хотя бы один файловый дескриптор готов к IO. В нашем случае, например, если в установленное между клиентом и редис-сервером tcp соединение от клиента пришли какие-то данные

И далее на этом единственном треде крутится event loop с примерно такой логикой

while true:
if epoll():
do_something_useful()


Таким образом, мы заменили блокирующее ожидание новых данных на периодические проверки, между которыми можно заниматься полезной работой
👍60🤯15🔥4💅211
Синхронная реплика не всегда повторяет мастер

Допустим, у нас есть конфигурация мастер + синхронная реплика

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

Почему такое может происходить? В postgres можно выбрать один из режимов синхронного коммита

Чем они отличаются:

off - не дожидаясь записи в WAL на мастере, говорим клиенту, что транзакция успешно завершилась

local - гарантируем запись в WAL на мастере

remote_write - гарантируем запись в WAL на мастере + доставку изменений до реплики

on - гарантируем запись в WAL на мастере + доставку и запись в WAL на реплике

remote_apply - гарантируем запись в WAL на мастере + коммит изменений на реплике

По умолчанию выбирается режим on, который позволяет закомитить транзакцию, не дождавшись применения изменений на реплике. И соотв-но какое-то время на реплике будут лежать неактуальные данные
👍56💅7🔥33
Частые архитектурные паттерны

При разработке небольших сервисов можно часто встретить повторяющиеся архитектурные паттерны. Мой личный топ 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🔥61
Привет! Внезапно осознал, что канал уже почти как год ведется под некоторой пеленой анонимности, пора исправлять

Меня зовут Александр Федькин, мне 21, сейчас руковожу командой бэкендеров в Яндексе

Мы разрабатываем:

- No-Code платформу для автоматизации клиентского сервиса: сейчас в ней можно в UI настраивать чат-ботов и пайплайны обработки пользовательских обращений, а также привязывать их запуск к некоторым событиям

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

До этого успел окончить программную инженерию матмеха СПбГУ и поучиться в питерском CSC до его закрытия

В канал обычно пишу про какие-то занятные вещи с работы либо когда прочитал/нашел что-то интересное



Для новичков: топ постов за прошедший год:

- Каскадное удаление за один запрос
- Индексирование больших таблиц
- Consistent hashing
- Почему батчевые update/delete не безопасны
- Почему Redis быстрый, несмотря на однопоточность
👍97🔥41🤯32💅9🤔22
Microservices Thoughts pinned «Привет! Внезапно осознал, что канал уже почти как год ведется под некоторой пеленой анонимности, пора исправлять Меня зовут Александр Федькин, мне 21, сейчас руковожу командой бэкендеров в Яндексе Мы разрабатываем: - No-Code платформу для автоматизации…»
Монотонность и PostgreSQL

Когда возникает потребность в монотонно возрастающих колонках обычно используются 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💅115
В процессе разработки зачастую возникает множество проблем, которые могут затормозить проект и снизить качество итогового продукта. Эти проблемы не ограничиваются только написанием кода: проблема может возникнуть на любом этапе, начиная с распределения задач в команде и заканчивая сниженным качеством итогового продукта вследствие неоптимального процесса тестирования

Часто можно встретиться со следующим:

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

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

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

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

Одним из решений, которое помогает минимизировать описанные выше проблемы, являются sensible defaults — внутренние стандарты команды/подразделения, описывающие кто чем занимается, как надо тестировать, каким правилам должна соответствовать архитектура проекта и так далее. Заранее установленные стандарты делают процесс разработки более предсказуемым и позволяют заранее иметь ответы на большинство возникающих вопросов
👍17🔥81
Entity state transfer

В микросервисной архитектуре часто возникает задача, чтобы один микросервис реагировал на изменения сущностей другого микросервиса. Для примера возьмем следующую задачу:

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🔥1643💅3
Channel name was changed to «Microservices Thoughts»
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, и тд. Правила могут динамически добавлятся, что позволяет управлять нагрузкой, если к примеру мощности шардов не одинаковые
👍41💅8🤯11
Вертикальное партицирование

В отличие от горизонтального партицирования / шардирования таблица разделяется не по строкам, а по столбцам

Например, из


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. Если фильтрация по каждому условию по отдельности возвращает много строк, то запрос начнет работать сильно медленнее
👍3210🔥3💅22
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, но также и переживать отказы некоторых узлов
👍52🤯6💅41