Глубокое погружение в DynamoDB. Часть вторая.

Глубокое погружение в DynamoDB. Часть вторая: Таблицы, типы данных, индексы, единицы емкости(Capacity Units)

Автор оригинала – Solution Architect, Creative Problem Solver, Pure Engineering Advocate, World-Class FizzBuzz Developer, AWS APN Ambassador EMEA, Data Community Builder Карен Товмасян

История одной из быстрейших в мире СУБД, рассказанная простыми, понятными человеку словами.



И снова здравствуйте! Прошло много времени с тех пор, как я написал первую часть этой серии Deep Dive, поэтому я не буду утомлять вас длинным предисловием.

В этой части:

  1. Таблицы, элементы, операция с таблицами и элементами.
  2. Типы данных DynamoDB
  3. Локальные и глобальные вторичные индексы
  4. Единицы емкости(Capacity Units), резервирование, автоматическое масштабирование

Поехали!

Таблицы

Прежде чем мы начнем, нам стоит детально разобраться, чем таблицы DDB отличаются от таблиц в традиционных реляционных базах данных.

В классической RBDMS база данных и ее таблицы хранятся в памяти или на диске в одном или нескольких файлах в форме структуры данных B-tree / LSM-tree. Точная структура, шаблоны доступа и организация зависят от реализации базы данных и различны, если даже не уникальны, для многих механизмов.

С DynamoDB обстановка иная. Он хранит данные в формате “ключ-значение” в структурах называемых элементами(Item). Каждый элемент принадлежит таблице и реплицируется между узлов хранения DDB. Термин Таблица(Table) выступает в роли логической группы элементов. Он позволяет нам получать и предоставлять доступ к данным на основании политик IAM ( это также способ настройки гранулярного доступа на уровне конкретного элемента – подробнее почитать можно тут ) и предоставляет первичный ключ в роли ограничивающего фактора необходимого для операций CRUD.

У таблицы имеется первичный ключ (также называется индексом первичного ключа или первичным индексом). Также имеется поддержка вторичных индексов (мы рассмотрим их позже в этой главе) и других функций, такие как TTL для элементов, потоков и прочих операций.

Чтобы создать таблицу, нам нужно передать обязательные аргументы, такие как атрибуты первичного ключа (типы данных и ключей) и имя таблицы. Рассмотрим пример из предыдущей главы.

$ aws dynamodb create-table \
               --table-name characters \
               --attribute-definitions \
               AttributeName=characterId,AttributeType=S \
               AttributeName=playerId,AttributeType=S \
               --key-schema \
               AttributeName=playerId,KeyType=HASH \
               AttributeName=characterId,KeyType=RANGE \ 
               --billing-mode PAY_PER_REQUEST

Итак, что я делаю, так это создаю таблицу с использованием объединенного первичного ключа. Hash-ключ (также известный, как ключ раздела) и Range-ключ ( также известный, как ключ сортировки) – не обязательно являются уникальными, так как, в моем примере, игрок может иметь несколько персонажей. Их атрибуты (для моих друзей, любителей реляционных СУБД – считайте атрибуты столбцами) должны иметь конкретный тип данных, и в моем случае это String.

Составной первичный ключ состоит из двух атрибутов и вот и их комбинация должна быть уникальной. Тем не менее, может быть два элемента с одним и тем же characterId, но с разными playerId (если один игрок «продал» своего персонажа другому) или два элемента с одним и тем же playerId, но разными characterId (если один игрок владеет несколькими персонажами). Использование составного ключа является довольно-таки частой практикой в архитектуре приложений работающих с DynamoDB.

Существует также более простой подход к организации таблиц, называемый «простой таблицей». В этом случае ваш первичный ключ состоит только из ключа раздела (Hash), как в примере ниже:

$ aws dynamodb create-table \
               --table-name shoppingList\
               --attribute-definitions \
               AttributeName=good,AttributeType=S \
               --key-schema \
               AttributeName=good,KeyType=HASH \
               --billing-mode PAY_PER_REQUEST

Давайте предположим, что вам нужна простая таблица формата “ключ-значение” для списка покупок, и вы знаете, что будете взаимодействать только с одним элементом за раз: добавлять какие-то товары в список для покупки (пять яблок, десять апельсинов) или помечать как уже “купленные”. Вам не нужен ключ сортировки (RANGE), так как здесь нет операции запроса – вы ведь не планируете сохранять список покупок после выхода из супермаркета.

