Распределённые системы

CRDT - Conflict-free Replicated Data Types

Цели урока

  • Понимать, почему обычные структуры данных не работают при конкурентных обновлениях
  • Реализовывать G-Counter, PN-Counter, LWW-Register, OR-Set
  • Различать CvRDT и CmRDT и выбирать подходящий тип
  • Знать реальные системы, использующие CRDT (Redis, Figma, Automerge)

Предварительные знания

  • Eventual consistency и CAP-теорема (урок ds-02-cap-theorem)
  • Логические часы и Lamport timestamps (урок ds-04-clocks)
  • Базовое понимание репликации и конфликтов при multi-master

Figma позволяет 10 дизайнерам редактировать один файл одновременно без конфликтов. Apple Notes синхронизирует изменения между iPhone и Mac без серверного арбитра. Под капотом - математика CRDT: коммутативные операции с гарантией сходимости.

  • **Redis Enterprise** - G-Counter и PN-Counter для Active-Active репликации между дата-центрами на разных континентах без блокировок
  • **Figma** - custom document CRDT для collaborative editing: позиции, цвета, слои сливаются без конфликтов
  • **Apple iCloud / Notes** - CRDT для синхронизации между устройствами офлайн и онлайн
  • **Automerge/Yjs** - библиотеки JSON CRDT, используемые в Linear, Actual Budget, Obsidian Sync
  • **Amazon Shopping Cart** - исторически один из первых примеров AP + CRDT-подобной семантики (Dynamo Paper 2007)

Рождение CRDT: Shapiro и Preguica, 2011

Термин CRDT появился в статье Марка Шапиро и Нуно Прегика (INRIA, 2011): "Conflict-free Replicated Data Types". Авторы формализовали условия, при которых структура данных гарантирует eventual consistency без координации. До этого похожие идеи использовались в Dynamo (Amazon, 2007) и Bayou (Xerox PARC, 1994), но без единой математической основы. Сегодня CRDT лежат в основе collaborative tools, используемых сотнями миллионов людей ежедневно.

Проблема конкурентных обновлений

**Figma, 2020. 2 пользователя одновременно редактируют один слой: один меняет цвет, второй - позицию. Сервер получает оба изменения с разницей в 3 мс. Чьё изменение правильное?** Традиционный ответ - "последний записал": один из дизайнеров теряет работу. Figma решила эту задачу через CRDT - структуры данных, которые сливаются без конфликтов математически.

**CRDT (Conflict-free Replicated Data Type)** - структура данных с гарантией: любые два узла, получившие одинаковый набор операций (в любом порядке), придут к одинаковому состоянию. Без координирующего сервера, без блокировок.

Обычный счётчик при конкурентном обновлении ломается: узел A читает значение 5, прибавляет 1, пишет 6. Узел B читает то же значение 5, прибавляет 1, пишет 6. После синхронизации счётчик равен 6 вместо 7. Одна операция потеряна.

Результат без CRDT: счётчик равен 6, хотя было выполнено два инкремента от 5. Одна операция потеряна навсегда.

Подход к конфликтамПримерПроблема
Last-Writer-WinsDynamoDB без векторных часовПотеря данных при конкурентных записях
Serializable transactionsPostgreSQL SERIALIZABLEВысокий latency, требует координации
Manual conflict resolutionGit merge конфликтыТребует участия человека
CRDTFigma, Automerge, Redis EnterpriseБез потерь, без координации

CRDT нужны только для offline-first приложений

CRDT решают задачу конкурентных обновлений в любой AP-системе, где узлы пишут независимо

Redis Enterprise использует CRDT для Active-Active geo-replication между дата-центрами с нулевым latency на запись. Приложение не offline - просто ближайший дата-центр не ждёт подтверждения от удалённых реплик.

Два узла читают счётчик = 10. Оба прибавляют 1 и сохраняют. После синхронизации без CRDT значение будет:

G-Counter и PN-Counter: счётчики без конфликтов

**G-Counter (Grow-only Counter)** - первый CRDT, понятный интуитивно. Вместо одного значения каждый узел хранит свой собственный счётчик. Глобальное значение - сумма всех. При слиянии берётся max по каждому узлу.

G-Counter обладает тремя математическими свойствами: **коммутативность** (merge(A,B) = merge(B,A)), **ассоциативность** (merge(merge(A,B),C) = merge(A,merge(B,C))), **идемпотентность** (merge(A,A) = A). Из этих трёх следует eventual consistency.

G-Counter умеет только расти. Для полноценного счётчика с декрементом используют **PN-Counter (Positive-Negative Counter)** - два G-Counter: один для инкрементов, второй для декрементов.

Redis Enterprise: Active-Active репликация

Redis Enterprise использует PN-Counter для репликации между географически распределёнными кластерами. Клиент пишет в ближайший узел (0 мс дополнительный latency). Узлы асинхронно обмениваются состоянием. Счётчики лайков, просмотров, рейтингов - всё без координации. При сетевом разрыве оба дата-центра продолжают работать независимо и корректно сливаются после восстановления связи.

