Skalowanie Graj! do tysięcy równoczesnych żądań
Opublikowany: 2022-03-11Scala Web Developerzy często nie biorą pod uwagę konsekwencji tysięcy użytkowników korzystających z naszych aplikacji w tym samym czasie. Być może dzieje się tak dlatego, że uwielbiamy szybko tworzyć prototypy; być może dlatego, że testowanie takich scenariuszy jest po prostu trudne .
Niezależnie od tego zamierzam argumentować, że ignorowanie skalowalności nie jest tak złe, jak się wydaje — jeśli używasz odpowiedniego zestawu narzędzi i przestrzegasz dobrych praktyk programistycznych.
Lojinha i gra! Struktura
Jakiś czas temu rozpocząłem projekt o nazwie Lojinha (co oznacza „mały sklep” w języku portugalskim), moją próbę zbudowania serwisu aukcyjnego. (Nawiasem mówiąc, ten projekt jest open source). Moje motywacje były następujące:
- Naprawdę chciałem sprzedać stare rzeczy, których już nie używam.
- Nie lubię tradycyjnych serwisów aukcyjnych, zwłaszcza tych, które mamy tutaj w Brazylii.
- Chciałem „pobawić się” Play! Ramy 2 (kalambur przeznaczony).
Więc oczywiście, jak wspomniano powyżej, postanowiłem skorzystać z Play! Struktura. Nie mam dokładnego obliczenia, ile czasu zajęła budowa, ale z pewnością nie minęło dużo czasu, zanim moja witryna została uruchomiona i uruchomiona z prostym systemem wdrożonym pod adresem http://lojinha.jcranky.com. Właściwie spędziłem co najmniej połowę czasu rozwoju nad projektem, który wykorzystuje Twitter Bootstrap (pamiętaj: nie jestem projektantem…).
Powyższy akapit powinien wyjaśnić przynajmniej jedną rzecz: nie martwiłem się zbytnio o wydajność, jeśli w ogóle, tworząc Lojinha.
I to jest dokładnie mój punkt widzenia: użycie właściwych narzędzi ma moc — narzędzi, które prowadzą Cię na właściwej drodze, narzędzi, które zachęcają Cię do przestrzegania najlepszych praktyk programistycznych poprzez samą ich konstrukcję.
W tym przypadku tymi narzędziami są Play! Framework i język Scala, z Akką pojawiającą się „gościnnie”.
Pokażę ci, co mam na myśli.
Niezmienność i buforowanie
Powszechnie uważa się, że minimalizowanie zmienności jest dobrą praktyką. Krótko mówiąc, zmienność utrudnia wnioskowanie o kodzie, zwłaszcza gdy próbujesz wprowadzić jakąkolwiek równoległość lub współbieżność.
Sztuka teatralna! Scala framework sprawia, że używasz niezmienności przez sporą część czasu, podobnie jak sam język Scala. Na przykład wynik wygenerowany przez kontroler jest niezmienny. Czasami możesz uznać tę niezmienność za „kłopotliwą” lub „irytującą”, ale te „dobre praktyki” są „dobre” z jakiegoś powodu.
W tym przypadku niezmienność kontrolera była absolutnie kluczowa, kiedy w końcu zdecydowałem się przeprowadzić kilka testów wydajności: odkryłem wąskie gardło i, aby je naprawić, po prostu buforowałem tę niezmienną odpowiedź.
Przez buforowanie mam na myśli zapisanie obiektu odpowiedzi i udostępnienie identycznej instancji, jaka jest, dla nowych klientów. To zwalnia serwer od konieczności ponownego obliczania wyniku. Nie byłoby możliwe udostępnienie tej samej odpowiedzi wielu klientom, gdyby ten wynik był zmienny.
Minus: przez krótki czas (czas wygaśnięcia pamięci podręcznej) klienci mogą otrzymywać nieaktualne informacje. Jest to problem tylko w scenariuszach, w których absolutnie potrzebujesz, aby klient miał dostęp do najnowszych danych, bez tolerancji na opóźnienia.
Dla odniesienia, oto kod Scala do ładowania strony startowej z listą produktów, bez buforowania:
def index = Action { implicit request => Ok(html.index(body = html.body(Items.itemsHigherBids(itemDAO.all(false))), menu = mainMenu)) }
Teraz dodając pamięć podręczną:
def index = Cached("index", 5) { Action { implicit request => Ok(html.index(body = html.body(Items.itemsHigherBids(itemDAO.all(false))), menu = mainMenu)) } }
Całkiem proste, prawda? Tutaj „indeks” jest kluczem do użycia w systemie pamięci podręcznej, a 5 to czas wygaśnięcia w sekundach.
Aby przetestować efekt tej zmiany, uruchomiłem kilka testów JMeter (zawartych w repozytorium GitHub) lokalnie. Przed dodaniem pamięci podręcznej osiągnąłem przepustowość około 180 żądań na sekundę. Po buforowaniu przepustowość wzrosła do 800 żądań na sekundę. To poprawa ponad 4x dla mniej niż dwóch linii kodu.