Учитывая все эти требования и способы использования, ваш элемент будет выглядеть следующим образом:

{
    "good": "apple",
    "number": 10,
    "bought": True|False           
}

Что ж, знания теории недостаточно для понимания работы DynamoDB. Давайте посмотрим, как можно работать с таблицами!

Работа с таблицами

Мы начнем с простого. Давайте добавим товар в наш список покупок (используя Python и Boto3):

>>> import boto3
>>> ddb = boto3.resource('dynamodb')
>>> shopping_list = ddb.Table('shoppingList')
>>> resp = shopping_list.put_item(
...     Item={
...             "good": "apple",
...             "number": 10,
...             "bought": False
...     }
... )

Давайте добавим еще несколько пунктов:

>>> goods = [
...     { "good": "orange", "number": 10, "bought": False },
...     { "good": "tomato", "number": 7, "bought": False },
...     { "good": "cucumber", "number": 1, "bought": False }
... ]
>>> for good in goods:
...     _ = shopping_list.put_item(Item=good)

Что ж, давайте посмотрим, что у нас в нашем списке покупок. И, так как я хочу получить все элементы моей таблицы, я запущу операцию Scan. Однако, сперва я должен вас предупредить

Scan – крайне неэффективная, дорогая и медленная операция. Крайне рекомендую вам избегать ее в продакшн-среде. (Более подробно смотрите в разделе “Единицы емкости/Capacity Units”)

Я чувствую себя обязанным вам это сказать, так как в своей практике я встречал множество катастрофически неудачных примеров использования DynamoDB из-за использования операций Scan. Однако, так как мой список очень мал и я хочу получить все элементы таблицы, я буду использовать эту операцию.

>>> shopping_list.scan()['Items']
[{'good': 'orange', 'bought': False, 'number': Decimal('10')}, {'good': 'cucumber', 'bought': False, 'number': Decimal('1')}, {'good': 'apple', 'bought': False, 'number': Decimal('10')}, {'good': 'tomato', 'bought': False, 'number': Decimal('7')}]

И это сработало. Теперь, давайте представим, что я купил столько апельсинов, сколько хотел. Таким образом, я отмечу, что я их купил (путем установки bought=True) и продолжу свои покупки согласно списку. Одним из способов сделать это – является использование вызова API PutItem с указанием первичного ключа.

>>> shopping_list.put_item(Item={"good": "orange", "bought": True})
# DDB output...
>>> shopping_list.scan()['Items']
[{'good': 'orange', 'bought': True}, 
# the rest of the output...
]

Подождите. Что? А куда делось количество апельсинов?

Как вы видите, вы можете использовать PutItem для добавления и замены текущих элементов. Но, если мы хотим заменить только один атрибут, мы должны использовать вызов UpdateItem, дабы избежать удаления других атрибутов.

