Последние лет 5 работаю над сложными высоконагруженными системами, и хотел бы поделиться нюансами перехода из разработки голосовых роботов в финтех.

Первые два голосовых проекта в разных компаниях мы реализовывали на связках .Net + Asterisk с преобразованием TCP/GRPC трафика. Более интересен именно второй проект в этой области, где в полной мере использовалась микросервисная архитектура (тогда как на первом, в рамках стартапа, несмотря на задел под микросервисы с тз организации кода, у нас сильно проседал деплой). 

Описание второй системы: голосовой робот, совершает/получает звонки с использованием core-функциональности в виде open-source проекта Asterisk. С точки зрения бизнеса, это выстроенный диалог с абонентом (умеем понимать что абонент говорит, подбирать ответ, отправлять ответ в озвученном виде обратно в канал, сохранять информацию о звонке для отчетов). С тз технической - это управление состоянием звонка и голосом (те связка Asterisk REST-API и веб-сокетов дает двустороннюю событийную модель для управления и получения обратной связи по стейту звонка, а основной ряд сервисов занимается расшифровкой голоса, получением текстового ответа, синтезацией текстового ответа); по завершению звонка данные сохраняются в БД. Система получается не «базоцентричной», так как основной упор идет на работу с сетью и трафиком (те голос в виде TCP/GRPC пакетов проходит с десяток наших сервисов), поддержкой высокого RPC (для одновременной обработки большого кол-ва звонков).

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

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

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

По стеку: микросервисы, RabbitMQ как ключевое связующее звено между процессами (подами), в том числе и для внешних к системе взаимодействий, в качестве БД - Postgres Sql. Не скажу что проект получился сильно большим (порядка 10 разных подов приложения, с масштабируемостью 1-2 в корневой части функционала).

Опыт отладки и эксплуатации системы в целевом варианте неизбежно сопровождается ошибками, а в нашем случае ошибки приводят либо к потере данных, либо их не консистентному состоянию.

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

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

  • используйте «сверки» в интеграциях с другими командами, умную статистику. Надо всегда знать сколько и каких записей вы получили из внешнего источника (забрали сами или передали вам), несоответствие — повод для разбора;

  • все кейсы кажущиеся не то что маловероятными, а невозможными, всегда выстрелят. Нагрузка и реальные данные подсветят любые микротрещины, которые вы будете искать часами с лупой. Где то недостаточно вдумчиво отнеслись к уровню транзакции? — потеряете обновление. Положились на целостность данных из других систем? — найдете в базе «тыкву» (отрицательные id шники, значения которых не существуют в справочниках).

  • как раз адаптивность к нагрузке и оказалась нашим «крайнем рубежом обороны», что мы, обнаружив проблемы в данных, могли перезапускать обсчеты которые восстанавливали их потери (главным образом при исправлении ошибок). Поэтому кейс: не понятно как так вышло, но давайте добавим еще полей для разбора проблемы и все перезапустим — стал вполне рабочим. Те можно и улучшить целевое решение (исправить дефекты), и восстановить потери в обсчетах;

  • не используйте ORM для серьезных проектов.
    EF под.Net Core, кто придумал совместить отслеживание сущности с ее кэшированием? не хочешь кэширования (так как нужны актуальные данные) — надо писать AsNoTracking() — и как тогда сохранять (на этот механизм завязан и стейт)? А самое главное, больно исправлять, если система уже сделана и это кэширование выплывает «боком». Приходится переходить на ручные запросы с полным контролем процесса. ОРМ полезен только для написания первой версии, в зрелых сложных проектах он скорее вредит.

  • не жалейте диагностических полей на этапе проектирования. Это временные метки (CreatedOn, MoidifiedOn), истории обновлений (таблицы исторических данных), идентификаторы систем которые изменяют данные. Лучше запилить ненужный столбец и потом удалить, чем не иметь инструмента для ручного разбора состояния на базе. Задним числом добавить уже не получится, чтобы достоверность не вызывала сомнений;

  • QA. Как оказалось, нужны 3 (а то и 4) комплекта тестов: функциональные, пайплайнов, уязвимостей, нагрузки. Да, первая версия выходит с упором только на функционал и пайплайны (интеграцию подов в инфраструктуре), но уязвимости могут похоронить проект. (Недоработка уровней транзакций, потерянные обновления, нежелательное кэширование и так далее).

  • В качестве тестов разработки, мы использовали интеграционные тесты с Docker‑контейнерами с XUnit фреймворком. Такие тесты скорее ближе к black‑box, где сервис запускается в полной интеграции со всеми зависимыми контейнерами. Решение несколько спорное, так как такие тесты сложные в написании, долгие (аффектят время сборки), к тому же проверить по факту можно только «картину» после запуска. С другой стороны, в микросервисах больше шансов ошибиться в каких нить настройках/обвязках/конфигурации/DI или запросе в БД, поэтому Unitы полезны только точечно, на отдельные функции с ветвлением или математической логикой.

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

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