Amazon DynamoDB
Изучите популярную NoSQL базу данных AWS DynamoDB: от базовых концепций ключей и типов данных до продвинутых техник моделирования, транзакций и борьбы с горячими партициями.
1. Философия и основы
1.1. Зачем вообще нужна DynamoDB?
Проблемы, которые решает DynamoDB
Представьте ситуацию: ваше приложение внезапно становится популярным. За неделю количество пользователей выросло с 10 тысяч до миллиона. Ваша тщательно настроенная база данных PostgreSQL, которая прекрасно справлялась с нагрузкой, начинает задыхаться. Cервер уперся в лимит соединений, CPU на 100%, IOPS на пределе. Репликация отстаёт, индексы не успевают обновляться, а попытки горизонтального масштабирования превращаются в кошмар с ручным шардингом и балансировкой нагрузки.
Или другой сценарий: вы разрабатываете IoT-платформу, где миллионы устройств отправляют телеметрию каждую секунду. Традиционная реляционная база данных требует постоянной оптимизации индексов, тщательного планирования партиционирования, и всё равно начинает деградировать при росте объёма данных. При этом 99% ваших запросов - это простые операции записи и чтения по ключу, для которых вся мощь SQL просто не нужна.
Третий пример: глобальное мобильное приложение с пользователями в Азии, Европе и Америке. Размещение master-базы в одном регионе (например, в Вирджинии) означает, что пользователи в Токио или Сиднее будут испытывать задержки в 200-300 миллисекунд только на сетевой round-trip. Multi-master репликация в PostgreSQL или MySQL - это отдельная, сложнейшая инженерная задача с риском конфликтов и split-brain сценариев.
Когда реляционные БД становятся узким местом
Реляционные базы данных - это прекрасный, проверенный временем инструмент. Их стоит использовать в большинстве проектов. Но есть сценарии, где их архитектурные принципы становятся ограничением:
-
Непредсказуемые всплески нагрузки. "Черная пятница", вирусный твит, внезапная популярность - реляционные БД требуют предварительного выделения ресурсов (provisioning). Вы либо переплачиваете за простаивающее железо 99% времени, либо рискуете упасть в самый важный момент. DynamoDB в режиме On-Demand масштабируется от нуля до практически неограниченной пропускной способности и обратно.
-
Операционная сложность. Управление репликами, настройка параметров памяти, vacuum в PostgreSQL, оптимизация индексов, планирование бэкапов, обновление версий с даунтаймом - всё это требует специализированной экспертизы (DBA/SRE) и постоянного внимания. Ваш телефон, звонящий в 3 часа ночи, потому что "на реплике кончилось место" - это цена эксплуатации. DynamoDB полностью управляется AWS.
-
Простые паттерны доступа при высокой нагрузке. Если 90% ваших запросов - это "получить объект по ID" или "получить последние N записей пользователя", то вы платите огромную цену за неиспользуемую функциональность SQL. Планировщик запросов, оптимизатор, поддержка транзакций MVCC - всё это добавляет накладные расходы (overhead) там, где они не нужны. DynamoDB оптимальнее для простых паттернов доступа.
-
Глобальная доступность с низкой латентностью. Настройка географически распределённой реляционной БД с активной репликацией (Active-Active) - это месяцы работы высококвалифицированных инженеров. В DynamoDB (с Global Tables) это включается одной кнопкой и работает с предсказуемой моделью разрешения конфликтов.
Сценарии использования DynamoDB
DynamoDB была рождена в недрах Amazon для решения главной проблемы - масштабирования корзины покупок во время Prime Day. Она создана для специфичных, но крайне требовательных задач:
-
Gaming (Игры): Индустрия, где задержка решает все. Хранение игровых сессий, лидербордов, инвентаря игроков. Игра может выстрелить с тысячи пользователей до десяти миллионов за неделю. Архитектура не должна меняться. Zynga, Electronic Arts, Ubisoft строят на DynamoDB критичные для геймплея системы.
-
IoT и временные ряды: Миллионы устройств генерируют непрерывный поток данных. Каждая запись имеет простую структуру:
device_id,timestamp,metrics. DynamoDB с TTL (Time-to-Live) автоматически удаляет старые данные, Streams позволяют в реальном времени агрегировать метрики, а стоимость растёт линейно с объёмом. -
AdTech (Рекламные технологии): Платформы для торгов в реальном времени (RTB). На принятие решения (показать рекламу или нет) у вас есть 10-20 миллисекунд. За это время нужно успеть прочитать профиль пользователя и записать результат. DynamoDB гарантирует задержку в единицах миллисекунд при любом масштабе.
-
Mobile & Serverless Backends: Ваш бэкенд - это AWS Lambda. Непредсказуемые паттерны использования, необходимость работы в разных регионах. Lambda может масштабироваться от 0 до 10,000 одновременных функций за секунды. Вам нужна база данных, которая так же масштабируется с нуля, не требуя пула соединений.
Сравнение экономики
Когда DynamoDB дешевле?
-
На старте и при очень неравномерной нагрузке. Ваш стартап. 100 пользователей днем и 0 ночью. В RDS вы платите за включенный сервер 24/7 (например, $8/мес за
t3.micro). В DynamoDB в режиме On-Demand (по требованию) вы платите за каждый запрос. 0 запросов = $0. Ваша стоимость будет $0.10/мес. -
В бессерверных (Serverless) приложениях. Связка Lambda + RDS - это боль. Вам нужен NAT Gateway, VPC, и ваш RDS должен быть постоянно включен. Связка Lambda + DynamoDB (через VPC Endpoint) работает "из коробки" и стоит $0 в состоянии простоя.
-
При учете "полной стоимости владения" (TCO). В RDS вы платите не только за сервер, но и за время ваших самых дорогих инженеров (SRE/DBA), которые тратят часы на настройку
vacuum, оптимизациюJOINов, планирование обновлений и тушение пожаров в 3 часа ночи. DynamoDB - это "нулевой" OpEx. Вам не нужен DBA для DynamoDB. Эта скрытая экономия на зарплате и времени инженеров часто оказывается более значительной, чем прямой счет от AWS.
Когда DynamoDB дороже?
-
При высокой, стабильной нагрузке. Если у вас ровно 1,000 запросов в секунду 24/7, режим On-Demand будет очень дорогим. Здесь дешевле использовать Provisioned (выделенный) режим DynamoDB или правильно настроенный кластер RDS/Aurora.
-
При больших объемах данных и редких запросах (aналитика). Хранить 10 ТБ данных в DynamoDB очень дорого. Если вы их редко читаете, гораздо дешевле хранить их в S3 и запрашивать через Athena.
Только теперь, поняв проблемы, мы можем дать определение:
AWS DynamoDB - это полностью управляемая, бессерверная NoSQL база данных, созданная для сценариев, требующих гарантированной производительности (единицы миллисекунд) при любом масштабе.
Это компромисс. Вы добровольно отказываетесь от гибкости SQL (JOIN, ad-hoc запросы) в обмен на почти бесконечную масштабируемость и нулевые операционные расходы.
1.2. Архитектурные принципы DynamoDB
NoSQL и Key-Document модель
Помните, как мы проектируем схему БД? Users, Orders, Products, OrderItems... Третья нормальная форма. Никаких дубликатов. Это красиво, элегантно, но... иногда дорого.
-
Цена
JOIN: Чтобы собрать страницу заказа, вашему PostgreSQL нужно "сбегать" в 5 разных мест на диске (5 B-Tree индексов), собрать данные и склеить их. При нагрузке в 1000 RPS это превращается в миллионы дисковых операций. -
Цена изменения схемы: SQL-схема - это жесткий контракт. Добавление нового поля или изменение существующего требует процесса миграции. Нужно разбивать процесс миграции на этапы, писать специализированные скрипты.
Модель DynamoDB (которая по факту key-document) переворачивает игру. Она бессхемная (schemaless). Каждый "элемент" (item, аналог строки) может иметь свой набор "атрибутов". Вы можете добавить social_links к одному пользователю, не трогая остальные 5 миллиардов, просто начав их записывать. И DynamoDB не поддерживает JOINы.
Почему отказ от JOIN'ов - это благо при больших объемах?
Запрещая JOINы на уровне движка, DynamoDB вынуждает вас делать то, что Amazon и так делал для масштабирования: денормализацию.
Вместо того чтобы джойнить на лету (при чтении), вы джойните данные при записи.
-
SQL-подход: Таблица
Ordersхранитuser_id. Чтобы показать имя, вы делаетеJOIN users. -
DynamoDB-подход: Элемент
Orderхранитuser_idиuser_name. Да, данные дублируются. Но чтение заказа - это одна быстрая операция. "Хранилище дешевле вычислений".
Отказ от JOINов - это фундаментальный компромисс: вы меняете гибкость запросов и эффективность хранения (нормализация) на предсказуемую, сверхнизкую задержку и бесконечную масштабируемость чтения.
Бессерверность (Serverless)
Представьте себе жизнь SRE-инженера, отвечающего за кластер PostgreSQL. Его телефон звонит в 3 часа ночи. Почему?
-
На реплике кончилось место на диске.
-
Начался
VACUUMи забил CPU. -
Забыли применить патч безопасности.
-
Нужно обновить мажорную версию (с даунтаймом в 2 часа).
-
Лимит соединений исчерпан.
Бессерверность (Serverless) в DynamoDB означает, что всех этих проблем больше не существует.
-
Патчинг ОС? - Не ваша проблема.
-
Настройка репликации? - Встроено (3 копии данных в 3-х ЦОДах).
-
Планирование бэкапов? - Включается одной кнопкой (PITR).
-
Масштабирование диска? - Он бесконечный.
-
Масштабирование соединений? - Их нет (API-вызовы по HTTP).
Когда это критично для бизнеса?
-
Time-to-Market: У вас команда из 3-х бэкенд-разработчиков и 0 DBA/DevOps. Вы можете запустить глобально-масштабируемый продукт.
-
Экономия на людях: Вам не нужно нанимать команду DBA для управления парком баз данных.
-
Безопасность и Compliance: AWS автоматически шифрует все данные и берет на себя огромную часть работы по соответствию стандартам (PCI, HIPAA и т.д.).
Что происходит под капотом при автоскейлинге?
Вы не управляете "серверами", но вы управляете "пропускной способностью", которая измеряется в RCU (Read Capacity Units) и WCU (Write Capacity Units).
-
1 WCU = 1 транзакционная запись в секунду для элемента до 1 КБ.
-
1 RCU = 1 строго консистентное чтение в секунду для элемента до 4 КБ (или 2 нестрого консистентных чтения).
Когда ваша нагрузка растет, DynamoDB на лету разрезает (split) ваши партиции, перемещая их на новые сервера и выделяя им новый бюджет RCU/WCU. Это происходит прозрачно и без даунтайма.
Режимы биллинга (тарифы)
-
Выделенный (Provisioned): Вы говорите: "Я хочу 1,000 WCU и 5,000 RCU". Вы платите за эту мощность по часам, независимо от того, используете вы ее или нет. Это дешево при предсказуемой, высокой нагрузке.
-
По требованию (On-Demand): Вы не говорите ничего. DynamoDB просто выполняет все ваши запросы. Вы платите за каждый запрос. Это дороже за запрос, чем Provisioned, но дешевле, если у вас 0 нагрузки или резкие, непредсказуемые пики.
Для Provisioned режима можно включить Auto Scaling, задав целевую утилизацию (например, 70%). Это позволяет AWS автоматически регулировать выделенную мощность: если потребление превышает 70%, Auto Scaling добавляет мощности для создания буфера, а если падает ниже - убирает излишки для экономии. Так вы можете платить за меньше WCU и RCU.
Как правило, для тестового окружения начинают с On-Demand, а для продакшена с предсказуемой нагрузкой используют Provisioned + Auto Scaling.
Гарантированная производительность
Традиционная БД - это как общая автомагистраль в час пик. Ваш SELECT может занять 5 мс (если дорога пустая), а может 500 мс (если аналитик запустил годовой отчет, создав "пробку" из JOINов).
DynamoDB - это как ваша личная выделенная полоса.
В каких сценариях задержка в единицах миллисекунд критична?
-
AdTech (Real-Time Bidding): Весь аукцион должен пройти за 100 мс. Ваша БД не может "задуматься".
-
Gaming: Таблица лидеров. Вы не можете заставить игрока ждать 2 секунды, пока обновится его счет.
-
Бэкенд для API: Если у вас сложная цепочка вызовов микросервисов, и каждый из 5 сервисов ходит в БД, которая иногда тормозит, то
5мс + 5мс + 500мс + 5мс + 5мс = 520мс. Один медленный запрос рушит весь SLA.
SLA и гарантии AWS
DynamoDB дает SLA на задержку в единицах миллисекунд (~2-9 мс). Это SLA для простых операций (GetItem, PutItem, Query по ключу). Это не SLA для Scan (полное сканирование таблицы).
Партиции
Вот почему DynamoDB может это гарантировать:
-
Изоляция: Когда вы запрашиваете
GetItem(user_id='123'), DynamoDB хэшируетuser_idи точно знает, на какой одной партиции (из возможных тысяч) лежит этот элемент. Запрос идет только туда. Он не затрагивает другие 99.9% системы. -
Нет
JOINов, нет блокировок: Нет сложных вычислений. Нет блокировок уровня таблицы. -
Бюджетирование (RCU/WCU): Это не только модель биллинга, это механизм защиты. Ваш RCU/WCU - это ваш выделенный "кусочек" мощности. Запрос аналитика (который делает
Scan) не отнимет RCU у запросов пользователей (GetItem), если они идут на разные партиции.
Философия и архитектура DynamoDB
Вопросов: 7
1.3. Практика
Выбор базы данных для конкурента Twitter
Вы - CTO стартапа, создающего конкурента Twitter. Обсудите с Назаром выбор базы данных (DynamoDB или PostgreSQL) для хранения твитов и профилей, сравнивая стоимость на старте и при масштабе в 100 млн пользователей, а также влияние на скорость разработки и сложность поддержки.
Назар - ваш персональный ИИ-наставник. Он поможет закрепить материал через практику и ответит на ваши вопросы.
💡 Все обсуждения с ИИ могут быть прочитаны администратором для улучшения качества обучения.
2. Старт и первые данные
2.1. Локальная разработка и инструменты
Разрабатывать приложение, постоянно дергая реальное облако (AWS) - это плохая практика.
- Медленно: Сетевая задержка замедляет тесты.
- Дорого: Ошибка в бесконечном цикле записи может стоить вам сотни долларов за час.
- Опасно: Случайно запустить
DELETEне на той таблице - классика жанра. - Нет интернета: Вы не можете кодить в самолете.
Для комфортной жизни вам нужны два инструмента: DynamoDB Local и NoSQL Workbench.
DynamoDB Local
Это официальная версия базы данных, упакованная в Docker-контейнер (или Java JAR). Она эмулирует API DynamoDB, но работает на вашем ноутбуке, храня данные в оперативной памяти или в локальном файле.
Как запустить (Docker Compose)
Вам не нужно ничего устанавливать, кроме Docker. Добавьте это в ваш docker-compose.yml:
version: '3.8'
services:
dynamodb-local:
image: amazon/dynamodb-local:latest
container_name: dynamodb-local
ports:
- "8000:8000"
# Флаг -sharedDb важен! Без него база создает разные файлы
# для разных Access Key / Region. С ним - база одна для всех.
command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ."
volumes:
- "./docker/dynamodb:/home/dynamodblocal/db"
working_dir: /home/dynamodblocal
Как подключить приложение
Это единственное, что нужно изменить в коде. Вам нужно переопределить endpoint_url.
import boto3
import os
# В проде эта переменная пустая, локально - 'http://localhost:8000'
dynamodb_endpoint = os.getenv('DYNAMODB_ENDPOINT')
dynamodb = boto3.resource(
'dynamodb',
region_name='us-east-1',
# Магия здесь:
endpoint_url=dynamodb_endpoint
)
# Теперь все операции (create_table, put_item) идут в ваш Docker
Ограничения Local-версии:
- Она не эмулирует всё. Например, Time To Live (TTL) в локальной версии не удаляет данные автоматически (вы должны делать это сами или терпеть).
- Мониторинга (CloudWatch) нет.
- Производительность не ограничена WCU/RCU (вы не сможете протестировать троттлинг).
NoSQL Workbench
Если DynamoDB Local - это "движок", то NoSQL Workbench - это "приборная панель". Это бесплатное GUI-приложение от AWS (для Windows/Mac/Linux), которое решает главную проблему Single Table Design: визуализацию.
В голове очень сложно представить, как данные пользователя и заказов лежат в одной таблице вперемешку. Workbench позволяет это нарисовать.
Ключевые возможности:
-
Data Modeler (визуальное проектирование):
- Вы создаете схему, определяете PK и SK.
- Вы добавляете примеры данных.
- Facets (грани): Это киллер-фича. Вы можете создать "Взгляд со стороны User" (отфильтровать только данные юзера) или "Взгляд со стороны Order". Это помогает проверить, работает ли ваш Single Table Design так, как вы задумали.
-
Visualizer (визуализация GSI):
- Вы нажимаете одну кнопку и видите, как ваши данные будут выглядеть во вторичном индексе (GSI).
- Вы сразу заметите ошибки типа "Ой, я выбрал плохой ключ для индекса, у меня тут перекос данных".
-
Operation Builder (генератор кода):
- Вы "накликиваете" сложный запрос (
Queryс фильтрами, проекциями и условиями) в GUI. - Workbench генерирует готовый код на Python, Node.js или Java.
- Больше не нужно гуглить "как написать KeyConditionExpression boto3". Вы просто копипастите готовый код.
- Вы "накликиваете" сложный запрос (
Идеальный рабочий процесс:
- Спроектировали таблицу в NoSQL Workbench.
- Нажали "Commit to DynamoDB" -> выбрали DynamoDB Local (localhost:8000). Workbench сам создаст таблицу и зальет тестовые данные в ваш Docker.
- Написали код и тесты, выполняя их с БД в Docker-контейнере.
- Когда все готово - задеплоили в реальный AWS.
Инструменты локальной разработки DynamoDB
Вопросов: 5
2.2. Структура и типы данных
Данные в DynamoDB хранятся в таблицах (Tables). Таблица - это коллекция элементов (Items), а каждый элемент - это набор атрибутов (Attributes). Думайте о таблице как о коллекции JSON-объектов (элементов), где у каждого объекта есть уникальный идентификатор.
Многие разработчики воспринимают DynamoDB просто как "KV-хранилище, куда можно положить JSON". Они делают json.dumps(obj) и сохраняют всё в одно строковое поле. Это ошибка.
DynamoDB - это строго типизированная база данных. Она понимает структуру вашего документа. Если вы используете нативные типы (Map, List, Set), вы получаете суперсилу: частичное обновление и атомарные операции без необходимости читать весь объект.
Все типы делятся на три категории:
Простые типы (Scalar)
Это "атомы" ваших данных.
-
String (S): Обычные строки (UTF-8). Это основной тип для ключей (PK и SK).
- Важно: DynamoDB не разрешает сохранять пустые строки
"". Это частая причина падения кода при миграции с SQL. Если строка пустая, либо не сохраняйте атрибут вообще, либо используйте placeholder (например,"-").
- Важно: DynamoDB не разрешает сохранять пустые строки
-
Number (N):
- Особенность: DynamoDB хранит числа как строки. Это сделано, чтобы сохранить точность (precision) до 38 знаков. В JavaScript
0.1 + 0.2 !== 0.3, а в DynamoDB - равно. - Математика: Вы можете делать атомарные инкременты.
SET views = views + :val. Вам не нужно знать текущее значение просмотров, чтобы увеличить его на 1.
- Особенность: DynamoDB хранит числа как строки. Это сделано, чтобы сохранить точность (precision) до 38 знаков. В JavaScript
-
Binary (B): Сжатые данные, картинки, криптографические ключи. Хранятся в Base64.
-
Boolean (BOOL):
true/false. -
Null (NULL): Используется редко, так как в NoSQL обычно отсутствие поля и есть
null.
Типы документов (Document Types)
Именно они делают DynamoDB документо-ориентированной базой.
-
List (L): Упорядоченный список (массив). Может содержать разные типы.
- Пример: История статусов заказа
[{"status": "NEW", "time": 100}, {"status": "PAID", "time": 200}].
- Пример: История статусов заказа
-
Map (M): Вложенный JSON-объект.
- Пример: Адрес доставки
{ "city": "Astana", "zip": 010000 }.
- Пример: Адрес доставки
Зачем использовать Map вместо JSON-строки? Представьте, что у вас есть огромный профиль пользователя (300 КБ). Пользователь сменил только аватарку.
- Подход "JSON-строка": Вы должны прочитать (Read) все 300 КБ, распарсить, поменять поле, запаковать и записать (Write) 300 КБ обратно. Вы платите RCU и много WCU.
- Подход "Native Map": Вы делаете
UpdateItem, указывая путь к вложенному полю:SET info.avatar = :new_url. DynamoDB сама найдет поле внутриMapи поменяет только его. Это быстрее, дешевле и, главное, не перезатирает изменения других полей, если они произошли параллельно.
Примеры в коде (Python/Boto3)
1. Map vs. JSON String (Почему Map лучше)
Плохой подход (Blob): Вы сохраняете объект как строку. База данных "слепа", она не видит, что внутри.
import json
# Мы "прячем" структуру данных в строку
user_profile = {
"city": "Astana",
"theme": "dark",
"notifications": True
}
table.put_item(
Item={
'user_id': 'alice',
# DynamoDB видит это как одну длинную строку (S)
'profile_blob': json.dumps(user_profile)
}
)
# Чтобы изменить тему на 'light', нам нужно:
# 1. GetItem (прочитать всё)
# 2. Распарсить JSON в коде
# 3. Изменить поле
# 4. PutItem (перезаписать ВСЮ строку обратно)
# Это дорого и вызывает Race Conditions.
Правильный подход (Native Map): Вы сохраняете объект как Map. DynamoDB видит структуру и дает доступ к вложенности.
table.put_item(
Item={
'user_id': 'alice',
# DynamoDB видит это как Map (M)
'profile': {
"city": "Astana",
"theme": "dark",
"notifications": True
}
}
)
# ТЕПЕРЬ: Изменяем тему одной командой (Точечное обновление)
table.update_item(
Key={'user_id': 'alice'},
# Обращаемся через точку к вложенному полю
UpdateExpression="SET profile.theme = :t",
ExpressionAttributeValues={':t': 'light'}
)
# Экономия: Мы не читали данные. Мы отправили байты только для одного поля.
Множества (Sets) - Скрытая жемчужина
Этого типа нет в стандартном JSON. Множество - это коллекция уникальных значений одного типа. Порядок не гарантируется.
- String Set (SS): Теги, роли, ID друзей.
- Number Set (NS): ID связанных сущностей.
- Binary Set (BS).
Зачем использовать Sets вместо List?
- Автоматическая дедупликация: Если вы добавите
{"admin", "user", "admin"}вSet, DynamoDB сохранит только{"admin", "user"}. List сохранит все дубликаты. - Атомарное удаление по значению: В Set можно удалить элемент по значению (
DELETE tags :val). В List - только по индексу (REMOVE tags[2]), что требует предварительного чтения.
Пример: Система тегов
У вас статья, которую редактируют два редактора:
- Редактор А хочет добавить тег "news".
- Редактор Б хочет добавить тег "hot".
С List и list_append - атомарное добавление работает:
- А делает
UpdateItem SET tags = list_append(tags, :new_tag)(где:new_tag = ["news"]). - Б делает
UpdateItem SET tags = list_append(tags, :new_tag)(где:new_tag = ["hot"]).
Итог: ["tech", "news", "hot"] - оба тега на месте.
Тогда в чём преимущество Set?
Проблема возникает при удалении:
- Редактор А хочет удалить тег "news".
- Редактор Б хочет удалить тег "hot".
С List:
- А читает список, находит "news" на индексе 1, делает
REMOVE tags[1]. - Б читает список, находит "hot" на индексе 2, делает
REMOVE tags[2]. - Но после удаления А индексы сдвинулись - Б удаляет не тот элемент.
С Set:
- А делает
DELETE tags :val(где:val = {"news"}). - Б делает
DELETE tags :val(где:val = {"hot"}).
Итог: Оба тега корректно удалены, без чтения и без race conditions.
Пример в коде: Магия множеств (Sets) для тегов
Представьте, что два админа одновременно добавляют теги пользователю.
# Сценарий: Добавить тег "senior", не зная, какие теги уже есть.
# Не нужно делать GetItem!
table.update_item(
Key={'user_id': 'bob'},
# Оператор ADD работает только с числами и Sets
UpdateExpression="ADD roles :new_role",
ExpressionAttributeValues={
# В Python Boto3 множество передается как set()
':new_role': {'senior'}
}
)
# Если у Боба были роли {'user', 'admin'},
# то теперь станут {'user', 'admin', 'senior'}.
# Дубликаты исключаются автоматически.
Ограничения, о которых нужно знать
-
Лимит размера элемента - 400 КБ. Это жесткий лимит. Он включает в себя не только значения, но и имена атрибутов.
- Совет: Если у вас миллионы записей, не называйте поля
customer_transaction_date_timestamp. Назовитеtx_ts. На миллионе записей это сэкономит гигабайты и деньги.
- Совет: Если у вас миллионы записей, не называйте поля
-
Вложенность и индексы. DynamoDB не умеет индексировать внутри JSON.
- Если у вас есть
Mapaddress: { city: "London" }, вы не можете создать GSI (вторичный индекс) по полюaddress.city. Ключом индекса могут быть только скалярные типы верхнего уровня (S,N,B). - Решение: Если вам нужно искать по городу, храните
cityкак отдельный атрибут верхнего уровня, а не внутриMap.
- Если у вас есть
-
Пустые Sets. Множество не может быть пустым. Если вы удалите последний элемент из
Set, атрибут исчезнет целиком. При чтении в коде вы должны быть готовы к тому, что поляtagsможет просто не быть в ответе (KeyError), а не к тому, что оно будет пустым списком[].
Когда выбирать что
| Критерий | List | Set |
|---|---|---|
| Нужен порядок элементов | ✅ | ❌ |
| Допустимы дубликаты | ✅ | ❌ |
| Атомарное добавление | ✅ list_append | ✅ ADD |
| Атомарное удаление по значению | ❌ | ✅ DELETE |
| Элементы - сложные объекты (maps) | ✅ | ❌ (только строки/числа/бинарные) |
2.3. Первичный ключ
Каждый элемент в таблице должен быть уникально идентифицирован первичным ключом (Primary Key). Это единственный обязательный атрибут для каждого элемента.
Первичный ключ определяет, как данные физически распределяются и как к ним можно обращаться. Он бывает двух типов:
-
Простой ключ (Simple): Состоит только из одного атрибута - ключа партиции (Partition Key или PK).
- Техническое определение: DynamoDB использует внутреннюю хэш-функцию для значения PK, чтобы определить, на какой физической партиции (сервере) будет размещен этот элемент.
- Как работает: Это обеспечивает равномерное распределение данных и операций ввода-вывода по множеству серверов.
- Ограничение: Позволяет выполнять только прямые операции
GetItem(получить один элемент по его PK) илиScan(полное сканирование таблицы). Например,GetItem(user_id='123').
-
Составной ключ (Composite): Состоит из двух атрибутов: ключа партиции (PK) и ключа сортировки (Sort Key или SK).
- Техническое определение: Все элементы с одинаковым значением PK физически хранятся вместе на одной партиции. Внутри этой партиции они физически отсортированы в порядке возрастания (по умолчанию) по значению SK.
- Как работает: PK по-прежнему определяет партицию (куда положить), а SK определяет точный порядок элементов внутри этой партиции.
- Преимущество: Это открывает мощные диапазонные запросы (
Query). Вы можете запросить один элемент поPKиSK, или группу элементов с однимPKи различнымиSK. Например,Query(PK='user_123')- получить все элементы пользователя 123. ИлиQuery(PK='user_123', SK.begins_with('ORDER#2024'))- получить все заказы пользователя 123, начинающиеся сORDER#2024.
Это фундаментальная концепция. Выбор PK и SK - это самое важное архитектурное решение, которое вы принимаете при проектировании, поскольку оно напрямую диктует, какие запросы к данным будут эффективными.
Типы данных и первичный ключ в DynamoDB
Вопросов: 6
2.4. Практика
Проектирование структуры данных для конкурента Twitter
Вы продолжаете работу как CTO стартапа-конкурента Twitter. Вместе с Назаром спроектируйте структуру данных для твитов и профилей пользователей в DynamoDB: выберите типы данных для каждого атрибута, определите Primary Key и обсудите, как правильно хранить счётчики лайков и хэштеги.
Назар - ваш персональный ИИ-наставник. Он поможет закрепить материал через практику и ответит на ваши вопросы.
💡 Все обсуждения с ИИ могут быть прочитаны администратором для улучшения качества обучения.
3. Базовые операции
3.1. Базовые операции (CRUD)
В SQL у вас есть универсальный SELECT, который может делать всё. В DynamoDB операции четко разделены по "стоимости" и эффективности. Понимание этого раздела спасет ваш бюджет.
Чтение данных: библиотечная аналогия
Представьте, что ваша таблица DynamoDB - это огромная городская библиотека. Есть способы найти книгу.
1. GetItem (заказ по шифру)
Самый быстрый и дешевый способ.
- Аналогия: Вы подходите к библиотекарю и даете точный шифр книги (ISBN): "Мне нужна книга с ID
user_123". Библиотекарь идет и приносит ровно одну книгу. - Что делает: Достает один элемент по его полному первичному ключу (PK + SK).
- Стоимость: 1 RCU (или 0.5 RCU при Eventually Consistent).
- Когда использовать: Когда вы точно знаете ID (например, открыть профиль пользователя по ссылке, открыть конкретный заказ).
# Дай мне пользователя "alice" (мы точно знаем ID)
response = table.get_item(
Key={'user_id': 'alice'}
)
item = response.get('Item')
(Примечание: Операции Query и Scan, позволяющие искать группы элементов, мы подробно разберем в следующем модуле).
Запись данных: Полная замена vs Редактирование
Многие новички используют PutItem для всего. Это ошибка.
1. PutItem (замена листа)
- Что делает: Создает новый элемент или полностью заменяет старый.
- Аналогия: Вы берете страницу с данными пользователя, выкидываете её в мусорку и пишете новую с чистого листа.
- Опасность: Если на старой странице были важные пометки (атрибуты), которые вы забыли переписать на новую, они исчезнут.
- Пример: Вы хотели обновить только
email, но использовалиPutItemтолько с полямиuser_idиemail. Поляname,age,address- стерлись, так как их не было в новом "листе".
# Осторожно! Это перезапишет весь объект.
table.put_item(
Item={
'user_id': 'alice',
'email': '[email protected]'
# Если тут были другие поля (Имя, Возраст) - они удалены!
}
)
2. UpdateItem (правка)
Инструмент точечного изменения. Используйте его по умолчанию.
- Что делает: Изменяет только указанные атрибуты. Остальные не трогает.
- Аналогия: Вы берете существующий документ и дописываете карандашом одну строчку. Остальной текст остается на месте.
- Суперсила: Поддерживает атомарные счетчики (
SET a = a + 1) и работу со списками/множествами (ADD tags :t). - Upsert: Работает как "Create or Update". Если элемента не было, он создастся.
# Безопасно. Меняем только email, остальное не трогаем.
table.update_item(
Key={'user_id': 'alice'},
UpdateExpression="SET email = :new_email",
ExpressionAttributeValues={':new_email': '[email protected]'},
ReturnValues="UPDATED_NEW" # Вернет обновленные данные
)
3. DeleteItem
Просто удаляет элемент по ключу.
- Совет: Можно использовать с
ConditionExpression. Например, "Удали пользователя, только если его статус 'Архив' (status = 'ARCHIVED')".
Резюме по операциям:
| Операция | Аналогия | Когда использовать |
|---|---|---|
GetItem | Книга по шифру | Точечный поиск по ID. |
PutItem | Чистый лист | Создание абсолютно нового элемента. |
UpdateItem | Правка карандашом | Редактирование, счетчики. |
Пример: Атомарный счетчик (Numbers)
Как реализовать счетчик просмотров страницы без транзакций и блокировок?
table.update_item(
Key={'page_id': 'home_page'},
# Мы используем значение поля в самом выражении
UpdateExpression="SET views_count = views_count + :incr",
ExpressionAttributeValues={
':incr': 1
},
# Опционально: вернуть новое значение сразу же
ReturnValues="UPDATED_NEW"
)
# Это потокобезопасно. Даже если 1000 человек кликнут одновременно,
# DynamoDB выстроит запросы в очередь и инкрементирует каждый.
3.2. PartiQL: SQL для NoSQL
Долгие годы входной порог в DynamoDB был высоким из-за специфического синтаксиса. Чтобы сделать простой SELECT, вам нужно было собирать JSON-монстров с параметрами вроде KeyConditionExpression и ExpressionAttributeValues.
AWS услышал эту боль и представил PartiQL - SQL-совместимый язык запросов для DynamoDB.
Теперь вы можете взаимодействовать с базой, используя старый добрый SELECT, INSERT, UPDATE и DELETE. Это работает и в консоли AWS, и в коде (через SDK).
Как это выглядит
Сравните, как выглядит получение заказа в классическом Boto3 и с использованием PartiQL.
Классический Boto3 (Native API):
response = table.get_item(
Key={'order_id': '1001'}
)
PartiQL:
# Выглядит как обычный SQL
response = client.execute_statement(
Statement='SELECT * FROM "Orders" WHERE "order_id" = \'1001\''
)
Кажется, что разницы мало. Но посмотрите на вставку данных:
PartiQL:
INSERT INTO "Orders" VALUE {'order_id': '1002', 'total': 50, 'status': 'NEW'}
Это гораздо понятнее для аналитиков и разработчиков, пришедших из мира реляционных БД. Вы можете делать простые выборки, фильтрацию и вставку, используя знакомый синтаксис.
Главная ловушка: Это НЕ реляционная база
Самая большая опасность PartiQL - иллюзия. Когда вы пишете SQL, ваш мозг переключается в режим реляционной базы. Вы думаете: "О, я могу пофильтровать по возрасту!".
Вы пишете:
SELECT * FROM "Users" WHERE age > 25
В PostgreSQL это нормальный запрос. В DynamoDB, если у вас нет индекса на поле age, этот запрос превратится в Full Table Scan.
- PartiQL - это просто транслятор (синтаксический сахар).
- Под капотом он преобразует ваш SQL в те же самые операции DynamoDB (
Scan,Query,PutItem). - Если вы не укажете Partition Key в
WHERE, PartiQL молча запуститScan, прочтет всю таблицу, сожжет ваши деньги (RCU) и вернет результат медленно.
Правило: Даже используя PartiQL, вы обязаны знать архитектуру ключей. Эффективный SQL-запрос в DynamoDB всегда должен содержать условие равенства по первичному ключу.
Когда использовать PartiQL?
- Ad-hoc запросы в консоли: Когда вам нужно быстро проверить, записались ли данные, или найти конкретный заказ. Писать
SELECTв консоли в 10 раз быстрее, чем заполнять формы фильтров. - Миграции и скрипты: Если нужно написать простой скрипт для массового обновления статусов, SQL-синтаксис часто лаконичнее.
- Onboarding новичков: Если в команду пришли люди, не знающие DynamoDB SDK, PartiQL позволит им начать писать код в первый же день, пока они изучают концепции PK/SK.
Когда НЕ использовать?
В сложном, высоконагруженном коде. Нативные методы API (get_item, query) более явные. Когда вы видите в коде table.scan(), вы сразу понимаете - тут опасно. Когда вы видите SELECT ..., опасность скрыта. В продакшене явное лучше неявного.
CRUD операции и PartiQL в DynamoDB
Вопросов: 6
3.3. Практика
Реализация базовых операций CRUD для конкурента Twitter
Вы продолжаете работу как CTO стартапа-конкурента Twitter. Вместе с Назаром напишите код для базовых операций DynamoDB: чтение профилей, обновление данных пользователей, реализация атомарных счётчиков лайков, работа с тегами и безопасное удаление твитов с проверкой прав доступа.
Назар - ваш персональный ИИ-наставник. Он поможет закрепить материал через практику и ответит на ваши вопросы.
💡 Все обсуждения с ИИ могут быть прочитаны администратором для улучшения качества обучения.
4. Поиск данных и индексы
4.1. Стратегии чтения
Query
Самая важная операция в DynamoDB. 90% ваших запросов должны быть Query.
- Аналогия: Вы знаете, что книги Толстого стоят на полке "Т" (Partition Key). Вы подходите к этой конкретной полке и берете все книги, написанные после 1860 года (Sort Key). Вы не бегаете по всей библиотеке, вы работаете в рамках одной секции (партиции).
- Что делает: Достает группу элементов, у которых одинаковый Partition Key.
- Суперсила: Вы можете фильтровать по Sort Key (диапазоны).
- Стоимость: Платите только за тот объем книг, который сняли с полки. Это очень эффективно.
from boto3.dynamodb.conditions import Key
# Дай мне все заказы Алисы (Полка), начиная с 2024 года (Фильтр по порядку)
response = table.query(
KeyConditionExpression=Key('PK').eq('USER#alice') & Key('SK').begins_with('ORDER#2024')
)
items = response['Items']
Scan
Операция, которую нужно избегать в повседневной работе.
- Аналогия: Вам нужно найти книгу, в которой на 5-й странице есть слово "синхрофазотрон". Вы не знаете ни автора, ни названия. Вам приходится брать каждую книгу в библиотеке, открывать её и читать. Библиотека парализована, библиотекари в мыле, вы платите огромные деньги за аренду зала.
- Что делает: Читает ВСЮ таблицу. Строка за строкой. С первой по последнюю.
- Фильтрация: Вы можете передать
FilterExpression, но DynamoDB применит фильтр ПОСЛЕ того, как прочитает данные с диска и снимет с вас деньги. - Стоимость: Огромная. Вы платите за прочтение всей таблицы, даже если найдете 0 книг.
- Когда использовать: Только для миграций или редких админских скриптов. Никогда на горячем пути пользователя.
4.2. Вторичные индексы
Ограничения Primary Key
Итак, вы построили идеальное хранилище (таблицу Users), где все лежит по user_id (PK). "Дай мне юзера user_123" - мгновенный ответ.
Но тут приходит менеджер и просит:
- "А найди мне пользователя по
email?" - "А покажи мне всех пользователей из
city='Pavlodar'?"
В SQL вы бы сказали "без проблем" и добавили CREATE INDEX ix_users_email ON users (email).
В DynamoDB ваш единственный способ запроса - это Query по ключу. Если у вас нет ключа для email, вы не можете сделать по нему запрос (кроме Scan, который мы не рассматриваем, так как это медленно и дорого).
Вторичный индекс (Secondary Index) - это копия вашей таблицы (или ее части), но с другим Primary Key. Это ваш "альтернативный паттерн доступа". Это как карточный каталог в библиотеке.
Global Secondary Index (GSI)
GSI - это индекс, у которого Partition Key (а опционально и Sort Key) отличается от основного ключа таблицы.
Пример: Таблица Users.
- Primary Key:
user_id(PK) - Бизнес-задача: "Найти пользователя по email".
- Решение: Мы создаем GSI (назовем его
EmailIndex) с ключом:- GSI PK:
email
- GSI PK:
Архитектура и ограничения
- Это копия: GSI - это, по сути, новая таблица, которой DynamoDB управляет за вас. Когда вы пишете в основную таблицу, DynamoDB асинхронно реплицирует эти данные в GSI.
- Асинхронность: Репликация занимает время (обычно < 1 секунды). Это значит, что GSI всегда Eventually Consistent. Вы не можете делать
Strongly Consistent Readиз GSI. (Про виды консистености будет рассказано ниже.) - Проекция (Projections): GSI не обязан хранить весь элемент. Вы можете выбрать:
KEYS_ONLY: GSI хранит только ключи (основной таблицы и GSI). Дешево по хранению, но чтобы получитьuser_name, вам придется сделать 2-й запрос к основной таблице.INCLUDE: Хранить ключи + некоторые атрибуты (указанные вами).ALL: Хранить полную копию элемента. Дорого по хранению, но быстро по чтению (не нужен 2-й запрос).
- Свой RCU/WCU: У GSI есть свой собственный бюджет RCU и WCU. Вы платите за хранение GSI и за его RCU/WCU отдельно.
Стоимость и производительность
- Запись: Каждая
PutItemв основную таблицу теперь потребляет WCU основной таблицы И WCU индекса (если атрибуты GSI изменились). GSI удваивает (или утраивает, ...) стоимость записи. - Чтение:
Query(IndexName='EmailIndex', KeyConditionExpression=...)потребляет RCU индекса, а не таблицы.
Редкий индекс (Sparse Index)
Это мощный паттерн. GSI копирует элемент только если в элементе присутствует атрибут Partition Key этого GSI.
- Пример: У вас есть
Orders. Вы хотите GSI для "открытых" заказов. - Решение: Вы создаете GSI с Partition Key
open_order_status. Вы записываете этот атрибутopen_order_status = 'OPEN'только еслиstatus = 'OPEN'. - Теперь ваш GSI хранит только 1% данных (открытые заказы). Он маленький, дешевый и быстрый.
Но вы должны помнить, что Primary Key (Partition Key + Sort Key) должен быть уникальным и в индексе тоже. Поэтому, когда у вас Partition Key одинаковый, вы должны так подобрать Sort Key, чтобы их комбинация была уникальной в каждом элементе.
Local Secondary Index (LSI)
LSI - это более редкий и специфичный зверь.
Сценарий: Таблица GameScores.
- Primary Key:
user_id(PK),game_id(SK). - Это позволяет мне делать
Query(user_id='alice')и получать все игры, в которые играла Алиса, отсортированные поgame_id. - Проблема: А что, если я хочу получить все игры Алисы, но отсортированные по
score(счету), чтобы построить ее личный топ?
Локальный вторичный индекс (LSI) - это индекс, у которого Partition Key тот же самый, а Sort Key - другой.
Таблица GameScores:
- Primary Key:
user_id(PK),game_id(SK) - LSI (назовем
ScoreIndex):- LSI PK:
user_id(тот же!) - LSI SK:
score(другой!)
- LSI PK:
Свойства LSI
- Общая партиция: Данные LSI хранятся на той же партиции, что и основные данные.
- Общий RCU/WCU: LSI не имеет своего бюджета. Он ест RCU/WCU из бюджета основной таблицы.
- Создание: LSI нельзя добавить к существующей таблице. Его можно создать только в момент создания таблицы. (GSI можно добавлять и удалять на лету).
- Лимит 10 ГБ: Суммарный размер всех элементов для одного PK (включая все его LSI) не может превышать 10 ГБ.
Когда LSI предпочтительнее GSI?
- Строгая консистентность: Так как LSI живет на той же партиции, вы можете делать
Strongly Consistent Readиз LSI. - Транзакции: LSI можно использовать в
TransactWriteItems. (Про транзакции будет рассказано ниже.) - Экономия: Если у вас и так read-heavy таблица, LSI "бесплатен" (он просто делит RCU/WCU).
Вывод:
- Нужен другой Partition Key? -> GSI.
- Нужна другая сортировка для того же Partition Key? -> LSI.
Query, Scan и вторичные индексы
Вопросов: 7
4.3. Практика
Реализация поиска данных и вторичных индексов для конкурента Twitter
Вы продолжаете работу как CTO стартапа-конкурента Twitter. Вместе с Назаром разберите стратегии чтения данных в DynamoDB: Query vs Scan, проектирование Global Secondary Index для поиска твитов по хэштегам, Local Secondary Index для сортировки ленты, и оптимизация паттернов доступа для высоконагруженного приложения.
Назар - ваш персональный ИИ-наставник. Он поможет закрепить материал через практику и ответит на ваши вопросы.
💡 Все обсуждения с ИИ могут быть прочитаны администратором для улучшения качества обучения.
5. Целостность данных
5.1. Модели консистентности
Представьте два сценария.
- Сценарий 1 (Банк): Вы переводите 100$. Вы делаете
Write(баланс -100), а затем сразуRead(показать новый баланс). Вы обязаны увидеть новый баланс. Это строгая консистеность (Strong Consistency). - Сценарий 2 (Соцсеть): Я лайкнул ваш пост. Я делаю
Write(likes +1). Вы сразу делаетеRead. Если вы увидите старое число лайков (10) еще полсекунды, а только потом новое (11), - ничего страшного не случится. Это консистентность в конечном счете (Eventually Consistency).
(На самом деле классификация разных типов консистентностей более сложная, но в контексте DynamoDB хватит этого понимания.)
Компромисс между консистентностью и производительностью
Под капотом DynamoDB хранит ваши данные в 3-х Availability Zones (ЦОДах).
- Когда вы делаете
Write, данные пишутся на лидера партиции, а он асинхронно реплицирует их на 2 другие реплики. - Eventually Consistent Read (по умолчанию): Ваш запрос на чтение может прийти к любой из 3-х реплик. Одна из них может отставать на 100 мс. Зато это быстро (т.к. можно читать с ближайшей) и дешево (1 RCU = 2 таких чтения).
- Strongly Consistent Read: Ваш запрос всегда идет на лидера партиции, который гарантирует, что у него самая свежая, подтвержденная запись. Но это больше нагружает лидера и дороже (1 RCU = 1 такое чтение).
Использование:
# Пример на Boto3 (Python)
import boto3
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('users')
# Eventually Consistent (по умолчанию)
response_eventual = table.get_item(
Key={'user_id': '123'}
)
# Strongly Consistent
response_strong = table.get_item(
Key={'user_id': '123'},
ConsistentRead=True # Вот этот флаг
)
Когда использовать каждую модель?
- Eventually Consistent: 95% всех сценариев. Лайки, комментарии, посты, IoT-данные, профили, кэш.
- Strongly Consistent: 5% сценариев. Финансовые транзакции, инвентарь ("осталось 3 товара, надо забронировать"), критичные сценарии "read your own writes" (вы изменили пароль и сразу должны с ним логиниться).
Модели консистентности в DynamoDB
Вопросов: 5
5.2. Транзакции
Долгие годы "NoSQL" был синонимом "никаких транзакций". Это было главным аргументом противников, и, честно говоря, главной головной болью разработчиков, которые пытались обеспечивать атомарность на уровне приложения.
Но что делать, если вы продаете билеты на концерт?
Update(ticket='A1', status='RESERVED', user='alice')Update(user='alice', balance=balance-100)
Что если шаг 1 прошел, а шаг 2 упал (например, ProvisionedThroughputExceededException)? Вы отдали билет бесплатно.
Что если шаг 2 прошел, а шаг 1 упал? Вы взяли деньги, но не дали билет.
Это классическая потребность в атомарности "все или ничего".
Но есть и вторая, не менее важная причина: атомарные проверки условий (Conditional Checks) на разных элементах.
Представьте тот же сценарий, но сложнее:
- Проверить, что
ticket='A1'имеетstatus='AVAILABLE'. - Проверить, что
user='alice'имеетbalance >= 100. - Только если ОБА условия верны, выполнить: a.
Update(ticket='A1', status='RESERVED'), b.Update(user='alice', balance=balance-100).
Вы не можете сделать это двумя отдельными запросами. Между шагом 1 и 2 другой пользователь может купить билет. Вам нужно, чтобы и проверка, и запись для обоих элементов произошли как одна неделимая операция.
Механизм транзакций в DynamoDB
DynamoDB предоставляет полноценные ACID-транзакции (Атомарность, Консистентность, Изоляция, Долговечность) для нескольких элементов (до 100 за раз). Они бывают двух типов:
TransactWriteItems
Это "рабочая лошадка" транзакций. Она позволяет вам сгруппировать до 100 операций Put, Update или Delete в один запрос "все или ничего".
- Атомарность: Либо все 100 операций успешно применяются, либо ни одна из них не применяется.
- Изоляция: Во время выполнения транзакции (которое занимает миллисекунды), другие запросы не увидят наполовину примененное состояние.
- Проверки условий: Это самое мощное. Каждая операция (
Update,Put...) внутри транзакции может иметь свой собственныйConditionExpression. Если хотя бы одно из 100 условий не выполняется (например,balance < 100илиticket_status != 'AVAILABLE'), вся транзакция немедленно откатывается.
import boto3
# Убедитесь, что у вас есть клиент
dynamodb_client = boto3.client('dynamodb')
try:
# Этот токен КРИТИЧЕСКИ важен для идемпотентности
# (подробнее об этом ниже)
client_token = "my-unique-transaction-id-123"
response = dynamodb_client.transact_write_items(
ClientRequestToken=client_token,
TransactItems=[
{
# 1. Забронировать билет
'Update': {
'TableName': 'Tickets',
'Key': {'ticket_id': {'S': 'A1'}},
'UpdateExpression': 'SET owner = :user',
# Условие: билет должен быть свободен
'ConditionExpression': 'attribute_not_exists(owner)',
'ExpressionAttributeValues': {':user': {'S': 'alice'}}
}
},
{
# 2. Списать деньги
'Update': {
'TableName': 'Users',
'Key': {'user_id': {'S': 'alice'}},
'UpdateExpression': 'SET balance = balance - :val',
# Условие: у пользователя должно быть достаточно денег
'ConditionExpression': 'balance >= :val',
'ExpressionAttributeValues': {':val': {'N': '100'}}
}
}
]
)
print("Transaction Successful!")
except dynamodb_client.exceptions.TransactionCanceledException as e:
# Это СПЕЦИАЛЬНАЯ ошибка для транзакций
print(f"Transaction Failed:")
# e.CancellationReasons покажет, какое условие не выполнилось
for reason in e.response.get('CancellationReasons', []):
if reason.get('Code') == 'ConditionCheckFailed':
print(f" Reason: Condition check failed for item {reason['Item']}.")
else:
print(f" Reason: {reason['Code']}")
except Exception as e:
# Другие ошибки (например, сетевые)
print(f"An error occurred: {e}")
TransactGetItems
Почему бы просто не сделать два GetItem подряд?
Представьте: У пользователя есть два счета: "Текущий" (checking) и "Накопительный" (savings). Вам нужно показать виджет "Общий баланс", который является суммой этих двух счетов.
- Ваше приложение делает
GetItem(account='checking')-> получает 100$. - В эту миллисекунду в бэкенде срабатывает
TransactWriteItems(перевод денег):Update(account='checking', balance=balance-50),Update(account='savings', balance=balance+50). - Ваше приложение (продолжая выполнять свой код) делает
GetItem(account='savings')-> и читает уже новое значение: 150$. - Результат: Приложение показывает "Общий баланс: 100$ + 150$ = 250$".
Хотя реальный общий баланс пользователя всегда был 200$ (100+100 до, 50+150 после). Вы только что создали 50$ из воздуха из-за "разорванного чтения" (torn read).
TransactGetItems решает эту проблему. Он "замораживает" время и читает до 100 элементов одновременно, гарантируя "снимок" (snapshot isolation). Выполняя TransactGetItems для checking и savings, вы гарантированно получите либо (100$, 100$), либо (50$, 150$), но никогда (100$, 150$). Ваш "Общий баланс" всегда будет 200$.
Важное замечание: TransactGetItems не решает гонку состояний (race condition) при записи. Если вы прочитали, что билет свободен, это не мешает другому процессу купить его в следующую миллисекунду. Для атомарной проверки-и-записи используется только TransactWriteItems с ConditionExpression. TransactGetItems решает проблему консистентности при отображении данных.
Ограничения и "подводные камни"
- Стоимость: Транзакции дорогие. Они потребляют в 2 раза больше RCU/WCU.
- Запись: Каждый
Updateв транзакции потребляет 2 WCU (вместо 1). 1 WCU за "подготовку" (чтение для проверки конфликтов) и 1 WCU за "фиксацию" (запись). - Чтение: Каждая
GetвTransactGetItemsпотребляет 2 RCU (вместо 1 RCU/0.5 RCU).
- Запись: Каждый
- Лимиты: До 100 элементов в одной транзакции. (Раньше было 25, так что это улучшение.) Общий размер транзакции не может превышать 4 МБ.
- Идемпотентность (Очень важно!):
TransactWriteItemsпо умолчанию не идемпотентна. Если вы отправили запрос, но получили тайм-аут сети, вы не знаете, прошла транзакция или нет. Если вы отправите тот же запрос еще раз, вы можете продать два билета и списать деньги дважды.- Решение: Вы обязаны использовать
ClientRequestToken. Это уникальная строка, которую вы генерируете. Если DynamoDB получает два запроса с одним и тем же токеном (в течение 10 минут), он выполнит его только один раз, а на второй запрос просто вернет сохраненный результат первого.
- Решение: Вы обязаны использовать
- Обработка ошибок: Не ловите
Exception. ЛовитеTransactionCanceledException. Оно содержит полеCancellationReasons, которое точно скажет вам, почему транзакция была отменена (например,ConditionCheckFailedилиThrottlingError).
Вывод: Транзакции - это мощнейший инструмент для обеспечения целостности данных, который закрыл большой пробел в NoSQL. Но они - "скальпель", а не "молоток". Они дороже и сложнее обычных операций. Используйте их только там, где атомарность "все или ничего" или атомарные проверки являются абсолютным бизнес-требованием.
Транзакции в DynamoDB
Вопросов: 6
5.3. Оптимистическая блокировка
Давайте вернемся к проблеме конкурентного доступа, но посмотрим на неё под другим углом.
Сценарий: Админ-панель и конфиги
Представьте, что у вас есть внутренняя админка для управления микросервисами.
- DevOps Ержан открывает страницу настроек сервиса
Payment-Gateway. Он видитtimeout: 30s. Он хочет изменить это на60s, так как сервис не справляется. - В эту же секунду SRE Катя открывает ту же страницу. Она тоже видит
timeout: 30s. Но она пришла, чтобы изменить другой параметр - включитьdebug_mode: trueдля отладки инцидента. - Ержан меняет таймаут, нажимает "Сохранить". В базу улетает JSON:
{timeout: 60, debug: false}. - Катя (которая всё еще смотрит на старую версию страницы, где таймаут 30) включает дебаг и нажимает "Сохранить". В базу улетает JSON:
{timeout: 30, debug: true}.
Проблема (Lost Update): Запись Кати полностью перезатерла изменения Ержана. Таймаут снова стал 30 секунд. Ержан уверен, что починил прод, но прод продолжает падать.
Решение: Оптимизм вместо блокировок
Оптимистическая блокировка работает по принципу git push. Если вы попытаетесь сделать пуш в репозиторий, но коллега успел залить свои коммиты раньше, Git отклонит ваш пуш и скажет: "Сначала стяни изменения (pull), разреши конфликты, а потом пушь".
В DynamoDB мы реализуем ту же логику через Versioning.
- Мы добавляем в каждый элемент атрибут
version(число). - Когда фронтенд читает конфиг, он запоминает версию (например,
v=1). - Когда фронтенд отправляет изменения назад, он говорит DynamoDB: "Обнови этот конфиг, НО ТОЛЬКО ЕСЛИ его версия в базе всё ещё равна
1".
В рамках этой же операции мы атомарно увеличиваем версию (v=2).
ConditionExpression - страж ваших данных
В DynamoDB нет команд LOCK или UNLOCK. Вся магия происходит в параметре ConditionExpression.
Это условие проверяется на сервере AWS атомарно перед записью. Если условие возвращает False (версия в базе уже 2, а мы прислали 1), запись отклоняется, и данные не портятся.
from botocore.exceptions import ClientError
def update_config_safely(service_name, new_config, version_read_by_admin):
try:
response = table.update_item(
Key={'service_name': service_name},
# 1. Мы обновляем поля конфига и инкрементируем версию
UpdateExpression='SET config = :new_conf, version = version + :incr',
# 2. ЖЕЛЕЗНОЕ УСЛОВИЕ:
# "Выполняй это, только если версия в базе совпадает с той, что была в браузере"
ConditionExpression='version = :expected_version',
ExpressionAttributeValues={
':new_conf': new_config,
':incr': 1,
':expected_version': version_read_by_admin
},
ReturnValues="UPDATED_NEW"
)
print("Успех! Конфиг обновлен. Новая версия:", response['Attributes']['version'])
except ClientError as e:
# 3. Перехватываем отказ
if e.response['Error']['Code'] == 'ConditionalCheckFailedException':
print("Конфликт! Кто-то (Катя?) изменил конфиг, пока вы редактировали.")
print("Нужно показать пользователю ошибку: 'Данные устарели, обновите страницу'.")
else:
raise
Транзакции vs Оптимистическая блокировка
Вы можете спросить: "Зачем мне это, если есть транзакции (TransactWriteItems)?".
Это вопрос цены и масштаба.
- Стоимость: Транзакции стоят x2 WCU (подготовка + коммит). Оптимистическая блокировка стоит x1 WCU. Это обычный
UpdateItem, просто с условием. - Сценарий:
- Транзакции нужны для атомарности нескольких разных элементов ("списать деньги у юзера А, начислить юзеру Б").
- Оптимистическая блокировка идеальна для защиты целостности одного сложного объекта, который долго редактируется на клиенте.
Где подвох?
Оптимистическая блокировка плоха при высокой конкуренции (High Contention).
Представьте, что вы реализуете счетчик "Количество мест на курсе" и 1,000 человек одновременно нажали "Купить".
- Все 1,000 прочитали
seats=10, version=5. - Все 1,000 пытаются сделать
Update ... Condition version=5. - Один запрос проходит.
- 999 запросов получают ошибку
ConditionalCheckFailedException.
В этом случае оптимистическая блокировка превращается в DDoS-атаку на самого себя, так как клиенты будут бесконечно пытаться перечитать и перезаписать данные. Для таких счетчиков лучше использовать атомарные инкременты (SET seats = seats - 1) без проверки версий, либо полноценные очереди.
5.4. Time To Live (TTL)
Ваша таблица user_sessions раздулась до 50 ТБ, и 99% из этого - сессии 5-летней давности. Это стоит вам денег за хранение.
Традиционный подход (плохой): Писать cron job, который Scan таблицу и Delete старые записи. Это ужасно: Scan дорогой, а Delete потребляет WCU.
Настройка и работа TTL
TTL - это бесплатное удаление элементов.
- Вы включаете TTL для таблицы, указывая имя атрибута (например,
expiration_time). - В этот атрибут вы должны записывать timestamp в формате Unix Epoch (секунды).
- Всё.
import time
# Записываем сессию, которая должна "умереть" через 24 часа
table.put_item(
Item={
'session_id': 'abc-123',
'user_id': 'alice',
'data': '...',
# Вот он, TTL
'expiration_time': int(time.time()) + 86400 # (now + 24h)
}
)
Особенности удаления
- Бесплатно: Удаление не потребляет WCU.
- Не мгновенно: DynamoDB гарантирует, что удалит элемент после
expiration_time, но не сразу. Обычно это занимает минуты, но AWS говорит, что может занять до 48 часов. - Не для логики: Не используйте TTL для точной бизнес-логики (например, "отменить заказ ровно в 12:00"). Используйте его для "сборки мусора".
- Streams: Удаленные через TTL элементы появляются в Streams, так что вы можете, например, триггерить Lambda и архивировать их в S3 перед удалением.
Оптимистическая блокировка и TTL
Вопросов: 5
5.5. Практика
Реализация целостности данных и транзакций для конкурента Twitter
Вы продолжаете работу как CTO стартапа-конкурента Twitter. Вместе с Назаром разберите модели консистентности (Eventually vs Strongly Consistent), транзакции для атомарных операций, оптимистическую блокировку для защиты от потерянных обновлений, и TTL для автоматической очистки устаревших данных.
Назар - ваш персональный ИИ-наставник. Он поможет закрепить материал через практику и ответит на ваши вопросы.
💡 Все обсуждения с ИИ могут быть прочитаны администратором для улучшения качества обучения.
6. Моделирование данных
Это самый сложный, самый важный и самый контр-интуитивный раздел. Здесь новички, пришедшие из SQL, могут наделать ошибок.
6.1. Single Table Design (STD)
- SQL-подход: Вы создаете 4 таблицы:
Users,Orders,Products,OrderItems. - Проблема:
- Производительность: Чтобы собрать страницу заказа, вашему приложению нужно сделать 4-5 запросов (
Get User,Get Order,Get OrderItems,Get Products). Это медленно (5 x 5мс = 25мс). - Экономика: У вас 4 таблицы.
Users- read-heavy.Orders- write-heavy. Вы должны настраивать RCU/WCU для каждой из них. Это сложно и дорого.
- Производительность: Чтобы собрать страницу заказа, вашему приложению нужно сделать 4-5 запросов (
Single Table Design - это паттерн, при котором вы храните разнородные сущности (Users, Orders) в одной физической таблице, чтобы получать их одним запросом.
Это реализуется через "перегрузку" ключей PK и SK.
- Таблица:
AppTable - Ключи:
PK(string),SK(string)
Пример схемы (E-commerce)
| PK | SK | type (атрибут) | data (атрибут) |
|---|---|---|---|
USER#alice | USER#alice | User | { "name": "Alice", "email": ... } |
USER#alice | ADDR#home | Address | { "city": "Astana", ... } |
USER#alice | ORDER#1001 | Order | { "status": "PENDING", ... } |
USER#alice | ORDER#1002 | Order | { "status": "SHIPPED", ... } |
ORDER#1001 | ITEM#SKU123 | OrderItem | { "product_name": "Book", ... } |
ORDER#1001 | ITEM#SKU456 | OrderItem | { "product_name": "Pen", ... } |
Что это дает? Вы делаете один запрос Query(PK='USER#alice') и получаете сразу и профиль, и адреса, и историю заказов. Это невероятно быстро.
6.2. Почему STD - не серебряная пуля
Долгое время STD продавали как единственный правильный способ. Но у этого подхода есть огромная цена, о которой часто молчат.
-
Высокий порог входа и сложность (Complexity). В SQL вы открываете таблицу
Ordersи видите заказы. Всё понятно. В STD вы открываете таблицу и видите "кашу" из всех данных сразу. Читать такую таблицу глазами в консоли AWS невозможно. Новому разработчику в команде (особенно джуниору) очень сложно объяснить, почему "чтобы найти пользователя, надо искать в поле PK строку, начинающуюся наORG#, а в поле SK - дату регистрации". -
Жесткость схемы (Inflexibility). Это главная проблема. STD требует абсолютного подхода "Access Patterns First". Вы проектируете таблицу под конкретные экраны вашего UI.
- Сценарий: Вы спроектировали ключи так, чтобы идеально показывать "Заказы пользователя".
- Изменение: Бизнес приходит через полгода и говорит: "Мы меняем логику, теперь заказы привязаны не к пользователю, а к отделу компании".
- Боль: В SQL вы бы просто поменяли
Foreign Key. В STD вам, возможно, придется переписывать всю таблицу (ETL-миграция), потому что ваши Primary Keys больше не подходят под новый запрос. Single Table Design оптимизирован под скорость чтения, но он хрупок к изменениям бизнес-требований.
Почему это больнее, чем в обычном подходе?
Может показаться, что в Multi-Table подходе при смене требований тоже нужно менять схему. Но разница фундаментальна:
- В Multi-Table (связь через атрибут): У заказа есть свой независимый ключ
ORDER#123, а связь с пользователем - это просто полеuser_id. Чтобы привязать заказ к отделу, вы делаете дешевый UpdateItem, меняя полеuser_idнаdept_id. Сама запись заказа физически остается на месте. - В STD (связь через ключ): Чтобы получить заказы пользователя одним запросом вместе с профилем, вы вынуждены "запекать" ID пользователя прямо в Partition Key заказа (например,
PK=USER#alice,SK=ORDER#123). - Проблема: В DynamoDB нельзя изменить Primary Key. Вы не можете сделать
UpdateItemи поменятьUSER#aliceнаDEPT#sales. Вам придется физически удалить старую запись и создать новую. Это превращает простое обновление ссылки в сложнейшую миграцию данных (ETL).
- Проблемы с аналитикой. Экспортировать данные в Data Lake сложнее. Если вы выгрузите таблицу в S3, аналитикам придется писать сложные фильтры: "Возьми строки, где SK начинается на
ORDER#, распарси их одним способом, а строкиUSER#- другим".
Когда не использовать Single Table Design?
Современная рекомендация звучит так: Не объединяйте сущности, если вы не запрашиваете их вместе.
- GraphQL: Если у вас GraphQL API (например, AppSync), он сам умеет эффективно "склеивать" данные из разных источников. Выигрыш от STD теряется, а сложность остается.
- Новые проекты (MVP): На старте стартапа вы не знаете своих паттернов доступа. Они поменяются 10 раз. Использовать STD на старте - это преждевременная оптимизация. Создайте отдельные таблицы (
Users,Orders). Это проще рефакторить. - Несвязанные данные: Нет смысла хранить "Логи аудита" и "Профили пользователей" в одной таблице. Они никогда не запрашиваются вместе.
Прагматичный подход (Multi-Table Design)
Вам можно использовать несколько таблиц в DynamoDB. Это не грех.
- Таблица
Users(Профили). - Таблица
Orders(Заказы и позиции заказов - здесь STD оправдан, так как заказ и его товары - это единое целое). - Таблица
Products.
Итог: Используйте Single Table Design только там, где у вас есть доказанная потребность в атомарном получении связанных данных (Parent + Children) за один запрос, и вы готовы платить за это сложностью поддержки. Во всех остальных случаях отдельные таблицы - это нормальный выбор.
6.3. Денормализация
В университете нас учили: "Никогда не дублируй данные! Третья нормальная форма!". В мире DynamoDB это правило меняется на: "Дублируй все, что нужно, чтобы чтение было быстрым".
- Проблема: В схеме выше у
OrderItemестьproduct_name. Но ведьproduct_nameможет измениться! - Ответ: Это нормально. Заказ - это "слепок" (snapshot) на момент времени. Когда заказ
ORDER#1001создан, мы копируемproduct_nameизPRODUCT#SKU123вORDER#1001. Если имя продукта изменится, в старом заказе останется старое имя. Это и есть правильное поведение.
Управление обновлениями дублированных данных
- Проблема: У
USER#aliceестьname: 'Alice'. Это имя скопировано в 100 ее заказов. Алиса меняет имя на 'Alice Smith'. - Решение 1 (Ничего не делать): Оставить 'Alice' в старых заказах. Это часто приемлемо.
- Решение 2 (Event-Driven):
- Обновление
PK: USER#aliceгенерирует событие в DynamoDB Streams. - Lambda-триггер ловит это событие.
- Lambda находит все связанные сущности и обновляет
user_nameв них. Это дорого и сложно. Используйте, только если критично необходимо.
- Обновление
6.4. Сначала - паттерны запросов
По-английски это называется Access Patterns First Approach. Это главное правило DynamoDB.
SQL-подход: Вы рисуете ER-диаграмму (сущности). Вы нормализуете ее. Потом вы пишете SELECT-запросы.
DynamoDB-подход:
- Вы садитесь с бизнес-аналитиком и сначала выписываете все запросы (Access Patterns), которые нужны вашему приложению.
- Нам нужно:
- A1: Найти юзера по ID.
- A2: Найти юзера по Email.
- A3: Найти все заказы юзера (последние первыми).
- A4: Найти заказ по ID.
- A5: Найти все товары в заказе.
- Только теперь вы проектируете таблицу (и ее GSI), которые удовлетворяют этим 5-ти запросам.
Вы не можете "просто построить" таблицу, а потом думать, как ее запрашивать. Вы проектируете таблицу под ваши запросы.
Single Table Design и моделирование данных
Вопросов: 6
6.5. Практика
Моделирование данных и Single Table Design для конкурента Twitter
Вы продолжаете работу как CTO стартапа-конкурента Twitter. Вместе с Назаром разберите Single Table Design (преимущества и недостатки), денормализацию данных, подход "Access Patterns First", и когда лучше использовать несколько таблиц вместо одной.
Назар - ваш персональный ИИ-наставник. Он поможет закрепить материал через практику и ответит на ваши вопросы.
💡 Все обсуждения с ИИ могут быть прочитаны администратором для улучшения качества обучения.
7. Производительность и масштабирование
7.1. Горячие партиции
Давайте проведём аналогию. Вы построили роскошное, 10-полосное шоссе для вашего приложения. Вы выделили (Provisioned) 1,000 WCU (единиц записи). DynamoDB, чтобы справиться с этим, разделила вашу таблицу на 10 физических партиций, и дала каждой партиции (полосе) свой собственный, изолированный бюджет в 1,000 WCU.
Но тут "Черная пятница", и все 100,000 машин (запросов) пытаются ехать по одной полосе - "купить PS5" (product_id='PS5'). Поскольку product_id является ключом партиции (PK), DynamoDB хэширует значение 'PS5' и направляет все 100,000 запросов на одну и ту же партицию.
- Проблема: Все 100,000 запросов хэшируются в одну партицию. Ее бюджет - 1,000 WCU. Она немедленно начинает троттлить (throttling) - возвращать ошибки
ProvisionedThroughputExceededException. - Ваша таблица в целом имеет 10,000 WCU, но она не работает, потому что одна партиция горячая.
Это и есть горячая партиция (Hot Partition). Это - ахиллесова пята DynamoDB. Она ломает главное обещание сервиса - "гарантированную производительность при любом масштабе". Потому что производительность всей таблицы приносится в жертву производительности одной самой загруженной партиции.
Другой классический пример: социальная сеть. user_id - отличный PK. Но когда Леди Гага (один user_id) пишет твит, и миллион человек одновременно пытаются обновить ее счетчик лайков (один item), они создают горячую партицию.
Диагностика
Начинающие разработчики часто совершают ошибку: они смотрят на общую утилизацию таблицы. "Моя таблица потребляет 800 WCU из 10,000 WCU (8%), но я получаю ThrottlingErrors! Как это возможно?"
Это и есть главный симптом горячей партиции.
- CloudWatch Metrics: Вы обязаны смотреть на два графика одновременно:
WriteThrottleEvents(илиReadThrottleEvents) иConsumedWriteCapacityUnits(потребляемые WCU). Если вы видите, чтоThrottleEventsрастут, аConsumedCapacityUnitsзначительно ниже, чемProvisionedCapacityUnits(ваш общий бюджет), - у вас на 99% горячая партиция. Общий бюджет есть, но он выделен не той партиции, куда идет нагрузка. - CloudWatch Contributor Insights: Это платный, но абсолютно незаменимый инструмент для продакшен таблиц. Метрики CloudWatch говорят вам: "У вас пожар". Contributor Insights говорит вам: "Пожар по адресу:
product_id='PS5'. Вот этот ключ получает 40% всего трафика". Без этого инструмента вы в затруднительном положении. Вы видите троттлинг, но не знаете, какой ключ его вызывает. Этот инструмент строит топ-N горячих ключей в реальном времени.
Техники решения (как охладить)
1. Выбрать хороший Partition Key (профилактика, 90% успеха)
Это ваш первый и главный рубеж обороны. Горячая партиция - это почти всегда следствие плохого дизайна ключа.
- Плохой PK:
status(значений всего 3: 'PENDING', 'SHIPPED', 'ERROR'). Миллионы заказов 'PENDING' свалятся в одну партицию. - Плохой PK:
date(в формате 'YYYY-MM-DD'). Все заказы за "сегодня" будут писаться в одну партицию. - Хороший PK:
user_id,device_id,order_id,session_id. Это ключи с высокой кардинальностью (high cardinality) - у них миллионы уникальных значений. Это гарантирует, что DynamoDB "размажет" ваши данные по тысячам партиций.
Но, как мы видели, даже ключ с высокой кардинальностью (как user_id) может иметь горячее значение (lady_gaga). Для этого нужно второе решение.
2. Write Sharding / Sharding Keys (10% успеха)
Это "ядерный" вариант, когда вы не можете избежать горячего элемента. "Черная пятница" реальна, и PS5 - это реальная, известная проблема.
Цель: Мы должны "обмануть" DynamoDB. Мы должны заставить ее распределить один 'PS5' на несколько партиций.
Техника (добавление суффикса):
-
Было (проблема):
PK: 'PS5'Все 100,000 WCU идут в одну партицию. -
Стало (решение): Вы решаете разбить 'PS5' на 10 виртуальных "контейнеров". Вы добавляете суффикс (случайное число от 0 до 9).
PK: 'PS5#0'PK: 'PS5#1'...PK: 'PS5#9' -
Как меняется код записи: При записи (покупке) вы больше не пишете в
PK='PS5'. Вы пишете вPK='PS5#' + random(0, 9). Теперь ваши 100,000 запросов равномерно распределяются по 10 разным ключам. DynamoDB хэширует их и отправляет на 10 разных физических партиций. Каждая партиция получает всего 10,000 WCU (вместо 100,000), что уже можно переварить (например, увеличив бюджет до 10,000 WCU на партицию). -
Как меняется код чтения - цена, которую вы платите: Вот и компромисс. Раньше, чтобы узнать, "сколько PS5 продано", вы делали один
GetItem(PK='PS5'). Это было быстро и дешево. Теперь вы не знаете, где лежит счетчик. Он "размазан" по 10 ключам. Чтобы получить общую сумму, вы обязаны в коде вашего приложения:- Выполнить 10
GetItem(илиQuery) параллельно:Get(PK='PS5#0'),Get(PK='PS5#1')...Get(PK='PS5#9'). - Получить 10 ответов.
- Суммировать эти 10 значений в коде, чтобы получить реальный итог.
- Выполнить 10
Это гораздо медленнее и дороже (10 RCU вместо 1 RCU) для чтения. Но это единственный способ масштабировать взрывную запись на один элемент.
Горячие партиции и их решение
Вопросов: 5
7.2. Тарифные планы (Capacity Modes)
On-Demand vs Provisioned
- On-Demand (по требованию):
- Когда: новый проект, dev/test, неизвестная или "рваная" нагрузка.
- Компромисс: Вы платите за запрос. Дорого при стабильной нагрузке, $0 при простое.
- Provisioned (выделенный):
- Когда: Стабильный продакшен с предсказуемой нагрузкой.
- Компромисс: Вы платите за час за выделенную мощность. Дешево при утилизации. Дорого при простое.
Пример расчёта стоимости
- Сценарий: 100 WCU и 100 RCU (стабильно).
- On-Demand: 259,200,000 WCU/мес. * ~$1.4/млн WCU = $362/мес
- Provisioned: 100 WCU * 24 ч * 30 дн * ~$0.00074/WCU-час = $53/мес
Вывод: При стабильной нагрузке On-Demand в 7 раз дороже.
Автоскейлинг в режиме Provisioned
Лучшее из двух миров. Вы ставите Provisioned режим, Min Capacity = 10, Max Capacity = 1000, и Target Utilization = 70%. AWS будет сам добавлять и убирать RCU/WCU. Это почти всегда дешевле, чем On-Demand.
7.3. Пакетные операции (Batch Operations)
- Проблема: Мне нужно загрузить 1,000 элементов. Я делаю 1,000 вызовов
PutItem. Это медленно из-за сетевой задержки (1,000 * 5мс = 5 секунд). - Решение:
BatchWriteItem.
BatchGetItem, BatchWriteItem
BatchWriteItem: Принимает список до 25PutItemилиDeleteRequestв одном вызове.BatchGetItem: Принимает список до 100GetItemв одном вызове.
Ограничения и обработка ошибок
- НЕ транзакция:
BatchWriteItem- это не "все или ничего". 20 из 25-ти запросов могут пройти, а 5 - упасть. UnprocessedItems: Это главная ловушка. Вы обязаны проверять ответ.BatchWriteItemможет вернуть200 OK, но в теле ответа будет полеUnprocessedItems. Ваше приложение обязано (с exponential backoff) повторить запрос сUnprocessedItems.
7.4. DynamoDB Accelerator (DAX)
На протяжении всего времени мы хвалили DynamoDB за "гарантированную задержку в единицах миллисекунд" (single-digit milliseconds). Это невероятно быстро для 99% приложений.
Но что, если вы - тот 1%? Что, если вы платформа AdTech (Real-Time Bidding), и у вас есть 10 миллисекунд на весь аукцион, а не только на один запрос к БД? В этом мире 5 миллисекунд - это уже "медленно". Что, если вы - сверхпопулярная игра, и ваш лидерборд читают 10 миллионов раз в секунду?
Внезапно у вас появляются две новые проблемы:
- Проблема задержки: single-digit milliseconds (2-9 мс) - это слишком медленно. Вам нужны микросекунды (µs).
- Проблема стоимости (RCU): Обслуживать 10,000,000 чтений в секунду с помощью Provisioned RCU - это астрономически дорого. Гораздо дешевле обслуживать их из оперативной памяти (кэша), чем постоянно "дергать" дисковую подсистему, даже если это NVMe.
DAX - "прозрачный" кэш на стероидах
DAX (DynamoDB Accelerator) - это полностью управляемый, in-memory кэш, созданный специально для DynamoDB.
Думайте о нем не как о Redis/Memcached (где вы вручную управляете Set(key, val) и Get(key)). Думайте о нем как о прозрачном слое, который притворяется самой DynamoDB.
Совместимость с API
В этом и есть гениальность DAX. Вам не нужно менять логику вашего приложения.
- Старый код (без DAX):
import boto3
# Обычный клиент, подключается к эндпоинту DynamoDB
dynamodb_client = boto3.client('dynamodb', region_name='us-east-1')
dynamodb_client.get_item(...)
- Новый код (с DAX):
import amazondax
# Специальный DAX-клиент, подключается к эндпоинту DAX-кластера
dax_client = amazondax.AmazonDaxClient(endpoint_url='my-dax-cluster.xxxxx.dax.amazonaws.com')
# ВАШ ОСТАЛЬНОЙ КОД НЕ МЕНЯЕТСЯ!
dax_client.get_item(...)
Вы просто меняете эндпоинт и библиотеку клиента. Вся ваша логика get_item, query, scan остается идентичной.
Механика работы (чтение и запись)
DAX-клиент умный. Когда вы делаете get_item:
- Запрос идет сначала в DAX-кластер (в RAM).
- Cache Hit (попадание): Если данные есть в кэше, DAX возвращает их немедленно. Время ответа - микросекунды. Запрос не доходит до DynamoDB. Вы не платите за RCU.
- Cache Miss (промах): Если данных нет, DAX автоматически сходит за вас в DynamoDB, получит элемент, сохранит его у себя в кэше и вернет вам. Следующий, кто спросит этот же элемент, получит cache hit.
А что с записью? DAX - это кеш со сквозной записью (Write-Through). Когда вы делаете PutItem (или Update, Delete) через DAX-клиент:
- DAX одновременно отправляет вашу запись в DynamoDB (как обычно, потребляя WCU).
- И в то же время он обновляет/инвалидирует эти данные в своем кэше.
Компромиссы и подводные камни
DAX - это не "серебряная пуля". За микросекунды приходится платить:
- Стоимость: DAX - это отдельный, платный сервис. Вы платите и за свою таблицу DynamoDB, и за кластер DAX (например, 3 ноды
dax.r5.large24/7). Это дорого. DAX становится экономически выгодным, только если стоимость RCU, которые он вам экономит, превышает стоимость самого DAX-кластера. - Сложность: Это еще один сервис в вашей архитектуре, за которым нужно следить (хоть он и "managed").
- Консистентность (главная ловушка): DAX - это кэш. По определению, он внедряет Eventual Consistency в вашу архитектуру, даже если вы в DynamoDB делаете Strongly Consistent Read. Когда
PutItem(запись) возвращает200 OK, данные уже в DynamoDB, но кэш DAX мог еще не успеть инвалидироваться (это занимает микро- или миллисекунды). Если вы сделаетеGetItemсразу послеPutItem, вы можете получить старые (stale) данные из кэша. DAX ломает паттерн "read your own writes".
7.5. Жесткие лимиты
DynamoDB - это распределенная система со своей "физикой". Если вы попытаетесь нарушить её законы, вы получите не просто медленную работу, а ValidationException и отказ в обслуживании.
Большинство лимитов в AWS - мягкие (Soft Limits), их можно поднять через тикет в поддержку. Ниже перечислены жесткие (Hard Limits), которые поднять нельзя. Вокруг них нужно строить архитектуру.
Лимиты одной партиции
Мы говорили, что таблица может иметь 1,000,000 WCU. Но физически она состоит из сотен серверов (партиций). Одна физическая партиция имеет жесткий потолок производительности:
- 3,000 RCU (чтение)
- 1,000 WCU (запись)
Это физический предел одного сервера AWS.
- Сценарий катастрофы: Если весь ваш трафик (например, 5,000 запросов на запись в секунду) идет в один Partition Key (например,
PK='STATUS_COUNTER'), вы упретесь в лимит 1,000 WCU. - Даже если вы купили 100,000 WCU на всю таблицу, этот конкретный ключ будет отдавать
ProvisionedThroughputExceededException.
2. Лимит на Query/Scan - 1 МБ
Когда вы делаете Query (получить заказы пользователя) или Scan (читать всё), DynamoDB читает данные с диска.
Максимальный объем данных, который DynamoDB обработает за один сетевой запрос - 1 МБ.
- Как это работает: DynamoDB начинает читать совпадения. Как только сумма прочитанных данных достигает 1 МБ, она останавливается, даже если нашла не всё.
- Последствия: Вы получаете
200 OK, список элементов и специальное полеLastEvaluatedKey. - Ошибка новичка: Думать, что
table.query(...)вернет все данные. Если у пользователя 5 МБ заказов, ваш код вернет только первую 1/5 часть и тихо проигнорирует остальное. - Решение (Пагинация): Ваш код обязан проверять наличие
LastEvaluatedKeyи делать повторный запрос (Loop), передавая этот ключ какExclusiveStartKey, пока поле не станет пустым. В Boto3 для этого есть удобныеPaginators.
3. Лимиты пакетных операций (Batch Limits)
Чтобы ускорить работу, мы используем BatchWriteItem и BatchGetItem. Но они не резиновые.
BatchWriteItem: Максимум 25 элементов за раз. (Общий размер пэйлоада < 16 МБ).BatchGetItem: Максимум 100 элементов за раз.TransactWriteItems: Максимум 100 элементов в одной транзакции (раньше было 25).
Если вам нужно загрузить 1,000 элементов, вы должны в коде разбить список на чанки по 25 штук (chunks(items, 25)) и отправить их в цикле.
Производительность, Batch-операции и DAX
Вопросов: 6
7.6. Практика
Производительность, горячие партиции и масштабирование DynamoDB
Вы продолжаете работу как CTO стартапа-конкурента Twitter. Вместе с Назаром разберите горячие партиции (диагностика и решения), выбор тарифного плана (On-Demand vs Provisioned), пакетные операции, DAX и жёсткие лимиты DynamoDB.
Назар - ваш персональный ИИ-наставник. Он поможет закрепить материал через практику и ответит на ваши вопросы.
💡 Все обсуждения с ИИ могут быть прочитаны администратором для улучшения качества обучения.
8. Эксплуатация
8.1. Безопасность
Если вы пришли из мира реляционных баз данных, ваша ментальная модель безопасности выглядит так:
- Есть один "супер-пользователь" БД (например,
app_user), прописанный в конфиге бэкенда. - Этот пользователь имеет права
ALL PRIVILEGESили хотя быSELECT/UPDATEна всю таблицу. - Вся логика разделения доступа ("Алиса видит только свои заказы") живет в вашем коде. Вы обязаны не забыть написать
WHERE user_id = 'Alice'в каждом SQL-запросе.
Проблема: Если хакер ломает ваш бэкенд (получает RCE) или джуниор (или ИИ-агент) забывает добавить WHERE в новом API-методе - вся база утекает. Злоумышленник через вашего app_user выкачивает данные всех клиентов.
DynamoDB предлагает фундаментально другой подход: безопасность вшита в инфраструктуру, а не в код.
Идентификация без паролей (IAM)
В DynamoDB нет логинов и паролей. Вообще. База данных "не знает", кто к ней подключается. Она доверяет только AWS IAM (Identity and Access Management). Каждый запрос подписывается криптографическими ключами (SigV4).
Это означает, что права доступа выдаются ролям, а не людям.
- Ваша Lambda-функция имеет IAM-роль.
- Ваш EC2-инстанс имеет IAM-роль.
- Мобильное приложение получает временные IAM-credentials через AWS Cognito.
Fine-Grained Access Control (FGAC)
Главная киллер-фича - это возможность ограничить доступ на уровне конкретных строк (Items) и колонок (Attributes) прямо в политике доступа AWS, не написав ни строчки кода на Python или Java.
1. Безопасность на уровне строк (Row-Level Security)
Представьте мульти-тенантное SaaS-приложение. У вас миллион пользователей в одной таблице AppTable. Как гарантировать, что пользователь с ID 123 никогда, ни при каких обстоятельствах не прочитает данные пользователя 456?
В IAM есть условие dynamodb:LeadingKeys. Оно работает как автоматический WHERE на уровне облака.
Сценарий: Мобильное приложение обращается к базе напрямую (или через тонкую Lambda). Мы выдаем ему IAM-политику, которая гласит: "Разрешено делать GetItem, НО ТОЛЬКО ЕСЛИ Partition Key совпадает с ID этого пользователя".
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:Query",
"dynamodb:PutItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:1234567890:table/AppTable",
"Condition": {
"ForAllValues:StringEquals": {
// Магия здесь:
// Разрешаем доступ, только если PK равен ID пользователя (из токена Facebook/Google/Cognito)
"dynamodb:LeadingKeys": ["${www.amazon.com:user_id}"]
}
}
}
Результат: Даже если вы допустите баг в коде и попытаетесь запросить чужой ключ, сама база данных отклонит запрос с ошибкой AccessDeniedException. Это "защита от дурака" (и от взлома) на самом низком уровне.
2. Безопасность на уровне атрибутов (Attribute-Level Security)
Иногда вам нужно дать доступ к строке, но скрыть чувствительные поля. Сценарий: У вас есть служба поддержки. Операторы должны видеть профиль пользователя (имя, телефон, историю заказов), чтобы помогать клиентам. Но они не должны видеть атрибут salary (зарплата) или passport_hash.
В SQL вы бы создавали VIEW или фильтровали поля в бэкенде. В DynamoDB вы используете условие dynamodb:Attributes.
{
"Effect": "Deny",
"Action": "dynamodb:GetItem",
"Resource": "arn:aws:dynamodb:us-east-1:1234567890:table/Users",
"Condition": {
"ForAllValues:StringEquals": {
// Запрещаем читать, если в списке запрашиваемых полей есть 'salary'
"dynamodb:Attributes": ["salary", "secret_question"]
}
}
}
Теперь, если приложение поддержки попытается сделать SELECT * (Scan или GetItem без проекции), DynamoDB вернет ошибку. Приложение обязано явно вернуть только разрешенные поля.
Шифрование
Здесь всё просто, потому что AWS делает это за вас.
- In Transit: Все запросы к API DynamoDB ходят только по HTTPS. Перехватить трафик "посередине" нельзя.
- At Rest: Данные на дисках AWS зашифрованы всегда. Вы не можете это отключить.
- AWS Owned Key (по умолчанию): Ключ шифрования управляется AWS. Бесплатно, прозрачно.
- AWS KMS (Customer Managed Key): Вы создаете свой мастер-ключ в KMS. Это нужно, если у вас строгий комплаенс (например, вы хотите иметь право "отозвать" ключ и сделать данные нечитаемыми даже для инженеров AWS).
Сетевая безопасность
По умолчанию DynamoDB - это публичный сервис. Когда ваш сервер (из частной сети) идет в DynamoDB, трафик выходит через NAT Gateway в публичный интернет (хоть и не покидает сеть AWS), а потом возвращается обратно.
Это создает две проблемы:
- Безопасность: Трафик формально покидает вашу приватную подсеть.
- Деньги: Вы платите за NAT Gateway (за каждый гигабайт трафика). При больших объемах это больно.
Решение: VPC Gateway Endpoint
Это "черный ход" из вашей VPC прямо в DynamoDB.
- Безопасность: Трафик никогда не покидает приватную сеть Amazon. Вы можете настроить политику так, чтобы к вашей таблице можно было обратиться только из вашей VPC.
- Экономия: Трафик через Gateway Endpoint бесплатен. Вы не платите за гигабайты.
- Простота: Не нужно менять код. DNS-имя
dynamodb.us-east-1.amazonaws.comпросто начинает резолвиться во внутренние IP-адреса.
Вывод по безопасности: DynamoDB позволяет построить архитектуру Zero Trust. Вместо того чтобы защищать "периметр" (файрволы) и надеяться на идеальный код бэкенда, вы настраиваете правила доступа прямо на данных. Даже если ваш сервер скомпрометирован, хакер упрется в жесткие IAM-лимиты: "этому серверу разрешено читать только заказы со статусом PENDING, и ничего больше".
Безопасность в DynamoDB
Вопросов: 5
8.2. Резервное копирование и восстановление
В мире классических баз данных бэкап - это всегда стресс.
- PostgreSQL/MySQL: Чтобы сделать надежный физический бэкап (без блокировок), вам нужно настраивать WAL-архивацию, использовать
pg_dump(который нагружает диск и процессор) или делать снапшоты дисков на уровне облака. - Влияние на прод: Часто бэкапы делают с реплики, потому что запуск бэкапа на мастере может "положить" производительность.
В DynamoDB бэкапы работают магически иначе. Они происходят на уровне системы хранения, вообще не затрагивая ваши RCU/WCU. Вы можете делать бэкап таблицы под нагрузкой 100 000 RPS, и ваше приложение не заметит даже микросекунды задержки.
Два типа бэкапов
DynamoDB предлагает два механизма защиты данных, которые закрывают разные потребности.
1. Point-in-Time Recovery (PITR) - "Машина времени"
Это ваша страховка от кривых рук и багов в коде. Представьте: вы выкатили релиз, в котором миграция данных случайно удалила email у 10% пользователей. Вы заметили это через 4 часа.
С традиционным "ночным бэкапом" вы бы потеряли данные за 20 часов (с момента прошлого бэкапа до момента аварии). С PITR вы можете восстановить состояние таблицы на любую секунду за последние 35 дней.
- RPO (Recovery Point Objective): 1 секунда.
- Как это работает: DynamoDB непрерывно пишет все изменения в скрытый лог.
- *Включение: Одной галочкой (или строкой в Terraform). Включите это прямо сейчас на всех прод-таблицах. Стоимость - копейки за гигабайт, а ценность бесконечна.
2. On-Demand Backup - "Вечные снапшоты"
Это классические полные бэкапы.
- Зачем: Для долгосрочного хранения. Например, закон требует хранить данные 5 лет. PITR хранит только 35 дней.
- Интеграция с AWS Backup: Вы можете настроить автоматическое создание таких снапшотов по расписанию, копирование их в другой регион (Disaster Recovery) и перемещение в холодное хранилище (Cold Storage) для экономии денег.
Главный архитектурный подвох: восстановление
Вот здесь многие архитекторы совершают ошибку, считая, что "Restore" работает как в SQL.
В MySQL вы можете сказать: "Восстанови бэкап в базу db_production", и она перезапишет испорченные данные. В DynamoDB восстановление (и из PITR, и из On-Demand) ВСЕГДА создает НОВУЮ таблицу.
Вы не можете восстановить данные "поверх" существующей таблицы.
Сценарий катастрофы:
- Таблица
Users(имя в проде) повреждена. - Вы запускаете Restore на состояние "час назад".
- DynamoDB создает новую таблицу. Вы обязаны дать ей новое имя, например
Users_Restored. - Процесс восстановления занимает время (от 10 минут до часов, зависит от объема).
- Финал: У вас есть живая, но битая таблица
Users(куда продолжает литься трафик!) и новая, здоровая таблицаUsers_Restored.
Как переключить трафик?
Это сложная инженерная задача, к которой надо быть готовым:
- Вариант А (быстрый, но грязный): Изменить конфиг приложения (переменную окружения
DYNAMODB_TABLE_NAME), чтобы оно начало писать вUsers_Restored.- Минус: Данные, записанные в старую таблицу
Usersза время восстановления, будут потеряны или их придется переносить вручную.
- Минус: Данные, записанные в старую таблицу
- Вариант Б (правильный, но сложный): Использовать Amazon Route 53 (если у вас глобальные таблицы) или абстракцию в коде, чтобы переключить запись.
- Вариант В (если нельзя менять конфиг): Удалить старую таблицу
Usersи сделать бэкап новой таблицыUsers_Restoredв таблицу с именемUsers. (Очень долго и опасно).
Совет: В ваших Lambda-функциях имя таблицы никогда не должно быть захардкожено (table = dynamodb.Table('Users')). Оно всегда должно браться из переменных окружения (os.environ['TABLE_NAME']). Только так вы сможете быстро переключить прод на восстановленную таблицу.
Удаление таблицы - это навсегда?
В старых версиях DynamoDB, если джуниор случайно запускал скрипт terraform destroy или нажимал "Delete Table" в консоли, таблица исчезала мгновенно. Вместе со всеми PITR бэкапами.
Сейчас (по умолчанию) AWS включает защиту, но вы должны знать о Backup on Delete. При удалении таблицы DynamoDB может автоматически создать финальный снапшот. Убедитесь, что эта опция не отключена в ваших IaC скриптах. Без неё удаление таблицы стирает и возможность сделать PITR-восстановление.
Бэкапы и восстановление в DynamoDB
Вопросов: 5
8.3. Глобальные таблицы
Как быть ближе к пользователю?
Вы запускаете вашу игру, и ваша база данных находится в Орегоне (us-west-2). Игроки в Портленде (рядом с Орегоном) счастливы (задержка 10 мс). Но тут игру скачивают в Берлине. Теперь игроки из Берлина жалуются на лаги - и они правы, их задержка до Орегона >150 мс.
Вам нужна архитектура Active-Active. Вы хотите, чтобы игроки из Берлина читали и писали в локальную копию базы во Франкфурте, а игроки из Портленда - в Орегоне. Эта архитектура в DynamoDB реализуется с помощью глобальных таблиц (Global Tables).
Механизм репликации и модели консистентности
Global Tables - это магия DynamoDB, но она работает в двух очень разных режимах, которые решают разные бизнес-задачи.
Режим 1: Multi-Region Eventual Consistency (MREC) (режим по умолчанию)
Это тот механизм, с которого начинались глобальные таблицы.
Как: Вы создаете таблицу в Орегоне (us-west-2), добавляете регион eu-central-1 (Франкфурт), и AWS автоматически настраивает двунаправленную асинхронную репликацию (построенную поверх DynamoDB Streams).
Зачем: Это невероятно быстро. Когда игрок пишет во Франкфурте, он получает 200 OK за ~10 мс. Данные асинхронно (обычно < 1 секунды) улетают в Орегон.
Разрешение конфликтов: Это асинхронная репликация. Что если два игрока в Токио и Берлине отредактируют один и тот же элемент в одно и то же время?
- DynamoDB использует простой механизм: побеждает последний записавший (Last Writer Wins - LWW).
- Каждая запись имеет внутренний timestamp. Когда реплики обмениваются данными, та запись, у которой timestamp позже, "побеждает" и перезаписывает другую.
- Проблема: Это просто, но это означает, что одна из записей (которая "проиграла") будет тихо потеряна. Это абсолютно неприемлемо для финансовых транзакций, бронирования билетов или управления инвентарем. Для
Set(user_name = 'Alex')- подходит.
Режим 2: Multi-Region Strong Consistency (MRSC)
Зачем: LWW (Режим 1) не подходит для критичных данных. Что, если вам нужно гарантировать, что ваш билет на концерт или бронь отеля уникальны глобально? Вам нужно, чтобы запись либо прошла глобально, либо не прошла нигде.
Как: Это более новый режим, который вы должны выбрать при создании глобальной таблицы (или при конвертации пустой таблицы).
-
Принцип работы: Этот режим полностью меняет механику. Когда вы пишете в один регион (например, Франкфурт):
- Этот регион (Франкфурт) становится координатором транзакции.
- Он не отвечает вам
200 OKнемедленно. Вместо этого он синхронно реплицирует это изменение как минимум в один другой регион (например, в ближайший - Лондон). - Он ждет подтверждения от этого другого региона.
- Только после получения подтверждения о записи в двух регионах (Франкфурт + Лондон) координатор (Франкфурт) возвращает
200 OKвашему приложению.
-
Ключевые преимущества:
- Глобальное строгое чтение: После того как
Writeвернул200 OK, Strongly Consistent Read (чтение со строгой консистентностью), выполненное в любом регионе-реплике (даже в Сиднее!), гарантированно вернет самую последнюю версию данных. - Глобальные условные записи:
ConditionExpression(условные выражения) всегда будут проверяться на основе глобально последней версии элемента, что предотвращает "гонки" (race conditions) между регионами.
- Глобальное строгое чтение: После того как
-
Компромиссы:
- Задержка (Latency): Ваша "быстрая" 10 мс запись (как в MREC) превращается в запись, чья задержка теперь равна RTT (Round Trip Time) до ближайшего другого региона. То есть запись во Франкфурте может занять 100+ мс (пока она "долетит" до Лондона/Дублина и вернется). Это гораздо медленнее, чем MREC.
- Доступность (Availability): Ваша запись теперь зависит от доступности как минимум двух регионов и сети между ними. Если Франкфурт не может "достучаться" ни до одного другого региона, ваша запись не пройдет.
- Стоимость: Этот режим значительно дороже, так как требует больше межрегионального трафика и координации.
-
Когда использовать: Только для низкочастотных, но сверхкритичных операций: регистрация уникального
usernameв глобальной игре, подтверждение финального платежа, бронирование одного оставшегося билета. 99% операций (обновление профиля, лайки) должны по-прежнему использовать быстрый MREC/LWW.
Глобальные таблицы и репликация
Вопросов: 5
8.4. Streams и Change Data Capture
DynamoDB может быть не просто базой данных. События в ней могут запускать другие события.
- Когда пользователь (
User) регистрируется, ему нужно отправить email. - Когда
Orderменяет статус на "Доставлен", нужно отправить нотификацию. - Когда
Productобновляется, нужно сбросить кэш и обновить поисковый индекс в Elasticsearch.
Традиционный подход (плохой): Писать cron job, которая раз в час сканирует таблицу orders на предмет изменений. Это медленно, дорого и создает пиковую нагрузку.
DynamoDB Streams - нервная система вашей таблицы.
DynamoDB Streams - это лог всех изменений в вашей таблице (Create, Update, Delete). Вы можете подписать на этот лог AWS Lambda.
Это позволяет создавать различные event-driven архитектуры:
DynamoDB (Write) -> Stream -> Lambda -> SNS/SES: Отправка нотификаций. Пользователь зарегистрировался - отправляем Welcome Email. Заказ сменил статус - пуш в мобильное приложение.DynamoDB (Write) -> Stream -> Lambda -> Другой сервис: Асинхронная бизнес-логика. При созданииOrderтриггерится Lambda, которая идет в сервис доставки и резервирует курьера.DynamoDB (Write) -> Stream -> Lambda: Агрегация в реальном времени. Обновление счетчиков, пересчет рейтингов, обновление материализованных представлений.
Streams vs Kinesis
- DynamoDB Streams: Встроенный, бесплатный. Хранит данные 24 часа. Идеален для триггеров Lambda.
- Kinesis Data Streams for DynamoDB: Отдельный платный сервис. Хранит данные до 1 года. Позволяет иметь множество потребителей (Lambda, Spark, etc.). Идеален для сложных аналитических пайплайнов.
8.5. Интеграция и экосистема
DynamoDB - это скальпель. Он идеально режет транзакционную нагрузку (OLTP), но совершенно непригоден для аналитики (OLAP) и сложного поиска. Если вы пытаетесь построить отчет "Продажи за квартал по регионам" или найти пользователя по части имени ("Ale..."), используя Scan в DynamoDB - вы забиваете гвозди микроскопом. Это дорого, медленно и убивает производительность продакшена.
AWS предлагает нативные интеграции, чтобы делегировать эти задачи специализированным сервисам.
S3: импорт, экспорт и холодные данные
Проблема стоимости хранения. Хранить данные в DynamoDB дорого ($0.25 за ГБ/мес). Хранить данные в S3 дешево ($0.023 за ГБ/мес). Разница в 10 раз. Если у вас есть терабайты логов или истории заказов за 5 лет, хранить их в горячей базе DynamoDB - это сжигать бюджет.
Решение: Native Export to S3. Раньше для выгрузки данных приходилось писать самописные скрипты на EMR или Glue, которые делали Scan таблицы, выжирая ваши драгоценные RCU.
Сейчас есть функция Export to S3.
- Zero Impact: Экспорт работает на уровне снапшотов (PITR). Он не потребляет RCU вашей таблицы и не влияет на производительность живого приложения.
- Форматы: DynamoDB JSON или Amazon Ion.
- Сценарий использования (Data Lake): Вы настраиваете периодический экспорт в S3. Там эти данные подхватывает AWS Athena. Аналитики пишут SQL-запросы к S3, строят отчеты в QuickSight, а ваш прод в DynamoDB даже не знает, что по его данным строят аналитику.
Решение: Import from S3. Обратная задача: вам нужно переехать с MongoDB или MySQL на DynamoDB. Если вы будете писать скрипт, который делает PutItem в цикле, вы разоритесь на WCU (Write Capacity Units). Функция Import from S3 позволяет залить CSV/JSON/ION файлы в новую таблицу.
- Экономика: Вы платите за гигабайты импортированных данных, а не за WCU. Это в разы дешевле стандартной записи.
Zero-ETL интеграция с OpenSearch
Проблема сложного поиска. DynamoDB умеет искать только по полному совпадению ключа (PK = '123') или диапазону (SK > 100). Она не умеет:
- Полнотекстовый поиск ("найти все товары с словом 'красный' в описании").
- Нечеткий поиск (Fuzzy search, опечатки).
- Гео-поиск ("найти рестораны в радиусе 5 км").
- Сложные агрегации ("средний чек по категориям").
Для этого существует OpenSearch (форк Elasticsearch).
Старый путь (The Hard Way). Раньше архитектура выглядела так: DynamoDB -> Streams -> Lambda -> OpenSearch. Вам приходилось писать и поддерживать Lambda-функцию, которая перекладывает данные. Нужно было обрабатывать ошибки, ретраи, следить за лагом стрима и версиями API OpenSearch. Это был "инфраструктурный клей", который постоянно ломался.
Новый путь: Zero-ETL Integration. AWS представил Zero-ETL интеграцию.
- Как это работает: Вы просто нажимаете кнопку в консоли, связывая таблицу DynamoDB и кластер OpenSearch Ingestion.
- Магия: AWS сам берет на себя репликацию данных. Без Lambda. Без написания кода. Без управления инфраструктурой.
- Результат: Данные появляются в OpenSearch через несколько секунд после записи в DynamoDB. Ваше приложение пишет в DynamoDB (для надежности), а читает из OpenSearch (для сложного поиска и фильтрации).
Итог: Используйте DynamoDB для того, для чего она создана (быстрая запись и чтение по ключу). Всё остальное (аналитика, архивация, сложный поиск) выгружайте в S3 и OpenSearch через нативные интеграции.
Zero-ETL интеграция с Amazon Redshift
Проблема: Вы хотите строить сложные финансовые отчеты, джойнить данные продаж с маркетинговыми кампаниями и строить дашборды в Tableau/PowerBI. DynamoDB этого не умеет. Athena (через S3) может быть медленной для интерактивных дашбордов. Вам нужен Data Warehouse.
Решение: Включаем Zero-ETL integration with Amazon Redshift.
- Как это работает: Это работает так же, как с OpenSearch. Вы связываете таблицу с кластером Redshift. AWS начинает реплицировать данные в фоновом режиме.
- Магия: В Redshift ваши данные появляются в виде обычных SQL-таблиц. Вы можете делать мощнейшие
JOIN,GROUP BYи оконные функции над терабайтами данных, которые изначально лежат в NoSQL. - Зачем это нужно, если есть S3 Export?: S3 Export - это снимок (Batch). Redshift Zero-ETL - это почти реальное время (near real-time). Ваш дашборд продаж отстает от продакшена всего на секунды, а не на сутки.
Kinesis Data Streams: когда обычных стримов мало
Ранее мы упоминали DynamoDB Streams. Это отличная штука для триггеров Lambda. Но у них есть ограничения:
- Данные хранятся только 24 часа. Если ваша Lambda упала на выходных, вы потеряли данные.
- Нельзя подключить много читателей (только 2 процесса одновременно).
Для серьезных Big Data задач (клики пользователей, телеметрия, финансовые потоки) используется интеграция с Kinesis Data Streams.
- Более долгая память: Данные могут храниться до 365 дней. Вы можете перемотать стрим на месяц назад и переиграть события.
- Fan-out: К одному потоку можно подключить 5-10 разных потребителей (один пишет в архив, второй считает фрод, третий обновляет кэш).
- Интеграция с Firehose: Kinesis умеет сам (без кода) складывать данные в S3, Redshift или Splunk, пачками, со сжатием и конвертацией форматов (например, JSON -> Parquet).
Итог по экосистеме
| Задача | Инструмент | Ключевое слово |
|---|---|---|
| OLTP (Приложение) | DynamoDB | Скорость (ms) |
| Search (Поиск) | OpenSearch | Нечеткий поиск |
| Data Lake (Архив) | S3 Export | Дешевизна |
| DW (Отчетность) | Redshift | Сложный SQL |
| Streaming (Big Data) | Kinesis | Глубина истории |
DynamoDB Streams и экосистема AWS
Вопросов: 6
8.6. Практика
Эксплуатация DynamoDB - безопасность, бэкапы, глобальные таблицы, Streams и интеграции
Вы продолжаете работу как CTO стартапа-конкурента Twitter. Вместе с Назаром разберите IAM и FGAC, бэкапы и восстановление, глобальные таблицы (MREC/MRSC), DynamoDB Streams и интеграции с S3, OpenSearch, Redshift.
Назар - ваш персональный ИИ-наставник. Он поможет закрепить материал через практику и ответит на ваши вопросы.
💡 Все обсуждения с ИИ могут быть прочитаны администратором для улучшения качества обучения.