>>> shopping_list.update_item(
...     Key = { 'good': 'orange' },
...     AttributeUpdates = {
...             'number': { 'Value': 10, 'Action': 'PUT' },
...             'bought': { 'Value': True, 'Action': 'PUT' }
...     }
... )
# API response...
>>> shopping_list.scan()['Items']
[{'good': 'orange', 'bought': True, 'number': Decimal('10')}, 
# the rest of the output...]

Выглядит намного лучше, не правда ли?

Как вы могли заметить, я запускал операцию Scan несколько раз, хотя мне нужен был только один элемент. Как я отмечал выше, Scan – очень неэффективная операция, особенно в тех случаях, когда вам не нужно все содержимое таблицы. Для того, чтобы получить один элемент, намного лучше использовать GetItem.

>>> shopping_list.get_item(Key={'good': 'orange'})['Item']
{'good': 'orange', 'bought': True, 'number': Decimal('10')}

Этот вызов также позволяет мне получить не все атрибуты, а только определенные. Допустим, я хочу проверить, купил ли я апельсины, поэтому меня интересует только атрибут bought.

>>> shopping_list.get_item(
...     Key = { 'good': 'orange' },
...     AttributesToGet = [ 'bought' ]
... )['Item']
{'bought': True}

Аргумент AttributesToGet является простым, человекочитаемым способом получения отдельных аргументов, но он устарел, и мы должны использовать другой, который называется ProjectionExpression.

>>> shopping_list.get_item(
...     Key = { 'good': 'orange' },
...     ProjectionExpression = 'bought'
... )['Item']
{'bought': True}
>>>

Да, выглядит не так читаемо, но, в отличии от AttributesToGet, данное выражение позволяет вам получить вложенные атрибуты(Да-да, вы можете поместить JSON в JSON!). Более подробно я расскажу об этом позже, когда мы доберемся до Типов Данных.

Нам необходимо изучить еще одну операцию, но она не работает с простой структурой, в которой есть только Первичный ключ. Вызов Query использует ключ сортировки (диапазона) и получает все элементы, соответствующие этому ключу. Чтобы продемонстрировать это, давайте вернемся к нашей многопользовательской игре.

Таблица characters содержит Составной ключ, где первичный ключ является идентификатором игрока, а ключ сортировки – идентификатор персонажа. Мы знаем, что наши игроки могут иметь несколько персонажей. Давайте же создадим датасет.

>>> characters = ddb.Table('characters')
>>> characters.put_item(
...     Item = { 'characterId': 'char1', 'playerId': 'player1' }
... )
>>> characters.put_item(
...     Item = { 'characterId': 'char2', 'playerId': 'player1' }
... )
>>> characters.scan()['Items']
[{'characterId': 'char2', 'playerId': 'player1'}, 
{'characterId': 'char1', 'playerId': 'player1'}]

И добавим несколько персонажей для другого игрока

>>> characters.put_item(Item={'characterId': 'char3', 'playerId': 'player2'})
>>> characters.put_item(Item={'characterId': 'char4', 'playerId': 'player2'})
>>> characters.put_item(Item={'characterId': 'char5', 'playerId': 'player2'})
>>> characters.scan()['Items']
[{'characterId': 'char3', 'playerId': 'player2'}, 
{'characterId': 'char2', 'playerId': 'player1'}, 
{'characterId': 'char4', 'playerId': 'player2'}, 
{'characterId': 'char5', 'playerId': 'player2'}, 
{'characterId': 'char1', 'playerId': 'player1'}]

Выполнение операции Scan вернет нам все элементы таблицы. И если бы нам необходимо было бы получить только список персонажей игрока player1 и мы жили бы в мире реляционных СУБД, то мы бы выполнили что-то такое:

>>> p1_chars = [c for c in characters.scan()['Items']
...             if c['playerId'] == 'player1']
>>> p1_chars
[{'characterId': 'char2', 'playerId': 'player1'}, 
{'characterId': 'char1', 'playerId': 'player1'}]

Но, опять же, это использует кучу Read Capacity Units(единиц емкости чтения), если будет запущено на большом датасете (Я все еще получаю все элементы таблицы). Поэтому более эффективной операцией в данном случае будет Query. Давайте попробуем:

# This module is needed to perform key value check.
>>> from boto3.dynamodb.conditions import Key
>>> characters.query(
...     KeyConditionExpression=Key('playerId').eq('player1')
... )['Items']
[{'characterId': 'char1', 'playerId': 'player1'}, 
{'characterId': 'char2', 'playerId': 'player1'}]

Мы можем выполнять множество операций, используя тип Key и условные методы (примеры для Python вы можете найти здесь).

Прежде чем мы перейдем к следующему разделу, я хотел бы быстро пройти по ограничениям DynamoDB, которые вы должны знать:

  • Максимум 256 таблиц на регион, на аккаунт
  • Максимум 40 000 единиц емкости чтения/записи на таблицу (в секунду)
  • Безлимитный размер таблицы
  • Максимальный размер ключа раздела(Partition Key) – 2Kb, минимальный – 1 байт (увеличить невозможно)
  • Максимальный размер ключа сортировки(Sort key) – 1Kb, минимальный – 1 байт (увеличить невозможно)
  • Максимальный размер элемента – 400 Kb (увеличить невозможно)

Конечно, существуют и другие лимиты, ознакомиться с которыми можно здесь.

Давайте рассмотрим еще одну важную тему. Типы данных или, что мы можем хранить в DynamoDB!

Типы данных

Список довольно простой; смотрите сами.

Скалярные

Числа, двоичные, логические, нулевые. Да уж, все просто, эт вам не ракету строить.

Документ

Также известный, как JSON в JSON в JSON (JSONификация)

Имейте в виду, что максимальная глубина вложенности такого типа – 32 (вы ведь проверяли лимиты, не так ли?)

Множество(Set)

Тип множество (не стоит рассматривать его, как список) – содержит уникальный набор числовых, строковых или байтовых значений. Следует помнить, что порядок элементов в множестве – не сохраняется.

В общем и целом, обсуждать тут не так много, так что можем двигаться дальше. Не так ли?

Индексы

А вот теперь переходим к серьезным штукам.

Индексы – это хорошо известные структуры в мире реляционных баз данных. Рассмотрим следующий SQL-запрос.

SELECT id, name, age FROM users WHERE name = 'John Doe';

Когда мы выполняем этот запрос к базе данных, он просматривает каждую строку и сопоставляет ее с нашим условием WHERE. Эта операция имеет линейную сложность (например, O (n)) и может занять много времени, когда у нас есть десятки миллионов строк в таблице. Чтобы повысить производительность, мы можем создать индекс по имени столбца, чтобы обработчик запроса выполнял поиск в индексе для того, чтобы найти все совпадающие записи.

Несмотря на то, что индексы имеют такое же название и похожую реализацию, в DynamoDB они служат иной цели. Они используются с операцией Query, но действуют как дополнение к существующей структуре Составного ключа(Partition + Sort). Чтобы более понятно объяснить это, потребуется более сложный пример, нежели список покупок.

Давайте вернемся к нашим игровым персонажам. У каждого игрока есть один или несколько персонажей, и у этих персонажей есть определенные атрибуты. Рассмотрим этот псевдокод:

type Player {
  id: string
  region: string
  server: string
  email: string
}type Character {
  id: string
  player: Player
  race: string
  class: string
  health: number
  mana: number
  strength: number
  speed: number
  level: number
}

Существует несколько подходов реализации данной модели данных в DynamoDB, но учитывая тему данного раздела и для того, чтобы познакомить вас с однотабличным форматом, я не буду использовать подход Документной базы данных, а буду использовать формат хранилища с широкими столбцами.

Item = {
  'playerId':      'string',    # partition key
  'characterId':   'string',    # sort key
  'playerRegion':  'string',
  'playerEmail':   'string',
  'currentServer': 'string',
  'race':          'string',
  'class':         'string',
  'health':        123,
  'mana':          123,
  'strength':      123,
  'speed':         123,
  'level':         10
}

Таким образом, пример элемента будет выглядеть следующим образом:

{
  'playerId':      '7877e1b90fe2',
  'characterId':   '74477fae0c9f',
  'playerRegion':  'us',
  'playerEmail':   'foo@bar.com',
  'currentServer': 'srv01.us-east.bestrpg.com',
  'race':          'human',
  'class':         'knight',
  'health':        9000,
  'mana':          10,
  'strength':      42,
  'speed':         23,
  'level':         7
}

Если я хочу получить всю статистику и метаданные конкретного персонажа, я просто запускаю GetItem, указывая идентификаторы игрока и персонажа. Но что я могу сделать, если мне нужно отправить уведомление о техническом обслуживании сервера всем игрокам, которые в настоящее время там находятся? Или мне нужно отправить приглашение на турнир всем персонажам, чей уровень выше 15? Или я хочу разослать промо-скины всем рыцарям, которые играют в США?

Количество подобных случаев может быть огромным, а использование операций Scan будет неоправданно дорого. Решением этой проблемы являются вторичные индексы, локальные и глобальные.

Локальные вторичные индексы (Local Secondary Indexes LSI)

Допустим, я хочу проверить всех персонажей, которые есть у игрока на определенном сервере. Первый вариант – запустить операцию Query с ключом раздела, а затем выполнить аналогичные действия, как мы делали с операцией Scan и составлением списков при помощи Python.

>>> srv = 'srv01.us-east.bestrg.com'
>>> player_id = '7877e1b90fe2'
>>> on_server = [ c for c in characters.query(KeyConditionExpression=Key('playerId').eq(player_id))['Items'] if c['currentServer'] == srv ]

Этот метод лучше, чем Scan, но все еще недостаточно эффективный – мы получаем всех персонажей, а потом фильтруем их на сервере. Слишком много ненужных вызовов API.

Вот тут то и вступают в действие локальные вторичные индексы. Я могу создать индекс для атрибута currentServer и использовать операцию Query.

>>> characters.query(
...     KeyConditionExpression = Key('playerId').eq(player_id) &\
...     Key('currentServer').eq(srv)
... )['Items']

Но тут есть одна ма-а-а-аленькая проблема: локальные вторичные индексы можно создавать только при создании таблицы, а вот, по крайней мере на момент написания данной статьи, к существующей таблице создать локальный вторичный индекс – невозможно. Таким образом, я буду вынужден удалить таблицу и пересоздать ее, но уже с локальным вторичным индексом.

$ aws dynamodb delete-table --table-name characters
$ aws dynamodb create-table \
               --table-name characters \
               --attribute-definitions \
               AttributeName=characterId,AttributeType=S \
               AttributeName=playerId,AttributeType=S \
               AttributeName=currentServer,AttributeType=S \
               --key-schema \
               AttributeName=playerId,KeyType=HASH \
               AttributeName=characterId,KeyType=RANGE \
               --billing-mode PAY_PER_REQUEST \
               --local-secondary-indexes \
               'IndexName=server,KeySchema=[{AttributeName=playerId,KeyType=HASH},{AttributeName=currentServer,KeyType=RANGE}],Projection={ProjectionType=KEYS_ONLY}'

После пересоздания таблицы и добавления элементов давайте запустим операцию Query. Единственное дополнение к приведенному выше запросу – это добавление имени индекса в запрос, таким образом DynamoDB узнает, что я хочу запросить индекс, а не таблицу.

>>> characters.query(
...     IndexName='server',
...     KeyConditionExpression = Key('playerId').eq(player_id) &\
...     Key('currentServer').eq(srv)
... )['Items']
[{'characterId': '74477fae0c9f', 'playerId': '7877e1b90fe2', 'currentServer': 'srv01.us-east.bestrpg.com'}]

Обратите внимание, что я получаю только три атрибута. Я указал для отображения только следующие ключи – идентификаторы персонажа и игрока, а также текущее имя хоста сервера. В моем случае этого достаточно, но вы должны знать об этом, так как если вы хотите получить больше атрибутов за один вызов, вам нужно либо указать все необходимы атрибуты при создании индекса, либо выполнить дополнительные вызовы GetItem.

Еще одним ограничением является то, что несмотря на то, что мы можем использовать другой ключ сортировки, ключ раздела должен оставаться тем же, что делает локальные вторичные индексы, дополнительным вариантов фильтрации, нежели прекрасным дополнением к шаблонам доступа(access patterns).

И последнее, но не маловажное: у вас может быть только 5 локальных вторичнных индексов на одну таблицу! И вы не можете ни добавлять, ни удалять индексы из таблицы после ее создания.

А теперь давайте рассмотрим более гибкую фичу DynamoDB – глобальные вторичные индексы.

Глобальные вторичные индексы (Global Secondary Indexes – GSI)

Если LSI похож на обычное B-дерево или индекс Bitmap в реляционной БД, GSI можно рассматривать как материализованное представление. И, хотя GSI используется для запроса данных из той же таблицы, что и LSI – у него есть несколько преимуществ перед LSI:

  • Ключ раздела может быть разным
  • Можно создавать GSI к уже существующей таблице
  • И удалять тоже можно(Да-да)
  • У GSI есть свои отдельные единицы емкости, таким образом их производительность не строго привязана к производительности таблицы

Рассмотрим следующий пример. У меня есть таблица с персонажами и игроками. Эти игроки находятся в определенном регионе и имеют электронную почту. Я хочу поздравить всех игроков в США с Рождеством. Я уже знаю, что использование операции Scan возможно, но неэффективно. Вместо этого я создам GSI, который принимает playerRegion в качестве ключа раздела и currentServerв качестве ключа сортировки.

Для того, чтобы это сделать – я использую операцию UpdateTable:

$ cat index.json                                                                                                                                                                            
[
    {
        "Create": {
            "IndexName": "region",
            "KeySchema": [
                {
                    "AttributeName": "playerRegion",
                    "KeyType": "HASH"
                },
                {
                    "AttributeName": "currentServer",
                    "KeyType": "RANGE"
                }
            ],
            "Projection": {
                "ProjectionType": "ALL"
            }
        }
    }
]
$  aws dynamodb update-table \
                --table-name characters \
                --attribute-definitions \
                AttributeName=playerRegion,AttributeType=S \
                AttributeName=currentServer,AttributeType=S \
                --global-secondary-index-updates \
                file://index.json

Обратите внимание на изменение в поле Projection. Посколько я хочу получить все атрибуты( пример будет дальше), я отображаю их все.

Я увижу подробное описание своей таблицы с последними изменениями, которые я сделал, в выходных данных. Давайте попробуем:

>>> characters.query(
...     IndexName='region',
...     KeyConditionExpression=Key('playerRegion').eq('us'),
...     ProjectionExpression='playerEmail'
... )['Items']
[{'playerEmail': 'foo@bar.com'}]

Хорошо, еще пример: мне нужны идентификаторы персонажей всех рыцарей на сервере.

>>> characters.query(
...     IndexName='region',
...     KeyConditionExpression=Key('playerRegion').eq('us') &\
...     Key('currentServer').eq('srv01.us.bestrpg.com'),
...     FilterExpression= Attr('class').eq('knight'),
...     ProjectionExpression='characterId'
... )['Items']
[{'characterId': '74477fae0c9f'}]
>>>

Чтобы это сделать, я запрашиваю тот же индекс, но, помимо этого провожу фильтрацию по атрибуту… И это тоже неэффективно, так как я сначала получаю все элементы соответствующие Первичному ключу, а потом их фильтрую локально.

Но я могу решить эту проблему путем создания другого GSI, используя атрибут class в роли ключа раздела, а currentServer, как ключ сотрировки.

Возможности GSI безграничны и критичны для реализации однотабличной модели. Вы наверняка слышали об однотабличной модели до этого, но я подробно расскажу об этом в четвертой части. А пока насладитесь блестящим выступлением Rick Houlihan.

Вдобавок ко всему, у GSI есть свои единицы емкости(Capacity units), что позволяет нам разгрузить исходную таблицу. И поскольку я много говорил об единицах емкости, сейчас самое время объяснить, что они из себя представляют.

Единицы емкости(Capacity Units)

Преимущества управляемых бессерверных предложений AWS заключаются в том, что мы стоимость измеряется по конкретным действиям, а не почасовую оплату за вычисления. Что касается DynamoDB, мы платим за хранилище и запросы, которые мы выполняем к таблице.

И, мы можем рассматривать оплачиваемую единицу, как единицу емкости, атомарную единицу нагрузки и запроса DynamoDB, как единицу ввода-вывода. Существуют единицы емкости чтения(Read Capacity Units – RCU) и записи(Write Capacity Units – WCU)

Как вы уже могли догадаться, когда вы пишете в таблицу (PutItem, DeleteItem, UpdateItem) вы потребляете WCU, и один WCU – это 1 (один) элемент размером в 1 (один) Kb в секунду. Таким образом если вы записываете элемент размером в 1.5 KB – вы потребляете 2 WCU. Два элемента размером по 100 байт каждый – тоже 2 WCU.

В свою очередь, 1 (один) RCU – это 1 элемент размером не более 4 (четыре) КБ при строгой согласованности или два элемента при случайной согласованности (мы рассмотрим согласованность DynamoDB в следующей главе) аналогичного размера.

При использовании DynamoDB в режиме по требованию (например, PAY_PER_REQUEST) нам выставляется счет за каждый использованный RCU / WCU. Это подводит нас мысли о важности использования Query вместо Scan.

Как видите, неважно, как вы фильтруете результаты полученные в результате выполнения операции Scan, в любом случае вы будете платить за каждый элемент полученный из таблицы(даже если вы их потом не используете). Таким образом, если у вас есть таблица с миллионом элементов, каждая операция Scan будет стоить вам 0.25 USD до налогов. Не звучит сильно дорого, не так ли, однако представьте себе, если вы отправляете десятки тысяч подобных запросов каждую минуту(или даже секунду!), и все это только для того, чтобы получить одну или, может быть, пару строк.

В отличии от этого, Query оплачивается за столько элементов, к которым получен доступ. Таким образом, если у вас есть миллион элементов и только десять из них соотвествуют вашему запросу – вы заплатите только 0.0000025 USD за вызов. Намного дешевле, не правда ли?

Еще одной опцией при использовании DDB является зарезервированная производительность, таким образом вы указываете, как много RCU/WCU вы хотите использовать и, при этом, оплачиваете фиксированную сумму каждый месяц. Единицы емкости указываются за секунду, и, если вы пошлете больше запросов, нежели вы указали, DynamoDB будет троттлить ваши запросы(При этом, AWS SDK обычно имеют встроенные механизмы повторов и увеличения задержек между повторами)

Использование зарезервированных RCU/WCU должно быть использовано только в тех случаях, когда нагрузка строго определена, вы не ожидаете каких-либо всплесков, а кроме того, ваше приложение может обрабатывать троттлинг. Изменение модели оплаты с “по-требованию” на “зарезервированную” и обратно – возможно, однако одновременно затрагивает как RCU, так и WCU(таким образом вы не можете использовать модель “по-требованию” для записи и “зарезервированную” для чтения)

Кроме того, необходимо отметить, что LSI использует единицы емкости самой таблицы, а вот GSI имеет свои. В то же время, несмотря на то, что GSI и таблица используют разные RCU/WCU, и те, и те, должны иметь одинаковую модель оплаты и быть либо “по требованию”, либо “зарезервированы”.

Для того, чтобы переключить таблицу в режим оплаты за зарезервированную производительность, я выполню операцию UpdateTable:

# Note the difference in the index settings.
# This time I use Update structure and only
# ProvisionedThroughput settins$ cat index.json                                                                                                                                                                                          
[
    {
        "Update": {
            "IndexName": "region",
            "ProvisionedThroughput": {
                  "ReadCapacityUnits": 10,
                  "WriteCapacityUnits": 1
            }
        }
    }
]
$ aws dynamodb update-table \
               --table-name characters \
               --billing-mode PROVISIONED \
               --provisioned-throughput \
               ReadCapacityUnits=1,WriteCapacityUnits=1 \
               --global-secondary-index-updates \
               file://index.json

Теперь я могу выполнять одну операцию записи в секунду и GSI (Да-да, вы можете также писать прямо в индекс!), одну операцию чтения с таблицы и десять операций чтения из GSI.

Если я захочу вернуть настройки на оплату “по-требованию”, все, что мне нужно – это запустить ту же операцию UpdateTable с указанием ключа --billing-mode PAY_PER_REQUEST.

Резервирование и автоматическое масштабирование

Предположим, что мы рассматриваем определенную стабильную рабочую нагрузку, которая в ближайший год не будет изменяться. В этом случае вы можете приобрести единицы зарезервированной емкости, что даст вам гарантированно более низкую годовую цену.

Другим требованием может стать необходимость использования автоматического масштабирования единиц емкости. И, хотя это не предоставляется из коробки для DynamoDB, можно использовать механизм Application Auto-Scaling для автоматического масштабирования

Application Auto-Scaling действует так же, как и EC2 Auto-Scaling. Когда определенное значение метрики CloudWatch достигнуто, Application Auto-Scaling выполнит операцию UpdateTable в DynamoDB. Процесс установки и настройки очень прост и понятно описан в Документации AWS.


Спасибо, что прочитали эту главу! В следующей мы рассмотрим согласованность DynamoDB и расширенные функции, такие как глобальные таблицы, TTL элементов и DynamoDB Accelerator.

Следите за обновлениями.

Целую, обнимаю.

Примечание от переводчика

Перевод выполнен с разрешения автора оригинального текста.

Постарался сделать максимально приближенный перевод. Отдельные термины перевести не смог, просто не смог подобрать удобоваримый аналог в русском языке.

Оглавление ссылается на оригинальные статьи, после перевода – обновлю.

Перевод этой части занял довольно таки длительный промежуток времени по причине занятности в разных активностях. По возможности постараюсь ускорить перевод остальных частей.

На сем позвольте откланяться.

Leave a Reply