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

Глубокое погружение в DynamoDB. Часть четвертая: Моделирование данных. Лучшие практики. Что дальше?

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

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



Здравствуйте и добро пожаловать в последнюю (на сегодняшний день) часть из серии статей о Amazon DynamoDB! Вы прошли длинный путь, попутно изучив функционал DDB и те вещи, на которые необходимо обратить внимание, а теперь самое время понять, когда DynamoDB лучше всего соответствует вашим потребностям.

В этой части:

  • Моделирование данных в мире NoSQL
  • Лучшие практики и шаблоны проектирования в DynamoDB
  • Что дальше?

Давайте начинать!

Моделирование данных

Моделирование данных определяет, какие данные мы храним, как они хранятся, как они изменяются и как к ним осуществляется доступ.

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

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

Навскидку мы можем выбрать следующие сущности:

  • Товар – пусть это будет какая-нибудь бытовая техника, книжка, игрушка, да, что угодно
  • Покупатель – тот, кто покупает Товар
  • Продавец – угадайте кто? – Тот, кто продает Товар
  • Заказ – сущнсоть, связанная с одним Покупателем и содержащая один или более Товаров

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

При проектировании структуры данных с необходимыми сущностями, нам необходимо учитывать возможности СУБД. В мире реляционных баз данных мы бы создали множество таблиц с первичными ключами и создали связи на основе ограничений внешнего ключа. Этот шаблон моделирования данных предполагает возможности и ограничения реляционной модели и внутренних структур B-дерева файлов данных и индексов, но является неприменимым для большинства баз данных NoSQL.

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

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

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

В нашем случае, мы можем начать с четырех таблиц:

buyers {
    "email":   string,  # primary key (partition key)
    "address": string,
    "name":    string,
    "age":     int,
    # and so on...
}
sellers {
    "name":    string,  # primary key (partition key)
    "email":   string,
    "phone":   string,
    "address": string,
    # and so on...
}
goods {
    "sellerName":  string,  # primary key (partition key)
    "goodName":    string,  # primary key (sort key)
    "price":       float,
    "description": string,
    # and so on...
}
orders {
    "buyerEmail":      string,  # primary key (partition key)
    "orderId":         string,  # primary key (sort key)
    "goodName":        string,
    "sellerName":      string,
    "deliveryAddress": string,
    "invoiceAddress":  string,
    # and so on...
}

Так как в DynamoDB нет понятия внешних ключей, как в реляционных СУБД, нам необходимо ссылаться на точные значения. Поэтому мы определяем первичные ключи для каждого набора данных. Для покупателей и продавцов нам не нужен составной ключ, вряд ли нам понадобятся какие-то расширенные запросы с использованием ключа сортировки, а каждый продавец и покупатель — это уникальная сущность. Для товаров и заказов мы создаем составные первичные ключи для удобства фильтрации, ведь может потребоваться получить все товары у одного продавца, продавец может отредактировать отдельные товары, покупатель может удалить часть товаров из заказа и так далее.

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

Рекомендации при работе с DynamoDB

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

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

Избегайте сканирования БД. Вместо этого используйте локальные вторичные индексы.

Сама по себе операция сканирования не делает DDB медленнее. Выполнение операции сканирования аналогично выполнению запроса select * from table_name; — эта операция имеет линейную сложность (O(n) — если говорить на языке О-нотации), и выполняется довольно медленно в любой СУБД.

В любом случае, большинство пользователей DynamoDB используется операцию сканирования для выполнения фильтрации ответа на стороне клиента – этот способ имеет право на жизнь, но является весьма дорогим. В процессе операции сканирования происходит запрос каждого элемента таблицы, таким образом стоимость каждого запроса будет формироваться из необходимого количества Read Capacity Units умноженных (как минимум) на количество объектов в таблице.

Для того, чтобы сократить затраты в AWS вы можете создать Локальные вторичные индексы и выполнять запросы (Query) для эффективной фильтрации данных. Операция запроса происходит на стороне сервера DynamoDB, таким образом вы запрашиваете только те элементы, которые попадают в выборку согласно шаблона запроса.

Храните большие объекты в S3, а ссылки на них в базе.

DynamoDB поддерживает хранение бинарных данных, однако у AWS уже есть сервис для них. Если вам необходимо оперировать объектами, то было бы неплохо предварительно сохранить объект в S3 и поместить его URL-адрес в таблицу. Таким образом вы сэкономите, как в RCU, так и в WCU, кроме того избежите ограничений связанных с максимальным размером элемента (400 Кб).

