Консистентность и CAP¶
В одном инстансе всё просто: записал — прочитал. В распределённой системе у тебя сразу две головные боли: «успели ли реплики догнать?» и «что мне показать клиенту, пока не успели?». Ответы — модели консистентности и теоремы CAP / PACELC.
Модели консистентности¶
🧩 Простыми словами. Консистентность — это правила игры: что клиент видит после записи. Можешь видеть мгновенно везде (strong), а можешь «через минуту, плюс-минус» (eventual). Чем строже правила — тем дороже система.
Strong consistency¶
Любое чтение после успешной записи видит запись. Звучит просто, но требует координации между всеми репликами (consensus, кворум, синхронная репликация) — дорого по latency.
sequenceDiagram
Client->>Primary: write x=10
Primary->>R1: replicate
Primary->>R2: replicate
R1-->>Primary: ACK
R2-->>Primary: ACK
Primary-->>Client: OK (committed everywhere)
Client->>R1: read x
R1-->>Client: 10
Примеры: Spanner, etcd, Zookeeper, Postgres synchronous_commit=remote_apply.
Eventual consistency¶
Запись попадёт на все реплики «когда-нибудь». Между записью и пропагацией клиент может увидеть старое значение.
sequenceDiagram
Client->>Primary: write x=10
Primary-->>Client: OK (только primary знает)
Note over R1,R2: async replication
Client->>R1: read x
R1-->>Client: 9 (старое!)
Note over Primary,R2: пропагация догнала
Client->>R1: read x
R1-->>Client: 10
Примеры: DynamoDB (по умолчанию), Cassandra, S3 (раньше), Postgres async replicas.
Read-your-writes¶
Слабее strong, но сильнее eventual: клиент сразу видит свои собственные записи, но чужие — eventually. Реализуется через session pinning (читаешь с того же шарда / реплики, куда писал) или через high-water-mark в куках.
Monotonic reads¶
Если клиент увидел версию 5, потом он не увидит версию 4. Тоже sticky-сессия.
Causal consistency¶
Если событие B зависит от события A, то все увидят сначала A, потом B. Реализуется через vector clocks или version vectors. Сложно, но иногда нужно (комменты, threads).
📊 Иерархия.
Чем выше — тем строже и дороже.
CAP-теорема¶
🧩 Простыми словами. В распределённой системе при сетевом сбое (Partition) ты можешь иметь либо строгую согласованность (Consistency), либо доступность (Availability) — но не оба одновременно. P случается всегда, выбор — между C и A.
graph TD
P((Partition tolerance<br/>не выбирается, реальность))
C[Consistency]
A[Availability]
P --- C
P --- A
C -.-> CP[CP: etcd, HBase,<br/>Spanner *]
A -.-> AP[AP: Cassandra,<br/>DynamoDB, Couch]
⚙️ Под капотом. Когда ноды не видят друг друга:
- CP — отказывает в записях/чтениях, чтобы не отдать рассогласованные данные. Выбираешь, если страшно отдать неверный баланс счёта.
- AP — продолжает отвечать с локальных данных, потенциально устаревших. Выбираешь, если лучше показать слегка старую ленту, чем 503.
💥 Ловушки.
- CAP не про обычную работу. Это про partition. Если сети ок — ты обычно получаешь и C, и A.
- «Spanner ломает CAP» — нет, не ломает: он CP. Когда сеть рвётся, отказывает. Просто его сеть Google настолько надёжна, что вы это редко замечаете.
- Неверная подмена. «Eventual ⇔ AP» — нет, eventual — это про модель данных, AP/CP — про поведение в partition.
PACELC — как читать систему в обычное время¶
CAP молчит про latency. PACELC говорит:
Partition? — выбирай A or C. Else — выбирай Latency or Consistency.
| Система | Partition | Else |
|---|---|---|
| Spanner | PC (отказ) | EC (медленно, но точно) |
| DynamoDB | PA | EL (быстро, eventual) |
| Cassandra | PA | EL |
| Postgres (single primary) | PC | EC |
| MongoDB (default) | PA | EC (доминантно) |
🧩 Простой смысл. PACELC помогает понять не только «что в шторм», но и «какова цена твоего обычного запроса». Hot path с Cassandra — миллисекунды. Hot path со Spanner — десятки миллисекунд (consensus стоит).
Реальные системы и их выбор¶
DynamoDB / Cassandra (AP, EL)¶
- Eventual consistency by default. Можно явно попросить strong consistent read ценой latency.
- Quorum: write ack от W нод, read от R нод; если
R + W > N— strong-ish consistency. - Для Go — официальный AWS SDK или gocql для Cassandra.
etcd / Zookeeper (CP, EC)¶
- Raft / Zab consensus.
- Маленькие кластера (3–5 нод). Используются для service discovery, coordination, config.
- Не для большого throughput.
Postgres (CP, EC)¶
- Один primary, sync или async реплики. Sync = strong, но lag убивает throughput.
- Под высокий throughput читают с реплик с риском stale read.
Spanner / CockroachDB (CP, EC)¶
- Globally distributed strong consistency через TrueTime / hybrid logical clocks.
- Дорого по latency (commit ≈ 100ms cross-region).
🛠 Применение в Go-проекте¶
// Postgres: явно используем primary для read-after-write
type DB struct {
primary *pgxpool.Pool
replica *pgxpool.Pool
}
func (db *DB) GetUser(ctx context.Context, id int64, opts ReadOpts) (*User, error) {
pool := db.replica
if opts.MustBeFresh {
pool = db.primary // strong consistency
}
var u User
err := pool.QueryRow(ctx, "SELECT ... FROM users WHERE id=$1", id).Scan(...)
return &u, err
}
Для read-your-writes без хитрых клиентских ухищрений: пиши и читай с primary короткое время после записи, потом перекидывайся на реплику.
🔥 Пример: «корзина в e-commerce»¶
graph LR
U[User adds item] --> A[API]
A --> Cart[(Cart store<br/>Redis / Dynamo)]
A --> Q[Kafka events]
Q --> Inv[Inventory service]
Inv --> InvDB[(Postgres strong)]
- Cart — AP, eventual. Если две вкладки добавили одинаковый товар — мерж по client-side. Latency важнее точности.
- Inventory — CP, strong. Списание со склада строго: иначе oversell.
- Checkout — saga (см. microservices.md) объединяет оба.
❌ Типичные ошибки¶
- «Сделаем eventual для всего, так быстрее» → потом на собесе клиент видит списание денег, но не видит купленный билет.
- «Spanner = CA» — нет, CP. Никогда не CA в распределённой системе.
- Sticky session как единственное решение read-your-writes — ломается при failover.
- Игнорят replica lag в монитоинге → редкие баги «то есть, то нет».
🤖 Что спрашивает AI-ментор¶
- Объясни на пальцах разницу между strong и eventual consistency.
- Сформулируй CAP-теорему. Что значит «теорема не работает в обычной жизни»?
- Зачем PACELC, если есть CAP?
- Когда выбирать AP, а когда CP? Приведи по примеру из жизни.
- Что такое read-your-writes consistency и как его реализовать в Go-сервисе?
📊 Уровни глубины¶
L1. Знаешь термин CAP, можешь сказать «strong и eventual». Понимаешь, что после записи в Postgres replica может не успеть.
L2. Различаешь все 5 моделей (strong, eventual, RYW, monotonic, causal). Можешь обосновать выбор для конкретного use-case. Знаешь PACELC, отличаешь от CAP.
L3. Спорил с командой о выборе AP vs CP с реальными цифрами latency. Помнишь расхождение primary/replica в incident'е. Знаешь quorum (R + W > N), vector clocks, hybrid logical clocks. Читал Spanner paper.
📝 Подумай¶
- Сервис лайков. Нужна ли strong consistency на инкременте счётчика лайков? Что выбираешь?
- У тебя Postgres с 3 async-репликами. Как сделать read-your-writes для пользователя после редактирования профиля?
- Что вернёт Cassandra при
R=1, W=1, N=3после записи и сразу чтения с другой ноды?
Ответ
- Не нужна. Лайки — eventual. Считаешь counter в Redis/Cassandra, точные числа никому не критичны. Расхождение в 1–2 — норма. Если хочешь точно — пишешь в Kafka и агрегируешь, но это медленнее.
- Варианты: (a) на короткий период после записи (например 5 секунд) читать
с primary; (b) хранить
last_write_tsпользователя в куке/JWT, и пока replica не догнала эту метку — читать с primary; (c) sticky to primary через session. Самый частый прод-вариант — (a) или (c). - Может вернуть старое значение. R=1 + W=1 = 2, но N=3, и
R+W > Nне выполняется (2 < 3+1). Чтение с реплики, куда запись ещё не пришла, увидит старое.
Дальше: caching.md — как делать быстро, не теряя консистентности.