Танцы с парсингом, kubernetes и миграция в Yandex Cloud: как мы делали Kontragent.io

Речь пойдёт о сервисе для проверки контрагентов, который мы продаём как коммерческий продукт и используем сами. Коротко расскажем о том, что делает система, с какими проблемами столкнулись при разработке и как их решали, как запускали, разворачивали, и немного коснемся того,   что «под капотом» у DevOps. Полагаю, что пост позволит оценить усилия команды при создании сервиса, подсветит использование некоторых технических решений, которые нам показались оптимальными, покажет типичные проблемы систем с парсингом из множества разнотипных источников. Думаю пост будет полезен коллегам при разработке других продуктов. Я честно постараюсь минимизировать рекламную составляющую до короткого дисклеймера и избавить пост от маркетинг булшит. 

332e16dc3d31e41061a60964748b6097.webp

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

Hidden text

Немного о том, с чего всё началось

В 2016-м году у ЕАЕ-Консалт накопился достаточно печальный и травматичный опыт сотрудничества с плохо проверенными контрагентами. При разборе факапов (зачеркнуть) полетов было выявлено, что при должной предварительной проверке этих компаний можно было заранее сделать вывод о проблемных перспективах сотрудничества. Партия и правительство(зачеркнуть), руководство  решило, что средство проверки контрагентов в виде сервиса, «раздающего редфлаги» потенциально опасным контрагентам, может понадобиться не только нам, но стать полноценным коммерческим продуктом. 

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

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

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

Hidden text
5347418486aeedb73535c3e5dbfae967.png

Функциональность и данные

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

  1. Формальные регистрационные данные

  2. Сведения о численности работников

  3. Данные учредителей

  4. Сведения об исполнительных производствах

  5. Список учрежденных юрлицом компаний

  6. Структура капитала

  7. Данные об уставном капитале

  8. Данные о заблокированных счетах

  9. Виды деятельности

  10. Бухгалтерский баланс

  11. Движение денежных средств

  12. Режим налогообложения и другие налоговые данные 

  13. Сертификаты и декларации

  14. Лицензии

  15. Приставы

  16. Проверки 

  17. Значимые факты о деятельности компании

  18. Данные о банкротстве

  19. Сведения о вакансиях компании;

  20. Исторические выписки из ЕГРЮЛ.

  21. Сведения о рисках

Сервис должен был автоматически собирать и обновлять информацию о каждом контрагенте, используя доступные источники, а также автоматически определять 37 факторов риска, рекомендованных ФНС. 

Для этого использовался парсинг по доступным для нас источникам (часть открыты, часть имеют платный доступ). Иными словами, задача состояла в том, чтобы спарсить данные по запросу и сформировать отчет для пользователя, позволяющий получить полезную, а в идеале исчерпывающую информацию о контрагенте для принятия решений о сотрудничестве или отказе и выделить данные, являющиеся триггерами  факторов риска. 

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

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
public class SimplestWebParser {
    public static void main(String[] args) throws Exception {
        // URL веб-страницы для парсинга
        String url = "http://example.com";
        
        // Loading HTML page content
        Document doc = Jsoup.connect(url).get();
        
        // Retrieval and output of data using inferred CSS selectors
        System.out.println("ФИО: " + doc.select("div.fio").text());
        System.out.println("Адрес: " + doc.select("div.address").text());
        System.out.println("ИНН: " + doc.select("div.inn").text());
    }
}

Разные ресурсы, которые служили источниками данных, требовали отличающихся методов для парсинга и множества ухищрений для обхода блокировок. Также коммерческое решение предполагало совсем другие возможности для масштабирования и изменения, другие нагрузки и другие требования к доступности, что определило выбор не самых простых архитектурных и DevOps решений. 

Выбор архитектуры 

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

Решили использовать GraphQL API, так как этот тип показался наиболее подходящим для такого сервиса, особенно в ситуации, когда работа ведется с большой разрозненной командой, а система требует взаимодействия между backend-ом и frontend-ом и мобильной разработкой. Аргументы в пользу такой архитектуры были следующими:

  • Упрощение агрегации данных из множества источников, таких как БД и сторонние API при помощи одного запроса. 

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

  • Уменьшение количества запросов: вместо множества REST-запросов использование одного GraphQL-запроса, чтобы снизить нагрузку на сеть и упростить интеграцию между фронтендом и бэкендом.

  • Типизация и самодокументирование. За счет автоматической генерации документации упрощается согласование интерфейсов и быстрее проводится онбординг новых участников разработки.

  • Возможность добавления, изменения и удаления полей и типов, без влияния на существующие запросы. 

  • Упрощение получения разных наборов данных с использованием одного запроса, возможность для фронтов самостоятельно формировать запросы.

  • Более тесное взаимодействие между командами за счет унификации обмена данными.

  • Простота интеграции и развертывания в различных средах, в частности, в kubernetes.

Танцы с парсингом

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

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

  1. Капчи и защита от ботов, которые затрудняли автоматизированный сбор данных. 

Там, где не было возможности использовать официальное API, приходилось обходить капчи, используя сервисы распознавания и обхода, пользовались https://anti-captcha.com/, а потом написали небольшой ИИ сервис, который решал конкретную капчу с точностью 95%. Иногда помогала ротация IP и пользовательских агентов, что снижало шансы идентификации автоматического доступа.

  1. Отсутствие идентификаторов. Имеются ввиду проблемы связи данных в источнике с нашей базой.