Разбивайте данные на разделы. Избегайте создания слишком широких разделов.

Ключ раздела (Partition Key) позволяет организовать данные в (угадайте что?) разделы. А ключ сортировки (Sort Key) позволяет фильтровать данные в разделе, обеспечивая гранулярность доступа к элементам данных. Таким образом, некоторые элементы могут иметь один и тот же ключ раздела, но разные ключи сортировки. Несмотря на то, что разделы распределены по нескольким узлам хранения данных, проектирование таблицы с неравномерным распределением может привести к проблеме производительности называемой Hot Partitions.

Эта проблема возникает, когда один раздел (например все элементы с одним ключом раздела) получают более чем 1000 WCU или 3000 RCU в секунду. Когда это происходит, DynamoDB использует троттлинг(пропуск запросов), даже в том случае, если у вас есть достаточное количество зарезервированных единиц емкости (Capacity units) или при использовании модели оплаты по-требованию.

Мы можем обойти эту проблему, выполнив правильные расчеты и разделив доступ к данным. Модель, приведенная ниже, не вызовет проблем:

{
    "playerId":    string,   # partition key
    "characterID": string    # sort key
}

Все потому, что один игрок может иметь одного, двух, трех, да даже десяток персонажей, которыми он играет. В любом случае, количество персонажей не достигнет сотен и тысяч, таким образом вероятность использования 3000RCU одним игроком – крайне мала.

А вот следующая модель наверняка вызовет проблемы:

{
    "serverName": string,  # partition key
    "playerId":   string,  # sort key
}

На одном сервере могут быть миллионы игроков, и с такой организацией ключей мы столкнемся с проблемой Hot Partition. Следуйте девизу «Ключ раздела для какого-то количества элементов, но не слишком большого», и у вас все будет хорошо.

Покупайте зарезервированные единицы емкости, когда нагрузка известна.

Планирование нагрузки – дело тонкое и сложное. А запуск стартапа часто несет в себе определенную долю неопределенности. Поэтому часто новые рабочие нагрузки следуют принципу “YOLO(ты живешь лишь раз)” *(примечание от переводчика – я бы тут перевел “И так сойдет”)*, так как DynamoDB довольно дешев на средних нагрузках.

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

Используйте DynamoDB Streams для операций CDC (Change Data Capture) и ETL (Extract, Transform, Load)

Если вы хотите хранить новый или обновленный элемент в отдельном пайплайне для последующей обработки (например в ETL или Data Lake), используйте DynamoDB Streams! Его использование не потребует использования единиц емкости, а возможность вызова функции Lambda позволяет интегрировать DynamoDB Streams практически с любым другим сервисом AWS.


Я рассказал вам о самых важных рекомендациях в работе с DynamoDB, но их намного больше!

Шаблон проектирования DynamoDB — однотабличник

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

Однотабличный дизайн означает, что данные — все данные, необходимые для чтения, — хранятся в одной таблице. Этот шаблон является наиболее эффективным с точки зрения затрат и производительности, поскольку нам не нужно запрашивать различные таблицы по отдельности.

Давайте воспроизведем структуру таблицы для платформы электронной торговли:

