₿
Po godzinach Platforma tradingowa real-time
Platforma, na której gracze obstawiają, w którym przedziale cenowym znajdzie się BTC na końcu krótkich, powtarzających się rund. Kursy są parimutuel, rundy rozliczają się automatycznie, a wszystko działa na żywo. Projekt, na którym samodzielnie nauczyłem się systemów rozproszonych i inżynierii wydajności pod realnym obciążeniem.
W czym to robiłem
.NET 10.NET AspirePostgreSQLRedisRabbitMQNext.jsWebSocketsk6Grafana / OpenTelemetryDockerEFCore
01
Co zrobiłem
- Zbudowałem platformę zakładów na .NET Aspire — osobne mikroserwisy silnika gry, kont i streamingu cen.
- Streamuję symulowane ceny BTC i żywe kursy parimutuel do frontu po WebSocketach (MessagePack).
- Zaimplementowałem atomowe operacje na saldzie (zwrot przy błędzie, idempotentne wypłaty) i automatyczne rozliczanie rund.
- Ponad dwukrotnie podniosłem przepustowość (~1,8 tys. → 4 tys.+ zakładów/s) przy ułamku latencji — test obciążeniowy po teście.
02
Czego się nauczyłem
- Inżynierii wydajności jako metody — obciąż → zmierz → znajdź wąskie gardło → napraw → powtórz.
- Czytania trace'ów rozproszonych (Tempo) i metryk, żeby lokalizować realny problem zamiast zgadywać.
- Modelowania danych w Redisie pod gorącą ścieżkę — atomowe skrypty Lua, brak read-modify-write, świadomość hot-keyów.
- Wnętrzności .NET pod obciążeniem (thread pool, GC, async, multipleksowanie połączeń) — i że setup pomiaru jest tak samo ważny jak sam system.
03
Trudności
- Ogon latencji, który okazał się generatorem obciążenia kradnącym CPU aplikacji na tym samym serwerze — pułapka metodologiczna, która kosztowała sporo czasu.
- Pojedyncze połączenie Redis i rozliczanie rundy (odczyt ~14,5 tys. zakładów naraz) blokujące gorącą ścieżkę zakładów.
- Utrzymanie poprawności operacji na pieniądzach — brak podwójnego obciążenia, idempotentne wypłaty — przy dużej współbieżności i częściowych awariach.
- Observability, które samo wywoływało incydenty — OOM kolektora telemetrii zamroził cały host; Prometheus po cichu przestał przyjmować metryki.
04
Kluczowe optymalizacje
- Wyniosłem zakłady z jednego rosnącego bloba JSON do osobnych struktur Redis z atomowym HSET/HINCRBYFLOAT — postawienie zakładu zeszło z read-modify-write pod CAS-em do O(1) (bet-engine 395% → 40% CPU, 6 GB → 370 MB RAM).
- Uczyniłem salda autorytatywnymi w Redisie (atomowy debit w Lua + zapis write-behind do Postgresa) — synchroniczny SQL UPDATE per zakład zszedł z gorącej ścieżki.
- Zastąpiłem broadcast per-zakład periodycznym projektorem (snapshoty co 100 ms), dzięki czemu zapis zakładu pozbył się odczytu zwrotnego — gorąca ścieżka to pojedynczy round-trip do Redisa.
- Przepisałem zapis zakładu na jeden atomowy skrypt Lua zamiast WATCH/MULTI/EXEC, usuwając kontencję optymistycznej blokady pod obciążeniem.
- Zcache'owałem walidację kolumny w pamięci — postawienie zakładu nie wymaga żadnych odczytów z Redisa do walidacji.
- Przełączyłem rozliczanie rundy na stronicowany HSCAN zamiast jednego HGETALL ~14,5 tys. wierszy, który blokował gorącą ścieżkę zakładów.
- Dodałem admission control — limiter współbieżności oddający łagodne 429 — żeby przeciążenie degradowało się czysto, zamiast zapadać.
- Dostroiłem Postgresa (celowane indeksy, synchronous_commit=off, batchowane wypłaty per użytkownik) i wstępnie powiększyłem thread pool .NET, by spłaszczyć skoki latencji przy burstach.