Files
trade-doc/data-ingest-strategy.md

345 lines
7.8 KiB
Markdown

# Data ingest strategy
## Cel
Zachowac niski latency dla logiki tradingowej i read-side, ale rownoczesnie miec trwala persystencje danych do UI, replayu, debugowania i analityki.
Nie robimy modelu `DB-first` jako jedynej sciezki przetwarzania.
## Rekomendowany model
Stosujemy `dual-path`:
1. `hot path`
`Yellowstone gRPC / Agave RPC -> consumer -> Redis / internal state -> strategy / read-side`
2. `durable path`
`ten sam consumer -> Postgres -> UI / Hasura / replay / debug`
Opcjonalnie pozniej:
3. `history / analytics path`
`consumer lub batch exporter -> ClickHouse`
## Dlaczego nie DB-first
Model:
`gRPC / RPC -> DB -> dopiero potem przetwarzanie`
nie jest dobry jako glowna sciezka, bo:
- dodaje latency do kazdego eventu
- uzaleznia ingest od kondycji bazy
- pogarsza odpornosc calego pipeline
- zwieksza write amplification przy duzym wolumenie zdarzen
- utrudnia low-latency strategie i read-side
DB ma byc miejscem persystencji i read modelu, nie bramka przed logika runtime.
## Rola Yellowstone gRPC
Yellowstone ma byc glownym zrodlem `live feed`:
- account updates
- tx updates
- slot updates
- program updates
Sciezka:
`Yellowstone -> consumer -> hot state`
oraz rownolegle:
`Yellowstone -> normalization / persistence -> Postgres`
Yellowstone nie powinien byc uzalezniony od tego, czy Postgres aktualnie pisze szybko.
## Rola Agave RPC
RPC nie jest naturalnym feedem eventowym.
RPC powinno sluzyc glownie do:
- point reads
- bootstrapu klienta
- recovery
- getAccountInfo / getMultipleAccounts
- getHealth / getSlot
- write path dla tx, jesli nie idziemy przez TPU/Jito
RPC nie powinno byc traktowane jak glowny event stream do zrzucania wszystkiego do DB.
## Rola Redis
Redis powinien byc `hot state layer`:
- szybki stan DLOB
- cache
- fanout wewnetrzny
- ewentualnie stream / queue dla workerow
To jest warstwa pod:
- strategy engine
- orderbook read-side
- szybkie projekcje
Nie trzymamy tam historii jako source of truth.
## Rola Postgres
Postgres powinien byc `durable read model`:
- dane dla UI
- dane dla Hasury
- znormalizowane ticki
- pochodne tabele DLOB
- stan aplikacji
- konfiguracja
- historia operacyjna
Postgres jest dobry do:
- frontend
- dashboard
- replay lekkiego zakresu
- audyt aplikacyjny
## Kiedy ClickHouse
ClickHouse dodajemy dopiero, gdy:
- wolumen raw feedu bedzie duzy
- potrzebna bedzie ciezsza analityka historyczna
- bedziemy chcieli trzymac dluzsza historie eventow i tx
Na obecnym etapie nie jest konieczny do uruchomienia runtime.
## Docelowy przeplyw
```text
mevnode_sol
Agave RPC
Yellowstone gRPC
|
v
mevnode_bot
consumer
|\
| +--> Redis -> DLOB/read-side -> strategy/workers
|
+----> Postgres -> Hasura/API -> frontend
```
## Zasada architektoniczna
- `gRPC / RPC` dostarcza dane do runtime
- `Redis` utrzymuje szybki stan operacyjny
- `Postgres` utrzymuje trwaly stan i read model
- `frontend` i `Hasura` czytaja z Postgresa
- `strategy` i `workers` nie czekaja na zapis do DB, aby przetwarzac dane
## Rekomendacja dla projektu
Na teraz:
1. `Yellowstone -> consumer -> Redis`
2. `consumer -> Postgres`
3. `Hasura / API / UI <- Postgres`
4. `strategy / read-side <- Redis`
5. `RPC` zostawic do bootstrapu, point reads i write path
To daje najlepszy kompromis:
- niski latency
- odpornosc runtime
- sensowna persystencja
- dobra baza pod UI i replay
## Ocena obecnego ukladu na k3s
Obecny runtime na `mevnode_bot / trade-staging` jest tylko czesciowo zgodny z docelowym modelem.
### Co jest zgodne
- `postgres` jest postawiony jako `StatefulSet`, wiec nadaje sie na trwały storage
- `dlob-redis` jest osobna lekka usluga i pasuje do roli cache / hot state
- `hasura` i `trade-api` siedza nad `Postgresem`, co jest poprawne dla durable read modelu
- `frontend` korzysta z `API/Hasury`, a nie bezposrednio z `Redis`
### Co nie jest zgodne
- `dlob-publisher` i `dlob-server` nie sa zdrowe, wiec hot path nie jest operacyjnie domkniety
- `dlob-publisher` jest podpiety do `gRPC`, ale nie jest glownym i pewnym writerem do `Postgresa`
- `trade-ingestor` nie ingestuje danych bezposrednio z chain source, tylko polluje dane pochodne z `Hasury`
- persistent path jest wiec dzisiaj czesciowo odwrocony:
- `derived view -> ingest -> Postgres`
- zamiast:
- `chain source -> normalize -> Postgres`
## Plan zmian
### Cel koncowy
Chcemy dojsc do ukladu:
```text
Yellowstone gRPC / Agave RPC
|
v
consumer
|\
| +--> Redis -> hot state / workers / strategy
|
+----> Postgres -> Hasura / API / frontend / replay
```
### Etap 1. Naprawa hot path
Najpierw trzeba przywrocic dzialanie:
- `dlob-publisher`
- `dlob-server`
Bez zdrowego publishera nie ma sensu budowac dalszej warstwy `Redis = hot state`.
Zakres:
- ustabilizowac `Agave RPC` i `Yellowstone`
- doprowadzic `dlob-publisher` do `Ready`
- potwierdzic, ze `Redis` dostaje aktualny stan
- doprowadzic `dlob-server` do `Ready`
### Etap 2. Uczynic publisher glownym consumerem danych chain
Obecnie `trade-ingestor` korzysta z danych pochodnych z `Hasury`.
To trzeba zmienic tak, aby glownym miejscem wejscia danych byl komponent konsumujacy:
- `Yellowstone gRPC`
- `Agave RPC`
Rekomendacja:
- rozszerzyc `dlob-publisher`
- albo wydzielic nowy `market-consumer`
ale w obu przypadkach komponent ma robic:
- odczyt live z `Yellowstone`
- wymagane point reads z `RPC`
- normalizacje danych
- jednoczesny zapis do `Redis` i `Postgresa`
### Etap 3. Ustalic twardy podzial rol Redis / Postgres
#### Redis
Do `Redis` trafia tylko to, co potrzebne jako szybki stan runtime:
- latest DLOB state
- latest L2
- quick cache dla workerow
- fanout do read-side i strategii
- dane z TTL lub dane odtwarzalne
`Redis` nie jest source of truth.
#### Postgres
Do `Postgresa` trafia trwaly model danych:
- znormalizowane ticki
- znormalizowane snapshoty rynku
- pochodne tabele pod `Hasure`
- historia operacyjna
- stan aplikacji
`Postgres` jest source of truth dla aplikacji i UI.
### Etap 4. Przebudowac worker path
Docelowy model dla workerow:
- `Redis in`
- `Postgres out`
To znaczy:
- workery czytaja szybki stan z `Redis`
- licza projekcje
- zapisują wynik do `Postgresa`
Dotyczy to szczegolnie:
- `dlob-worker`
- `dlob-depth-worker`
- `dlob-slippage-worker`
### Etap 5. Wygasic obecny odwrocony ingest
Obecny `trade-ingestor` zapisuje dane do `Postgresa` na podstawie danych juz pochodnych z `Hasury`.
To nie powinno byc glowna sciezka ingestu.
Mozliwe role po zmianie:
- backfill
- sanity check
- fallback
- testowy importer
Ale nie:
- glowny chain ingest
### Etap 6. Utrwalic read model pod UI
Po zmianach:
- `Hasura` czyta tylko z `Postgresa`
- `trade-api` czyta tylko z `Postgresa`
- `frontend` czyta przez `API/Hasure`
Frontend nie powinien zalezec od `Redis`.
### Etap 7. Zostawic RPC w waskiej roli
`Agave RPC` zostawiamy do:
- bootstrapu
- point reads
- recovery
- write path dla tx
Nie robimy z `RPC` glownej magistrali trwałego ingestu.
## Plan wdrozenia krok po kroku
1. Naprawic `dlob-publisher` i `dlob-server`.
2. Potwierdzic, ze `Redis` jest zasilany poprawnym hot state.
3. Rozszerzyc publisher albo dodac nowy consumer tak, aby pisal rownolegle do:
- `Redis`
- `Postgresa`
4. Zdefiniowac schemat tabel normalized data w `Postgresie`.
5. Przepiac workery na model:
- read z `Redis`
- write do `Postgresa`
6. Zostawic `Hasure` i `trade-api` jako warstwe odczytowa nad `Postgresem`.
7. Zdegradowac `trade-ingestor` z roli glownego ingestu do roli pomocniczej albo go usunac z glownej sciezki.
## Rekomendacja praktyczna
Najmniejsza sensowna zmiana to nie budowac od razu nowego systemu od zera, tylko:
1. naprawic obecny `dlob-publisher`
2. dodac mu zapis do `Postgresa`
3. stopniowo wyprowadzac `trade-ingestor` z glownego path
To jest najmniej ryzykowna droga do modelu:
- `Redis = cache / hot state`
- `Postgres = persistent normalized store`