← Все исследования

01 — Масштабирование переноса данных

Миллион записей. Простой скрипт.

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

↓ Прокрутка меняет ситуацию. Управление позволяет её запустить.

01 / 08

Начнём с самого простого копирования.

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

Запустите и посмотрите на число обращений: один запрос на чтение, затем по одному обращению на каждую запись. Каждое ждёт ответа по сети.

Управление переносом
02 / 08

Теперь — миллион.

Тот же скрипт, в тысячу раз больше данных. Единственный запрос должен держать весь результат в памяти до первой вставки, а у процесса 512 МБ.

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

Управление переносом
03 / 08

Читаем лениво.

Будем читать потоком. Курсор отдаёт страницу из 1 000 строк, скрипт записывает их и просит следующую. Сколько бы ни выросла таблица, память не растёт.

Снова запустите миллион. Перенос завершается — примерно за полчаса. И всё это время курсор в исходной базе и его соединение остаются открытыми.

Управление переносом
04 / 08

Пишем пакетами.

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

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

Управление переносом
05 / 08

Сеть иногда рвётся.

Перенос миллиона записей пакетами идёт около минуты, а сеть и базы иногда отвечают ошибкой. На 121-м пакете соединение обрывается дважды подряд. Без повторов скрипт падает, и всё приходится начинать заново.

Запустите без повторов, затем включите повтор с растущей задержкой: 100 мс, 200 мс… Перенос доходит до конца. Но посмотрите на пакет 371: приёмник его записал, подтверждение потерялось, и повтор вставил те же строки ещё раз.

Управление переносом
06 / 08

Повтор не должен дублировать.

Если у строк есть уникальный ключ, вставку можно заменить на upsert: строка с тем же ключом перезаписывается, а не добавляется. Такую запись безопасно повторять. Добавим ещё один сбой: на пакете 641 падает сам скрипт, и его перезапускают.

Сравните INSERT и UPSERT. С upsert дублей нет, но после перезапуска скрипт начинает с нуля и заново переписывает 641 пакет. И каждая запись upsert немного дороже обычной вставки.

Управление переносом
07 / 08

Запомним, где остановились.

После каждого пакета скрипт сохраняет checkpoint — номер следующего пакета — и после перезапуска продолжает с этого места. Но запись в приёмник и сохранение checkpoint — две отдельные операции, и скрипт может упасть между ними.

Сохраняйте checkpoint после записи: при падении один пакет запишется повторно, а upsert сделает это безопасным. Переключите на «до записи»: перенос завершится, но 1 000 записей молча пропадут. Если хранить checkpoint в той же транзакции, что и данные, не будет ни пропусков, ни повторов.

Управление переносом
08 / 08

Несколько читателей.

Один скрипт переносит пакеты по очереди. Разделим ключи на диапазоны и запустим читателей параллельно. У приёмника пул из 8 соединений, и без замедления он выдерживает около четырёх пишущих одновременно.

Добавляйте читателей: 2 и 4 ускоряют перенос почти пропорционально, 8 почти ничего не добавляют, а 16 ждут свободных соединений. Включите неравномерные данные — половина строк в первом диапазоне — и весь перенос ждёт одного читателя. Общий порядок вставки строк при этом больше не сохраняется.

Управление переносом

Загрузить всё, вставлять по одной

Источник1 000 записей1запрос на все строкиСкрипт переносаПамять0 КБпредел 512 МБ1 000вставокПриёмникзаписано 0Источник1 000 записей1запрос на все строкиСкрипт переносаПамять0 КБпредел 512 МБ1 000вставокПриёмникзаписано 0
Прошло0 мсЗаписано0 / 1 000

Готово к запуску. Запустите перенос и посмотрите, как он идёт.

Все числа — оценки модели. Анимация сжимает расчётную длительность до нескольких секунд.

Другая модель: push

Когда данные приходят сами

Здесь скрипт сам просил данные. Если источник сам присылает события, работает push — со своими сбоями: лаг, повторная доставка, ядовитые сообщения.

Когда данные приходят сами
Что осталось за пределами модели

Сцены 1–4: один последовательный скрипт переноса, один источник и один приёмник. Каждый запрос или оператор вставки стоит одного сетевого обращения в 2 мс. Работа базы — 0,005 мс на чтение записи и 0,05 мс на запись. Запись занимает 1 КБ памяти, страница содержит 1 000 записей, скрипт может держать в буфере 512 МБ. Чтение и запись не перекрываются. Длительность вычисляется из этих констант, ничего не измеряется. Не моделируются: индексы, блокировки и конкуренция, разброс сетевых задержек, установка соединений, ограничения на размер транзакций, накладные расходы драйвера и протокола, повторы и сбои — о них сцены 5–7. Модель помогает сравнивать решения, а не предсказывать длительность вашей миграции.

Сцены 5–7: 1 000 пакетов по 1 000 записей, ленивое чтение. Сбои заданы заранее и повторяются при каждом запуске: два обрыва соединения на пакете 121, потерянное подтверждение на пакете 371, падение скрипта на пакете 641. Ожидание ответа — 1 с, задержки повторов — 100, 200, 400, 800 мс без случайного разброса, перезапуск скрипта — 10 с. UPSERT стоит 60 мс работы приёмника на пакет против 50 мс у INSERT, сохранение checkpoint — одно обращение в 2 мс. У приёмника нет уникального ключа для INSERT, поэтому повтор создаёт дубли. Сбой самого приёмника, частичная запись пакета и бесконечные ошибки не моделируются.

Сцена 8: те же 1 000 пакетов делятся на диапазоны ключей поровну, а при неравномерных данных половина строк попадает в первый диапазон. Пул приёмника — 8 соединений; при более чем четырёх одновременных писателях работа приёмника над пакетом замедляется пропорционально их числу. Нагрузка на источник, блокировки и порядок строк не моделируются.

Глубже в механизм

Как реальные системы читают лениво и пишут пакетами:

PostgreSQL — DECLARE, курсоры ↗PostgreSQL JDBC — чтение через курсор ↗PostgreSQL — массовая загрузка данных ↗Заметки, на которых основано исследование ↗