buyers {
    "email":   string,  # primary key (partition key)
    "address": string,
    "name":    string,
    "age":     int,
    # and so on...
}
sellers {
    "name":    string,  # primary key (partition key)
    "email":   string,
    "phone":   string,
    "address": string,
    # and so on...
}
goods {
    "sellerName":  string,  # primary key (partition key)
    "goodName":    string,  # primary key (sort key)
    "price":       float,
    "description": string,
    # and so on...
}
orders {
    "buyerEmail":      string,  # primary key (partition key)
    "orderId":         string,  # primary key (sort key)
    "goodName":        string,
    "sellerName":      string,
    "deliveryAddress": string,
    "invoiceAddress":  string,
    # and so on...

Хотя нам по-прежнему нужно записывать данные в отдельные таблицы, мы можем получить их из одного места.

Мы хотим выполнить следующие запросы:

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

Исходя необходимости данных запросов, наша таблица заказов должна выглядеть следующим образом:

{
    "buyerEmail":  string,  # partition key
    "orderId":     string,  # sort key
    "goodName":    string,  
    "sellerName":  string, 
    "buyerZipCode: string, 
    "goodPrice":   string,
    # to be added ...
}

Давайте создадим эту таблицу путем выполнения следующей команды:

$ aws dynamodb create-table \
               --table-name orders \
               --attribute-definitions \
               AttributeName=buyerEmail,AttributeType=S \
               AttributeName=orderId,AttributeType=S \
               --key-schema \
               AttributeName=buyerEmail,KeyType=HASH \
               AttributeName=orderId,KeyType=RANGE \
               --billing-mode PAY_PER_REQUEST

И давайте посмотрим, как мы сможем выполнить необходимые запросы.

Дайте мне все заказы конкретного покупателя.

Ну это легко

>>> orders = orders.query(
...     KeyConditionExpression=Key('buyerEmail').eq('someEmail'),
...     ProjectionExpression='orderId'
... )['Items']

Давайте уж что-нибудь посложнее.

Дайте мне все товары из одного заказа.

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

$ cat index.json 
[
  {
    “Create”: {
      “IndexName”: “goodsFromOrder”,
      “KeySchema”: [
        {
          “AttributeName”: “orderId”,
          “KeyType”: “HASH”
         },
         {
           “AttributeName”: “goodName”,
           “KeyType”: “RANGE”
         }
      ],
      “Projection”: {
        “ProjectionType”: “ALL”
      }
    }
  }
]
$ aws dynamodb update-table \
               --table-name orders \
               --attribute-definitions \
               AttributeName=orderId,AttributeType=S \
               AttributeName=goodName,AttributeType=S \
               --global-secondary-index-updates \
               file://index.json

Ну, а теперь я могу получить все товары, входящие в заказ.

>>> orders = orders.query(
...     IndexName='goodFromOrder'
...     KeyConditionExpression=Key('orderId').eq('order12345'),
...     ProjectionExpression='goodName'
... )['Items']

Дайте мне все товары одного продавца.

Очередной пример использования глобального вторичного индекса. Но в данном случае мне понадобится другой индекс

$ cat index.json 
[
  {
    “Create”: {
      “IndexName”: “goodsFromSeller”,
      “KeySchema”: [
        {
          “AttributeName”: “sellerName”,
          “KeyType”: “HASH”
         },
         {
           “AttributeName”: “goodName”,
           “KeyType”: “RANGE”
         }
      ],
      “Projection”: {
        “ProjectionType”: “ALL”
      }
    }
  }
]
$ aws dynamodb update-table \
               --table-name orders \
               --attribute-definitions \
               AttributeName=sellerName,AttributeType=S \
               AttributeName=goodName,AttributeType=S \
               --global-secondary-index-updates \
               file://index.json

А вот запрос останется примерно таким же

>>> orders = orders.query(
...     IndexName='goodsFromSeller'
...     KeyConditionExpression=Key('sellerName').eq('BustBey'),
...     ProjectionExpression='goodName'
... )['Items']

Дайте мне всех покупателей, живущих в районе с одним почтовым индексом.

Точно такой же вариант с глобальным вторичным индексом, как и выше, просто указываем ZipCode, в роли ключа раздела.

Назовите мне общую стоимость заказа.

В данном случае нам даже не нужны никакие манипуляции с индексами, делаем обычный запрос к таблице заказов и в роли ProjectionExpression указываем цену.

Дайте мне всех покупателей, которые купили у конкретного продавца.

Вновь используем индекс goodsFromSeller, а в роли ProjectionExpression указываем buyerEmail

Дайте мне все товары отдельного покупателя.

Делаем обычный запрос к таблице заказов и в роли ProjectionExpression указываем goodName.


Как видите, все мои запросы можно обработать с одного таблицы (отсюда и название «Single-Table design»). Этот шаблон является очень мощным и устраняет необходимость использования многократного чтения, имитирующего запросы JOIN, как в реляционных базах данных. Этот шаблон также широко интегрируется с GraphQL — реализация API подразумевает повышение производительности за счет сокращения количества запросов API.

Что дальше?

Если вы дочитали до сюда, мое вам “увожение”!

Вот и все, что касается этой серии статей посвященной глубокому погружению в DynamoDB!

Надеюсь, вам понравилось, и я, как всегда, приветствую ваши отзывы.

Если вы планируете активно использовать DynamoDB в проде, вам не помешает изучить дополнительные материалы:

Если есть, что добавить полезного в этот список – пишите.


P.S. Несмотря на то, что я называю этот пост последней частью глубокого погружения в DynamoDB, я совсем не уверен, что это мой последний материал по этой теме. Если в DDB появятся новые функции, то я расскажу и о них. К слову, дайте мне знать, есть ли сервис AWS, в который вы хотите, чтобы я погрузился глубже!

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

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

Ура! Я сделал это!

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

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

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

Leave a Reply