← К содержанию навыка

Производительность и масштабирование

30 минут
Раздел 7 из 8

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! Как это возможно?"

Это и есть главный симптом горячей партиции.

  1. CloudWatch Metrics: Вы обязаны смотреть на два графика одновременно: WriteThrottleEvents (или ReadThrottleEvents) и ConsumedWriteCapacityUnits (потребляемые WCU). Если вы видите, что ThrottleEvents растут, а ConsumedCapacityUnits значительно ниже, чем ProvisionedCapacityUnits (ваш общий бюджет), - у вас на 99% горячая партиция. Общий бюджет есть, но он выделен не той партиции, куда идет нагрузка.
  2. 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 ключам. Чтобы получить общую сумму, вы обязаны в коде вашего приложения:

    1. Выполнить 10 GetItem (или Query) параллельно: Get(PK='PS5#0'), Get(PK='PS5#1') ... Get(PK='PS5#9').
    2. Получить 10 ответов.
    3. Суммировать эти 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: Принимает список до 25 PutItem или DeleteRequest в одном вызове.
  • BatchGetItem: Принимает список до 100 GetItem в одном вызове.

Ограничения и обработка ошибок

  • НЕ транзакция: 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 миллионов раз в секунду?

Внезапно у вас появляются две новые проблемы:

  1. Проблема задержки: single-digit milliseconds (2-9 мс) - это слишком медленно. Вам нужны микросекунды (µs).
  2. Проблема стоимости (RCU): Обслуживать 10,000,000 чтений в секунду с помощью Provisioned RCU - это астрономически дорого. Гораздо дешевле обслуживать их из оперативной памяти (кэша), чем постоянно "дергать" дисковую подсистему, даже если это NVMe.

DAX - "прозрачный" кэш на стероидах

DAX (DynamoDB Accelerator) - это полностью управляемый, in-memory кэш, созданный специально для DynamoDB.

Думайте о нем не как о Redis/Memcached (где вы вручную управляете Set(key, val) и Get(key)). Думайте о нем как о прозрачном слое, который притворяется самой DynamoDB.

Совместимость с API

В этом и есть гениальность DAX. Вам не нужно менять логику вашего приложения.

  1. Старый код (без DAX):
import boto3
# Обычный клиент, подключается к эндпоинту DynamoDB
dynamodb_client = boto3.client('dynamodb', region_name='us-east-1')
dynamodb_client.get_item(...)
  1. Новый код (с 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:

  1. Запрос идет сначала в DAX-кластер (в RAM).
  2. Cache Hit (попадание): Если данные есть в кэше, DAX возвращает их немедленно. Время ответа - микросекунды. Запрос не доходит до DynamoDB. Вы не платите за RCU.
  3. Cache Miss (промах): Если данных нет, DAX автоматически сходит за вас в DynamoDB, получит элемент, сохранит его у себя в кэше и вернет вам. Следующий, кто спросит этот же элемент, получит cache hit.

А что с записью? DAX - это кеш со сквозной записью (Write-Through). Когда вы делаете PutItem (или Update, Delete) через DAX-клиент:

  1. DAX одновременно отправляет вашу запись в DynamoDB (как обычно, потребляя WCU).
  2. И в то же время он обновляет/инвалидирует эти данные в своем кэше.

Компромиссы и подводные камни

DAX - это не "серебряная пуля". За микросекунды приходится платить:

  1. Стоимость: DAX - это отдельный, платный сервис. Вы платите и за свою таблицу DynamoDB, и за кластер DAX (например, 3 ноды dax.r5.large 24/7). Это дорого. DAX становится экономически выгодным, только если стоимость RCU, которые он вам экономит, превышает стоимость самого DAX-кластера.
  2. Сложность: Это еще один сервис в вашей архитектуре, за которым нужно следить (хоть он и "managed").
  3. Консистентность (главная ловушка): 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.

Назар - ваш персональный ИИ-наставник. Он поможет закрепить материал через практику и ответит на ваши вопросы.

💡 Все обсуждения с ИИ могут быть прочитаны администратором для улучшения качества обучения.