Последние лет 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ы полезны только точечно, на отдельные функции с ветвлением или математической логикой.
и — бизнесу — реализация первой версии и получение стабильно работающей системы, это два разных продукта. При этом на отладку и стабилизацию может уйти даже больше ресурсов чем на имплементацию первой версии:‑)