Zużycie pamięci
Innym obszarem, w którym odpowiednie narzędzia Scala mogą mieć duże znaczenie, jest zużycie pamięci. Tutaj znowu Play! popycha cię we właściwym (skalowalnym) kierunku. W świecie Javy, w przypadku „normalnej” aplikacji internetowej napisanej za pomocą serwletowego API (tj. prawie każdej dostępnej struktury Java lub Scala), bardzo kuszące jest umieszczanie dużej ilości śmieci w sesji użytkownika, ponieważ API oferuje łatwe do- wywołuj metody, które na to pozwalają:
session.setAttribute("attrName", attrValue);
Ponieważ tak łatwo jest dodać informacje do sesji użytkownika, często jest to nadużywane. W konsekwencji ryzyko wykorzystania zbyt dużej ilości pamięci bez uzasadnionego powodu jest równie wysokie.
Z grą! framework, to nie jest opcja — framework po prostu nie ma przestrzeni sesji po stronie serwera. Sztuka teatralna! ramowa sesja użytkownika jest przechowywana w pliku cookie przeglądarki i musisz z nią żyć. Oznacza to, że przestrzeń sesji jest ograniczona pod względem rozmiaru i typu: możesz przechowywać tylko ciągi. Jeśli potrzebujesz przechowywać obiekty, będziesz musiał użyć mechanizmu buforowania, który omówiliśmy wcześniej. Na przykład możesz chcieć przechowywać adres e-mail bieżącego użytkownika lub nazwę użytkownika w sesji, ale będziesz musiał użyć pamięci podręcznej, jeśli chcesz przechowywać cały obiekt użytkownika z modelu domeny.
Ponownie, na początku może się to wydawać uciążliwe, ale tak naprawdę, Play! utrzymuje Cię na właściwej ścieżce, zmuszając do starannego rozważenia wykorzystania pamięci, co daje kod pierwszego przejścia, który jest praktycznie gotowy do użycia w klastrze — zwłaszcza, że nie ma sesji po stronie serwera, która musiałaby być propagowana w całym klastrze, co ułatwia życie nieskończenie łatwiej.
Wsparcie asynchroniczne
Dalej w tej grze! przegląd ramowy, zbadamy, jak Play! świeci również w async (hronous) wsparciu. Poza natywnymi funkcjami Play! pozwala osadzić Akka, potężne narzędzie do przetwarzania asynchronicznego.
Chociaż Lojinha nie wykorzystuje jeszcze w pełni Akka, jego prostej integracji z Play! sprawiło, że naprawdę łatwo:
- Zaplanuj asynchroniczną usługę e-mail.
- Przetwarzaj oferty dla różnych produktów jednocześnie.
Krótko mówiąc, Akka jest implementacją Modelu Aktora rozsławionego przez Erlanga. Jeśli nie znasz modelu aktorskiego Akka, wyobraź sobie go jako małą jednostkę, która komunikuje się tylko za pomocą wiadomości.
Aby wysłać e-mail asynchronicznie, najpierw tworzę odpowiednią wiadomość i aktora. Wtedy wszystko, co muszę zrobić, to coś takiego:
EMail.actor ! BidToppedMessage(item.name, itemUrl, bidderEmail)
Logika wysyłania e-maili jest zaimplementowana wewnątrz aktora, a wiadomość mówi aktorowi, który e-mail chcemy wysłać. Odbywa się to w schemacie „uruchom i zapomnij”, co oznacza, że powyższa linia wysyła żądanie, a następnie kontynuuje wykonywanie tego, co mamy później (tj. Nie blokuje).
Aby uzyskać więcej informacji o natywnej Async Play!, zapoznaj się z oficjalną dokumentacją.
Wniosek
Podsumowując: szybko opracowałem małą aplikację Lojinha, która jest w stanie bardzo dobrze skalować się w górę i w dół. Kiedy napotkałem problemy lub odkryłem wąskie gardła, poprawki były szybkie i łatwe, z dużym uznaniem ze względu na używane narzędzia (Play!, Scala, Akka itd.), co skłoniło mnie do stosowania najlepszych praktyk w zakresie wydajności i skalowalność. Z niewielką troską o wydajność mogłem skalować do tysięcy jednoczesnych żądań.
Opracowując kolejną aplikację, rozważ dokładnie swoje narzędzia.