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

Те, кому интересна исключительно техническая сторона вопроса, могут не открывать часть под спойлером (там немного истории продукта, первичные требования и список функций). Но действительно хорошо понять логику выбранных технических решений, на наш взгляд, сложно без контекста требований и общих представлений о системе.
Hidden text
Немного о том, с чего всё началось
В 2016-м году у ЕАЕ-Консалт накопился достаточно печальный и травматичный опыт сотрудничества с плохо проверенными контрагентами. При разборе факапов полетов было выявлено, что при должной предварительной проверке этих компаний можно было заранее сделать вывод о проблемных перспективах сотрудничества. Партия и правительство Руководство решило, что средство проверки контрагентов в виде сервиса, “раздающего редфлаги” потенциально опасным контрагентам, может понадобиться не только нам, но стать полноценным коммерческим продуктом.
Мы проанализировали сервисы, доступные на тот момент, и выяснили, что информация, которая нам нужна, не может быть получена из одного источника. Её приходится собирать по сусекам по не всегда очевидным закоулкам рунета, что приводит к дополнительным расходом на оплату услуг нескольких ресурсов. При достаточно длительной рутинной процедуре сбора данных, выглядит это всё очень странно: платишь за несколько сервисов, чтобы проверить одну компанию.
Как итог, родилась идея собирать все данные в одном месте, проводить анализ триггеров риска и предоставлять компаниям возможность самим размещать часть данных о себе. Так появился ранний вариант “Контрагентио”, он впервые появился на рынке открытых решений под названием “Владелец.Онлайн”.
Ранние версии системы мы использовали и как корпоративный инструмент проверки контрагентов, интегрировали с системой электронного документооборота в компании. Параллельно начали продажи доступа к сервису, к его API, а также постоянную доработку и развитие новых функций. В 2017 году, решение было признано лучшим в номинации «Развитие инфраструктуры открытых данных» на третьем всероссийском конкурсе «Открытые данные»:
Hidden text

Функциональность и данные
Для определения надежности контрагента было принято решение использовать следующую информацию:
-
Формальные регистрационные данные
-
Сведения о численности работников
-
Данные учредителей
-
Сведения об исполнительных производствах
-
Список учрежденных юрлицом компаний
-
Структура капитала
-
Данные об уставном капитале
-
Данные о заблокированных счетах
-
Виды деятельности
-
Бухгалтерский баланс
-
Движение денежных средств
-
Режим налогообложения и другие налоговые данные
-
Сертификаты и декларации
-
Лицензии
-
Приставы
-
Проверки
-
Значимые факты о деятельности компании
-
Данные о банкротстве
-
Сведения о вакансиях компании;
-
Исторические выписки из ЕГРЮЛ.
-
Сведения о рисках
Сервис должен был автоматически собирать и обновлять информацию о каждом контрагенте, используя доступные источники, а также автоматически определять 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.
Танцы с парсингом
Наиболее проблемной частью разработки был парсинг из различных источников. Изначально казалось, что сбор данных — это тривиальная задача, но мы серьёзно ошиблись. Источники данных в нашем случае совсем не просты для парсинга, приходилось прибегать к множеству изощрений для того, чтобы подключать парсеры, доставать нужную информацию.
При сборе данных мы столкнулись со следующими сложностями:
-
Капчи и защита от ботов, которые затрудняли автоматизированный сбор данных.
Там, где не было возможности использовать официальное API, приходилось обходить капчи, используя сервисы распознавания и обхода, пользовались https://anti-captcha.com/, а потом написали небольшой ИИ сервис, который решал конкретную капчу с точностью 95%. Иногда помогала ротация IP и пользовательских агентов, что снижало шансы идентификации автоматического доступа.
-
Отсутствие идентификаторов. Имеются ввиду проблемы связи данных в источнике с нашей базой.
Например, есть реестр исполнительных производств, в источнике нет полей ИНН и ОГРН, но есть только название компании и юр. адрес. Если две компании с похожим названием находятся по одному адресу, то сложно однозначно их сопоставить с данными реестра. Также иногда тяжело понять, относятся ли исполнительные производства к компании при изменении юр. адреса. Пришлось искать дополнительные источники и связывать их с алгоритмом валидации данных.
-
Сильно мешали ограничения на количество запросов с одного IP адреса с лимитом по времени.
Для обхода ограничений помогала ротация прокси с распределением запросов по пулу серверов между различными IP. Это помогает избежать блокировки по IP и обходить ограничения на количество запросов. Одновременно снижали скорость запросов, чтобы трафик пула сложнее было идентифицировать. Пробовали искусственные задержки между запросами, но желаемого эффекта не получили.
-
Использование JavaScript для динамической подгрузки контента, в ряде случаев не позволяло напрямую получить данные через обычный HTTP запрос
История не частая в нашем случае, но раздражающая. Использовали Selenium, для имитации действий в браузере, что позволяло выполнить JavaScript и получить динамически подгружаемые данные. Пробовали анализ AJAX запросов, но результат нас огорчил.
-
Постоянные изменения в структуре — было несколько ресурсов, заметно меняющих структуру в ограниченный промежуток времени.
В качестве решения пробовали модульный подход для парсеров и мониторинг сайта на предмет изменения структуры. Проблема решилась не нами, сайт прекратил менять структуру, но успел “попить нашей крови”.
-
Беды с распознаванием данных. Структурированные данные тяжело извлекать из неструктурированных 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. Для поддержки сервиса мы решили размещать небольшие рекламные блоки в веб-версии. Мы постарались сделать их не навязчивыми и не мешающими использовать интерфейс личного кабинета.
Если у вас есть предложения о том, как мы можем улучшить работу сервиса — напишите нам в комментариях.
ссылка на оригинал статьи https://habr.com/ru/articles/792362/
Добавить комментарий