Почему и как в Prozorro.Sale перестраивают СУБД через два с половиной года после запуска
Привет, меня зовут Гриша, я СТО «Прозорро.Продажі». Уже больше года вместе с командами Raccoon Gang и Triangu мы перестраиваем систему электронных аукционов, недавно пробившую отметку в 33 миллиарда гривен продаж. Про большие цифры и крупные сделки говорят часто и по телевизору. На DOU уже выходила статья о том, как в целом организована работа нашего проекта. Но вот о том, как эта штука работает технически, сказано далеко не всё.
Это должно быть интересно всем, кто хотел бы узнать, как государственная система может быть построена изнутри и почему мы решили переработать ее целиком всего через два с половиной года после запуска. Для прочтения материала необязательно быть разработчиком :)
Стек (Блиц!)
Языки и библиотеки: Python 3.8, aiohttp, Motor, schematics.
Для хранения данных: MongoDB, Elasticsearch и Swift.
Для работы с метриками: Prometheus, Grafana, Loki.
Для автоматизации развертывания и управления инфраструктурой: GitLab CI, Docker, Kubernetes, Helm.
Для документирования: Swagger, GitLab, Confluence.
Для тех, кто хотел бы контрибьютить: gitlab.prozorro.sale/explore :)
Зачем Цэ-Бэ-Дэ
Стоит начать с того, что такое двухуровневая система. До Революции достоинства одним из больных мест во взаимодействии государства и бизнеса был вопрос доверия: использование информационных систем часто ассоциировалось с ручными вмешательствами и корректировками в пользу «правильных» пользователей.
После революции группа энтузиастов взялась решать задачу с повышением доверия техническими методами на примере государственных закупок. Блокчейн почему-то в ход не пошел (почему — история умалчивает). Зато была разработана двухуровневая система. В нее входят:
- электронные площадки: частный бизнес разрабатывает клиентскую часть приложения, в которой есть фронт, управление пользователями и всяческие фичи для повышения удобства пользователя. Площадок по задумке должно быть много (так и получилось), и каждая защищает интересы своих клиентов;
- центральный модуль, задача которого получать информацию со всех площадок, централизировать и распределять ее одновременно на все площадки. Центральным модулем владеет государство, оно программирует бизнес-правила и помогает площадкам в сложных ситуациях.
То есть такая себе топология «звезда», где «лучи» генерируют данные, а «центр» — обеспечивает синхронизацию всех участников системы. Центральную часть для простоты было решено называть «центральной базой данных», она же ЦБД.
На самом деле, ЦБД — это не только база, но и API, сервис хранения документов, инфраструктурные сервисы и другие компоненты полноценного приложения.
Путь развития
Двухуровневая система закупок успешно работает с 2015 года. А в
При схожести технической модели природа развития бизнес-составляющей у систем существенно отличается. Госзакупки (вместе с ІТ-составляющей) обеспечивают очень сложную и важную, но гомогенную сферу жизни.
С аукционами «Прозорро.Продажі» все иначе: буквально через три месяца работы стало понятно, что потенциал аукционов выходит далеко за рамки продаж банков-банкротов (с которых все началось). Успех быстро расширили на добровольную продажу имущества госпредприятий, позже появилась добровольная аренда, затем — малая приватизация. И вал новых сфер деятельности с каждым месяцем только рос!
Одновременно с этим выяснилось, что даже двух инстансов ЦБД («1» и «2», нейминг — наша сильная сторона!) явно мало. Причина крылась в архитектуре: разработчик системы закупок, от которой происходило наследование, не предполагал такого профиля использования, а потому запуск каждого нового направления становился все более длительным, дорогостоящим и болезненным. Помимо этого, проявилось определенное количество болезней роста, не все из которых можно было решить малой кровью.
Собрав все procs and cons вместе, было принято решение о разработке новой версии ЦБД (3!), про которую дальше и пойдет речь.
Обзор ограничений ЦБД-1/2
Срок эксплуатации ЦБД-1/2 на момент принятия решения про полную переработку составлял порядка трех лет. За это время был собран большой объем эксплуатационной информации, а также данных о профиле использования системы. В дополнение к этому уже существовала дорожная карта по развитию бизнес-составляющей: увеличение количества аукционов, новые сервисы для площадок, количество и сроки запуска новых направлений (эта часть роадмапа вам может быть известна из экономического блока новостных сайтов).
Что же выяснили в результате анализа? Поговорим более детально.
Версионирование и деплой
Ввиду специфики финансирования разработки (большая часть функционала разрабатывалась как fixed-price проекты на средства международных доноров) построение цикла непрерывной разработки было невозможным, а каждое новое решение писала хоть немного, но другая команда, со своими правилами и подходами. Как следствие, кодовая база начала страдать от весьма типичных, но от того не менее болезненных факторов:
- отсутствие версионирования кода сервисов и компонентов;
- компоненты разбросаны по множеству репозиториев, часть из которых используются в системе закупок;
- ручная сборка сервисов, влекущая за собой дополнительную нагрузку на DevOps-команду и повышенную вероятность человеческой ошибки;
- нарушение инкапсуляции сервисов и компонентов, версии зависимостей определяются не самим сервисом, а в процессе развертывания;
- сложности с пониманием того, какие версии каких компонентов развернуты на определенном окружении в данный момент.
На ранних этапах вышеописанное не создавало регулярных сложностей. Однако к 2019 году система разрослась до таких масштабов, что регулярный деплой мог превращаться в операцию на полный рабочий день без гарантии успеха.
Консистентность данных
В качестве основной СУБД-системы использовался CouchDB. Такие базы не поддерживают strong consistency, а для финансовых данных это, вообще говоря, жизненно необходимо. Для синхронизации инстансов понадобился дополнительный сервис Consul, который, с одной стороны, обеспечивал достижение консистентности, а с другой — добавлял точку отказа, которая неоднократно становилась причиной инцидентов.
CAP-теорема: невозможно одновременное достижение всех трех составляющих — целостности, распределённости и высокой доступности
Синхронизация площадок
Сложилась практика, в которой каждая отдельная площадка хранила полную копию центральной базы данных и синхронизировала ее с помощью запросов на поиск. С увеличением объема данных даже пассивное существование одной площадки стало создавать заметную нагрузку на инфраструктуру. А площадок к тому моменту было порядка 50. Отсюда — линейный рост потребностей инфраструктуры без создания какой-либо добавочной стоимости.
Дополнительное ограничение — наличие нескольких одновременно работающих нод, перманентно находящихся в состоянии неполной синхронизации. Поэтому потребовалось ввести дополнительный механизм, который обеспечивал сохранение сессии без смены ноды.
Простота разработки
Из-за ряда проблем с версионированием кода и компонентов на окружениях ручным деплоем значительно усложнилось тестирование системы и скорость поставки нового функционала снизилась. Это не позволяло реализовать планы со стороны бизнеса по увеличению количества аукционов и их участников, а также ввод в эксплуатацию новых типов процедур.
Немаловажным фактором был и высокий порог входа новых разработчиков. Чтобы разобраться в проекте хотя бы в общих чертах, опытный специалист мог потратить недели и все равно упустить важные детали.
Отказоустойчивость
Чтобы исключить возможные манипуляции с аукционами, в архитектуре одного из модулей — в модуле аукционов, где непосредственно происходят онлайн-торги — не предусмотрели failover-механизмы.
Попыток манипулировать аукционами за три года зафиксировано не было. А вот отказы с последующей потерей до 30% проходящих аукционов случались.
Технологические зависимости
Основным языком разработки ЦБД-1/2 является Python версии 2.7, для которой end of life должен был наступить 01.01.2020 (и с этим квестом
Кроме этого, в архитектуру системы было заложено нативное использование AWS S3 для хранения скан-копий документов. С этим фактом связано два возражения. Во-первых, никто не любит vendor-lock как таковой (во всяком случае, в «Прозорро.Продажі»). А во-вторых, система должна была получить аттестат КСЗИ, что, в свою очередь, предполагает физическое размещение компонент на территории Украины (где, как известно, до сих пор не построены дата-центры AWS). Потому от этой зависимости нужно было избавиться.
Что решаем
Список ограничений получился весьма внушительный. Даже поверхностная оценка показала, что точечные исправления будут менее эффективны и более затратны, чем кардинальный рефакторинг с полной заменой отдельных компонент.
Цели предстоящего усовершенствования сформулировали так:
- срок запуска новой процедуры торгов — 1 месяц, включая тестирования;
- автоматизированный деплоймент длительностью до 1 часа;
- количество одновременно поддерживаемых аукционов — 10х от текущего количества;
- все конфигурации — в одном месте;
- масштабируемость, отказоустойчивость, быстрое восстановление и вот это вот все, что есть в каждом первом техзадании на enterprise-систему.
Однако все перечисленное выше — это уже результат. Давайте углубимся в то, какими средствами было решено этого достигать и что в итоге получилось. Естественным образом все прикладные задачи поделились на то, что происходит внутри ЦБД, и то, что снаружи — у площадок.
Новое в ЦБД-3: изнутри
Замена СУБД
Для решения вопроса консистентности очевидным ходом была смена СУБД. Переход на реляционную базу не рассматривался ввиду профиля данных (один аукцион — одна структура), который хорошо согласуется с логикой нереляционной базы.
Выбор остановился на MongoDB. Основными аргументами в пользу этой СУБД стала гарантированная консистентность данных, а также нативная система репликации. На тестовом стенде подтвердилась как скорость синхронизации инстансов без дополнительных средств, так и эффективное восстановление при отказе одной из нод.
Результат замены — повышение отказоустойчивости в целом и существенное упрощение процесса синхронизации площадок с ЦБД.
Стейт-машина и хронограф
Здесь нужно немного рассказать о логике работы ЦБД. Не сильно покривив душой, можно сказать, что ЦБД — это BPM-система с дополнительными нетипичными модулями (собственно, теми, где происходит торг). На более ранних этапах даже рассматривалась возможность использовать адаптированное коробочное решение, но затраты по кастомизации оказались больше, чем плюсы от внедрения.
Как и в любой BPM-системе, вся бизнес-логика держится на двух китах: статусах и расписании. Именно на оптимизации работы этих компонент было сконцентрировано низкоуровневое проектирование.
Для работы со статусами был выбран паттерн стейт-машины (она же конечный автомат). В результате применения такого решения удалось инкапсулировать бизнес-логику и существенно унифицировать код. С точки зрения бизнеса удалось сократить время на разработку похожих статусов.
Что касается работы с расписанием, то основной сложностью стало обеспечение высокой производительности в периоды пиковых нагрузок с учетом потребности в гибкой кластеризации. Время, когда должны были происходить те или иные действия, часто прописано в законах, и особых неточностей не допускает.
Чтобы добиться нужного результата, был применен метод блокировки хронографов. Это решение потребовалось для того, чтобы избежать конфликтов между несколькими копиями сервиса, которые по плану должны работать одновременно.
Для избежания ситуаций, когда один и тот же объект попадает в обработку сразу нескольких хронографов, в момент начала работы одного из хронографов первым шагом он смещает время следующей плановой обработки объекта на одну секунду. Таким образом для остальных копий сервиса объект перестает представлять интерес, и риск двойной работы с ним исчезает. Как и риск того, что из-за неуспешности объект не будет обработан вовсе: в случае неудачи буквально через секунду произойдет новая итерация.
Кроме того, одна секунда — это эмпирически определенная константа. Если когда-либо наступит момент, при котором этого времени не хватит для завершения большого количества транзакций, его можно будет увеличить, незначительно пожертвовав временем обработки очереди. Однако нагрузочные тесты говорят, что для этого «Прозорро.Продажі» должны начать продавать больше раз в 50, не добавляя ни гигагерца ресурсов.
Конфигурации
Хорошее понимание структуры разных процессов продажи помогло еще на этапе проектирования определить, из каких «кубиков» строятся типичные процедуры, что в разных этапах повторяется, а что нет. Благодаря этим знаниям удалось сформировать список элементов с повторяющейся логикой и различиями исключительно в численно задаваемых параметрах (обычно в длительности). А отсюда и выполнить одну из прямых задач проекта — свести описание одного бизнес-процесса к одной конфигурации (конфиг-файлов в реальности получилось чуть больше, но принципиальная цель — прозрачность и отсутствие хардкода — была достигнута):
Имея на руках такой функционал, смогли существенно снизить скорость создания новой процедуры торгов: фактически это перестало быть задачей разработки, а ушло в основном в плоскость конфигурирования и тестирования (исключения бывают, только когда заранее заготовленных «кубиков» не хватает для специфической бизнес-логики). Когда-то в будущем может даже появиться UI, через который бизнес-аналитик сможет создавать процессы без участия разработчика.
Отказоустойчивость
Добиться приемлемого уровня толерантности к отказам удалось традиционным на сегодняшний день путем. Первым шагом стала контейнеризация и перевод всех сервисов под управление Kubernetes. Дальше — отказ от сервисов в единственном экземпляре (каждый компонент разрабатывался с учетом того, что у него будет несколько копий, как, например, это получилось с хронографом). Третье — гарантированная stateless-архитектура всех сервисов. А еще много часов экспериментов с инфраструктурой (но об этом когда-нибудь потом).
Еще один фактор, обеспечивающий устойчивость решения, — полная автоматизация развертывания. Все компоненты ЦБД, включая конфигурацию инфраструктуры, хранятся в GitLab и доставляются на окружения с помощью Helm и GitLab CI.
Доставка, среды и площадки
В своих процессах мы используем четыре среды. Помимо целиком классических сред разработки и промышленной эксплуатации, используется две дополнительные. Первая — идентичная продуктиву, для симуляции поведения основой системы (называем ее staging). Вторая — для раннего доступа к вновь собранным релизам (эту называем sandbox).
Причина именно такого набора сред — в жизненном цикле разработки. В отличие от продуктов, полностью управляемых изнутри компании, здесь свою роль играет двухуровневость системы. От логики ЦБД напрямую зависит успешность работы площадок. А каждая площадка — сама по себе уникальный продукт, который должен как адаптироваться к внешним изменениям (ЦБД), так и к внутренним потребностям бизнеса: UX-оптимизации, собственным технологическим аспектам и другим вызовам, про которые разработчики ЦБД даже не догадываются.
Чтобы обеспечить синхронность движения площадок и ЦБД по версиям, мы обязательно следуем такой логике их движения:
- после формирования очередного релиза обновляется версия sandbox. Обновление сопровождается подготовкой детального описания предстоящего релиза (высокий уровень культуры коммитов спасает от лишней ручной работы);
- следом происходит смещение версии staging на версию, ранее доступную на sandbox. Безопасность действия гарантируется прикладыванием персонального токена, без которого не запускается соответствующий пайплайн;
- после полного тестирования на совместимость версий, прогонки интеграционных тестов и полных бизнес-процессов эта же версия незамедлительно доставляется в продуктив;
- все версии «замирают» до выполнения следующего цикла. За это время
(2–3 недели) разработчики ЦБД успевают подготовить следующую версию, а площадки — адаптироваться к только что выпущенной:
При выходе очередной существенной функциональности (например, запуск новой процедуры) «Прозорро.Продажі» проводят приемочное тестирование площадок: готова ли вся экосистема к изменениям. Но даже в таком случае цикл доставки новых версий остается неизменным. Использование новых функций ограничивается на уровне доступа к частям API, система управления которым специально для этого сделана весьма гибкой.
Версии техзаданий
В некоторых аспектах «Прозорро.Продажі» сильно похож на другие бизнесы. Например, это касается постоянных изменений в технических заданиях, внезапного версионирования и разночтение требований. После многих итераций и обсуждений был найден простой и надежный способ избежать сложностей с документацией. Для этого оказалось достаточно перевести все управление требованиями в GitLab, перед этим научив бизнес-аналитиков правильно делать мерж-реквесты, а проектных менеджеров — закладывать в расписание все этапы проверок.
В результате получилась модель, в которой в течение одного спринта после подготовки нового техзадания команды тестирования и разработки знакомятся с требованиями и дают обратную связь, а аналитик успевает ее отразить в требованиях к началу следующего спринта. GitLab в этом аспекте стал гарантом прозрачности того, что происходит с документацией.
Модели
Ключевой составляющей техзадания и, собственно, ЦБД является модель данных. Прежде чем добиться устойчивой структуры, удовлетворяющей как требования оптимальности, так и пожелания бизнеса, было проведено несколько итераций рефакторинга. Результатом стало выделение типичных структур, неизменных для всех процедур продажи, и последующее их наследование с изменениями, присущими специфическому бизнес-процессу.
Для сериализации/десериализации и управления доступом к непубличным данным было принято решение использовать Python-библиотеку Schematics, которая позиционируется как ORM без слоя DB. После внесения всех необходимых изменений получилась ощутимо кастомизированная версия, которая и ушла в продуктив.
Одновременно появилось еще две связанные задачи. Во-первых, публикация данных в Swagger: хотелось дать площадкам именно те модели, которые фактически работают в ЦБД. Во-вторых, хотелось укоротить цикл работы над структурой данных от бизнеса до имплементации: дать возможность аналитику готовить структуру данных самостоятельно в виде какой-нибудь конфигурации.
Решением обеих задач сразу стало написание Swagger-Schematics генератора, который бы формировал модели непосредственно из Swagger-файла. В итоге процесс получился таким:
- аналитик собирает требования и формирует из них структуру в формате, поддерживаемом Swagger;
- файл попадает в пайплайн, где сначала происходит автоматическая валидация структуры, а потом разработчик проводит код-ревью на предмет общей логичности;
- готовый файл одновременно публикуется в API-документации и отдается генератору, который создает рабочие модели, непосредственно используемые кодом.
Решение оказалось весьма удачным, хотя и имеет свои ограничения: некоторые конструкции не поддерживаются форматом OpenAPI. В таком случае выполняется наследование динамических классов статическими, которые, в свою очередь, имеют специально разработанные методы валидации.
Еще одним слабым местом можно назвать усложнение восприятия решения новыми разработчиками: никаких моделей в коде нет, фактически был создан дополнительный уровень абстракции, который нужно «представлять» при тестировании и внесении изменений. В свое оправдание можем сказать, что это самый «абстрактный» участок решения. Во всем остальном это эталон the zen of Python!
Новое в ЦБД-3: снаружи
Зеркало
Как упоминалось ранее, одним из источников роста нагрузки на инфраструктуру стало увеличение количества площадок, которые фактически выполняли синхронизацию как постраничный поиск. Эту механику было решено заменить целиком. На смену ей пришло зеркалирование изменений.
Решение основано на встроенном механизме реплицирования MongoDB, который определяет и передает все непосредственные изменения клиенту. Клиенту достаточно открыть и держать поднятым WebSocket, чтобы иметь свою постоянно актуальную копию данных.
По общим правилам разработчики площадок весь свой код пишут самостоятельно. Однако в данном случае в виде исключения специалисты ЦБД создали клиент для зеркала, который может быть использован площадкой «из коробки». Официальная версия написана на Python 3.8 и умеет работать с MongoDB и Elasticsearch, неофициальные есть на Go, Rust и TypeScript (ну, знаете, увлеклись, когда писали). На таком же клиенте был построен и поиск в ЦБД.
Нотификации
Где-то через полгода после начала работ внутри команды прозвучал вопрос: а что с нотификациями? Дело в том, что, согласно бизнес-требованиям, площадки должны оповещать клиентов о событиях в аукционах в виде писем: начало — конец — отмена — всякое. Эта функция исторически находилась на стороне площадок, которые вводили нотификации согласно техзаданию и общему здравому смыслу.
Мы решили воспользоваться здравым смыслом несколько иначе: зачем писать логику формирования нотификаций сорок раз (на каждой площадке), если можно сделать это один раз в ЦБД. Был добавлен довольно простой сервис, который стал генерировать события с темой, адресатом и минимально необходимым текстом в специальном эндпоинте.
На стороне площадки стало достаточно «слушать» эндпоинт и заворачивать подготовленное сообщение в свой брендированный шаблон. Использовать эту функцию предложили опционально, и в начале популярностью она не пользовалась вообще. Однако через несколько месяцев при инфраструктурных работах, когда эндпоинт мигрировал, выяснилось, что им пользуется больше половины площадок. Иногда стоит делать хорошие дела, не спрашивая разрешения!
Торги
В отличие от нотификаций, непосредственно процесс торгов в старых инстансах был монополией ЦБД.
Для ЦБД-3 был разработан новый дизайн с продуманным UX и оптимизацией под современные браузеры. Над дизайном интерфейсов на волонтерских началах работала Юлия Снитко, дизайнер с
Однако все равно оставалось ощущение, что сделано не всё.
Правильная идея пришла во время работы над аукционами по продаже древесины. В отличие от всего, что продавалось ранее (уникальные лоты с уникальными участниками), древесина продается часто и массово. На практике это означает, что один участник может торговаться сразу в десятках аукционов. А держать каждый новый аукцион в отдельной вкладке как минимум неудобно.
Внутренние обсуждения вариантов интерфейса не привели к результату: договориться не удалось, да и профили пользователей выходили слишком разные. Именно на этом моменте было решено попробовать нарушить традицию. Мол, давайте дадим право формировать интерфейсы площадкам самостоятельно!
Техническое решение в ЦБД — максимально простое. Достаточно при инициализации Websocket’а, который формируется для доставки на фронт обновлений по аукциону, передать ключ не от одного, а сразу от нескольких аукционов, и, соответственно, слать обновления по всем запрошенным торгам. А уже площадки сами решают, как их клиентам будет удобнее.
Эксперимент оказался удачным: десятки разных интерфейсов торгов, от консервативных серых таблиц до динамичных карточек, которые сами сортируются по времени, важности или цене. При этом интерфейс, предоставляемый ЦБД, остался доступен для пользователей. Судя по мониторингу, «родной» интерфейс все так же выбирают покупатели уникальных лотов, а интерфейсы площадок — покупатели древесины, для которых он и разрабатывался.
А что дальше
Мир уже давно ушел от концепции «написал — эксплуатируешь — выбросил». Для того чтобы время, инвестированное в ЦБД-3, не пропало зря, система будет постоянно поддерживаться, развиваться и дополняться. В бэклоге еще сотни задач. Есть очевидные: идти в ногу с технологиями и запускать новые направления (теперь здесь совсем мало кода и много бизнес-анализа). А есть и более амбициозные: превратить ЦБД в коробочное решение, обеспечив тем самым запуск такой же платформы в других странах.
Что бы там не получилось, это только начало пути.
Чтобы не пропустить новые статьи Григория Легенченко — подпишитесь на него в телеграм-боте Ленты DOU.