GWINT jako gra online nastawiona na obsługę milionów graczy wymaga wyjątkowo skalowalnej architektury. Opowiemy między innymi o tym:
– jak API zbudowane na Symfony w modelu mikro serwisów wykorzystuje asynchroniczną komunikację pomiędzy usługami i klientem (system notyfikacji),
– jak optymalizujemy procesowanie zdarzeń wymagających współpracy kilku usług (kolejki zadań), jak monitorujemy i testujemy integrację poszczególnych webservice’ów. Przewiną się także technologie takie jak: PHP7, HHVM, RabbitMq, Redis, Go i inne.
15. Trwałe powiadomienia
A JEŚLI NIE DOSTARCZYMY POWIADOMIENIA OD RAZU?
• Http API dla niedostarczonych powiadomień
• Stanowią mechanizm zastępczy dla usługi socket’owej
• Kopia powiadomień w trwałym miejscu
18. System kolejkowy
KLUCZOWE ASPEKTY
• Potwierdzanie produkcji / konsumpcji wiadomości
• Wystarczająca wydajność
• Trwałość wiadomości
• Skalowalność
19. Rozszerzenie AMQP
/php-amqplib/php-amqplib /pdezwart/php-amqp
• Biblioteka implementująca
protokół AMQP
• Interfejs PHP dla
rozszerzenia librabbitmq
Napotkane problemy:
• Brak pełnego potwierdzania
wiadomości
• Słaba kontrola w przypadku
problemów z RabbitMq
Napotkane problemy:
• Brak pełnego potwierdzania
wiadomości
• Słaba kontrola w przypadku
problemów z RabbitMq
20. Potwierdzanie / wydajność
TEST NA KLASTRZE 3 NODE’ÓW
BEZ POTWIERDZANIA
KLIENT W LOKALNEJ SIECI
11000 / s
Z POTWIERDZANIEM
750 / s
// w trakcie badania
możliwych optymalizacji
26. Federation
OFICJALNE ROZSZERZENIE RABBITMQ
• Umożliwia przenoszenie wiadomości pomiędzy usługami (klastry, vhosty)
• Wymaga tej samej nazwy exchange zdalnego i lokalnego
• Mapowanie exchange’y poprzez wyrażenie regularne
• Konfiguracja poprzez panel RabbitMq lub API
• Automatyczne odbudowywanie powiązań w przypadku awarii
29. Case study: system powiadomień #1
• Kolejki tworzone po ustanowieniu połączenia z użytkownikiem
RABBITMQ
• Kolejka per użytkownik [~500k ]
• Kolejki usuwane po zerwaniu połączenia z użytkownikiem
PROBLEM?
• Restart Node.js oznacza ~500k kasowanych/tworzonych kolejek
• Za duże obciążenie Node.js w trakcie inicjalizacji
• Przy więcej niż jednym node w klastrze restart potrafi trwać nawet 30min!
31. Case study: system powiadomień #2
• Kolejka per użytkownik [~500k]
REDIS
• Wydajność 10k połączeń / s po restarcie klastra
• Publikacja do wszystkich podłączonych klientów
• Utrzymuje bez problemów kilkaset tysięcy połączeń
• Mechanizm Publish / Subscribe
• Konsumuje zdecydowanie mniej zasobów niż RabbitMq
33. Case study: system powiadomień #2
• Na potrzeby kompresji bufor ok. 4K per połączenie (kilkaset tysięcy)
PROBLEM #2 Z NODE.JS
• Ogromne zużycie pamięci sięgające 20GB
• Wystarczyło wyłączyć kompresję (wiadomości i tak są małe)
• Wycieki pamięci widoczne po dniach/tygodniach
• Nadal duże obciążenie w przypadku restartu Redis
PROBLEM #1 Z NODE.JS
• Trudność debugowania aplikacji w środowisku produkcyjnym
35. Case study: system powiadomień #3
• Brak nadmiarowego obciążenia po restarcie Redis
• Brak problemów z pamięcią i procesorem
• Okazał się przystępniejszy w rozwijaniu niż Node.js
GOLANG
• Prostsze debugowanie na produkcji (np. podgląd goroutines)
37. Problem wersjonowania
• Możliwe, że wiadomość została rozpoczęta po stronie serwerowej –
wtedy brak informacji o wersji
• Propagacja informacji o wersji razem z wiadomością
• Możemy skorzystać z wersji ostatnio używanej przez użytkownika
ROZWIĄZANIE #2
ROZWIĄZANIE #1
41. Testy – integracyjne
KILKA ŚRODOWISK TESTOWYCH
TESTY API POPRZEZ KLIENTA HTTP
KLIENT POWIADOMIEŃ
Odizolowane, umożliwiające weryfikację
poprawnej integracji systemów
Niezależny klient odpytujący non-stop
wszystkie usługi i według scenariuszy
Niezależny klient powiązany z klientem
HTTP weryfikujący poprawność
otrzymywanych powiadomień
43. Testy – symulacyjne
• Analogiczny klient, ale pozbawiony UI
• Nie można uruchomić ich zbyt wiele z racji na wymagane zasoby
• Klient gry wyposażony w UI realizuje określone scenariusze
raportując błędne odpowiedzi webservices
• Można uruchomić ich dużo (bardzo) generując więcej
losowych, trudnych do przewidzenia sytuacji
BOTY – BEZ UI
BOTY – Z UI
44. Monitoring operacji asynchronicznych
CO MOŻEMY SPRAWDZAĆ
• Wyniki działania botów na produkcji
• Wypełnienie kolejek wiadomości
• Czas ostatniej przetworzonej wiadomości (per consumer)
• Metryki serwerowe (load, pamięć, zużycie dysk, IOPS)
• Metryki biznesowe np. Ilość przyznanych nagród w ciągu ostatniej doby
Jako GOG.com jesteśmy firmą działającą w przemyśle growym
Ogólny model komunikacji synchronicznej HTTP
Przykład dobrego zastosowania.
Gracz wchodzi w Multiplayer
Potrzebujemy trochę informacji o nim, więc pytamy zdalną usługę
Jakie mogą być problemy z usługą ”2”?
1) Może być niewydajne samo częste odpytywanieMożemy trzymać lokalny cache (z soft + hard ttl)
2) Może w ogóle nie istnieć powiązany użytkownik
Naturalnie to błąd ze strony gry
3) Usługa może ”leżeć”.
Jeśli leży to nie wiemy nawet czy odpowiedni użytkownik istnieje, sprawdzamy lokalny cache – jeśli jest to spoko, jak nie to:
Zwracamy błąd (pewnie z puli 5xx)
Przyjmujemy jakieś uśrednione parametry matchowania
Przykład dobrego zastosowania.
Gracz wchodzi w Multiplayer
Potrzebujemy trochę informacji o nim, więc pytamy zdalną usługę
Jakie mogą być problemy z usługą ”2”?
1) Może być niewydajne samo częste odpytywanieMożemy trzymać lokalny cache (z soft + hard ttl)
2) Może w ogóle nie istnieć powiązany użytkownik
Naturalnie to błąd ze strony gry
3) Usługa może ”leżeć”.
Jeśli leży to nie wiemy nawet czy odpowiedni użytkownik istnieje, sprawdzamy lokalny cache – jeśli jest to spoko, jak nie to:
Zwracamy błąd (pewnie z puli 5xx)
Przyjmujemy jakieś uśrednione parametry matchowania
Przykład złego zastosowania.
Server gry chce zgłosić koniec rozgrywki do pewnej usługi.
Przetwarzamy żądanie, a potem chcemy spowodować:1) aktualizację profilu użytkownika pod kątem zmiany doświadczenia, levelu itd.
2) Zdecydować o ewentualnych przynależnych nagrodach i dostarczyć je
Jakie mogą być problemy?
Jeśli dowolna z usług 2 i 3 nie działa to nie możemy poprawnie zakończyć gry
Możliwe, że niesprawność usługi 2 powoduje złe decyzje ze strony usługi 3
Request jest długi, bo zawiera dużo logiki, co zajmuje procesy PHP i tym samym blokuje kolejne zapytania + może powodować timeouty
Rezultaty zapytań są potrzebne do pokazania kolejnego ekranu gry.
Przykład złego zastosowania.
Server gry chce zgłosić koniec rozgrywki do pewnej usługi.
Przetwarzamy żądanie, a potem chcemy spowodować:1) aktualizację profilu użytkownika pod kątem zmiany doświadczenia, levelu itd.
2) Zdecydować o ewentualnych przynależnych nagrodach i dostarczyć je
Jakie mogą być problemy?
Jeśli dowolna z usług 2 i 3 nie działa to nie możemy poprawnie zakończyć gry
Możliwe, że niesprawność usługi 2 powoduje złe decyzje ze strony usługi 3
Request jest długi, bo zawiera dużo logiki, co zajmuje procesy PHP i tym samym blokuje kolejne zapytania + może powodować timeouty
Rezultaty zapytań są potrzebne do pokazania kolejnego ekranu gry.
Ogólny model komunikacji asynchronicznej
Ogólny model komunikacji asynchronicznej
Ogólny model komunikacji asynchronicznej
Promisy itd..
Promisy itd..
Ogólny model komunikacji asynchronicznej
Głównie Symfony + PHP7 (nie HHVM),
Korzystaliśmy z HHVM i było szybko ale:
warmup aplikacji był dużym problemem – był w trakcie deploy, ale nie rozgrzewał wszystkiego, dużo timeout’ów (powyżej 5sek)
Problem ze wsparciem dla dodatkowych kompilowanych bibliotek
Głównie Symfony + PHP7 (nie HHVM),
Korzystaliśmy z HHVM i było szybko ale:
warmup aplikacji był dużym problemem – był w trakcie deploy, ale nie rozgrzewał wszystkiego, dużo timeout’ów (powyżej 5sek)
Problem ze wsparciem dla dodatkowych kompilowanych bibliotek
Trwałość – zapis na dysku, na zreplikowane na node’ach klastra
Potwierdzanie
Skalowalność – wiele node’ów w klastrze
Wystarczająca wydajność – wyniki testów async? (Kafka być może szybsza, ale mniej niezawodna)
With use_socket set to false producing messages (to rabbit that does not respond) will cause destructor to hang infinitely.
With use_socket set to true
consumers will not be blocking (they will crash after 1s of being idle)
producers will work and use timeout, but they will simply ignore all errors (there's no way to tell that the message wasn't delivered)
This bundle doesn't use confirm channels nor transactions, so basically rabbit does not guarantee that the message was delivered anyway.
php-amqplib is poorly written and poorly maintained. It's incoherent, there's no way to tell which behavior was intended, and it often introduces regressions. I think original authors did not understand how sockets work.
We could probably fix those two problems (timeouts and lack of confirmation), but I still think it's unwise to use php-amqplib, plus I wouldn't bother with pull request since it's better to not trust their commits.
PHP extension ( http://php.net/manual/pl/book.amqp.php ) has outdated documentation and isn't available for hhvm.
TBH I think we should look into "Pattern: reliable queue" here: http://redis.io/commands/rpoplpush, or write our own amqp lib and fork rabbitmq bundle.
Możiiwy także crash w trakcie przetwarzania
Co w sytuacji gdy nie działa Redis? (jest tylko dodatkową warstwą zabezpieczenia przed błędem RabbitMq, więc jak nie działa to ok)
Motywacja dla takiego a nie innego flow
Limit ponowień
Dynamiczny delay -> 5, 100, 600, 3600 sek.
Deadletter bez consumera
Deadletter dla malformed messages
Delay – natywny mechanizm RabbitMq poprzez TTL
Odseparowanie usług
Zachowana logika zarządzania – klient pyta o login, hasło i nazwę exchange i potem sam sobie ogarnia
Czemu nie Shovel? Bo jego funkcje zawierają się Federation, Shovel nie ma API
Ogólny model komunikacji asynchronicznej
Ogólny model komunikacji asynchronicznej
Ogólny model komunikacji asynchronicznej
Ogólny model komunikacji asynchronicznej
Ogólny model komunikacji asynchronicznej
Ogólny model komunikacji asynchronicznej
Ogólny model komunikacji asynchronicznej
Ogólny model komunikacji asynchronicznej
Ogólny model komunikacji asynchronicznej
Ogólny model komunikacji asynchronicznej
Ogólny model komunikacji asynchronicznej
Ogólny model komunikacji asynchronicznej
Klient tworząc access token wysyła swoje session id i ono pozostaje aktywnym session id.
Jeśli ktoś odbierze powiadomienie o session ID niezgodnym to developer (gra) decyduje co zrobić – w tym przypadku wylogować.
Ogólny model komunikacji asynchronicznej
Ogólny model komunikacji asynchronicznej
Dlaczego nie, dlaczego nadal coś może pójść nie tak
Ogólny model komunikacji asynchronicznej
Ogólny model komunikacji asynchronicznej
Ogólny model komunikacji asynchronicznej
Ogólny model komunikacji asynchronicznej
Ogólny model komunikacji asynchronicznej
Mają szansę na recovery z chwilowych awarii
Poprzez dodawanie consumerów, niezależne skalowanie rabbitów itd..
Po zastosowaniu federation – z punktu widzenia usługi wszystko dzieje się lokalnie