DynamoDB managed wide-column NoSQL by AWS — single-table design with composite (pk, sk) keys, GSI/LSI, on-demand vs provisioned capacity, Streams CDC, Global Tables active-active. Six scenarios: GetItem (single-digit ms), Query на partition с sk-range, GSI inverse lookup, PutItem→Stream→Lambda fan-out, hot-partition throttle с adaptive capacity, Global Tables LWW. Two ADRs covering single-table design vs RDBMS thinking and on-demand vs provisioned capacity.
DynamoDB — managed KV/wide-column от AWS, на котором крутится Amazon retail, Lyft dispatch, Disney+, Snap, Capital One и десятки фронтенд-сервисов любой serverless-команды. Знать его нужно по трём причинам.
hash(pk) → партиция → sort key. Внутренности DynamoDB (single-digit ms latency, auto-sharding, quorum в 3 AZ, leaderless под капотом) — учебник распределённых KV в управляемой обёртке.Дорогой урок: команды приходят из Postgres и инстинктивно делают «одна таблица = одна сущность» (users, orders, products). Потом обнаруживают, что нет JOIN. Решают client-side: GetItem(user) → Query(orders) → BatchGet(products). Три round-trip, каждый — отдельный RCU, latency складывается, на page render — 50-200ms. Этот документ — про то, как сразу не наступить.
«DynamoDB — это managed (hash, range) → blob KV с auto-sharding по
hash(partition_key). Ты платишь за RPS и storage, не за compute, но дизайн схемы — твоя ответственность: single-table design, тщательно подобранный partition key, и забудь про JOIN.»
Три измерения, в которых принимаются все решения:
Любое решение по DynamoDB — это точка в этом 3D-пространстве. PutItem c ConditionExpression, BatchWriteItem, Query c FilterExpression, GSI с projection ALL/KEYS_ONLY/INCLUDE — всё это комбинации трёх осей.
На канвасе — типовая serverless архитектура поверх DynamoDB.
Request router (точка ADR-001 и ADR-002, отсюда читать про single-table design и capacity mode) плюс три партиции (по 3K RCU / 1K WCU / 10 GB каждая — реальные limits per partition).hash(pk), eventual consistency, своя capacity).eu-west-1 реплика (async, ~500ms-1s lag, last-writer-wins).Edges:
hash(pk) определяет какая).Каждая партиция в реале — 3-way replicated по AZ с quorum write (2/3 ack для strong) и одиночным read для eventual. На диаграмме репликация скрыта внутри партиции — это абстракция AWS, ты её не видишь и не управляешь.
getitem-pk-sk — Канонический readClient → Lambda → Router → hash(pk) → одна партиция → SSD lookup → ответ. p50 ~3ms, p99 ~10ms. 0.5 KB item = 0.5 RCU (eventual) или 1 RCU (strong, округление до 4 KB).
Чему учит: ради этого DynamoDB и существует. Single-digit ms latency не зависит от объёма таблицы — 10 GB или 10 PB, всё равно один hash + один disk seek. Стоимость предсказуема (RCU per request). Это бенчмарк, с которым сравнивают все остальные операции.
query-partition-range — Single-table design в действииОдин Query на pk=USER#123, sk begins_with "ORDER#" достаёт все заказы пользователя из ОДНОЙ партиции за один RCU-batch. 100 items × 0.8 KB ≈ 80 KB = 20 RCU. Pagination через LastEvaluatedKey (cursor), 1 MB cap на response.
Чему учит: Query против Scan — главное различие в DDB. Query трогает одну партицию (или одну GSI-партицию), Scan читает ВСЁ. Если access pattern «все X для Y» — закладывай его в (pk, sk) при дизайне таблицы. Это то самое «single-table design» Рика Хулихана: одна table содержит много entity types, ключи спроектированы под конкретные queries.
gsi-inverse-lookup — Reverse lookup через GSIНайти всех, кто купил PRODUCT#X: Query GSI1 pk=PRODUCT#X. GSI — это вторая таблица с другим (pk, sk), eventual consistency, своя capacity. Eventual = write в base 200ms назад может ещё не отразиться в GSI.
Чему учит: GSI решает inverse pattern ценой eventual. 20 GSI на table максимум, каждая ест WCU base table (write amplification: 1 base write × N GSI = N+1 WCU). Strong consistent reads на GSI не работают вообще — eventual всегда. Для analytics-like queries это ок; для read-your-writes — нет.
streams-lambda-cdc — Event-driven через StreamsКаждый Put/Update/Delete в base table эмитит change record в Stream (24h retention, async). Lambda триггерится poll-based, batch до 1000 records, и fan-out'ит: индексирует в OpenSearch, инвалидирует Redis, шлёт SNS-нотификацию.
Чему учит: Streams = foundation для event-driven AWS архитектуры. Outbox pattern «бесплатно» (DDB сама пишет change log). Идемпотентность обязательна — Lambda может ретраить batch. Для долгого retention или больших объёмов — Kinesis Data Stream for DynamoDB (отдельная фича, не Streams).
hot-partition-throttle — Главный operational gotchaОдин pk с 10K writes/s → одна партиция в 1000 WCU = ProvisionedThroughputExceededException (HTTP 429). Adaptive capacity (с 2018) автоматически перераспределяет from idle partitions, но не магия — если все pk равномерно горячие, не поможет.
Чему учит: DDB шардится по pk, и распределение нагрузки — твоя проблема. Решения: (1) write sharding — USER#42#0, USER#42#1, …, USER#42#9, scatter writes, gather через 10× Query; (2) adaptive capacity (auto); (3) пересмотреть partition key (cardinality, monotonic-ли). Hot pk — самый частый production-инцидент в DDB.
global-tables-lww — Multi-region active-activeWrite в us-east-1 → local quorum 2/3 AZ → ack ~5ms → Stream → cross-region replicator → eu-west-1 (~500ms-1s lag). Read из eu сразу после write в us — старое значение. Concurrent writes в разных регионах резолвятся last-writer-wins по timestamp.
Чему учит: Global Tables — это multi-leader replication, не magic synchronous DB. Вы получаете локальный low-latency read/write в каждом регионе ценой eventual consistency и silent data loss на конфликтах (LWW = тихо теряем). Для строгой глобальной consistency нужен Spanner / CockroachDB, а не DDB.
Status: accepted.
Context. Команды из Postgres делают одна table = одна entity, страдают от отсутствия JOIN, накручивают client-side joins (три API-вызова на page render, RCU amplification, p99 latency растёт). AWS-инженеры (Rick Houlihan, Alex DeBrie) много лет учат: DynamoDB не RDBMS, schema-on-read, access-pattern-driven design.
Decision. Single-table design: все entity types в одной таблице с composite key (pk, sk). pk = "USER#123", sk = "PROFILE" / "ORDER#456" / "ADDRESS#789". Один Query pk=USER#123 возвращает ВСЁ про юзера за один RCU-batch (1 MB cap, ~5ms). GSI с inverted (pk, sk) — для reverse lookups. Pays off когда (1) access patterns стабильны и известны до проектирования, (2) throughput > 10K ops/s, (3) нужна single-digit ms latency at scale, (4) serverless workload.
Consequences. Жёсткая привязка к access patterns: смена pattern = миграция данных или новый GSI (online, но дорогой backfill). Ad-hoc analytics невозможна на DDB напрямую — выгружай в S3/Athena/Redshift через Streams. Сложнее onboarding новых разработчиков — они привыкли к JOIN.
Status: accepted (per-workload).
Context. Provisioned — pre-paid RCU/WCU 24/7, перерасход = throttling (5-минутный auto-scale lag), но в 7× дешевле per-op. On-demand — pay-per-request, scales мгновенно, дорого. Spike workload на provisioned убьёт UX; constant workload на on-demand сольёт деньги.
Decision. Per-workload:
| Workload | Mode | Обоснование |
|---|---|---|
| Production API с известным QPS | Provisioned + Application Auto Scaling | predictable, cheap |
| Batch jobs (ночные) | Provisioned with scheduled scaling | known peak |
| Dev / staging | On-demand | low traffic, не стоит provision |
| Viral content / launch / неизвестный baseline | On-demand первые 2-4 недели | потом перевести на provisioned |
| Event-driven Lambda со spiky pattern | On-demand | spike absorption |
Switch capacity mode разрешён 1 раз в 24h — план миграций заранее.
Consequences. Команды должны мерить utilization и считать TCO. Provisioned без auto-scaling = throttling в пиках; on-demand «по умолчанию» = bill shock через месяц. CloudWatch alarms на ConsumedReadCapacityUnits и ThrottledRequests обязательны.
Status: accepted.
Context. DDB — это PA/EL в терминах PACELC (при partition — availability, в нормальной работе — latency). Eventual reads = 1 RCU, ~5ms, читают любую из 3 реплик. Strong reads = 2 RCU, ~10ms, читают с leader replica (quorum read). Transactions = 2× cost (RCU и WCU), ACID на нескольких items в одной таблице или между таблицами.
Decision. Default — eventual. Strong consistency включаем точечно: financial state, inventory checks, leader election. Transactions — только для критичных инвариантов (multi-item updates с rollback). GSI всегда eventual — для GSI strong невозможен в принципе. Для глобальной строгой consistency — не DynamoDB, а Spanner / CockroachDB (см. ::concept{slug="cap-theorem"} и ::concept{slug="pacelc-theorem"}).
Consequences. App-код должен явно помечать места, где нужен strong (документировано в коде, не «как-нибудь»). Read-your-writes на GSI не работает — для UX-критичных flow либо читать с base table, либо ждать ~200ms-1s. Transactional API — не silver bullet, 2× стоимость и лимит 100 items в транзакции.
Общий паттерн: serverless / Lambda-heavy команды используют DDB как primary store; крупные multi-region apps — Global Tables; analytics всегда уходит в S3 через Streams.
Scan в production hot path. Full-table read = тысячи RCU, секунды latency. FilterExpression применяется после чтения — всё равно платишь за RCU. Использовать Query или GSI; для аналитики — экспорт в S3.ReturnValues. Conditional check fails (ConditionalCheckFailedException) часто индикатор concurrent write — клиент должен retry/merge, не silently swallow.Scan + FilterExpression). Через Streams выгружай в S3 / Athena / Redshift. DDB не для OLAP.writeConcern: 1 ментальность — игнор ProvisionedThroughputExceeded. Клиенты обязаны retry с exponential backoff + jitter; AWS SDK делает это, но max retries и timeouts — твоя ответственность.Scan + FilterExpression будет дорого и медленно. Используй Athena (на S3 экспорте) или Redshift.