Node A: counters={A:3, B:2}. Node B: counters={A:1, B:5}. После merge значение G-Counter равно:

LWW-Register и OR-Set: регистр и множество

**LWW-Register (Last-Writer-Wins Register)** - CRDT для одиночного значения. При конкурентных записях побеждает та, у которой timestamp больше. Простота подкупает - но именно здесь часто ошибаются.

LWW с Date.now(): если часы двух узлов расходятся на 100 мс (реально в AWS multi-AZ), более поздняя запись с отставшими часами будет потеряна. Google Spanner решает это через GPS+атомные часы. В обычных системах используют HLC (Hybrid Logical Clock) из урока о логических часах.

**OR-Set (Observed-Remove Set)** решает классическую проблему: что происходит когда add и remove одного элемента происходят одновременно на разных узлах? Каждая операция add получает уникальный тег. Remove удаляет только наблюдаемые теги - те, которые уже были известны узлу в момент remove.

**Семантика OR-Set при конфликте:** Node A делает add(apple) одновременно с тем, как Node B делает remove(apple). После merge - apple присутствует. Побеждает add. Это интуитивно: кто-то добавил элемент, не зная об удалении - его намерение сохранено.

LWW всегда теряет данные при конкурентных записях

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

В документных CRDT (Automerge, Yjs) каждое поле - отдельный LWW-Register. Concurrent edit поля 'color' и поля 'position' не конфликтуют вообще. Только concurrent edit одного поля требует более сложной семантики.

Node A делает add('item') с тегом T1. Одновременно Node B делает remove('item'), зная только о теге T0 (более раннем). После merge 'item' в множестве?

CvRDT, CmRDT и реальные применения

**Теория CRDT делит все структуры на два класса.** CvRDT (state-based) передают полное состояние при синхронизации - merge гарантирует сходимость. CmRDT (operation-based) передают только операции - нужен reliable broadcast. Все примеры выше - CvRDT.

ТипЧто передаётсяТребования к сетиПлюсы
CvRDT (State-based)Полное состояниеЛюбая сеть, потери OKПростая реализация merge
CmRDT (Op-based)Операции (delta)Exactly-once deliveryМеньше трафика
Delta-CRDTТолько изменения состоянияAt-least-once + идемпотентностьБаланс трафика и простоты
СистемаCRDTПрименение
Redis EnterpriseG-Counter, PN-Counter, LWW-RegisterActive-Active Geo-Replication
RiakOR-Set, LWW-Map, G-CounterKey-value хранилище без координации
FigmaCustom document CRDTCollaborative editing: цвет, позиция, слои
Apple Notes / iCloudCRDT на основе операцийSync между устройствами без конфликтов
Automerge / YjsJSON CRDT (OR-Map + RGA для текста)Offline-first приложения
Google DocsOT (Operational Transform) - предшественник CRDTReal-time collaborative text

**CRDT vs OT (Operational Transformation):** Google Docs использует OT - более старый подход, требующий центрального сервера для упорядочивания операций. CRDT работает без центра. Именно поэтому Figma, Apple и новые collaborative tools переходят на CRDT.

Automerge: JSON-документ как CRDT

Automerge превращает любой JSON-объект в CRDT автоматически. Каждое поле - LWW-Register. Массивы - RGA (Replicated Growable Array). Текст - специальный CRDT с позиционными идентификаторами. Два пользователя редактируют офлайн - после синхронизации получают детерминированный результат без потерь. Используется в Linear, Actual Budget, ряде LSP-инструментов.

CRDT гарантируют, что все операции будут выполнены

CRDT гарантируют сходимость состояния, но не отсутствие потерь при LWW-семантике

При конкурентной записи одного LWW-Register одно из значений будет отброшено по timestamp. CRDT не теряет ни одну операцию structural merge-а (G-Counter: все инкременты учтены), но при LWW одно значение замещает другое. Потеря данных возможна, она просто детерминирована и предсказуема.

Вопросы для размышления

  • Система хранит рейтинг продукта как сумму оценок пользователей. Пользователей миллионы, оценки приходят из 5 дата-центров параллельно. Какой CRDT подходит и какие трейдофы он несёт?

Связанные уроки

  • ds-08-vector-clocks — CRDTs rely on vector clocks for conflict tracking
  • ds-09-gossip-protocols — Gossip disseminates CRDT state across replicas
  • dist-12-consistency — CRDTs provably achieve eventual consistency
  • dist-11-replication — CRDT-based replication avoids coordination overhead
  • crypto-04-algebraic-structures
CRDT - Conflict-free Replicated Data Types

0

1

Войти

Приложение работает в условиях нестабильной сети, иногда теряет пакеты. Какой тип CRDT предпочтительнее?