CAP, идемпотентность и модели согласованности
Зачем знать: Распределённые системы живут в мире компромиссов. CAP-теорема, PACELC, модели consistency и идемпотентность — это фундамент, на котором строятся платежи, очереди, репликация и любая интеграция между сервисами. Middle Go-разработчик должен уметь объяснить, почему банковская транзакция использует CP, а лента новостей — AP, и как написать идемпотентный HTTP-handler, который выдерживает шторм retry от webhook-провайдера.
Содержание
Заголовок раздела «Содержание»- Концепция: CAP, PACELC, модели consistency
- Глубже: идемпотентность, реализация Idempotency-Key, BASE vs ACID
- Gotchas
- Real cases
- Вопросы (25)
- Practice
- Источники
1. Концепция
Заголовок раздела «1. Концепция»1.1 CAP теорема (Brewer 2000, Gilbert-Lynch 2002)
Заголовок раздела «1.1 CAP теорема (Brewer 2000, Gilbert-Lynch 2002)»Eric Brewer в 2000 году на PODC keynote сформулировал гипотезу, формально доказанную Gilbert и Lynch в 2002. Теорема говорит: в распределённой системе из трёх свойств — Consistency, Availability, Partition tolerance — при возникновении сетевого разделения (partition) можно сохранить только два.
Consistency /\ / \ / \ / \ / CP \ / \ / CA (миф) \ / \ /________________\Partition Availability toleranceОпределения:
- Consistency (C): все ноды видят одинаковое состояние данных в одно время. Каждый read возвращает либо последний write, либо ошибку.
- Availability (A): каждый запрос получает ответ (success или error), без гарантии актуальности. Никаких таймаутов или зависаний.
- Partition tolerance (P): система продолжает работать при потере сообщений между узлами (network partition).
Practical insight: P неизбежен в реальной распределённой системе. Сеть рвётся, switch’и падают, rack’и теряют power. Поэтому выбор реально стоит между CP и AP во время partition.
Примеры:
- CP: PostgreSQL с синхронной репликацией, etcd, ZooKeeper, MongoDB (в strong-consistency режиме), HBase. При partition — ноды на minority side не отвечают на writes.
- AP: Cassandra (с tunable consistency), DynamoDB (eventually consistent reads), Riak, CouchDB. При partition — все ноды отвечают, но могут давать stale data.
⚠️ CA — это миф. Система без partition tolerance может работать только на единственной машине. В распределённой среде partition неизбежен.
1.2 PACELC расширение (Daniel Abadi, 2010)
Заголовок раздела «1.2 PACELC расширение (Daniel Abadi, 2010)»CAP описывает только поведение при partition. Но 99% времени partition нет — что тогда? Abadi расширил CAP:
If Partition (P) — choose between Availability (A) and Consistency (C); Else (E) — choose between Latency (L) and Consistency (C).
PACELC классификация:
PA/EL (max availability, min latency): Cassandra, Riak, DynamoPA/EC (avail при partition, consist иначе): MongoDB (default)PC/EL (consist при partition, low latency иначе): редкий случайPC/EC (strong consistency всегда): HBase, BigTable, VoltDBTrade-off в нормальной работе: хочешь strong consistency — плати latency (синхронная репликация на N реплик). Хочешь low latency — мирись с eventual consistency (асинхронная репликация, чтение с любой реплики).
1.3 Модели согласованности (consistency models)
Заголовок раздела «1.3 Модели согласованности (consistency models)»От самой строгой к самой слабой:
Strict (Strong) consistency
Заголовок раздела «Strict (Strong) consistency»Каждый read видит результат последнего write мгновенно. Невозможно в распределённой системе (нарушает теорию относительности: нет понятия “одновременно” при разнесённых узлах).
Linearizability (Herlihy-Wing 1990)
Заголовок раздела «Linearizability (Herlihy-Wing 1990)»“Single-copy semantics”. Операции кажутся атомарными и упорядоченными по real-time. Если op B стартовала после завершения op A, B видит результат A.
Это самая сильная достижимая модель. Реализована: etcd, ZooKeeper, Spanner (через TrueTime), Aurora с raft consensus.
⚠️ Linearizability ≠ serializability. Linearizability — про read/write одного объекта. Serializability — про транзакции (multi-object). Strict serializability = serializability + linearizability.
Sequential consistency (Lamport 1979)
Заголовок раздела «Sequential consistency (Lamport 1979)»Операции выглядят упорядоченными в некотором глобальном порядке, и порядок каждого процесса сохраняется. Но real-time порядок может нарушаться.
Слабее linearizability. Достаточно для CPU memory models, не подходит для финансов.
Causal consistency
Заголовок раздела «Causal consistency»Операции, связанные причинной зависимостью (happens-before, см. Lamport timestamps), упорядочены. Независимые — могут быть в любом порядке.
Пример: если ты лайкнул пост, потом написал коммент “круто!”, другой пользователь не должен увидеть коммент раньше лайка. Но порядок twoindependent комментариев не важен.
Реализация: vector clocks, version vectors. Используется в COPS, Riak.
Eventual consistency
Заголовок раздела «Eventual consistency»Если writes прекратятся, в конце концов все реплики сойдутся к одному значению. Никаких гарантий о том, когда и в каком порядке.
DNS, S3 (раньше; теперь strong read-after-write для PUT), Cassandra, Dynamo.
Convergence механизмы:
- Last-Write-Wins (LWW): конфликты решаются по timestamp. Опасно при clock skew.
- CRDT (Conflict-free Replicated Data Types): структуры с математически коммутативными операциями. G-Counter, OR-Set, LWW-Register. Используются в Redis (HyperLogLog), Riak, Automerge.
- Vector clocks: обнаружение concurrent writes, разрешение на уровне приложения.
Session guarantees
Заголовок раздела «Session guarantees»- Read-your-writes (RYW): клиент после своего write видит свой write при последующем read. Обычно реализуется через sticky session или client-side caching.
- Monotonic reads: последующие reads клиента не возвращают более старые версии.
- Monotonic writes: writes одного клиента применяются в порядке отправки.
- Writes-follow-reads: write после read видит то, что было прочитано.
Bounded staleness
Заголовок раздела «Bounded staleness»“Read данных не старше T секунд / N версий”. Компромисс: latency низкий, но stale data ограничен.
Поддерживается в Azure Cosmos DB как уровень consistency. Также в Spanner (через snapshot reads).
1.4 BASE vs ACID
Заголовок раздела «1.4 BASE vs ACID»ACID (traditional RDBMS):
- Atomicity — всё или ничего
- Consistency — переход между валидными состояниями (constraints, FK)
- Isolation — конкурентные транзакции изолированы
- Durability — committed данные не теряются
BASE (NoSQL philosophy, Brewer):
- Basically Available — система всегда отвечает (возможно, stale)
- Soft state — состояние меняется без input (background convergence)
- Eventual consistency — сходимость со временем
ACID и BASE — не взаимоисключающие. Современные системы (Spanner, CockroachDB, FoundationDB) показали, что ACID масштабируется. Но цена — latency и сложность реализации.
1.5 Eventual consistency в практике
Заголовок раздела «1.5 Eventual consistency в практике»Когда eventual consistency работает:
- Counters лайков, просмотров (точное число не важно)
- Activity feed (порядок может слегка плыть)
- Поисковые индексы (Elasticsearch async pipeline)
- Кэши (TTL invalidation)
- DNS, CDN
Когда eventual consistency НЕ работает:
- Деньги: транзакции, балансы, переводы
- Inventory: бронирование последнего товара
- Authentication: блокировка скомпрометированного токена
- Locking: распределённые блокировки
⚠️ Gotcha: eventual consistency требует от приложения умения работать со stale data. UI должен показывать loading, retries должны быть идемпотентными, конфликты — резолвиться.
2. Глубже
Заголовок раздела «2. Глубже»2.1 Идемпотентность: определение
Заголовок раздела «2.1 Идемпотентность: определение»Идемпотентность (от лат. idem potens — “то же самое могущественное”): операция f идемпотентна, если f(f(x)) = f(x) для любого x.
В распределённых системах: операция идемпотентна, если её повторное выполнение приводит к тому же результату.
Зачем нужна:
- Сети ненадёжны. TCP retry, прокси retry, клиентский retry.
- At-least-once delivery в message queues (Kafka, RabbitMQ).
- Webhook providers (Stripe, GitHub) ретраят при ошибках.
- Mobile клиенты дублируют запросы при flaky network.
Без идемпотентности: двойной retry создаёт двойной заказ, двойной перевод, двойной email.
2.2 HTTP методы и идемпотентность (RFC 9110)
Заголовок раздела «2.2 HTTP методы и идемпотентность (RFC 9110)»| Метод | Идемпотентен | Safe |
|---|---|---|
| GET | Да | Да |
| HEAD | Да | Да |
| OPTIONS | Да | Да |
| PUT | Да | Нет |
| DELETE | Да | Нет |
| POST | Нет | Нет |
| PATCH | Нет | Нет |
Safe — операция не меняет состояние (read-only). Идемпотентность — повтор операции даёт тот же эффект.
Почему POST не идемпотентен: POST /orders создаёт новый ресурс каждый раз. Чтобы сделать его идемпотентным — нужен Idempotency-Key.
PUT vs POST:
PUT /users/123— заменить ресурс с id=123. Идемпотентно: повтор даст то же состояние.POST /users— создать. Неидемпотентно: повтор создаст дубль.
2.3 Idempotency-Key pattern
Заголовок раздела «2.3 Idempotency-Key pattern»Клиент при отправке POST включает заголовок Idempotency-Key: <unique-id>. Сервер сохраняет результат для key и возвращает кэшированный ответ при повторе.
Алгоритм:
- Клиент генерирует UUID (на retry — тот же ключ).
- Сервер при получении POST:
- Проверяет таблицу
idempotency_keys— есть ли запись с этим key? - Если есть и status=completed — возвращает saved response.
- Если есть и status=in_progress — возвращает 409 или ждёт.
- Если нет — создаёт запись со status=in_progress, выполняет операцию, сохраняет result.
- Проверяет таблицу
- TTL для cleanup (24-48 часов обычно).
Схема таблицы:
CREATE TABLE idempotency_keys ( key VARCHAR(255) PRIMARY KEY, request_hash VARCHAR(64) NOT NULL, -- SHA256 от body, для detection mismatch status VARCHAR(20) NOT NULL, -- in_progress | completed | failed response_status INT, response_body JSONB, created_at TIMESTAMPTZ DEFAULT NOW(), completed_at TIMESTAMPTZ, expires_at TIMESTAMPTZ NOT NULL -- TTL для cleanup);
CREATE INDEX idx_idempotency_expires ON idempotency_keys(expires_at);2.4 Реализация в Go (middleware)
Заголовок раздела «2.4 Реализация в Go (middleware)»package idempotency
import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "io" "net/http" "time"
"github.com/jackc/pgx/v5/pgxpool")
type Store struct { db *pgxpool.Pool}
type Record struct { Key string RequestHash string Status string ResponseStatus int ResponseBody []byte}
func (s *Store) Get(ctx context.Context, key string) (*Record, error) { row := s.db.QueryRow(ctx, `SELECT key, request_hash, status, response_status, response_body FROM idempotency_keys WHERE key=$1 AND expires_at > NOW()`, key) r := &Record{} err := row.Scan(&r.Key, &r.RequestHash, &r.Status, &r.ResponseStatus, &r.ResponseBody) if err != nil { return nil, err } return r, nil}
func (s *Store) StartOrLoad(ctx context.Context, key, hash string) (*Record, bool, error) { // Атомарно: INSERT, иначе вернуть существующую var r Record err := s.db.QueryRow(ctx, ` INSERT INTO idempotency_keys (key, request_hash, status, expires_at) VALUES ($1, $2, 'in_progress', NOW() + INTERVAL '24 hours') ON CONFLICT (key) DO UPDATE SET key = idempotency_keys.key -- noop RETURNING key, request_hash, status, response_status, response_body `, key, hash).Scan(&r.Key, &r.RequestHash, &r.Status, &r.ResponseStatus, &r.ResponseBody) if err != nil { return nil, false, err } created := r.Status == "in_progress" && r.RequestHash == hash return &r, created, nil}
func (s *Store) Complete(ctx context.Context, key string, status int, body []byte) error { _, err := s.db.Exec(ctx, ` UPDATE idempotency_keys SET status='completed', response_status=$2, response_body=$3, completed_at=NOW() WHERE key=$1 `, key, status, body) return err}
// Middlewarefunc Middleware(store *Store) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { key := r.Header.Get("Idempotency-Key") if key == "" || r.Method == http.MethodGet { next.ServeHTTP(w, r) return }
// Hash body для detection mismatch body, _ := io.ReadAll(r.Body) r.Body = io.NopCloser(bytes.NewReader(body)) h := sha256.Sum256(body) hash := hex.EncodeToString(h[:])
rec, created, err := store.StartOrLoad(r.Context(), key, hash) if err != nil { http.Error(w, "internal", 500) return }
// Mismatch: same key, different body — конфликт if rec.RequestHash != hash { http.Error(w, "idempotency key reuse with different body", 422) return }
// Кэшированный ответ if rec.Status == "completed" { w.WriteHeader(rec.ResponseStatus) w.Write(rec.ResponseBody) return }
if !created && rec.Status == "in_progress" { http.Error(w, "request in progress", 409) return }
// Захват response rw := &responseRecorder{ResponseWriter: w, status: 200, body: &bytes.Buffer{}} next.ServeHTTP(rw, r) store.Complete(r.Context(), key, rw.status, rw.body.Bytes()) }) }}
type responseRecorder struct { http.ResponseWriter status int body *bytes.Buffer}
func (r *responseRecorder) WriteHeader(s int) { r.status = s r.ResponseWriter.WriteHeader(s)}
func (r *responseRecorder) Write(b []byte) (int, error) { r.body.Write(b) return r.ResponseWriter.Write(b)}2.5 Lock per key (предотвращение race на параллельных запросах)
Заголовок раздела «2.5 Lock per key (предотвращение race на параллельных запросах)»Если два параллельных request с одним Idempotency-Key пришли одновременно — нужна сериализация:
Через advisory lock PostgreSQL:
hashID := int64(crc32.ChecksumIEEE([]byte(key)))_, err := db.Exec(ctx, "SELECT pg_advisory_xact_lock($1)", hashID)// в той же транзакции — работа с idempotency_keysЧерез Redis SETNX:
ok, _ := rdb.SetNX(ctx, "idem:lock:"+key, "1", 30*time.Second).Result()if !ok { return errors.New("conflict")}defer rdb.Del(ctx, "idem:lock:"+key)2.6 Идемпотентность на разных уровнях
Заголовок раздела «2.6 Идемпотентность на разных уровнях»Database level:
INSERT ... ON CONFLICT DO NOTHING(PostgreSQL upsert).- Unique constraint на бизнес-ключ.
- Условные updates:
UPDATE accounts SET balance=balance-100 WHERE id=$1 AND tx_id NOT IN (SELECT id FROM processed_tx WHERE tx_id=$2).
Message queue:
- Хранить
processed_message_idsв БД. - Использовать message_id из брокера (Kafka offset, RabbitMQ deliveryTag).
- Outbox pattern для согласованности (см. файл 13 middle 1).
HTTP API:
- Idempotency-Key header (Stripe-style).
- PUT вместо POST там, где возможно.
Side effects (email, push):
- Двухэтапный pattern: сохрани intent в БД с unique key, отправь в background worker, маркируй as sent.
2.6.1 Stripe API Idempotency-Key — практический разбор
Заголовок раздела «2.6.1 Stripe API Idempotency-Key — практический разбор»Stripe — gold standard implementation. Что они делают:
Запрос:
POST /v1/charges HTTP/1.1Host: api.stripe.comAuthorization: Bearer sk_test_...Idempotency-Key: my-unique-key-123Content-Type: application/x-www-form-urlencoded
amount=2000¤cy=usd&source=tok_visaПоведение Stripe:
- Если key новый — выполняет операцию, сохраняет result.
- Повторный запрос с тем же key + same body → cached response.
- Same key + different body → 422
idempotency_error. - TTL — 24 часа.
- Не работает для GET (которые и так идемпотентны).
- Ключ scope’an per account.
Best practices от Stripe:
- Использовать UUID v4.
- Хранить key в client до получения final response (success или 4xx).
- При network timeout — retry с тем же key.
- Не использовать timestamp как key.
2.6.2 Идемпотентность для микросервисов — общий шаблон
Заголовок раздела «2.6.2 Идемпотентность для микросервисов — общий шаблон»В микросервисной архитектуре каждый сервис должен быть идемпотентным consumer:
type IdempotentHandler[T any] struct { store IdempotencyStore fn func(ctx context.Context, msg T) error}
func (h *IdempotentHandler[T]) Handle(ctx context.Context, msg Message[T]) error { eventID := msg.ID // unique per event
// Begin tx tx, err := h.store.BeginTx(ctx) if err != nil { return err } defer tx.Rollback()
// Check + reserve locked, err := tx.TryLock(ctx, eventID) if err != nil { return err } if !locked { // already processed by another consumer return nil }
// Process if err := h.fn(ctx, msg.Payload); err != nil { return err // tx rollback, retry next time }
// Mark processed if err := tx.MarkProcessed(ctx, eventID); err != nil { return err }
return tx.Commit()}Это абстрагирует idempotency для любого message handler.
2.7 Convergence: CRDT практически
Заголовок раздела «2.7 Convergence: CRDT практически»G-Counter (Grow-only counter): Каждая нода хранит свой counter. Сумма всех counters = общее значение. Merge — поэлементный max.
type GCounter map[string]int // nodeID -> count
func (c GCounter) Increment(node string) { c[node]++ }func (c GCounter) Value() int { sum := 0 for _, v := range c { sum += v } return sum}func (c GCounter) Merge(other GCounter) GCounter { out := make(GCounter) for k, v := range c { out[k] = v } for k, v := range other { if v > out[k] { out[k] = v } } return out}PN-Counter: G-Counter для increment + G-Counter для decrement. OR-Set (Observed-Remove Set): add/remove с unique tags. LWW-Register: значение + timestamp; merge — берёт более новое.
Используется в Redis CRDTs (Redis Enterprise), Riak, Automerge, Yjs.
3. Gotchas
Заголовок раздела «3. Gotchas»⚠️ CAP не про “выбери 2 из 3” каждый день. Это про поведение при partition. Большую часть времени система работает в normal mode, и трейдоффы описываются PACELC.
⚠️ Linearizability дорогая. Каждая операция требует consensus (Raft, Paxos). Минимум 1 round-trip к majority. Latency 10-50ms.
⚠️ Eventual ≠ “eventually”. Без gossip/anti-entropy convergence может не случиться (потерянные обновления). Riak Read Repair, Cassandra hinted handoff — обязательны.
⚠️ Clock skew ломает LWW. NTP даёт ±50ms погрешность. Использование time.Now() для conflict resolution может потерять writes. Решение: hybrid logical clocks (HLC), vector clocks, или TrueTime (Spanner).
⚠️ Read-your-writes требует sticky session. Если read попал на replica без вашего write — баг. Решение: route к primary, или wait for replication lag, или session token с replica position.
⚠️ Idempotency-Key не равно request-id. Request-id — для трейсинга. Idempotency-Key — для дедупликации. Клиент генерирует его до первой попытки и переиспользует для retry.
⚠️ Different body, same key. Если клиент по ошибке использовал тот же key для другого запроса — отклонять с 422. Иначе вернёшь чужой ответ.
⚠️ In-progress race. Два параллельных request с одним key. Без блокировки — оба создадут заказ. С блокировкой — второй ждёт и видит результат первого.
⚠️ TTL для idempotency table. Без cleanup таблица растёт бесконечно. 24-48 часов — стандартный диапазон. Stripe — 24 часа.
⚠️ Не идемпотентные операции: auto-increment counter, time.Now(), генерация случайного ID внутри операции. Все они дают разный результат при повторе.
⚠️ At-least-once и dedup в Kafka. Exactly-once в Kafka работает только при enable.idempotence=true + transactional producer + read_committed consumer. Без этого — at-least-once + dedup в приложении.
⚠️ POST vs PUT в REST. Многие “PUT /users” роуты на самом деле создают нового user — это нарушение REST. PUT должен заменять ресурс полностью; идемпотентен.
⚠️ GET с побочными эффектами. “GET /counter/increment” — антипаттерн. Любая prefetch/bot заспамит. Используй POST.
⚠️ PATCH не идемпотентен. PATCH = частичное обновление. PATCH /counter {"op": "increment"} — повтор увеличит дважды. Используй PUT /counter {"value": 42} для идемпотентности.
⚠️ CDN кэширует GET. Если GET имеет side-effect — CDN не отправит origin при повторе. Кэшированный ответ выдаст.
4. Real cases
Заголовок раздела «4. Real cases»Stripe — Idempotency-Key API.
Все POST endpoints поддерживают Idempotency-Key. Stripe рекомендует UUID v4. Хранение 24 часа. При mismatch body — 422 error. Это стало де-факто стандартом для платёжных API. PayPal, Adyen, Square — все скопировали.
Yandex.Money / YooKassa.
Также используют Idempotence-Key (через “e”). При повторе с тем же ключом возвращает первоначальный ответ. TTL — 24 часа.
Tinkoff Bank API.
“Все запросы должны передавать поле Idempotency-Key, чтобы избежать дублирования операций при network errors”. Это критично для переводов.
Amazon DynamoDB.
Eventually consistent reads по умолчанию (дешевле, быстрее). ConsistentRead=true — strong consistency, но 2x стоимость и latency. Это PACELC в действии.
Cassandra.
Tunable consistency: для каждого запроса можно выбрать ONE, QUORUM, ALL. Quorum reads + Quorum writes = strong consistency (W+R > N). Используется в Netflix viewing history, Discord messages.
Google Spanner. Strong consistency + global distribution. Использует TrueTime API (atomic clocks + GPS) для linearizability. Подход — заплати latency (~7ms), получи ACID на глобальном уровне.
Kafka exactly-once semantics. Введена в Kafka 0.11 (2017). Включает: idempotent producer (sequence numbers), transactional API, read_committed isolation. Используется LinkedIn, Uber, Netflix для критичных pipelines.
GitHub webhooks retry.
GitHub ретраит webhook delivery до 3 раз с exponential backoff при non-2xx response. Все webhooks включают X-GitHub-Delivery UUID, который handler должен использовать для дедупликации.
S3 read-after-write consistency (2020). До 2020 — eventual consistency для overwrite. После — strong read-after-write для всех PUT. AWS перешла на CP-like модель, что упростило приложения. Конкуренция с GCS, Azure.
Cloudflare R2. Strong consistency, no egress fees. PACELC: EC (latency higher than S3, but consistency wins).
MongoDB readPreference + write concern.
writeConcern: { w: "majority" } — write на большинство реплик. readPreference: primary — read с primary. Combo = strong consistency. readPreference: nearest + w: 1 = eventually consistent, low latency.
Slack webhook delivery.
Slack отправляет webhooks с at-least-once гарантией. X-Slack-Retry-Num и X-Slack-Retry-Reason headers — для понимания при retry. Endpoints должны быть идемпотентны. После 3 retries — drop.
Telegram Bot API webhooks.
HTTPS endpoint обязателен. Telegram ретраит при non-200 status. Update_id уникальный — handler использует для дедупликации. Кроме того, Telegram offers getUpdates long polling как альтернативу webhook с offset-based consumption.
Notion API — eventual consistency view. Notion REST API имеет eventual consistency: write возвращает success, но read через 1-2 секунды может показать old data. Documentation предупреждает об этом, рекомендует retry/poll для critical reads.
Apache Kafka — exactly-once “marketing” vs reality. Когда Confluent объявили EOS в 0.11 — это было “internal Kafka EOS”. External side effects (БД writes, HTTP calls) не покрыты. Это часто непонимание у новичков: “Kafka EOS = я могу не думать об идемпотентности.” Нет.
DynamoDB conditional updates.
DynamoDB поддерживает ConditionExpression: write выполняется только если условие выполнено. Реализует optimistic concurrency control. Например, UpdateItem с attribute_not_exists(processed) — атомарная “обработать только если не обработано”.
Linux NFS — early eventual consistency. NFS v3 — eventual consistency через client-side caching. Можно получить stale read после write от другого client. NFS v4 — close-to-open consistency, лучше но не linearizable.
git push с force vs idempotent rebase.
git push идемпотентен (одинаковый push → одинаковый remote). git push --force — нет (зависит от прошлых пушей). git rebase — операция, повторное применение которой даёт тот же результат при одинаковом state.
5. Вопросы
Заголовок раздела «5. Вопросы»Q1: Сформулируйте CAP теорему. A: В распределённой системе из Consistency, Availability, Partition tolerance можно сохранить только 2 из 3 при возникновении partition. На практике P неизбежен, выбор стоит между CP и AP.
Q2: Почему CA-система невозможна в распределённой среде? A: P (network partition) — данность, не выбор. Сеть всегда может разорваться. CA-система — это монолит на одной машине без репликации.
Q3: Что добавляет PACELC к CAP? A: PACELC описывает поведение в нормальном режиме (Else). Если нет partition — компромисс между Latency и Consistency. Strong consistency требует синхронной репликации, что повышает latency.
Q4: В чём разница между linearizability и serializability? A: Linearizability — про read/write одного объекта в real-time порядке. Serializability — про транзакции (multi-object): транзакции выглядят последовательными. Strict serializability = оба.
Q5: Что такое eventual consistency? Приведите пример. A: Гарантия, что при остановке writes все реплики сойдутся к одному состоянию. Примеры: DNS, S3 раньше, Cassandra. Подходит для счётчиков лайков, не подходит для переводов денег.
Q6: Что такое read-your-writes? A: Гарантия, что клиент после своего write видит этот write при следующем read. Реализуется через sticky session, чтение с primary, или session token с positions.
Q7: Что такое CRDT? A: Conflict-free Replicated Data Types — структуры данных с математически коммутативными операциями. Merge независим от порядка. G-Counter, OR-Set, LWW-Register. Используется в Riak, Yjs, Automerge.
Q8: ACID vs BASE — в чём разница? A: ACID — традиционный подход RDBMS: атомарность, согласованность, изоляция, durability. BASE — NoSQL: Basically Available, Soft state, Eventual consistency. Современные системы (Spanner) сочетают.
Q9: Что такое идемпотентность операции?
A: Свойство f(f(x)) = f(x). Повторное выполнение даёт тот же результат. Критично для retries в распределённых системах.
Q10: Какие HTTP методы идемпотентны? A: GET, HEAD, OPTIONS, PUT, DELETE — идемпотентны. POST, PATCH — нет. Идемпотентный POST реализуется через Idempotency-Key header.
Q11: Как реализовать Idempotency-Key на сервере? A: Таблица idempotency_keys (key, request_hash, status, response). При запросе: проверяем key. Если есть completed — возвращаем сохранённый response. Если есть in_progress — 409. Если нет — INSERT, выполняем, COMPLETE.
Q12: Что делать, если запрос с тем же Idempotency-Key пришёл с другим телом? A: Возвращать 422 Unprocessable Entity. Это ошибка клиента: один key — одно намерение. Хранить SHA256 hash тела для detection.
Q13: Какой TTL для Idempotency-Key выбрать? A: Stripe — 24 часа, PayPal — 30 минут для некоторых, типично 24-48 часов. Запас на retry от клиента после реконекта.
Q14: Что произойдёт при двух параллельных запросах с одним Idempotency-Key? A: Без блокировки — race condition: оба выполнят операцию. Решение: advisory lock PostgreSQL или Redis SETNX или unique constraint + retry.
Q15: Можно ли использовать timestamp как Idempotency-Key? A: Нет. Timestamp может совпасть, может различаться между retries. UUID v4 — стандарт. Можно SHA256(business_data) — если бизнес-данные уникальны.
Q16: Что такое at-least-once delivery? Как с ней работать? A: Брокер гарантирует доставку хотя бы один раз, дубли возможны. Работать через идемпотентных consumer’ов: проверять processed_id перед обработкой.
Q17: Exactly-once в Kafka — миф или реальность?
A: Реальность при правильной настройке: enable.idempotence=true + transactional producer + isolation.level=read_committed consumer. Но только внутри Kafka. Side effects (HTTP, БД) требуют отдельной идемпотентности.
Q18: Что такое vector clock? A: Структура для обнаружения causal relationship между событиями. Каждый процесс хранит счётчики версий всех процессов. При concurrent updates vector clocks несравнимы — конфликт.
Q19: Чем linearizability отличается от sequential consistency? A: Linearizability учитывает real-time порядок (если op B стартовала после завершения op A — B видит A). Sequential consistency — только порядок внутри процесса; real-time может нарушаться.
Q20: Почему clock skew опасен для LWW? A: Если node A имеет clock на 100ms впереди node B, то write на B с реальным timestamp может потеряться (его “побьёт” более старый write с A). Решение: HLC, vector clocks, atomic clocks (Spanner).
Q21: Когда eventual consistency допустима? A: Lightweight операции: counters, feeds, recommendations, search indexing. Где stale data на секунды-минуты приемлема. Не подходит: деньги, inventory, auth.
Q22: Как обеспечить идемпотентность отправки email? A: Сохранить intent (email_to_send) в БД с unique business_key. Background worker отправляет и маркирует as sent. При retry проверяем status. Уровень: at-most-once отправка.
Q23: Что такое read repair в Cassandra? A: Механизм: при read с несколькими репликами выявляются несоответствия. Background — отправляется correct value на stale реплики. Помогает eventual consistency сходиться.
Q24: Bounded staleness — что это? A: Гарантия, что read возвращает данные не старше T секунд / N версий. Промежуточный уровень между strong и eventual. Azure Cosmos DB, Spanner snapshot reads.
Q25: Какие проблемы у GET с побочным эффектом? A: Браузеры prefetch’ат GET, CDN кэшируют, web crawlers заспамят. GET должен быть safe. Side effects — только в POST/PUT/DELETE/PATCH.
Q26: Что такое hybrid logical clock (HLC)? A: Комбинация physical time и logical counter. Гарантирует monotonicity и approximation real-time. Используется CockroachDB. Решает проблему clock skew для distributed timestamps.
Q27: Чем write quorum (W) отличается от read quorum (R)? A: W — сколько реплик должны подтвердить write. R — сколько прочитать. W+R > N (N=total replicas) → strong consistency. Cassandra, Riak — tunable per-query.
Q28: Stripe Idempotency-Key TTL — почему 24 часа? A: 24 часа — баланс: достаточно длинно для клиента ретраить после network/maintenance issues, достаточно коротко для cleanup и privacy. Reasonable retry window для финансовых операций.
Q29: Можно ли использовать Idempotency-Key для GET? A: Нет смысла. GET идемпотентен по природе. Idempotency-Key — для POST/PATCH, где нужна дедупликация side effects.
Q30: Что такое monotonic writes consistency? A: Гарантия, что writes одного клиента применяются в порядке отправки. Если client пишет A потом B, любая реплика, видевшая B, видит и A. Слабее linearizability, но полезно для UX.
6. Practice
Заголовок раздела «6. Practice»-
Реализуй middleware Idempotency-Key. Используй PostgreSQL для хранения. Поддержи concurrent requests с advisory lock.
-
Симулируй eventual consistency. Запусти 3 реплики (горутины) с асинхронной репликацией. Покажи stale reads. Добавь read repair.
-
Напиши G-Counter CRDT. Тесты: merge коммутативен, ассоциативен, идемпотентен.
-
Протестируй at-least-once с Kafka. Запусти consumer, который иногда падает после processing, но до commit offset. Покажи дубли. Добавь dedup через ID.
-
Имитируй partition. Сетевой proxy между client и сервером с поддержкой “drop traffic”. Запусти 3 replica setup. Покажи, как ведут себя CP (etcd) и AP (Cassandra) при partition.
-
Реализуй retry с exponential backoff и jitter для HTTP клиента. Используй
cenkalti/backoff/v4. Покажи, как jitter снижает thundering herd. -
Сравни Idempotency-Key стратегии. Хеш body vs UUID от клиента. Когда какой подход лучше?
-
Напиши тесты идемпотентности. Property-based: для случайных запросов второй раз возвращает то же, что первый.
-
Реализуй PN-Counter CRDT. Тесты на конвергентность: 3 ноды делают increment/decrement, merge в любом порядке — одинаковый результат.
-
Симулируй clock skew проблему. Два узла с timestamps, расходящимися на 1 минуту. LWW resolver. Покажи потерянные writes. Затем заменить на HLC.
-
Webhook receiver с идемпотентностью. Имитируй GitHub-style webhook с дедупликацией по
X-GitHub-DeliveryUUID. -
Stale read demo. Запусти 2 instance Postgres (primary + read replica). Сделай write на primary, read с replica — покажи lag. Добавь
read_after_writeчерез session. -
Outbox idempotency. Реализуй consumer Kafka, который пишет в БД с дедупликацией через unique constraint на event_id.
-
Bench: cost of strong consistency. Сравни latency и throughput Postgres single primary vs Postgres с sync replication к 2 replicas.
7. Источники
Заголовок раздела «7. Источники»- Eric Brewer. “CAP Twelve Years Later: How the ‘Rules’ Have Changed.” IEEE Computer, 2012. — оригинальная переоценка CAP.
- Daniel Abadi. “Consistency Tradeoffs in Modern Distributed Database System Design.” IEEE Computer, 2012. — PACELC.
- Martin Kleppmann. “Designing Data-Intensive Applications.” O’Reilly, 2017. — главы 5, 7, 9 — must read.
- Stripe API Docs. “Idempotent Requests.” https://stripe.com/docs/api/idempotent_requests
- Werner Vogels. “Eventually Consistent.” ACM Queue, 2008. — основы от Amazon CTO.
- Pat Helland. “Idempotence Is Not a Medical Condition.” ACM Queue, 2012.
- Hewitt et al. “Cassandra Documentation.” — tunable consistency.
- Marc Shapiro et al. “Conflict-Free Replicated Data Types.” 2011. — CRDT foundation.
- Jepsen.io reports. https://jepsen.io/analyses — реальные тесты consistency баз данных.
- RFC 9110 — HTTP Semantics (методы и идемпотентность).