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

Меня зовут Анатолий Яшкир. Я руководитель разработки финтеха ВКонтакте. В этой статье расскажу о специфике финтеха и нашем кейсе рефакторинга исторического кода с переходом на сервисную архитектуру. 

Финтех: базовые задачи и регулирующие механизмы

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

Стандартный пул задач финтеха:

  • управление доступными товарами и услугами, механиками продаж

  • постпроцессинг, оформление и сверка чеков, отправка документации в налоговую

  • контроль всех процессов — от желания пользователя получить услугу до её выдачи

Примечание: ВКонтакте финтех объединяет услуги платежей, монетизации и выплат.

При этом работу финтеха комплексно регулируют различные механизмы:

  • Ответственность продукта перед пользователями. Сервис должен решать задачи пользователя и при этом быть безопасным

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

  • Финансовая ответственность. Команде финтеха нужно проверить все чеки, передать данные в налоговую службу, провести тщательную сверку всех бухгалтерских документов и убедиться, что балансы совпадают. При этом учесть особенности расчётов разных подразделений бизнеса друг с другом

  • Техническая ответственность. Важно, чтобы финтех-сервисы всегда были доступны пользователям. Желательно на уровне SLA 99,99%

Возможные предпосылки для смены архитектуры

Часто на этапе создания и раннего развития IT-продуктов приоритет команды — не адаптировать ПО к долгосрочному росту, а создать проект, который можно легко запустить и обслуживать. Поэтому многие проекты изначально строятся на базе одноуровневой архитектуры. Но у неё есть определённые особенности.

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

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

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

Поддерживать баланс вдолгую проблематично — по мере роста платформы скорость разработки снижается. Это происходит из-за накопления архитектурного долга: ошибок в первоначальной структуре, чрезмерно сложной бизнес-логики и обилия временных решений, которые усложняют поддержку и развитие.

В итоге и возникает замкнутый круг. В рамках одноуровневой архитектуры выйти из него трудно, поэтому по мере роста проектов такие реализации стараются не использовать.

Продолжаем погружение в контекст: потенциальные риски построения одноуровневой архитектуры

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

  • Масштабируемость. Позволяет быстро наращивать ресурсы конкретного сервиса, например расчёта транзакций, не увеличивая мощности всей системы

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

  • Простота сопровождения. Обновлять отдельные службы, например верификации клиента, можно изолированно, минимизируя риск ошибок

  • Технологическое разнообразие. Под каждый сервис можно выделить отдельные технологии, например Python для машинного расчёта рисков или Go для быстрых расчётов

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

Со временем многие компании в финтехе (и не только) понимают, что им нужно переходить к сервисной архитектуре.

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

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

От общего к частному: наш кейс рефакторинга архитектуры 

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

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

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

Этапы и задачи рефакторинга архитектуры

Архитектура платформы

Мы готовились к реорганизации архитектуры так:

  • описали зоны ответственности домена

  • исследовали кодовую базу

  • выделили ключевые компоненты

  • проработали связи между компонентами

  • зафиксировали внешние зависимости

Получили схему старой одноуровневой архитектуры, которая наглядно демонстрирует все зависимости и связи между компонентами. После нескольких итераций мы составили первый вариант TO BE схемы, которую продолжили улучшать.

Архитектура приложения

Архитектура компонентов без детализации сужает возможности проработки. Если в документации описаны только основные sequence-диаграммы, это скрывает часть логики. Поэтому мы опустились на уровень архитектуры приложения и проанализировали:

  • сценарии, которые будут выполняться в приложении

  • связи между его компонентами

  • точки роста, которые помогут улучшить приложение и адаптировать его к работе на базе гибридной архитектуры

На основе анализа составили схему, которая поможет сделать выводы, что и как можно вынести в сервис.

Согласование контрактов

Детальная структура всех компонентов платформы позволила нам виртуально протестировать любой пользовательский сценарий. Мы могли детально прорабатывать даже самые сложные процессы — даже те, где значительная часть логики выполнялась в фоновом режиме.

У sequence-диаграмм есть ограничение. Они идеально отображают последовательность шагов, но в них нет информации о структуре передаваемых данных. Это определило нашу следующую ключевую задачу: проработать и формализовать связи между компонентами системы. Мы должны были разобраться с контрактами — соглашениями между различными компонентами или системами, которые касаются форматов взаимодействия, способов передачи данных и поведения при обмене информацией. Контракты было важно детально продумать, поскольку даже в понятной схеме взаимодействия могут быть пробелы, которые навредят результату. 

