Как отслеживать изменение стоимости активов в портфеле - NestJS, Temporal
По выходным я стараюсь не тратить время впустую. Пробую различные активности вне дома или работаю над своими pet-проектами. В данный момент я разрабатываю FuncFund — инструмент для отслеживания средств в широком портфеле в реальном времени с использованием ИИ-аналитики. Появилась сложная задача: нужно отображать разницу за разные периоды (1 день, 1 месяц, 1 час и другие).
Проблематика
Чтобы отображать разницу за период, нам нужно несколько значений: текущая дата, смещение времени и стоимость активов в прошлом. Проблему вызывает только последнее.
Есть два глобальных подхода к хранению исторических данных:
- Сохранять все активы в первичном виде и применять к ним исторические данные из внешних API (московская биржа, Binance)
- Регулярно сохранять исторические значения сумм по активам для разных курсов
Выделим требования и оценим что больше подходит:
- Пользователь в любое время может менять активы (оба варианта выполняют)
- Валюта также изменяется со временем (оба варианта выполняют)
- Сравнение должно быть максимально точным (второй вариант - дискретный, редкие сохранения создадут неточности)
- Сервис сравнения должен быть доступен почти всегда (первый вариант зависит от множества API, не все обладают историческими данными)
Взвесив все критерии, я решил остановиться на первом варианте, так как доступность важнее точности. Кроме того, точность можно регулировать, изменяя частоту сохранения данных.
Расчеты
Вводные: 1 миллион регистраций, 10 валют, 8 групп активов у каждого пользователя, точность сравнения p95, одна историческая запись - 42 байта, сохранение данных одного пользователя - 60ms. Доступные пользователю интервалы от 1 часа до одного года.
Сколько места будут занимать данные за максимальный период - 1 год? Как часто нужно сохранять данные, где золотая середина? Сколько Pod`ов должно сохранять данные?
- Вес всех записей одного пользователя в моменте: 10 валют ⋅ 8 групп ⋅ 42 байта = 3`360 байт
- Минимальный интервал сохранения по теореме Котельникова: 1 час / 2 = 30 минут
- Сколько времени занимает сохранения всех данных пользователей? 60ms ⋅ 1`000`000 = 16.6 часов
- Получается, один Pod не успеет сохранить столько данных, нужно больше: 16.6 часов / 30 минут ⋅ 1.25 фактор масштабирования= 42 пода
- Посчитаем, объем всех записей за год: 1 год ⋅ 365 дней ⋅ 24 часа ⋅ 60 минут / 30 минут ⋅ 3`360 байт ⋅ 1`000`000 пользователей = 54`825 гигабайт = 55 терабайт максимум
- В принципе, частоту можно и увеличить, но в данном варианте, Selectel на 8 февраля 2025 будет съедать 409`634 рублей в месяц.
Отказоустойчивость
Можно заметить, записи на диск намного больше, чем чтений, из-за этого сразу хочется применить паттерн CQRS (Command and Query Responsibility Segregation). Например, сохранять данные будут 42 пода, а читать 10, это намного выгоднее.
Решено использовать Temporal.io — это платформа для создания устойчивых и масштабируемых процессов в распределённых системах на разных платформах Java, NodeJs и прочих. Она позволяет создавать надёжные и согласованные оркестрации, которые продолжают работать даже в случае сбоев или ошибок. Удобный UI позволяет отслеживать ошибки, время выполнения, это очень важно в данном проекте.
Аналитические данные было решено хранить в ClickHouse. База данных обладает большим русским сообществом, хорошо себя показывала в прошлых проектах, берет AP из CAP теоремы.
Заключение
Получилось создать новую feature! Теперь пользователи моего pet-проекта могут получать временную аналитику, а я научился работать с Temporal.