В Альфа-Банке безостановочно ведутся работы по созданию и обновлению статистических моделей. Чтобы выявить момент, когда модель перестала соответствовать критериям качества установленным в банке, требуется регулярный мониторинг. Это задача нашего подразделения — регулярно мониторить модели, которые находятся в продуктивном контуре, собирать метрики по качеству моделей. Так как мы за эффективность, то мониторим модели автоматизировано.
В задаче автоматизированного мониторинга есть два условия:
Нам нужен реестр моделей, для понимания, что мониторить сейчас, а что завтра.
Нам нужно ПО которое будет выполнять мониторинг с необходимой периодичностью и с заданными критериями оценки.
Для выполнения первого условия у нас есть интеграция с Jira и пользовательский интерфейс, если необходимо внести модель, которой нет в Jira. Для выполнения второго до недавнего момента использовали разработку иностранной компании. Но теперь у нас есть своё.
Что не так с иностранным ПО
Итак, что из себя представляло предыдущее решение и почему мы решили с него мигрировать? Для начала покажу возможности системы мониторинга.
Ведение реестра моделей.
Возможность расширения метаданных модели (позволяет добавлять новые поля).
Возможность настройки параметров запуска теста.
Плюс возможность настройки расписания запуска тестов и написания новых тестов для моделей без доработки коробки.
Но на этом плюсы заканчиваются. Описание минусов начнём с тестов модели.
Тест — это, по сути, Groovy-скрипт, который запускает Система Мониторинга (СМ) по настроенным правилам. Groovy потому, что решение — коробочное Java-приложение, собранное из различных Java-фреймворков c пользовательским UI и возможностью писать расширение на Groovy.
Важная особенность: каждый тест универсален и может быть запущен для любой модели, а у каждой модели своя структура входных данных и каждый тест должен быть рассчитан на это. Так как возможностей языка Groovy, мягко говоря, недостаточно для написания математических тестов, то для любой, более-менее сложной математической функции, мы вынуждены были использовать дополнительный язык. В нашем случае это R.
Из-за этого время разработки было долгим — ведь необходимо писать на двух языках: Groovy и R.
Но время — это не самая страшное. Со связкой Groovy - R связана крупная архитектурная проблема.
Данная связка имеет двойной расход оперативной памяти.
Двойной расход памяти связан с особенностью интеграции между R и Groovy. Фактически, чтобы передать 1 кбайт данных из Groovy-скрипта в R-скрипт, нужно этот 1 кбайт зарезервировать как со стороны Groovy, так и со стороны R.
Когда данных немного, а в самом начале использования системы в банке так и было, это было не заметно. Но система взрослела и на мониторинг стали поступать входные данные для теста с объёмом в десятки Гб, и данный нюанс стал очень большой проблемой.
Объёма в 128 Гб на сервере нам стало не хватать.
Это привело к сильному увеличению человеко-часов разработки, так как просто написать тест мы уже не могли, теперь каждый тест было необходимо оптимизировать. Часть логики пришлось переложить на SQL для уменьшения выгружаемого объема данных, а то что выгружалось пришлось разбивать на блоки и считать последовательно.
Бонусом при эксплуатации СМ выявили следующие проблемы:
У системы нет выделенного API для интеграции с другими системами. Единственный способ — это мимикрировать запросы внешней системы под запросы UI.
Невозможно доработать интерфейс под пожелания пользователей. Хотя система и позволяет частично менять интерфейс добавляя новые поля, но их практически никак нельзя использовать для улучшения пользовательского опыта в работе с UI-системы. Это просто информационные поля для просмотра и хранения данных.
Расписание и параметры запуска тестов имеют не достаточную вариативность. Система позволяет настраивать ограниченный список самых необходимых параметров, под наши задачи этого катастрофически не хватает.
Система не позволяет горизонтально масштабироваться.
И добил этот список уход поставщика с российского рынка.
Соответственно, больше использовать СМ не было никаких причин.
Прорабатываем требования
Пропустив стадии гнева, торга, депрессии и принятия, перейдём сразу к этапу требований к новому решению.
Все команды, с которыми мы взаимодействуем, используют в своей работе Python. Поэтому мы решили для написания тестов, как и самой системы, его и использовать.
Для поддержания и мелких доработок новой системы должно быть достаточно 1 человека.
Система должна горизонтально масштабироваться.
Система должна иметь API для возможности интеграции с другими системами.
Проведя RFI мы поняли, что поставщиков готового ПО, удовлетворяющих нашим требованиям, нет.
Одни предложения представляли из себя только интерфейс, а сами тесты нужно было где-то запускать вручную, что убивало изначальную идею автоматического тестирования. Вторые предлагали большие и сложные системы, где мониторинг реализовался в виде точечных доработок функционала, изначально для того не предназначавшегося. Это позволяло автоматически запускать тесты, но не позволяло их гибко настраивать. Возможность гибкой настройки теста для нас важно также, как и автоматическое тестирование.
Просмотрев все варианты, взвесив все за и против, решили рассмотреть альтернативный вариант — разработку ПО силами подрядчика. Уже от этого варианта отказались в пользу самостоятельной разработки ввиду более низкой стоимости и наличия необходимой экспертизы внутри банка.
Взяв за основу указанные выше требования, приступили к разработке архитектуры.
Прописываем целевую архитектуру
Она получилась следующая:
№1. Пользовательский сервис — это то, с чем будет работать пользователь. Состоит из 3 частей:
Фронт — SPA приложение на ReactJS. Выбор был сделан просто: для этого фреймворка есть готовая корпоративная библиотека компонентов.
Авторизация внутрибанковская: для нас это обычный JWT токен.
Бэкэнд — асинхронный сервис на FastAPI (RESTFull c некоторыми отклонениями), SqlAlchemy 2.0 и Python 3.11.
Последняя связка позже преподнесла нам неприятный сюрприз — дело в том, что необходимая для доступа к Hadoop библиотека PyHive (на март 2023 года) не поддерживала Python 3.11 и SQLAlchemy 2.0.
Откатывать версию Python и SQLAlchemy было крайне трудоёмко, поэтому было принято волевое решение адаптировать библиотеку под наши версии библиотек. Основная сложность в том, что функционал, который мы должны были реализовать на основе данной библиотеки, оценивался в 4 часа разработки.
Как водится, эти 4 часа удалось выделить только в самом конце — перед показом реализованной вехи заказчику и переписывать библиотеку пришлось за одну ночь. За ночь в библиотеку была интегрирована pure-sasl и переписаны методы, работающие с SQLAlchemy 1.4 на SQLAlchemy 2.0. Самое удивительное, что переписанная библиотека оказалась полностью работоспособной и без каких-либо ошибок продолжает работает и сейчас.
№2. Подсистема исполнения тестов — это там, где будут исполняться наши тесты на Python. С данной подсистемой было больше всего сложностей. Вариантов архитектуры было много: от написания собственного сервиса, до использования в каком-либо виде Open Source решения. В конечном итоге выбрали Open Source, а точнее — Apache Airflow. Основные факторы в его пользу:
Большое количество различных бэкэндов для исполнения Python-скриптов.
Обновление скриптов без необходимости перезапуска приложения.
Встроенные средства логирования и мониторинга исполнения скриптов.
Возможность горизонтального масштабирования.
№3. Инфраструктура.
Все сервисы поднимаем под Docker, включая воркеры Airflow. От Kubernetes пришлось отказаться, потому что банковская инфраструктура не позволяет создавать поды с объемом RAM большим 64 Гб, а нам нужно 256 Гб минимум.
Развёртывание на исполняемых серверах через Ansible.
В качестве gateway фронта nginx.
База данных PostgreSQL 15.
Интересные моменты из разработки
Согласно требованиям, состав полей, входящих в сущность «Модель» должен меняться на лету. Соответственно, все сервисы системы после изменения должны иметь возможность полноценно работать с новыми, отредактированными или удалёнными полями без каких-либо перезапусков сервисов.
В соответствии с ТЗ, мы должны поддерживать поля следующих типов:
Строка.
Число.
Текст (многострочная строка).
Справочник (возможно присвоение только заданных значений).
Файл (должна быть возможность приложить к модели файл).
Также данные поля при отображении в UI должны визуально иметь разный размер и группироваться по назначению, иметь валидаторы вводимых данных. Дополнительно при нахождении модели в различных каталогах системы, состав полей модели должен меняться.
Сейчас это у нас реализовано как на иллюстрации выше. А в момент разработки это была довольно интересная техническая задача.
Мы изучили различные материалы на предмет возможных подходов к решению подобной задачи. Как оказалось, подходов для реализации подобного функционала множество. Например, в замещаемой системе это решается путем создания таблицы с замыканием её на себя же, и организацией в ней нескольких уровней хранения данных за счет перекрёстных ссылок. Данный подход позволяет хранить любую иерархию данных, но он же порождает проблему с высокой нагрузкой на БД при получении списка моделей. Все остальные найденные варианты нам также не подходили из-за различных ограничений. Остается только пробовать свой вариант.
Свой вариант выглядит как два уровня конфигурации.
Первый уровень хранит общую конфигурацию модели с историей изменений. Конфигурация — это JSON со списком полей, типом данных, а также информацией о визуальном представлении.
Второй уровень хранит «конфигурацию разницы» для каждого варианта каталога. Конфигурация — это JSON со списком полей, которые неактивны для конкретного каталога.
Сами данные хранятся в таблице в поле в виде JSON.
Для работы UI с изменяемой структурой созданы два метода API.
Первый метод возвращает структуру данных и тип объектов для отображения списка моделей в виде реестра.
Второй возвращает структуру данных и метаданные для формирования интерфейса для отображения данных модели.
Отображение данных идёт в 3 шага. Первый шаг запрашивает структуру данных, второй запрашивает данные, третий отрисовывает пользовательский UI на основе данных, полученных на первом и втором шагах.
В новой системе мы решили все дополнительные поля хранить в одном поле JSON. Тем самым одна строка в таблице может хранить несколько измерений данных. В столбцах таблицы хранятся неудаляемые поля, такие как: автор, дата изменения, имя модели, описание модели, а всё остальное в JSON. Такой подход при массовом обработке списка моделей сильно снижает нагрузку на базу данных.
Вернемся к полям хранящимся в JSON — они у нас могут быть разных типов. Могут быть обязательными и необязательными. Почти все типы полей классические, к которым мы уже давно все привыкли, но есть и сложные типы, к примеру, справочники: определённым полям можно присвоить только заранее определённые значения, список доступных полей и их вариативность задаётся в конфигурации.
Тестирование
Для упрощения работы пользователя с изменяемой структурой данных предполагалось, что мы будем использовать GraphQL. После интеграции технологии всё выглядело отлично: можно было вернуть пользователю любые данные, которые ему действительно были нужны в данный момент. Но при первом тестировании системы обнаружилось, что библиотека, реализующая GraphQL, не умеет обновлять структуру данных на лету — обновление происходит только при рестарте кода.
Попробовали другие подобные библиотеки — у всех та же проблема. После оценки трудоемкости доработки библиотеки, реализующую GraphQL с поддержкой изменения на лету, пришли к выводу, что проще сделать решение, которое мы описывали выше — использовать запрашивание данных через два метода API, когда первый получает структуру данных, а второй сами данные.
Сейчас основные компоненты системы закончены, в формат Airflow DAG были перенесены несколько тестов. Результаты первых запусков тестов для нас крайне положительные.
Уменьшился TimeToMarket.
Разработка теста под Airflow DAG занимает существенно меньше времени, чем разрабатывать аналогичную функциональность на связке Groovy + R.
Уменьшились требования к оборудованию.
Время старта теста снизилось существенно. Раньше время старта среднестатистического теста на Groovy было около 2 минут, а новые тесты на Python в Airflow DAG стартуют в среднем за 2 секунды. Уже только это на большом количестве запускаемых тестов даёт существенную экономию времени исполнения тестов.
Снизилось общее время выполнения теста.
Уменьшились требования к объему RAM.
Улучшился пользовательский опыт.
У пользователей системы появилось больше возможностей по настройке тестов. Например, существенно возросло количество вариантов по определению дат окна мониторинга. Стала возможна настройка параметров расчета на нескольких периодах — данная функция автоматически рассчитывает дополнительные периоды и запускает на них тест . Например, так выглядит стартовая страница Alfa-MRM, а ниже — пример настройки окна мониторинга.
Появилась возможность горизонтального масштабирования. Так как тесты теперь представляют из себя Airflow DAG, у нас появилась возможность распределять выполнение Task, входящих в состав DAG, на разные узлы исполнения. Для этого мы используем Airflow Celery Executor: он позволяет, используя очередь сообщений, загружать все доступные узлы исполнения. Каждый узел исполнения представляет из себя VM, размещенной в облаке, с запущенным Celery Worker.
Упростилась поддержка системы. Так как все компоненты, кроме DB, развернуты под Docker и управляются через Ansible, это сильно экономит время поддержки.
Вывод
Вот так из «подручных» материалов мы сделали СМ, которая по возможностям гораздо лучше иностранного аналога. В этой истории маленького импортозамещения больших человеко-ресурсов также не потребовалось. В нашей команде было всего шесть человек.
Риск менеджер. Роль: Бизнес аналитик, Тестировщик.
Руководитель направления. Роли: Тим-лид, Архитектор системы, Системный аналитик, DevOps, Тестировщик (да, 5 в 1).
Разработчик Python.
Фронтенд-разработчик на ReactJS.
Python-разработчик DAG для Airflow.
Дизайнер UI (позвали на 2 недели рабочего времени).
Да распределение ролей у нас вышло не в пользу Руководителя направления, но, как это обычно бывает, из-за ограниченности выделенного бюджета особо выбора не было.
После восьми месяцев разработки и фактически завершенной разработки системы, могу сказать что это был прекрасный опыт. Совмещение ролей позволило точно отслеживать сроки разработки и быстро выявлять проблемные места не учтенные на этапе проектирования системы.
Это ускорило разработку: фактически, за 8 месяцев сделана работа, которая, по моему опыту, выполняется за год.
Но минусы также есть, как же без них. Эмоциональная нагрузка в эти 8 месяцев была очень высока, часто рабочий день в 8 часов не укладывался, но мы справились. А это главное. И, главное, довольны, что у нас есть свое независимое и работающее решение.