Представим схему взаимодействия компонентов упрощённо и в лицах. Например, Маша отвечает за приложение 1, а Петя — за приложение 2. Маша может предложить Пете алгоритм взаимодействия, при котором приложение 2 отвечает на запрос от приложения 1. В реальности ответственные могут договориться о контракте, но разойтись с разным пониманием того, какие наборы данных в итоге нужно сформировать. То есть на практике может оказаться, что приложение 1 отправляет не тот запрос, который ожидает приложение 2, а части нужных данных у приложения 1 и вовсе нет.

Чтобы избежать этого, мы решили вместо простого разделения функциональности сначала детально прорабатывать конкретные сценарии использования. 

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

Так постепенно формируется согласованный контракт. Он чётко определяет условия, при которых задача будет успешно выполнена. 

Этот подход изменил наше изначальное видение архитектуры и определение последовательности миграции. Он поможет исключить много потенциальных проблем в будущем.

Миграция

В нашем проекте есть несколько особенностей:

  • любая БД в обязательном порядке шардируется

  • управление шардами находится в коде

При этом возможны различные кейсы. Например, таблицы могут быть размещены по-разному:

  • на всех шардах кластера (shard_0… shard_15)

  • на половине шардов кластера (shard_0… shard_7)

  • на нескольких шардах кластера (shard_13, shard_14, shard_15)

  • на одном шарде кластера (shard_10)

Одновременно с этим могут быть кейсы, когда данные одной шардированной таблицы ссылаются на данные другой. Для таких случаев мы предусмотрели отдельный алгоритм:

  • Используем псевдокод для определения конкретного сегмента (шарда), извлекаем оттуда уникальный идентификатор (ID), делаем необходимые арифметические расчёты и сохраняем полученный внешний ключ в отдельной таблице базы данных 

    # Internal to External
    Insert to 10 shard 
    Return i.id = 128 (internal)
    e.id = i.id * len(shards) + shard = 2058 (external)
  • Когда требуется извлечь этот ключ обратно, мы повторяем аналогичные математические операции в обратном порядке

    # External to Internal
    Get e.id = 2058
    shard = id % len(shards) 
    shard = 2058 % 16 = 10
    i.id = (e.id – shard) / len(shards)
    i.id = (2058 – 10) / 16 = 128
    …
    Get id 128 from 10 shard

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

Например, чтобы не допустить ошибок, мы реализовали миграцию по классической схеме с последовательным переключением источников и целевой БД:

  1. Write/read (старый источник)

  2. Write/read (старый источник) + Write (новый источник)

  3. Write (старый источник) + Write/read (новый источник)

  4. Write/read (новый источник)

Для начала мы развернули миграцию данных и запустили фоновый процесс двусторонней синхронизации (dual-write). Все операции на запись стали дублироваться как в старую, так и в новую базу данных — это обеспечивало их постоянную консистентность.

Когда первоначальная синхронизация и перевод системы в устойчивый режим работы завершились, мы выдержали контрольный период. Эта пауза была необходима, чтобы:

  • убедиться в полном и точном совпадении данных в обоих источниках

  • оценить производительность и стабильность новой системы под нагрузкой

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

Только после этого мы смогли перейти к следующему этапу.

Мы перенесли всю нагрузку по чтению на новую базу данных, в то время как запись по-прежнему велась в оба источника параллельно. Это позволило досконально протестировать производительность и корректность работы новой системы в реальных условиях, но с минимальными рисками для целостности данных.

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

Определение последовательности

Важная задача при рефакторинге архитектуры — определить корректную последовательность отделения компонентов. 

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

Приложение 1 отделять первым нежелательно: если удалить его раньше остальных, возникнут проблемы с доступностью данных, необходимых приложению 2 и приложению 3. А они пока остаются частью исходной системы.

Если начать разделение с приложения 3, появляются подводные камни. Как только вы отделяете последнее звено, архитектура меняется — возможно, изменится и формат данных, которые передаются между оставшимися приложениями. Поскольку новые версии приложений 1 и 2 ещё не готовы и не проверены в рабочей среде, вы получаете некорректно работающее приложение 3. Это превращает процесс в длительный и неопределённый проект — бесконечный эпик.

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

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

PCI DSS

