Темы
AWS · DynamoDB · AppSync · Производительность
  1. R1Экран спрашивает

    • Двадцать строк, новые сверху
  2. R2Резолвер отвечает

    • N+1 — один список, затем запрос на каждый элемент
  3. R3Решают ключ или индекс

    • Scan там, где подошёл бы Query
    • Нет индекса под этот вопрос
    • Ключи, по которым чтения не группируются
  4. R4Ответ несёт

    • Всё, что есть в таблице, без пагинации
Где каждый из пяти паттернов добавляет стоимость к одному чтению. Три стоят в одном месте — поэтому починка остальных двух сама по себе не держится.

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

Ничего не сломалось. На этом стоит задержаться, потому что именно это решает, куда смотреть.

00 / Рост не ломает модель данных. Он её показывает

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

Меняется объём, а объём превращает ошибку с постоянной стоимостью в линейную. Полное чтение таблицы на тысяче элементов не стоит ничего. На ста тысячах оно и есть продукт.

Поэтому вопрос не «что сломалось». Ничего не сломалось. Вопрос в том, какое предположение только что перестало выполняться и в каком месте кода оно записано.

01 / Читайте систему, а не документацию

На проекте, где я занимался этим подробно — продакшен-SaaS с веб- и мобильным клиентом, — документация модели данных устарела, и общего понимания, как она на самом деле работает, в команде не было. Это нормально. Документация описывает модель, которую кто-то задумал; код и работающая система описывают ту, которая у вас есть.

Три источника, в таком порядке:

  1. Код. Каждый путь чтения. Не то, как называется резолвер или метод, а то, какой запрос он на самом деле отправляет.
  2. Запросы. С профилированием, с реальными параметрами, на реальных объёмах. Запрос, быстрый на тестовых данных, не говорит ни о чём.
  3. CloudWatch. Поведение запросов во времени, чтобы отличить медленную операцию от медленного паттерна.

Там, где код было трудно читать, я рисовал поток данных схемой, прежде чем что-то менять. Это не ритуал. Если вы не можете это нарисовать, вы не знаете, чего коснётся изменение, а на живом продукте в этом и состоит весь риск.

02 / Пять паттернов

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

Scan там, где подошёл бы Query по ключу

Scan читает таблицу целиком и потом фильтрует. Query идёт к ключу и читает только нужное. Фильтр в Scan отрабатывает после чтения, поэтому отфильтрованный Scan, вернувший три элемента, всё равно прочитал всё, за что вы платите временем и ёмкостью.

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

Отсутствующие глобальные вторичные индексы

GSI существует, чтобы у частого способа доступа был путь, отличный от первичного ключа. Без него ответить на вопрос «все заказы этого клиента, новые сверху» можно только через Scan или прочитав больше элементов, чем нужно.

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

Ключи, не совпадающие с тем, как данные читают

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

Как убедиться: для каждого способа доступа спросите, лежат ли нужные элементы рядом. Если ответ «они разбросаны, и мы фильтруем», ключ выбран неверно.

N+1 в резолверах

В AppSync — как и в любом слое GraphQL — резолвер списка отрабатывает один раз, а вложенное поле по разу на каждый элемент. Один запрос списка из 50 превращается в 51 запрос. Код выглядит невинно, потому что каждый резолвер по отдельности корректен.

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

Весь набор данных на клиент, без пагинации

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

Как убедиться: смотрите на размер ответа, а не на длительность.

03 / Как менять это на живом продукте

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

Что я сделал бы так же:

  • Выпускать вехами, проверяя каждую по шагам на staging, пока команда параллельно смотрит код. Не одним большим изменением в конце.
  • Релизить вне рабочих часов, когда клиенты собраны в одном часовом поясе. Это дешёвое преимущество, и оно есть не у всех.
  • Обновить документацию модели данных, когда закончили. Вы только что потратили недели на её восстановление. Оставить это ненаписанным значит гарантировать, что следующий повторит ту же работу.

Что я сделал бы иначе:

  • Планировать путь назад для каждого изменения до того, как его делать. Прохождение dev → staging → production снижает риск, но не гарантирует, что релиз в продакшен пройдёт успешно. Миграция данных — ровно то место, где этот зазор бьёт больнее всего, а «откатываться вперёд» не план.
  • Сначала оценивать радиус поражения. Когда отдельно разворачиваемые клиенты делят один слой данных, изменение общей схемы достаёт дальше репозитория, который вы правите. Я понял это при проверке, а не при планировании.

04 / Три вещи, которых я делать не стал бы

Полный редизайн в single-table как первый шаг. Часто это верное конечное состояние и почти никогда не верный первый ход: это максимально возможное изменение той части, которую вы понимаете хуже всего, сделанное, пока симптом ещё не объяснён.

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

Гнаться за цифрой, не определив измерение. Решите, что именно вы измеряете, на какой операции и при каком объёме данных, прежде чем что-либо трогать. Иначе улучшение не отличить от шума, и вы не сможете ответить на единственный вопрос, который задаст клиент.

05 / Как выглядело «стало лучше»

Процента я не назову. Измерения до и после на том проекте к публикации не одобрены, а цифра без источника стоит меньше, чем метод.

Что можно утверждать: после работы таймауты в поиске сократились, а жалоб от клиентов стало существенно меньше. Продукт всё это время продолжал работать.

Если нужна длинная версия именно этого проекта — ограничения, что мигрировали и как выпускали, — он описан отдельным кейсом.