Одиссея Технолоджи
Как мы ускорили импорт 500 тысяч записей до минуты и сохранили p99 API ниже 50 мс
Одиссея Технолоджи
#Проектирование сайта#Программирование сайта#Тестирование сайта

Как мы ускорили импорт 500 тысяч записей до минуты и сохранили p99 API ниже 50 мс

31 
Одиссея Технолоджи Россия, Смоленск
Поделиться: 0 0 0
Как мы ускорили импорт 500 тысяч записей до минуты и сохранили p99 API ниже 50 мс
Сфера

Информационные технологии и интернет

Сдано

Апрель 2026

Задача

Продукт регулярно получал крупные каталоги из нескольких внешних источников. В одном файле могло быть до 500 тысяч записей. При этом источники использовали собственные форматы, сокращения и правила заполнения: одинаковые сущности могли называться и описываться по-разному, а часть строк содержала неполные или неконсистентные данные.

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

Первый вариант импорта был последовательным. Файл проходил через один длинный процесс, промежуточные операции создавали лишнюю нагрузку на память и PostgreSQL, а сбой ближе к концу мог обесценить уже выполненную работу. По мере роста объёма обновление занимало всё больше времени. Если процесс зависал, инженеру приходилось вручную определять его состояние и запускать обработку повторно.

У этого подхода было несколько ограничений:

- производительность зависела от одного процесса и одного сервера;

- рост файла одновременно увеличивал время обработки и потребление памяти;

- тяжёлая запись конкурировала с пользовательскими запросами к базе;

- промежуточный результат нельзя было безопасно показать пользователям;

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

Нам было важно решить сразу четыре задачи:

- сократить время загрузки большого каталога до предсказуемого значения;

- не ухудшить скорость пользовательского API во время обновления;

- исключить публикацию неполного или непроверенного набора данных;

- автоматически восстанавливать импорт после временного сбоя.

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

Решение

Последовательная обработка превращала один этап в узкое место: с ростом файла росли время, расход памяти и цена сбоя.

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

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

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

Новый конвейер: потоковое чтение, нормализация, параллельная обработка, массовая загрузка и публикация только после проверки.

Этап 1. Разделили файл на управляемый поток

Importer на Go начинает работу сразу после получения файла и читает его небольшими пакетами. Система не ждёт полного разбора входного набора: подготовленные строки последовательно передаются следующим обработчикам через ограниченные очереди.

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

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

Этап 2. Параллелизировали очистку и сопоставление

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

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

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

Параллелизм ограничен намеренно: бесконтрольное увеличение числа workers ускорило бы вычисления, но могло создать новую точку перегрузки на стороне PostgreSQL. Баланс подбирался по метрикам очередей, времени этапов и загрузке базы.

Этап 3. Заменили одиночные операции массовой загрузкой

Подготовленные записи загружаются в PostgreSQL через COPY. Вместо сотен тысяч отдельных INSERT-запросов база получает крупные согласованные пакеты. Это уменьшает количество сетевых обменов, разбор SQL и накладные расходы на отдельные транзакции.

Данные сначала попадают в staging-таблицы, изолированные от читающего API. После загрузки система проверяет обязательные поля, количество записей, связи и контрольные показатели. Ошибка на этом этапе останавливает публикацию, но не влияет на текущую активную версию.

Так мы разделили две разные операции: тяжёлую подготовку данных и короткое изменение состояния продукта. Первая может занимать ресурсы и повторяться, а вторая выполняется только после успешной проверки.

Этап 4. Сделали обновление незаметным для пользователей

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

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

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

Новая версия готовится отдельно. Читающий API переключается на неё только после завершения всех проверок.

Этап 5. Автоматизировали контроль и восстановление

Каждый запуск импорта получает собственный идентификатор и проходит через фиксированные состояния. Importer регулярно отправляет heartbeat и сохраняет прогресс. Watchdog отслеживает не только наличие процесса, но и реальное продвижение обработки: живой, но зависший worker также должен быть обнаружен.

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

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

Этап 6. Проверили систему под нагрузкой

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

Нагрузочные сценарии включали пакеты до 500 тысяч записей и пользовательский трафик свыше 200 запросов в секунду. Отдельно проверяли повторный запуск после ошибки, отказ до публикации и переключение подготовленного набора при продолжающемся чтении.

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

С какими сложностями столкнулись

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

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

  • Автоматический повтор не должен создавать дубли. Повторяемые операции спроектировали идемпотентными: система знает состояние конкретного запуска и не публикует один и тот же набор повторно.

  • Параллелизм мог перенести узкое место в базу данных. Увеличение числа workers само по себе не гарантирует ускорения. Мы ограничили очереди и подбирали параллелизм по фактической пропускной способности PostgreSQL, чтобы вычислительные этапы не создавали лавину операций записи.

  • Среднее время ответа не отражало худшие пользовательские сценарии. Поэтому основным показателем задержки стал p99. Он позволил контролировать редкие медленные ответы во время импорта, а не довольствоваться хорошим средним значением.

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

Результат

После переработки импорт пакета до 500 тысяч записей занимает менее одной минуты. Пользовательский API сохраняет p99 ниже 50 мс при нагрузке свыше 200 запросов в секунду, а обновление выполняется без остановки чтения.

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

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

  • крупный файл не загружается в память целиком;

  • этапы конвейера масштабируются независимо;

  • новая версия данных проходит проверки до публикации;

  • зависший процесс обнаруживается и перезапускается автоматически;

  • состояние импорта и API наблюдается через единые метрики.

Что это дало продукту

  • данные можно обновлять чаще, не планируя технические окна;

  • рост входного объёма не требует пропорционального роста памяти;

  • команда быстрее понимает причину сбоя и этап, на котором он произошёл;

  • временные ошибки восстанавливаются без постоянного ручного контроля;

  • архитектуру можно масштабировать по отдельным этапам, а не заменять сервер целиком;

  • пользовательский API остаётся предсказуемым во время фоновой обработки.

Где применим такой подход

Такая схема подходит не только для каталогов. Она применима в маркетплейсах, логистических системах, финансовой аналитике, агрегаторах, корпоративных справочниках и любых сервисах, которые регулярно принимают большие неоднородные наборы данных.

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

Итоговые показатели подтверждены нагрузочными и интеграционными тестами в контуре проекта.

Комментарий агентства

Шалыгин Артём
Шалыгин Артём

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


Стек технологий


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

Хотите заказать похожий проект?

Одиссея Технолоджи с удовольствием обсудит вашу задачу

Оставить заявку