На любой финтех-продукт распространяются требования стандарта информационной безопасности PCI DSS. Он устанавливает требования по защите данных держателей банковских карт при обработке платежей кредитными и дебетовыми картами. Стандарт содержит:

  • 12 высокоуровневых требований (high-level requirements)

  • 51 контрольное требование (requirements)

  • около 300 тестируемых процедур (testing procedures)

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

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

PCI DSS: эксплуатация

Стандартный процесс эксплуатации разработки:

  • разработчик пишет код

  • получает инструменты и инструкции для деплоя продукта в рабочую среду

  • улучшает механизмы мониторинга (Observability)

  • контролирует стабильность своего приложения

Процесс всем известен и не вызывает вопросов.

В моделях с чётким разделением ответственности разработчик фокусируется на качестве продуктового кода, а задачами эксплуатации занимается отдельная команда. Обеспечить это при одноуровневой архитектуре относительно просто. С переходом на сервисную архитектуру нужно отслеживать больше компонентов, но общая схема работы остаётся прежней. Но всё кардинально меняется при работе со стандартами, которые требуют изолированных мощностей, недоступных для стороннего влияния — такими, как PCI DSS. Здесь нельзя использовать общие CI/CD-процессы, так как они не соответствуют требуемым ограничениям. В результате на команду разработки ложится большая часть операционных задач, где нужно обеспечивать соответствие стандарту. Это значительно расширяет зону ответственности команды.

PCI DSS: доступность

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

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

  • максимально изолировать (сегментировать) все рабочие окружения

  • настраивать RBAC для строгого ограничения доступа

  • перестраивать процессы, чтобы реализовать процесс раскатки, отвечающий требованиям

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

PCI DSS: артефакты

В большинстве проектов практически никто извне не контролирует, как работает приложение и как быстро устраняются проблемы. Типичный пример риска — подключение в Golang-приложение новой библиотеки с уязвимостью. При этом многие компании либо вообще не внедряют практику регулярного сканирования кода, либо откладывают выявленные проблемы на потом и забывают о них. У нас ситуация иная — мы должны:

  • проводить регулярное сканирование базовых образов и кодовой базы

  • заводить задачи в случае обнаружения уязвимостей

  • вносить задачи в реестр для аудитора

  • исправлять любые выявленные уязвимости строго в установленный SLA срок

PCI DSS: сегментация

Команда финтеха работает в рамках одноуровневой архитектуры. И несмотря на использование отдельных серверов, оборудования и баз данных, некоторые процессы невозможно перенести без изменений.

Чтобы полноценно мигрировать, предстоит серьёзная работа:

  • Переработать инфраструктуру хранения артефактов — планируем переход с физических серверов (Bare Metal) на внутреннее облако

  • Модифицировать структуру баз данных из-за смены технологического стека

  • Изолировать аппаратное обеспечение с ограниченными доступами

  • Развернуть множество специализированных решений для кибербезопасности

  • Создать изолированную фронтенд-часть и разработать специфичный код, интегрированный в CI/CD-процессы с обязательной процедурой подписания изменений

  • Полностью покрыть компоненты средствами мониторинга и анализа производительности (observability), чтобы своевременно выявлять проблемы и оперативно реагировать на них

  • Создать сложную систему защиты от несанкционированного доступа и нарушений безопасности

Эти меры необходимы, чтобы соответствовать жёстким стандартам информационной безопасности и поддерживать надёжную работу сервиса.

Таким образом, нам предстоит создать абсолютно новый отдельный контур.

При этом если раньше в нашей зоне ответственности было всего 37 компонентов, то после частичного рефакторинга архитектуры их стало 100.

Рекомендации на основе нашего опыта

Мы уже проделали большую работу, чтобы обеспечить баланс, в том числе:

  • скорректировали продуктовые планы

  • изменили политику распределения времени на технические задачи

  • нашли эффективные методы взаимодействия с архитектурой. Критическое делаем в исходной архитектуре, всё новое — сразу за её пределами

На основе этого опыта мы сформулировали несколько рекомендаций, которые будут полезны как в финтехе, так и в других сферах.

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

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

  • Тесно взаимодействовать с командой информационной безопасности. При любых манипуляциях с архитектурой важно, чтобы никакие действия не повлияли на безопасность и не привели к инцидентам. Поэтому лучшая практика — строить защищённую архитектуру по принципу Secure by Design

  • Тщательно документировать любую работу. Артефакты от проделанных работ в любом случае будут полезны: упростят поддержку, онбординг и разбор инцидентов. А в финтех-проектах документация и вовсе обязательна, чтобы соблюдать требования регуляторов

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

Комментарии (0)