Например, есть реестр исполнительных производств, в источнике нет полей ИНН и ОГРН, но есть только название компании и юр. адрес. Если две компании с похожим названием находятся по одному адресу, то сложно однозначно их сопоставить с данными реестра. Также иногда тяжело понять, относятся ли исполнительные производства к компании при изменении юр. адреса. Пришлось искать дополнительные источники и связывать их с алгоритмом валидации данных.

  1. Сильно мешали ограничения на количество запросов с одного IP адреса с лимитом по времени. 

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

  1. Использование JavaScript для динамической подгрузки контента, в ряде случаев не позволяло напрямую получить данные через обычный HTTP запрос

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

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

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

  1. Беды с распознаванием данных. Структурированные данные тяжело извлекать из неструктурированных HTML страниц, особенно если данные в различных форматах. 

С такой ситуацией столкнулись лишь однажды. Помогло использование jsoup (Java), библиотека серьёзно упростила процесс извлечения.

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

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

DevOps-решения 

В силу выбранной архитектуры приложения, основной задачей девопс стала оркестрация контейнеров. Все сервисы «Контрагентио» —  stateless, т.е. хранят данные во внешних БД, и для этого идеально подходит kubernetes. Нельзя сказать, что здесь были какие-то уникально-нетривиальные решения. Архитектура, задачи и условия работы продукта определили выбор инструментария. 

Развернуто всё было в Yandex Cloud, для чего были использованы их managed-сервисы, в частности, Yandex Managed Service for Kubernetes®. Для основной БД использовали документо ориентированную СУБД MongoDB. К слову, в Yandex Cloud переезжали из Microsoft Azure. Переезжали давно, чтобы выполнить требования 152-ФЗ о хранении персональных данных. Как выяснилось позже, мигрировали совсем не зря. Кроме того, Yandex Cloud было в 2 раза дешевле, что ускорило решение руководства по этому поводу. Задача тоже была обычной — реплицировать БД из одного из Managed Kubernetes в другой.

 И тут возникла проблема, т.к. размер базы составлял ок 2 ТБ. В тот момент Managed Service for MongoDB у Yandex Cloud уже был, но он поддерживал объемы БД до 600 Гб, соответственно пришлось бы проводить шардирование и получить геморрой (зачеркнуть) разбираться с другими проблемами. Но руководство достаточно жестко ограничило дедлайны миграции, и времени использовать такой метод не было. Из-за исполинских размеров БД копирование пришлось проводить поднятием реплики: создали вторичную ноду, которая стягивала на себя данные из основной. Решение позволило успешно мигрировать. 

В связи со стабильной нагрузкой, что характерно для b2b-продуктов, где нету условных «черных пятниц» и других внезапных всплесков, не возникло необходимости в скейлинге, что в целом упростило задачи. Был рассчитан размер кластера и выделены достаточные для работы ресурсы. 

Для поиска в БД использовали ElasticSearch, чтобы индексировать данные по заданным ключам. Ещё один сервис Elastic — ELK (Elastic logstash kibana) для сбора логов, сторонние сборщики мы не используем, в приложении есть нативная функция логирования. Elastic нативно архивирует собранные логи и удаляет индексы из «горячего хранилища» и переносит их в «холодное» (S3 хранилище). Аналогично дублируются и выгружаются в s3 хранилище логи, хранение которых лимитировано одним годом. Через год логи «аннигилируются».

Параллельно с Yandex Managed Service for Kubernetes в Yandex Cloud, для хранения собранных образов используется Container Registry. Образы собираются на виртуалке, развернутой в нашем частном ЦОД. Сбор проводится в Jenkins, он, по сути, является шедулером заданий. В нём стоят модули SSH-доступа к нодам Kubernetes. Агенты связываются с доступными системе источниками, такими как ЕГРЮЛ, ЕГРИП и другими, которые мы парсим. 

Для сбора данных создан специальный контейнер, который запускается внутри оркестратора, Jenkins автоматически «поднимает» контейнер, передаёт ему задания на сбор и обработку, а затем складывает данные в БД и обновляет по ним индексы в Elasticsearch. Это реализовано за счет набора bash-скриптов и библиотек БД, остальное собирается http-запросами, через скрипты. Код храним в GitHub, есть разделение репозиториев на основной и DevOps (там лежат пайплайны дженкинс, конфиги, хэлп-шаблоны).

Про безопасность рабочих нод и администрирования контейнеров, мы шутим о том, что она у нас она на «предельно высоком уровне». Так как правами администратора в системе не обладает никто (зачеркнуть) наделен только один человек и этим простым решением мы закрываем большинство уязвимостей.

Если коротко, мы использовали решение настолько стандартное, насколько это возможно в подобной системе. У нас были мысли о том, чтобы пойти альтернативными путями и разворачивать все сервисы напрямую из репозитория. Использовать какие-нибудь Argo CD или Flux CD, но любое подобное решение, на мой взгляд, должно отвечать требованиям, которых в данном случае не было. 

Немного рекламы

Kontragent.io высокоинформативный инструмент проверки партнёров, клиентов, заказчиков и оценки рисков сотрудничества. Для проверки и оценки рисков достаточно ввести название, ФИО руководителя ОГРН или ИНН компании. Несколько важных событий:
1. Веб-версия
Kontragent.io становится бесплатной. Все функции, ранее доступные по платной подписке, стал доступен после регистрации (http://kontragent.io/auth/sign-up).
2. Доступ по API остается платным.
3. Появилась возможность размещать Контрагентио на собственном оборудовании. За подробностями обращайтесь по адресу info@eae-consult.ru.
4. Для поддержки сервиса мы решили размещать небольшие рекламные блоки в веб-версии. Мы постарались сделать их не навязчивыми и не мешающими использовать интерфейс личного кабинета.

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

© Habrahabr.ru