Как мы синхронизируем 380M+ записей из 40+ источников данных, которые постоянно падают
Multi-IP импорт, автоматический scheduler, freshness-мониторинг, международная экспансия — инженерия data pipeline для открытых данных. От первого 404 до стабильного обновления 110+ таблиц каждую ночь.
Как мы синхронизируем 380M+ записей из 40+ источников данных, которые постоянно падают
Когда строишь юридическую AI-платформу на открытых данных, главный вызов — не AI и не поиск. Это надёжное получение данных из десятков источников — украинских государственных реестров, международных баз, санкционных списков — каждый из которых имеет свои ограничения, форматы и проблемы со стабильностью.
Эта статья — инженерный разбор того, как мы построили полностью автоматизированный pipeline синхронизации для 380+ миллионов записей из 40+ источников. От архитектуры multi-IP импорта до cron-scheduler'а, системы мониторинга freshness и международной экспансии на 6 юрисдикций.
Обновлено: май 2026 — актуальные цифры с production-серверов.
Проблема: государственные API — это не Stripe
Когда вы работаете с API data.gov.ua, НАИС, УИПВ или spending.gov.ua, вы сталкиваетесь с реальностью:
- Rate limits без документации — один сервис блокирует после 100 запросов/мин, другой — после 10
- Форматы меняются — JSON-поле вдруг становится null вместо строки, или ответ приходит как HTML-страница ошибки
- Таймауты — ZIP-архив реестра должников на 200MB может загружаться 20 минут или не загрузиться вообще
- Отсутствие idempotency — нет
ETag,Last-Modified, diff endpoint'ов. Каждая синхронизация — полная перезапись - URL исчезают — ресурсы на data.gov.ua переезжают без предупреждения, возвращая 404
Мы не можем позволить себе ручной импорт. Юристы рассчитывают на актуальность данных: реестр разыскиваемых лиц должен обновляться ежедневно, а не ежемесячно.
Архитектура: три уровня надёжности
Наш pipeline состоит из трёх независимых компонентов:
┌─────────────────────────────────────────┐
│ opendata-sync (Docker container) │
│ ├─ node-cron scheduler │
│ ├─ 26 источников по расписанию │
│ └─ Triggers → backend / openreyestr │
└───────────┬─────────────────┬───────────┘
│ │
▼ ▼
┌───────────────────┐ ┌──────────────────┐
│ ImportTaskService │ │ OpenReyestr │
│ (mcp_backend) │ │ sync-registry │
│ ├─ 10 source IP │ │ ├─ ZIP download │
│ ├─ round-robin │ │ ├─ XML parsing │
│ ├─ retry logic │ │ └─ UPSERT │
│ └─ progress track │ │ │
└────────┬──────────┘ └────────┬─────────┘
│ │
▼ ▼
┌─────────────────────────────────────────┐
│ PostgreSQL: 110+ data таблиц (1.26 TB) │
│ Мониторинг: db-status.py + freshness │
└─────────────────────────────────────────┘
Уровень 1: Scheduler — opendata-sync
Первый уровень — лёгкий Node.js микросервис, который не загружает данные сам. Он отвечает только за расписание и триггеры.
Конфигурация источников
Каждый источник описан декларативно:
{
name: 'mvs_wanted_persons',
title: 'МВД — Лица в розыске',
cron: '0 3 * * *', // 03:00 ежедневно
target: 'backend', // куда отправить триггер
sourceName: 'mvs_wanted_persons',
enabled: true
}
Расписание синхронизации
| Время | Источники | Целевой сервис |
|---|---|---|
| 03:00 ежедневно | МВД розыск, МВД пропавшие, МВД авто, МВД недействительные паспорта, НАЗК коррупционеры, НАЗК правонарушители | backend |
| 03:30 ежедневно | Статусы дел, расписание заседаний, адвокаты, люстрация, госпомощь, крупные налогоплательщики, должники по зарплате | backend |
| 04:00–05:00 ежедневно | Арбитражные управляющие, банкротство, исполнительные, должники | openreyestr |
| Воскресенье 02:00 | УИПВ патенты, марки, модели, образцы | backend |
| Понедельник 02:00–05:00 | Нотариусы, судебные эксперты, спецбланки, улицы, АТУ | openreyestr |
Защита от дублирования
Перед каждым триггером scheduler проверяет, не запущен ли уже импорт этого источника. Если статус — running, новая задача не создаётся.
Уровень 2: ImportTaskService — multi-IP импорт
Это сердце pipeline. Когда scheduler отправляет триггер, ImportTaskService берёт на себя всю работу по загрузке.
Три режима импорта
Государственные источники используют разные форматы, поэтому мы поддерживаем три стратегии:
| Режим | Источники | Как работает |
|---|---|---|
api_paginated |
УИПВ (патенты, марки) | Постраничный обход API, 1100ms между запросами |
json_array |
МВД, НАЗК | Один HTTP-запрос → массив JSON объектов |
file_download |
НАИС реестры | ZIP → XML → парсинг → UPSERT |
Multi-IP: 10 адресов × 5 потоков = 50 параллельных загрузок
Для источников с rate limits по IP-адресу мы используем пул из 10 сетевых интерфейсов (AWS ENI). Страницы распределяются round-robin:
Страница 1 → IP 172.31.x.1
Страница 2 → IP 172.31.x.2
...
Страница 10 → IP 172.31.x.10
Страница 11 → IP 172.31.x.1 (снова первый)
С 5 потоками на каждый IP получаем 50 параллельных соединений. Для УИПВ с rate limit 1100ms/запрос это даёт ~45 страниц/секунду вместо 0.9.
Retry с exponential backoff
Каждый запрос имеет до 5 попыток с нарастающей задержкой:
Попытка 1: сразу
Попытка 2: через 2 секунды
Попытка 3: через 4 секунды
Попытка 4: через 8 секунд
Попытка 5: через 16 секунд
Для ошибки 429 (Too Many Requests) — отдельная логика: ждём Retry-After из ответа сервера.
Отслеживание прогресса без нагрузки на базу
Прогресс хранится в памяти и записывается в PostgreSQL каждые 100 страниц:
// В памяти — обновление каждую страницу (микросекунды)
taskProgress.set(taskId, {
pagesDone: 4521,
recordsImported: 45210,
currentPage: 4522,
lastError: null
});
// В базу — flush каждые 100 страниц
// UPDATE import_tasks SET pages_done=$2, records_imported=$3 WHERE id=$1
Это даёт точный real-time прогресс через API без нагрузки на базу тысячами UPDATE-запросов.
MCP-инструменты для управления
Весь процесс управляется через 4 MCP-инструмента:
| Инструмент | Назначение |
|---|---|
list_import_sources |
Каталог всех источников: URL, тип, таблица, rate limit |
start_import |
Запуск фоновой задачи: source_name → task_id |
get_import_status |
Прогресс: %, ETA, скорость, ошибки |
cancel_import |
Остановка через AbortController с сохранением прогресса |
Это означает, что AI-ассистент может сам запустить импорт, следить за прогрессом и уведомить юриста, когда данные обновлены.
Уровень 3: Мониторинг freshness
Данные без мониторинга — это тикающая бомба. Мы построили систему, показывающую насколько свежие данные в каждой таблице.
Матрица ожидаемой частоты
| Частота | Таблиц | Примеры |
|---|---|---|
| Ежедневно (1д) | 24 | Розыск МВД, недействительные паспорта, коррупционеры НАЗК, должники, исполнительные, статусы дел, адвокаты |
| Еженедельно (7д) | 48 | Патенты, марки, санкции OpenSanctions, депутаты, судьи, законопроекты |
| Ежемесячно (30д) | 8 | Графики заседаний, крупные налогоплательщики, судебные эксперты, спецбланки |
Индикаторы freshness
🟢 в пределах нормы (freq × 1.5) — всё работает
🟡 немного просрочено (freq × 1.5–2.5) — стоит проверить
🟠 просрочено (freq × 2.5–4) — что-то пошло не так
🔴 критично (> freq × 4) — нужно вмешательство
⛔ импорт завершился с ошибкой
🔄 импорт работает сейчас
Dashboard: db-status.py
Скрипт подключается к production-базе через SSH и показывает полную картину:
═══════════════════════════════════════════════════════════════
📦 SecondLayer (основная) — 110+ таблиц, 1.26 TB всего
═══════════════════════════════════════════════════════════════
# Таблица Строк Размер Норма Давность
──────────────────────────────────────────────────────────────────────
1 opendata_vehicle_registrations 19.6M 5.9 GB 7д 3д назад 🟢
2 spending_acts 9.45M 8.3 GB 7д 5д назад 🟢
3 opendata_invalid_passports 2.89M 1.0 GB 1д 2мин назад 🟢
4 opendata_court_case_status 1.25M 846 MB 1д 12мин назад🟢
5 opensanctions_entities 1.25M 522 MB 30д 8д назад 🟢
6 opendata_trademarks 382K 4.3 GB 7д 3д назад 🟢
7 opendata_patents 345K 5.0 GB 7д 3д назад 🟢
8 opendata_missing_persons 117K 119 MB 1д 12мин назад🟢
9 opendata_wanted_persons 71K 49 MB 1д 2мин назад 🟢
10 opendata_corruption 58K 106 MB 1д 3ч назад 🟢
...
Реальные проблемы и как мы их решили
Проблема 1: Docker не может bind к ENI IP
json_array источники (МВД, НАЗК) — это один HTTP-запрос, не пагинация. Когда мы передавали ENI IP для bind, Docker-контейнер получал EADDRNOTAVAIL — он не видит host-сеть.
Решение: multi-IP нужен только для пагинированных источников. Для json_array — обычный fetch без bind.
Проблема 2: URL исчезают без предупреждения
data.gov.ua периодически обновляет resource ID для МВД и НАЗК. Старые URL возвращают 404.
Решение: URL хранятся в таблице import_source_catalog, а не захардкожены. Обновление URL — один UPDATE-запрос, без пересборки кода.
Проблема 3: NULL bytes в PDF/XML
Некоторые реестры содержат \x00 символы, которые PostgreSQL отклоняет:
ERROR: invalid byte sequence for encoding "UTF8": 0x00
Решение: strip null bytes на этапе парсинга, до INSERT.
Проблема 4: Ответ — не JSON
Когда сервер перегружен, некоторые API возвращают HTML-страницу ошибки или пустую строку вместо JSON.
Решение: парсинг обёрнут в try/catch с проверкой Content-Type. Если ответ не JSON — retry с следующего IP.
Проблема 5: Утечка памяти на больших импортах
Импорт 9.45M записей spending_acts держал все записи в памяти.
Решение: streaming парсинг — обработка chunk'ами по 1000 записей, UPSERT, освобождение памяти.
Цифры
| Метрика | Значение |
|---|---|
| Общий объём данных | 380M+ записей, 1.26 TB (2 базы) |
| Количество источников | 26 в import_source_catalog + 20 международных импортёров |
| Количество таблиц | 110+ data-таблиц (31 opendata + 20 spain + 43 openreyestr + 50+ ЕГРСР партиций) |
| MCP-инструментов для поиска | 30+ (opendata + spending + registries + international) |
| Ежедневная синхронизация | 12 источников (03:00–05:00 UTC) |
| Еженедельная синхронизация | 14 источников (выходные) |
| Параллельных соединений | до 50 (10 IP × 5 потоков) |
| Время полного импорта УИПВ | ~45 мин (345K записей) |
| Время импорта МВД розыск | ~30 сек (71K записей, один запрос) |
| Крупнейшая таблица | enforcement_proceedings: 29.4M записей, 19 GB |
| Международные юрисдикции | 6 (Испания, Ирландия, Нидерланды, Швейцария, Люксембург, ЕС) |
Международная экспансия: от 15 украинских источников до 40+ глобальных
С марта 2026 pipeline вышел далеко за пределы украинских реестров. Вот что добавилось:
ICIJ Offshore Leaks — 4.9M записей
Полная база Panama Papers, Paradise Papers, Pandora Papers. 814K entities, 771K officers, 2.9M relationships, 402K addresses. Импорт из CSV за ~2 минуты, данные обновляются при каждом новом leak.
Испания — 20 таблиц, 780K записей
Самый сложный международный импорт. 14 источников: Tribunal Constitucional (27K решений), BOE (48K объявлений + 12K законов), BORME (276K компаний), EUR-Lex (8.6K актов), CENDOJ (2.3K уголовных решений). CENDOJ оказался geo-blocked для non-EU IP — пришлось использовать Playwright + auto IP rotation (81 ротация EIP, 3 параллельных EC2 workers).
Нидерланды — 1.1M судебных решений
Rechtspraak Open Data API — 1,106,921 решение. Один из самых чистых API среди всех источников: XML с чёткой схемой, пагинация работает, rate limits документированы.
Швейцария — 661K судебных решений
Entscheidsuche.ch — федеральные и кантональные суды. Zefix (1.7M компаний) и SHAB (2.18M HR записей) пока заблокированы из-за 403/timeout.
Ирландия — 812K компаний
Companies Registration Office (CRO) — полный реестр ирландских компаний.
Люксембург — 3.3M записей
GLEIF LEI — Global Legal Entity Identifier. 3,282,067 записей международных юридических лиц.
OpenSanctions — 1.25M записей
Агрегированный санкционный список: 1,020K физических лиц, 108K компаний, 71K юридических лиц. 330 уникальных датасетов со всего мира.
Что дальше
✅ Сделано из предыдущего плана
- Больше источников — с 15 до 26 автоматизированных + 20 международных импортёров
- Incremental sync — реализован для ЕГРСР (
sync-edrsr-incremental.sh) - Data quality checks — базовая проверка падения row count после импорта
🔜 Следующие шаги
- ЕГРСР fulltext gap 2022-2026 — 32.9M документов без полного текста, активный backfill через /Review/ endpoint (~4M уже восстановлено)
- Qdrant hybrid search — векторы ЕГРСР (103M+ points) таймаутят на 60с, нужен tune HNSW или ожидание завершения индексации
- Испания Tier 2 — ещё 12 импортёров: Plataforma Contratación (~5-8M тендеров), Congreso votes (~25M), CENDOJ non-penal, Catastro INSPIRE
- Швейцария — 12 импортёров на ~9.2M записей: kantonsblatt.ch, fedlex, parlament.ch, Zefix, opendata.swiss
- data.gov.ua OSINT — обнаружено 150+ новых датасетов категорий P0-P2, постепенная интеграция
- Alerting — Telegram-бот для уведомлений о failed imports
Вывод
Построить pipeline для открытых данных — это не про fetch → insert. Это про инженерию надёжности: retry, rate limit, multi-IP, мониторинг freshness, graceful degradation. А когда pipeline выходит на международный уровень — это ещё и про Playwright для geo-blocked сайтов, EIP rotation для обхода бан-листов и парсинг XML-схем 6 разных юрисдикций.
Каждый из 40+ источников — это отдельная история с уникальными проблемами. Но когда pipeline работает стабильно, юрист задаёт вопрос в чат и получает актуальные данные из МВД, НАЗК, УИПВ, НАИС, spending.gov.ua, ICIJ, Rechtspraak и CENDOJ — даже не задумываясь, сколько инженерной работы стоит за каждым ответом.
Регистрация: legal.org.ua