Transkrypcja
Wprowadzenie do mierzenia wydajności
Cześć, Wojtek.
Cześć, Michał.
Może sobie dzisiaj zmierzymy tę naszą wydajność, którą teoretycznie badaliśmy w dwóch poprzednich odcinkach i coś nam wreszcie z tego wyjdzie. Bo my tu gadu, gadu, pitu, pitu, a tam jeszcze nic nie zmierzone.
Jak zmierzymy, to wyjdzie, że nic nie poprawiliśmy.
Nie no, to się tam takie wyniki zapoda, że wyjdzie, iż poprawiliśmy. Przyjdzie Product Owner, zapyta, czy jest jeszcze jakiś stojak, to zapyta jak. I wtedy mu się powie, że musimy opracować wyniki.
No ale do takiego mierzenia wydajności to już tam sobie podchodziliśmy tak delikatnie w poprzednim odcinku i były wzmianki o tym, że mierzymy sobie czas i… Mniej lub bardziej łopatologicznie można to zmierzyć zwykłym zegarkiem, ale wiadomo, że to nie za bardzo dobrze działa. Widzieliśmy już nieraz takie próby, jak system checked: jakieś milisekundy przed i po, i na końcu odejmowanie i sprawdzenie, ile to zajmuje. Ale co sprytniejsi mogą sobie takie rzeczy zrobić. Aspektem… na aspekt każdej metody też mamy abstrakcję na aspekt, więc jeszcze to nam zamula i jeszcze mamy tutaj dodatkowe zaburzenie.
W każdym razie w kodzie nie widać tego „old time minus new time”. No, ale jak masz takie „old time” i „new time”, to od razu wiesz, że tam się coś mierzy. Że to jest taki bardzo fajnie zmierzony kodzik i tam na pewno będzie dobrze. Ale jakby tak chcieć faktycznie zmierzyć tę wydajność, to można by jakieś lepsze narzędzia zastosować.
Narzędzia do pomiaru wydajności: Mikro benchmark
I tutaj mogę polecić takie proste narzędzie, które zresztą też jest w tej książce polecane, o której wspominaliśmy ostatnio, i nazywa się wbudowany mikro benchmark. I to jest takie narzędzie, które po prostu nam zmierzy to, co chcemy. W formie testów wydajnościowych musimy to napisać w formie testu badania sieciowego, i też tam była wzmianka, że dlatego to jest test wydajnościowy, bo to się pisze podobnie do testów po prostu. No i de facto on testuje tę naszą wydajność, mierzy coś chyba też.
Co do zasady, taki test musi mieć troszkę podobne cechy do zwykłego testu. Musi być tak reprodukowany, musi być również podobny, tak. No i ten kodzik musi być odpowiednio odizolowany od reszty, żebyśmy zaraz – tutaj będziemy sobie omawiać, że tak powiem, kategorie tych testów – ale na pewno do pojedynczego takiego uruchomienia, żeby powtarzalność zachować, musimy mieć dobrze wyizolowany ten kodzik, który też nie zależy. Jest wyizolowany od reszty, czyli to, co zbyteczne, mamy odcięte, ale dwa, że kolejne uruchomienia jego samego nie wpływają na siebie, też nie. Czyli że one trochę będą wpływać, bo już sama maszyna może się optymalizować w trakcie, czyli te jego wcześniejsze odpalenia w pętli – bo to się w pętli będzie działo – już dogrzewają maszynę nam jeszcze bardziej.
Oczywiście nie zapominamy tam o rozgrzaniu, i ten cały JMH nam pozwoli o tym nie zapomnieć, i też zrobić takie pętle, żeby właśnie on… On ma standardowe ustawienia, które wykonują na początek pięć pętli tak, po pięć liczone, dziesięć pętli rozgrzewania. I pięć pętli później właściwego testowania, o ile mu się tego oczywiście nie zmieni. Można by to zmienić, bo różne kolejki będziemy testować i też różny będzie rozmiar tego testu.
Optymalizacja środowiska i konfiguracja testów
No tak, ale żeby te testy dały nam dobre wyniki, to musimy właściwie użyć narzędzia. Żeby właściwie użyć narzędzia, to właśnie musimy zapewnić tę izolację kodu, automatyzację środowiska w ogóle. Izolację samego środowiska też musimy zapewnić, bo o tym mówiliśmy, że takie środowisko to już nie jest byle jakie środowisko, na którym sobie do JUnit odpalamy i tyle. Bo do JUnit ma powiedzieć, czy coś działa. A takie coś ma powiedzieć, jak dobrze działa? Więc to musi być ta cała konfiguracja środowiska. Ona musi być wyprodukowana i najlepiej zautomatyzowana, żeby to po prostu nie wymagało żadnych kliknięć i żebyśmy to mogli na tym środowisku naszym testowym gdzieś tam w świecie sobie też odpalić, bo tam pewnie mamy gdzieś jakiś serwerek. I do niego będziemy wrzucali te nasze przypadki wydajnościowe i one tam będą się chciały w określony sposób odpalać. Czyli on nie będzie obciążony niczym innym. Będzie tam po prostu prosty obraz, który ma za zadanie odpalić ten nasz benchmark, i praktycznie nic innego, po to żeby wykorzystać, przez prostotę środowiska, jak najwięcej tej powtarzalności uruchomień.
I ta automatyzacja plus dobre środowisko prowadzi nas do tego, że możemy już odpalić sobie testy. Tylko one też muszą być odpowiednio przygotowane. No i teraz wiadomo, że nie wolno pominąć etapu rozgrzewania maszyny i trzeba dobrze dobrać ilość iteracji samych testów. To jest to rozgrzewanie. Tak jak już wspomniałem, tam jest standardowo ustawione na pięć iteracji i powtarzanie samego końca też, jeśli tego nie zmienimy, jest na pięć iteracji. Oczywiście to wszystko możemy sobie skonfigurować czy to adnotacjami, chyba parametrami wywołania też możemy to skonfigurować. To jest nawet wygodniejsze, bo możemy sobie robić wiele scenariuszy i w zależności, jak odpalimy tego naszego Darka testując jego wydajność, takie dostaniemy warunki.
Tenże tool jeszcze ma fajne narzędzia do podsumowania wyników. To potrafi statystyczne jakieś mierniki pokazać, na podstawie których możemy wnioskować, czy kod nam się polepszył, czy pogorszył, czy co tam się z nim stało. Nie, że jakieś tam odchylenia, jakieś tyle i jakieś inne jeszcze gorsze rzeczy – to pewnie ta analiza statystyczna będzie… Będzie chyba naszym przyjacielem, chcąc nie chcąc, no bo jednak takich pojedynczych wartości to ciężko. Może trochę jej nie przeceniać, no ale pod względem przyjaźni narzędziem jest świetnym i jest wykorzystywana, tylko tak przyjacielsko względem nas bywa różna.
Tak, mi chodzi o to, że trochę pozwala łatwiej wyciągnąć pewne wnioski, bo z takich pojedynczych liczb czy pojedynczych uruchomień to i tak pewne zaburzenie wyników, pewna niedoskonałość tego procesu zawsze dość dokładnie, więc to będzie trudniejsze w wnioskowaniu. Tym sprytnym sposobem dochodzimy od razu do pierwszego wniosku, że taka pięcioiteracyjna pętelka to nam nie wystarczy. Bo jeżeli mamy kodzik, na przykład który się wykonuje – on tam się może wykonywać milisekundy, to już długo jest tam niecałe milisekundy – jakieś dziesiątki milisekund czy coś, to już są takie interwały, które ciężko nawet zmierzyć i pojedyncze jakieś iteracje takiej pętli. To one by mogły być obarczone jakimś tam sporym błędem statystycznym. Dlatego właśnie dobrze jest dobrać sobie ilość iteracji przynajmniej na dziesiątki tysięcy, żeby ten kod zdążył też sobie trochę pochodzić. To też rzutuje na rozgrzewanie młyna na pierwszy etap, czyli na rozgrzewanie maszyny, bo tam też trzeba dobrać tyle iteracji. Tego rozgrzewania też pięć nie wystarczy, bo po pięciu takich śmieciach to nie wiem, czy maszyna się skusi na jakąś optymalizację. I tak ma pewnie jeszcze… ma później wykonać jeszcze pięć i koniec. Pomyśli, że to się nie opłaca. Nie każemy 10 000 razy ten sam kod przebąkiwać, to już go może zechcieć zoptymalizować. Przy czym to też może być takie trochę mylące, bo ona sobie go optymalizuje pod nasz konkretny benchmark. Mieści się. Też można zastanawiać się, jak sobie te benchmarki poukładać i czy może ten benchmark…
Kategorie testów wydajnościowych: Mikro, Mezo i Makro
Trochę wkraczamy już w kategoryzację tych testów. Czy ten nasz test mamy zaprojektować jako mikro test, czy jako makro test? Bo nasz mikro test to jest coś takiego, jak przez analogię do dżungli: tak jak pojedyncze testy jednostkowe bierzemy, pojedynczą funkcjonalność – siup – skonfigurowaną, odpalamy i dostajemy tyle. Natomiast makro test to jest już nasza funkcjonalność taka grubsza, czyli kawałek feature’a taki, jaki sensownie da się zastopować w teście bez uciekania się do jakichś tam dziwactw, który jeszcze ma też sens biznesowy. Zasilamy biznesowymi danymi i odpalamy to w pętli. I wtedy dostajemy makro test.
No i są… Wiadomo, że natury tych testów są różne. Są swoim przeciwieństwem, bo ten mikro mierzy ten jeden fragmencik malutki pod względem wydajności. To może być na przykład nasz kluczowy fragment algorytmu jakiś tam, albo po prostu algorytm odpalony w tej pętli. Ale pamiętamy, że to wszystko dla takich danych, które mają sens. Oczywiście, zaraz do tego wrócimy pewnie jeszcze. Natomiast ten makro pokazuje, jak się cały klocek domenowy zachowuje, ten, który akurat badamy. Czyli na przykład, jeżeli to będzie jakiś klocek biznesowy, który coś tam ma wyliczyć i wypluć dla klienta, czy jakąś tam analizę zrobić czy coś tam, to on musi… musi tam mieć… musi być zasilony tym wszystkim, co mu pozwoli to wyliczyć, więc tam pewnie będzie trochę tych komponentów.
No tak, z tym że jakby taki wzorzec użycia, no to jednak pewnie nie do końca da się oddać, jeśli chodzi o częstotliwość wykonania tego ćwiczenia. No bo na produkcji raczej ciężko oczekiwać, że jakaś funkcjonalność będzie uruchomiona 10 000 razy w ciągu pięciu sekund.
No tak, laktator się rozłoży w czasie, pewnie też może być pewne zaburzenie w kontekście naszego testu, a faktycznego produkcyjnego użycia jest wciąż przybliżeniem. Tak, tak, tak, cały czas przybliżamy, bo tutaj zwielokrotnia ilość powtórzeń i mając zagwarantowane, że te uruchomienia są powtarzalne. Właśnie. To tak jakby… No właśnie uśrednianie. Ten czas wykonania chyba porównał do takiego… Mamy w maszynie do treningu takie coś, jak overfitting. To jest sytuacja, kiedy nasz model jest tak idealnie nauczony naszych danych testowych, że sprawa trafia centralnie. Trafia centralnie. To jest trochę podobna chyba sytuacja. Jest taki overfitting do naszego etapu testu, a na produkcji do tego prawdziwego modelu prawdziwego. Powtarzam, tak nie będzie, już tak nie będzie dobrze pasował. Więc to jest taka troszkę podobna sytuacja, bym powiedział, tak. I tu właśnie, żeby się troszkę przestrzec takich sytuacji, które i tak mimo wszystko takiego przypadku i tak pewnie nie zawsze da się uniknąć, bo chcielibyśmy ten nasz kluczowy element i tak zbadać i nawet mieć to przybliżenie, po prostu poznać rząd wielkości tej wydajności jego. Ale właśnie żeby uciec od tego problemu, to stosujemy albo makro testy, albo mezo testy. Mezo testy to jest coś pomiędzy mikro i makro, czyli tutaj mikro, mezo i makro testy mówią nam o tym, jak duży obszar poddajemy testowaniu. Na cały algorytm testowania się nie zmienia. Czyli tak czy siak musimy najpierw zrobić ten setup, wygrzać maszynę, a później zasilić ją tymi naszymi wywołaniami. Znaczy to odpalić te wywołania zasilone naszymi danymi.
Znaczenie danych i interpretacja wyników
I tutaj pamiętamy, że dane w każdym przypadku muszą być sensowne, żebyśmy przypadkiem nie doznali jakiejś niechcianej optymalizacji w tym względzie. Jeżeli one będą się na przykład w stanie zmieniać, to mogą się chcieć zmieniać, bo możemy na boczku wygenerować 10 milionów przypadków testowych, które będą po prostu ładowane z pliku i ten i dostarczane. Możemy już mieć nawet je załadowane i przygotowane tylko po to, żeby to łyknąć do tego algorytmu. To… to tym lepiej, jeżeli… Jeżeli tę zmienność sensowną domeną zapewnimy na tyle, żeby wyeliminować jakieś takie niechciane techniczne błędy, że nagle algorytm maszyny, konkretnie kompilatora, dojdzie do wniosku, że tu ma tak powtarzalny kodzik, że praktycznie stabilizuje się na chwilę, albo skasuje go, odpukać. Nie?
Tak. Bo może to te dane powtarzalne gdzieś tam wylądować w koszu procesora czy w innych miejscach zoptymalizować. Wiadomo, z każdym dostępem będą jednak szybsze, bardziej niż na takich prawdziwych danych, które będą rozsiane i troszkę bardziej zdefragmentowane. No i dostajemy wyniki zgodne z jakimś tam rozkładem statystycznym. Im lepsza próba tutaj tego całego wszystkiego, tym lepsze wyniki. Czyli ilość musi faktycznie odpowiadać temu, co my tam robimy.
I tutaj jeszcze przypomnę też nietrywialną rzecz, mianowicie żeby te wyniki doprowadzać w ogóle do uruchomienia tego algorytmu, żeby się nie okazało, że testujemy coś, co i tak spodziewamy się, że szybko działa, ale przez naszą nieuwagę maszyna tak to zoptymalizowana, że w ogóle kodu nie wykonuje, bo stwierdziła, że nie wykorzystujemy wyniku tego, co tam zawarliśmy, więc to wszystko jest zbyteczne. Odkrywa tam jakieś martwe meandry i je po prostu wyrzuca, nie? I odpukać, ten nasz właśnie testowany kodzik jest w takim martwym miejscu, więc pętla wykonuje się na pusto, bez niczego, i wtedy jest świetna powtarzalność, cudowne wyniki. Tylko nic nie testujemy, bo to nie nasz kod, tylko pętla tego benchmarku.
I ten JMH, tak jak wspomniałem, ma tam jeszcze szereg intrygujących narzędzi, które za nas praktycznie wyleczą to, co my chcemy, czyli te wszystkie odchylenia, jakie tam tam chcemy i ten. No, można go skonfigurować na sucho, to tak trochę. I z pamięci, tym bardziej ciężko to wyłuszczyć, ale można sobie te wszystkie parametry pozbierać i co najważniejsze, jeżeli to będziemy mieli o skryptowane. Taka jest właśnie zasada, żeby to wszystko było skryptowe, żebyśmy to mogli nie dość, że uruchamiać automatycznie na tym środowisku, to jeszcze żebyśmy mogli te wyniki automatycznie z niego zbierać i archiwizować, i porównywać sobie później, bo będziemy musieli stwierdzać, czy będziemy sobie budowali takie punkty odniesienia przy każdym uruchomieniu, jeżeli to będzie miało oczywiście sens. Bo jeżeli nam się algorytm zmieni tak dramatycznie, że nam wychodzą kolejne jego elementy, to już te poprzednie punkty odniesienia już są nieważne. Bo na przykład, jeżeli zwiększamy obciążenie algorytmu szacunkowo o kilkadziesiąt procent, to on się nam będzie dłużej wykonywał.
Musi najpierw zmierzyć, jak on się wykonuje po tym czymś, a później od tego już na etapie optymalizacji dopiero go tam statystycznie badać i szukać regresji w nim. Jakiś trend poprawną w stanie będziemy wykryć i przynajmniej monitorować. A tak pracować na wczesnym etapie i te parametry, które nam podaje narzędzie, to właśnie nam pomagają wykryć te trendy, bo będziemy po cyferkach widzieli, jak średnio się nam tam ten wynik zmienia. Jak szybko ten skrót tam pod spodem działa. No. To na skróty, tak zwane skomplikowane. Wtedy zmierzymy, jak szybko ten Hibernate działa.
Wyzwania i specyficzne przypadki: Zasoby współdzielone i optymalizacje JVM
No i teraz tak, jeszcze tutaj można zwrócić uwagę, że jakieś tam współdzielone zasoby mogą być problemem. No ale to jest kwestia właśnie tego, że one zaburzają nam obraz sytuacji, bo i zaburzają nam środowisko. Właściwie powinniśmy unikać tego, żeby one były współdzielone. Ciężko uniknąć współdzielenia procesora gdzieś tam fizycznego na jakiejś chmurze i maszynce, ale tutaj ufamy, że tacka do wirtualizacji. Dają nam takie klocki, które jednak mają ten nasz dedykowany kawałek procesora. Więc tak trochę musimy zaufać i trochę polegać temu, że dostawca wirtualizacji w chmurze da nam powtarzalny też ten procesor. Ilość pamięci też da taką, jaką chcemy, ale że ta pamięć też jest gdzieś tam w podobnym miejscu, niż była poprzednio. Tak.
Bo to jest kwestia właśnie takich głównych grup zasobów, które są często problemem jakimś tam wydajnościowo. Wiadomo, procesor i pamięć to jest pierwsza rzecz, która gdzieś tam nam przychodzi na myśl. Ale warto też nie zapominać o dyskach czy o wszelkiej maści tak zwanego IO, gdzie nasze aplikacje często właśnie czekają na odpowiedź czy to z dysków, czy to z sieci, czy socket, czy czegoś takiego. I to też powoduje bardzo dużo wszelkiej maści wydajnościowych problemów. No bo to są o rzędy wielkości najczęściej większe i zgodnie właśnie niż samo liczenie, czyli dokładnie zgodnie z tą wskazówką, którą tam podałem. Na tę chwilę, kiedy objaśniam, jak to może być skonstruowane, to dałem przykład, że te dane wejściowe, które byśmy chcieli zaserwować do naszego algorytmu, one by chciały już leżeć gotowe w pamięci po to, żeby właśnie nie dłubać ich z pliku. One jak najbardziej muszą być gdzieś tam przygotowane offline’owo i już wiesz, po prostu w takim formacie gotowym do czytania i gdzieś tam sobie leży na jakimś storage’u, ale my nie możemy jej wczytywać w pętli testu, no bo wtedy dostaniemy te zaburzenia, o których mówisz. No i to nam totalnie zepsuje wyniki. Już nie mówiąc o tym, że nam wydajność się zepsuje. Bo to są rzędy wielkości, różnice rzędu rzędów wielkości. Także to sobie trzeba przygotować zawczasu. I to jest ogólnie dobra wskazówka o przygotowywaniu rzeczy zawczasu w ogóle.
Interpretacja wyników i rola intuicji
No i teraz, jakie wyzwania tutaj mamy? W takim całym podejściu to już chyba doszliśmy do tego etapu, że można sobie o tych wyzwaniach powiedzieć, bo już tę część techniczną układaliśmy i wszyscy z ludzi z tego słuchali. No i tego, że ktoś książkę czytał tę czy tamtą, to już w ogóle wyłączył radioodbiornik. Tutaj już trochę naukowo też chcieliśmy błysnąć z tym etapem testów. I teraz dochodzimy do wyzwań, gdzie chyba głównym wyzwaniem jest interpretacja wyników. Po prostu mamy tutaj podejście, w którym królują dwie dziedziny: jedna jest ścisła, a druga humanistyczna. I ta ścisła to jest ta właśnie nauka w różnych przejawach, w technicznych i matematycznych i w ogóle w tych wszystkich ścisłych. A ta humanistyczna to jest sztuka, w jaki sposób do tego podejdziemy. No bo co nam po tym, że świetnie sobie wyliczymy statystycznie coś, kiedy źle dobraliśmy coś, co mierzymy codziennie w naszej domenie? Więc musimy tutaj… Tutaj musimy zwrócić uwagę na to, że faktycznie będziemy wywoływać to, co jest godne wywołania i zmierzenia. Podchodzimy naukowo, bo chcemy zmierzyć, jak to się szybko dzieje. Ale jeżeli wybraliśmy niewłaściwy fragment domeny, to nie za bardzo dobry. Dostaniemy nie za bardzo dobry ogląd sytuacji po tych naszych optymalizacjach.
Zawsze też warto tak naprawdę zastanawiać się, po co coś mierzymy i co to ma za efekt nam przynieść, bo wszystkie te nasze optymalizacje, no to koniec końców, mają za zadanie jednak przynieść pewien wynik temu użytkownikowi. Wszelkich rezultatów. Ten przysłowiowy wynik rezultatu czy to w postaci szybszego renderowania się strony, czy szybszego pokazania się wyniku, czy szybszego renderowania klatki. Ale to nieco szybszego renderowania klatki czy obsłużenia większej ilości użytkowników, co się przekłada de facto na wzrost szansy sukcesu naszego biznesu. No bo o to w końcu chodzi. Nie chodzi o to, żebyśmy dla samej natury obsłużenia większej ilości użytkowników coś tam robili, tylko jednak te systemy po coś działają, więc koniec końców to wszystko sprowadza się do jakichś biznesowych KPI-ów, wskaźników i parametrów, które chcemy monitorować, i dopiero na tym najniższym poziomie są to nasze techniczne wskaźniki, które składają się na te biznesowe wskaźniki, które pewnie będą oczywiście zawarte w jakichś KPI-ach, które będą monitorowane, które prawdopodobnie będą musiały być przestrzegane, bo to mogą być też równie dobrze wskaźniki dostępności systemu, czyli SLA, które mogą być częścią jakiejś umowy i możemy chcąc nie chcąc płacić kary, jeśli nasz system nie będzie dostępny, ponieważ jakiś jego fragment nie był na tyle wydajny, żeby tę dostępność… będzie odcinany na przykład przez dokładnie taką wydajność. Tak, więc to wszystko ma jakiś wyższy cel i ma jakieś konkretne zadania. Te nasze najmniejsze klocki tego procesu właśnie są… są częścią tych większych, większych parametrów.
Czyli my mamy tę naukę właśnie w formie statystyki i wszystkiego tego, co nam pomaga w zapewnieniu stabilności takich testów i w dobrej interpretacji wyników. I tę sztukę, która się opiera w głównej części też na naszej własnej intuicji i doświadczeniu. Więc tutaj intuicja i doświadczenie to są równie ważne w testowaniu wydajności, jak te względy techniczne, bym powiedział. Tym bardziej, że same wyniki statystyczne to też potrafią dać zagmatwany obraz. Tym bardziej, że nie możemy polegać na samych suchych wynikach statystycznych, jeżeli wiemy, że pod spodem w algorytmie tym, który testujemy, czy tam w tej logice, którą testujemy, jakieś bardziej złożone… jeżeli na przykład będziemy testować makro testem, nie zmieniło się na tyle, że faktycznie ten wynik ma taki być. I to wcale nie jest regresja, że dostaliśmy pogorszenie algorytmu i to nie jest źle, a wręcz dobrze, że nie o aż tyle, jak się spodziewaliśmy, tylko o tyle, ile dostaliśmy. I to jest właśnie ten sukces naszej optymalizacji jednocześnie, czyli te nasze punkty odniesienia już stare dawno przestały być ważne, bo nasz system robi dużo, dużo więcej.
Skalowalność i wnioski z doświadczeń
Mimo to nasze takie mikro wskaźniki mogą nam też fajnie pokazać pewien wzrost wydajności, na przykład w kontekście dołożenia mocy obliczeniowych. Czyli dokładamy sobie, bierzemy lepszą maszynkę, dokładamy pamięć, dokładamy RAM i patrzymy, jak wzrost dostępnych zasobów przekłada się na wzrost wydajności naszego algorytmu, naszego systemu. Bo też jest nam w stanie wtedy powiedzieć, czy faktycznie opłacalne jest na przykład wzięcie większej instancji, czy może dołożenie większego czegokolwiek, czy to pamięci, czy to RAM-u, czy pokazuje zwyczajnie skalowalność tego naszego algorytmu, naszego procesu na dostępnych zasobów. To też jest jakiś tam współczynnik, który warto wziąć pod uwagę w kontekście rozszerzenia biznesu i jakby z takiej horyzontalnej skalowalności naszego systemu. Czy on… Czy ta skalowalność tak naprawdę jest w jakimś dobrym parametrze? Czy to tak naprawdę możemy w nieskończoność dokładać zasobów, ale wzrost wydajności będzie niewielki, bo jednak na przykład blokujemy się na odczycie z jakiegoś tam API, albo na jakimś elemencie, który tak naprawdę nieważne, ile dołożymy procesorów, to i tak nam nic to nie da, bo tu jesteśmy zablokowani, bo tu mamy dead locka, tu mamy race condition, tu mamy coś jeszcze i tak naprawdę możemy z tego nigdy nie wyjść, niezależnie na jak szybkim sprzęcie będziemy to uruchamiać.
Tak jest, i właśnie to zróżnicowanie na mikro, makro i mezo testy nam może pomóc na przykład zdiagnozować takie przypadki, bo okaże się nagle, że algorytm zwolnił. To znaczy inaczej, że optymalizacje nie dają nam spodziewanych wyników rezultatu, a ten statystycznie wychodzi nam bardzo podobnie, może z jakimś tam lekkim zyskiem po tych optymalizacjach, ale właśnie nie takim, jak się spodziewaliśmy w takim makro teście. Natomiast poszczególne mikro testy pokazują, że na przykład ten klocek, który optymalizuje, szaleje jak dziki i po prostu wydajność jego wskoczyła na wyżyny. Już ciężko cokolwiek więcej z niego wykrzesać, ale właśnie zatyka się na tych dalszych, innych klockach, z którymi on się łączy, i nic nie wskóra, optymalizując całe cacko, dopóki nie pozbędziemy się tych przeszkadzajek, tak zwanych wąskich gardeł.
Przykład z Netflixa: Głęboka analiza problemów wydajnościowych
No tutaj takim ciekawym przykładem, który ostatnio czytałem, jest case, jaki mieli w Netfliksie, gdzie dla jednego z mikroserwisów postanowili, że żeby spadła ta oglądalność… lecz max… to jest inny case. Tego chyba nie optymalizują. Jeszcze do tego wrócimy. Akurat skończyłem trzeci sezon oglądać. Jak ja skończyłem „Drwala” słuchać. Chyba jeszcze będzie miał dużo materiałów na temat trzeciego sezonu na pewno. Natomiast wracając do meritum. Otóż w Netfliksie mieli taki case, gdzie dla pewnego mikroserwisu postanowili zwiększyć rozmiar maszyny wirtualnej z 16 do 48 procesorów, bo mieli tam już coraz większy load i chcieli to jakoś sobie zeskalować, żeby zwiększyć liczbę obsługiwanych [zapytań] na sekundę i zmniejszyć opóźnienia tych kilku.
No i przeprowadzili sobie dość gruntowne, dość gruntowne testy. Sprawdzili, czy to wszystko będzie fajnie działać w takich warunkach testowych. Powiedzmy, wstępnie wyszło im, że tak. Więc przeprowadzili tę migrację właśnie na produkcji i ze zdziwieniem odkryli, że pomimo praktycznie trzykrotnego wzrostu mocy obliczeniowych, ustrojstwo ośmiu rdzeni praktycznie nic się nie dzieje, jakby liczba [zapytań] na sekundę pozostała na podobnym poziomie. Latencja troszkę nawet wzrosła, ale też stała się taka bardziej szarpana. Zaskoczenie. No bo to jednak jest dość nieoczekiwane zjawisko. Dokładamy więcej mocy, a tu wcale nie jest szybciej. Wciskamy gazu i jedziemy szybciej.
Czyli na gaźniku się dławi.
No, prawie. Prawie. To coś właśnie mocno, mocno analizować tę sytuację. I koniec końców problem sprowadził się do pewnej specyficznej sytuacji, a właściwie implementacji już w trzewiach wirtualnej maszyny tej wielkiej, bo to akurat było najwięcej, gdzie w sytuacji bardzo dużego loadu ich dane w cache’u L1 jakoś tak się rozkładały, że różne wątki, starając się do tych danych dostać, powodowało to odświeżanie tych danych. To, bo jest tam pewna lokalność tych danych i jeden wątek, nawet jeśli nie zbiera tych danych, musi sobie całą linię tego cache’a odświeżyć. Czyli taką szesnastkę. Że jeżeli to wszystko właściwie sobie nawzajem…
I troszkę właśnie nastąpiło takie czyszczenie sobie nawzajem się ścigały trochę.
Kto lepiej skasuje?
Troszkę tak, troszkę tak. Ich fix polegał na tym, że dopiero do tej dekady wrócili do jakiegoś profilu związanego z konfiguracją takich kieszonkowych rzeczy. Dorzucili taką małą rzecz powodującą rozkład tych obiektów w pamięci. Będzie… Nie będzie tak na zakładkę, żeby to nie powodowało tego czyszczenia. Jakąś lokalność wprowadzono, taką lokalność można powiedzieć. To pisane na blogu, to on może sobie więcej szczegółów doczytać, a tu przybliżam. I po wprowadzeniu takich tego typu poprawek gdzieś, to jeszcze wcześniej by śledzili w ogóle, w którym miejscu kodu to się dzieje, bazując na analizie bajt kodu. Ulubiona to było przy przeładowaniu klas, no i po wrzuceniu tego fixa rzeczywiście ta wydajność około trzy razy była większa. Praktycznie tak, jak była większa maszyna, ale musieli zejść aż na poziom implementacji, czy szczegółów implementacji samej JVM-ki, i wtedy dopiero ten problem to jest głęboko pod maską. To już nie wystarczy zerknąć w kodzik.
To nie był problem ich kodu. To nie był problem systemu. To nie był problem otoczenia. Poza tym w szczegółach już samej maszyny wirtualnej, więc taki bardzo, bardzo… taki to jest są boki skok, jakby jeśli chodzi o analizę tego przypadku.
Super przykład na to właśnie, co przed chwilą powiedzieliśmy, czyli że tutaj ta sztuka weszła. I jeszcze sztuka dedukcji i w ogóle grzebania w tych trzewiach, żeby dotrzeć do problemu. No i też wiedzą się popisali, bo jeżeli problem rozwiązali właśnie w taki sposób fajny, no to te czapki z głów można im powiedzieć. Dobra robota.
No tak, też pewnie mieli na to trochę czasu i trochę presji pewnie mieli, bo jak zwykle pali takie fajne cacko, co miało trzy razy szybciej hulać, to lepiej, żeby hulało niż nie hulało. Nie są tanie rzeczy, takie trzy razy szybsze są. To nie są tanie rzeczy. Tym bardziej sam Henry już nie chce w czwartym sezonie tak, sprzętu nie chce, ale to czasami nie trzeba będzie mu płacić, więc zawsze zostaje. Więc może te maszyny? No dobra, to mogli, kurde, aż bardzo nie stać. No, ale fajnie, że się postarali. I fajnie, że mamy ilustrację do tego, że właśnie trzeba się też czasami niestety, niestety wykazać doświadczeniem, jakąś intuicją, żeby wyczuć intuicję, żeby wyczuć, co boli, a doświadczeniem, żeby wejść tam i naprawić, bo to nie zawsze będzie takie proste. I jakkolwiek jej wiem, to jest po prostu wspaniała maszyneria i ma tam multum tych parametrów, to jednak to też sama żonglerka tymi parametrami to też jest złudna rzecz, bo jeżeli coś ustawimy nieodpowiednio, to się może zepsuć, bo jeżeli czegoś nie ustawimy, a powinniśmy, tak jak w tym przypadku. To też może nie naprawić, nie tak, bo coś tam gdzieś tam zostanie pominięte, więc tutaj trzeba bardzo ostrożnie do tego podchodzić i właściwie na wszystkich płaszczyznach, z jakimi mamy do czynienia.
Bo trzeba jasno powiedzieć, że sytuacja taka jak w Netfliksie i workflow, ilość danych czy ilość requestów, jakie są w tego typu systemach, które obsługują telefony milionów użytkowników do streamowania setek milionów godzin dziennie, to są zupełnie inne warunki pracy czy systemu operacyjnego. I tamte, te sytuacje, które napotykamy, są naprawdę skrajne i to są takie warunki brzegowe, o których ciężko powiedzieć, żeby twórcy tego czy innego języka, czy systemu w ogóle sobie wyobrażali takie, że takie problemy to już to tylko wcale nie mają.
Gorzej, jak mówiłeś.
Tak, to już powiem, tylko to są usiądź, może nie oglądają tam „Wiedźmina”, nie stosując…
Nie wiem, czy teraz więcej się stresuję. Więcej danych leci jakby w całej sieci Netflixa, czy więcej danych leci z tych stu wektorów cząstek? Kurde, żeby tylko nie zrobili filmu o… Co by to było? Trzeba, bo tu było zmierzone. Racja. Jednak biorąc pod uwagę, że taki film 4K kodowany tym kropeczek, tym kodekiem, a tu res dwugodzinny film potrafi zająć więcej niż 1 TB do postaci nieskompresowanej, to możemy sobie wyobrazić, tylko jakie to są ilości danych.
To też system odczytujące taśmy magnetyczne muszą być odpowiednio zoptymalizowane. To jest jednak, jednak dość duże ilości danych, a wiadomo, wszyscy naraz to oglądać i będą usterka.
No chyba, że im zamula net. No chyba, że zamula bólem. Ale to tyle dobrego dla mnie.
No tak, to tak bez intuicji i doświadczenia to ciężko takie grube rzeczy naprawiać, ale można sobie wyrobić z takiego doświadczenia właśnie dłubiąc w takich mniejszych procesach i po prostu fajnie jest sobie po prostu podłubać w tych benchmarkach. Jeżeli ktoś oczywiście ma chęci, chciał, ma też potrzebę, jak ma potrzebę, to jeszcze lepiej, bo też będzie miał i chęć tam podłubać. No i takie właśnie sobie mniemanie wyrobić o tych narzędziach. I ja tak miałem taką potrzebę, może by można jeszcze powiedzieć, bo już to wreszcie stać by było.
Nawiązanie do praktycznych doświadczeń
No chyba już czas. Bo chyba już wszystkie dyrdymały teoretyczne poruszyliśmy. Jak się coś przypomni, to odegra jakąś rolę czy coś. W każdym razie byłem kiedyś w takim fajnym projekcie, gdzie gra w szachy, ale nie w takie zwykłe szachy, tylko w szachy dla czterech zawodników. I to są całkiem inne szachy. Tam się ruchy wykonuje po kolei, zgodnie z kierunkiem zegara. Tylko że skutki tych ruchów są odczuwalne dla wszystkich naraz, czyli na przykład odsłonięcie jakiegoś czegoś, jakiejś figury przez inną figurę dla gracza, czy przez gracza N. Dla gracza N+2 robi na przykład szacha, a dla N+3 mata od razu w tym samym pociągnięciu. Takie scenariusze mieliśmy tam testowane, czyli… no to to już jest taka mała wskazówka dla wnikliwego słuchacza, że żeby takie coś przetestować, to trzeba mieć maszynerię do ustawiania tych scenariuszy, jak to w Gamedevie. Bo to jest de facto gra. Czyli musimy mieć możliwość ustawienia sobie, czy to testy jednostkowe, czy testy wydajnościowe, w tym punkcie, w którym trzeba. Więc taką maszynerię sobie stworzyliśmy.
No, ale po co w ogóle optymalizować taki silnik do szachów, jak jest napisany i działa? Bo okazało się w pewnym momencie, że koledzy, którzy pisali silnik taki prób koncertowych, napisali go tak, żeby zachować funkcjonalność tego, żeby uzyskać funkcjonalność tej całej gry, ale jeszcze nie przykładając się do kwestii wydajnościowych. I okazało się, że na przykład, kiedy się to robi w sposób obiektowy, to aż korci, żeby tam zrobić pętle w pętli, bo inaczej się tego nie da przetworzyć. Nie tylko szachownica ma N na N pól. Jest tam ilość figur i czasem trzeba sobie po prostu torować w jedną i w drugą stronę, żeby zobaczyć, czy coś przypadkiem się nie odsłania, czy coś się nie pojawia w naszym zasięgu, jeżeli rozpatrujemy sytuację danej figury, bo gra jest z jednej strony niby prosta, bo reguł nie ma tyle co w Warcrafcie, ale z drugiej strony. Może się okazać, że tam na przykład figura nie może się poruszyć na jakieś pole, tak jak jest w przypadku króla, który ma już kategoryczny zakaz wstawiania na pole, które jest atakowane. Figura normalna, nie będąca królem, na przykład nie chciałaby się znaleźć nagle pod stołem, jeżeliby miała tam stanąć. Nie możemy. Te wszystkie zagrożenia musimy wykrywać.
Narodziny Bit Engine
No i ten kodzik pierwotny, jakkolwiek działający i realizujący pełen algorytm tych szachów, nawet z takimi hardkorowymi scenariuszami odsłaniania się w locie i ubijania tam zawodnika przez jakiś pozornie niegroźny ruch – to wszystko było tam już zakodowane i były to już klasy opisane. No i się okazało, że działa to na tyle wolno, że się nie da w to grać. Więc co teraz zrobić? Podjęto tam pomysł, żeby być może uprościć struktury danych. Jakby tak poczytać książki do szachów, to by się okazało, że klasyczne, takie zwykłe szachy, te właśnie, które teraz już są dobrze implementowane. One operują bardziej na prymitywnych strukturach danych, czyli w sensie opartych o typy prymitywne. Tam się nie używa obiektowości, tam jest wszystko stabilizowane. I tak też właśnie tutaj podeszliśmy do tego. I powstało coś, co roboczo nazwaliśmy „Bit Engine”. I kod tego wyglądał tak, że lepiej tam nie zaglądać, bo to tak totalnie nie…
Jak wszystko, co ma w nazwie „bit”.
W sensie, że jak ktoś spoza zespołu zaglądał w ten nasz kodzik, to czym prędzej uciekał z krzykiem, bo w ogóle odstraszały go operatory arytmetyczne. Przede wszystkim właściwie logiczne, te operujące na przesunięcia bitowe. Tak że od przesunięć to się w ogóle zaczynało. Także tam pojawiały się takie rzeczy jak tam te bitowe tablice longów, gdzie jeden long opisywał cały wiersz, w tym cały wiersz na szachownicy, i ponieważ szachownica ma N na N pól, no to takich longów tam było też N. Więc jakoś logicznie brzmi. Logicznie i bardzo.
I to się bardzo logicznie bierze. To bez ironii, oczywiście.
Nie podejrzewałbym, więc te struktury danych już dały. Z jednej strony utrudniły kodowanie, wiadomo, ale przy odpowiednim działaniu tego dało się całkiem przyjemnie z tego skorzystać. To właśnie z pomocą przychodził jakiś tam setup. Taki, który pozwalał, pozwalał w czytelny sposób, w formie wręcz obrazka takiego bitu. Widziałeś te paski „Art”?
Siedzieli i rysowali szachownicę.
To było bardzo czytelne i fajnie pokazywało taki jako input.
Takie coś jak ultra przyjemnie robiło. Jak później jeszcze to przyjęliśmy, że nie trzeba było tego bezpośrednio w kodzie testu klepać, tylko można było w tym pliku tekstowym wyklepać samą szachownicę i pokazać, z jakiego stanu do jakiego stanu dochodzimy i że ma się zgadzać. Nie asercja, a de facto była stanem szachownicy po ruchu. Więc to ładowało się stan przed, stan po i porównywanie się wykonywało się ruch i porównywała się, czy to się zgadza. Linia asercji była w formie takiej pseudograficznej.
Tak, tak, taki tam ma asercje. Nawet dzięki temu, że ona była w formie pseudograficznej, to mogła nam pokazać różnicę. Czyli ten wiersz szachownicy, w którym się coś puściło, na przykład, nie? Ale mogliśmy sobie to zrobić w formie prawie takiego animowanego filmu. Coś, co się zmieniało, jak sobie to zrzuciliśmy, bo to też trochę analogią zalatuje. Wtedy każdy taki stan szachownicy to jest nasz event, i de facto tak to działało, bo jak sobie to podłączyliśmy, to normalnie odgrywali sobie jak w filmie. Już widzieliśmy to na pięknych figurach animowanych w reakcje, a czemuż by nie? Więc takie rzeczy cudownie się debuguje. Oczywiście, jeżeli się zapewni do tego odpowiednie wsparcie. No i można też pisać benchmarki do tego.
Wyniki optymalizacji i weryfikacja
No i się okazało, że jak powstała taka już cokolwiek odpowiadająca wersja tej szachownicy bitowej, to puściliśmy benchmark na tym, jakiś tam scenariusz. Poruszamy się stąd, tu coś tam robimy i tyle. No i na początek wielka euforia, bo coś takiego hulało. Dla tego samego scenariusza kilka… zapomniałem cyferek, bo nie spojrzałem. Już nie mogę sporo wymyśleć.
Można wymyśleć, tak? No nie, bo się nagrywamy. To się wytnie. Później kilkanaście tysięcy razy szybciej się wykonywała. Taka bardzo prosta rzecz. Taka totalnie bzdurna, jak przesunięcie pionka z miejsca w miejsce. Nie? To oczywiście jest już sugestią, że to jest wartość myląca, bo jakkolwiek tam kilkanaście tysięcy razy szybciej, bo tu mamy, jesteśmy prawie przy metalu, a tam byliśmy w obiektowych strukturach, no to w tym poprzednim to dobra, można szybciej i zdecydowanie. Fajnie, że wyszło szybciej. Tylko że okazuje się, że jeżeli testujemy, jeżeli porównujemy już gotowy silnik, który robi przy tym ruchu wszystkie sprawdzenia, jakie tylko są dozwolone, a my mamy pierwszą wersję silnika, który nic nie robi, to to się nie nadaje do porównywania. Nie wiem, ale to było tak, żeby po prostu poczuć, w jakich rzędach wielkości się znajdujemy.
No i to dokładaliśmy feature’ów po kolei do tego silnika, aż wszystkie figury już potrafiły obsługiwać wszystkie swoje scenariusze. Takie pojedyncze dla figury, czyli po prostu przesuwanie figury, atak, unikanie ataku i takie tam rzeczy, nie? No i później przeszliśmy już do sprawdzania tych scenariuszy. Wydajność tego, na co później te różnice w wydajności już spadły. Już to były pojedyncze setki, ale nadal on był o trzy rzędy wielkości szybszy od tego obiektowego, który po prostu jechał, pętle w pętli robił i czasem robił jeszcze pętle na tym wszystkim, no bo trzeba było na przykład królowa, zwana hetmanem. Niekiedy jest po prostu upierdliwy tym strasznym, bo atakuje w osiem stron świata i przez całą długość wszechświata. Czyli my tu musimy dla wszystkich pól szachownicy mieć najlepiej wygenerowane maski ataku i się suszyć. No wiadomo, to tam już szczegóły techniczne, ale musimy sprawdzić w dowolnym miejscu szachownicy, czy ona sięga do dowolnego innego miejsca. Które to może być już zajęte przez, uwaga, nasz albo nie nasz obiekt. Więc tutaj musimy wiedzieć, czy my tym atakiem dotykamy siebie, co nie boli, czy dotykamy kogoś, komu już robimy kuku. Także tego typu zagadnienia są w takiej fajnej grze. I to one komplikują nam całą tę logikę.
Mimo wszystko ta logika i tak była o trzy rzędy wielkości szybsza od tej pierwszej, a mierzyliśmy ją też, dostarczając takich realnych scenariuszy. Czyli po prostu trzeba było ten setup, setup planszy tej szachownicy, mieć wrzucony w benchmarki, i ten benchmark musiał też się rozgrzewać odpowiednio długo, i… No i wszystkie te cechy musiał mieć te, o których mówiliśmy wcześniej, czyli musiał ten wynik być na pewno przechwytywane, żeby to na pusto nie latało i musiał sobie wykonać tyle razy, żebyśmy byli w stanie zmierzyć statystycznie, zmierzyć szybkość tej odpowiedzi. Niemniej dało się to zrobić i pokazał dokładnie te wszystkie rzeczy po drodze, nawet dochodząc do tych ostatnich wyników, które już tu sobie toczyliśmy.
Zalety prymitywnych typów danych i przyszłość Valhalli
Tak, to jest taki klasyczny przykład, co daje optymalizacja z użyciem typów prymitywnych w algorytmach, które potrzebują właśnie dużej ilości obiektów, i narzut z tym związany sprawia, że ten algorytm nawet jeśli jest w miarę prosty. Teoretycznie jednak ilość tych obiektów generowanych sprawia, że to wszystko jest dość wolne. Taka może powiedzieć klasyczna optymalizacja. Jak już będzie Valhalla, to wtedy będzie to można napisać obiektowo za pomocą klas i będzie tak samo, jak na prymitywnych.
Śmiem tutaj lekko zaoponować, bo mimo wszystko nawet z Valhalli to lepiej by było napisać to w takiej formie, bo to już dowiedziono wręcz matematycznie na przykładzie wcześniejszych szachów, że może jednak bilbordy, bo tak to się nazywa, po prostu tutaj działają lepiej.
To rzeczywiście trochę znak, bo tutaj kodowanie bitowe to wiadomo, to robi robotę.
A jeśli chodzi o reprezentację naszego problemu.
Dokładnie, ponieważ tutaj powrócę do tego, o czym mówiliśmy w poprzednim odcinku, a mianowicie do tablicy wyników cząstkowych. Cały ten algorytm, oprócz tego, że odcinał nam niedogodności obiektowe, w tym przypadku niedogodności, bo to nie potrzebowaliśmy tych wszystkich narzutów, jakie nam obiekty dają, to jeszcze nam dawał możliwość robienia tych porównań, które wcześniej były… musiały być robione w pętli, czasami też w pętli, bo nie da się inaczej, jeżeli na ukos przez szachownicę patrzymy. Ale to była pętla o pojedynczym przebiegu i iteracja trwała w niej naście razy. I ta pętla nie robiła „i to, to tamto, jak nie to else, bo tam coś tam i na końcu wyjątek”, tylko po prostu brała sobie jedną liczbę z tablicy. Brała sobie drugą liczbę z drugiej tablicy i robiła na tym na przykład end logiczny. I jeżeli to było różne od zera, to coś się działo, bo mieliśmy stabilizowane sytuacje, kiedy na przykład dla każdego możliwego położenia figury na planszy – wróć, na szachownicę – coś się dzieje, że na przykład to dotyka jakiegoś konkretnego położenia naszej figury albo nie naszej figury, i to wtedy te maski bitowe, bo to się do tego sprowadza, już wcześniej zostały wygenerowane w ilościach tysięcy. Nawet bym powiedział, bo to już ilość pól na szachownicy razy ilość figur razy ilość graczy i razy ilość scenariuszy, to już tam dawała konkretne wartości. To oczywiście to jeszcze nawet nie było ładowane z pliku, bo to się generowało na starcie aplikacji. Jako takie dane inicjalizacji, które sobie siedział w pamięci cały czas, więc nie trzeba było tego pamiętać. Pamiętać trzeba było, nie trzeba było tego ładować z pliku, nie trzeba tylko było mieć wygenerowanego w pliku. No i jak się coś zmieniło? Bo na przykład wykryliśmy jakiś błąd w samym tym mechanizmie, to można było poprawić to też w tej części cywilizacyjnej, i ten i nie trzeba było prze generować tych danych, bo się generowały w locie, nie? Więc to jest taka fajna technika, która daje kolosalne wręcz możliwości. No i jest bardzo elegancka.
Jednak wygląda. Wygląda dość elegancko w takim rozumieniu. Jeżeli ktoś… rzeczywiście w algorytmiczne elegancko wygląda totalnie nietypowo. Jeżeli ktoś by chciał się przyczepić do tego, że ma być jak w Javie, to nie są. To nie było jak w Javie, bo potem było jak w grze. Bo de facto to jest tak trochę postać podchodzi pod taki silnik gry, czy to tamto jest z silnikiem gry, a te z natury muszą być zoptymalizowane do tego, a nie pięknie napisane.
Mimo wszystko to jest jeszcze bardzo prosty silnik gry, bo się okazuje, że szachy, jakkolwiek by nie były zakręcone, a te były zakręcone względem tych szachów podstawowych, które wszyscy znamy, to i tak nie jest to tak dużo, jak w takiej przeciętnej grze strategicznej, gdzie tam się musi dziać, bo już nawet MMO nie wspomnę, bo to zupełnie inny poziom skomplikowania.
Problem przeciążenia serwerów: Blizzard i Netflix
No to super przykład, jak można zoptymalizować działa. Natomiast przyszedł mi do głowy taki troszkę taki troszkę kreatywny sposób na rozwiązanie pewnych problemów wydajnościowych. To już nie będzie z projektów, w którym byłem, bo to będzie o firmie Blizzard, w której nie byłem, ale pewnie zawsze chciałeś.
Czy chciałem? Niekoniecznie, może.
Ale generalnie Blizzard zawsze wydawał jakąś nową grę, czy to „World of Warcraft”, czy właśnie „Diablo”. Bo to były problemy z tymi serwerami. Wiadomo, na dzień dobry, na dzień dobry. Tak, tak to mówiłem oczywiście o dniu premiery, o pierwszych dniach, kiedy wszyscy rzucają się, żeby sprawdzić dany tytuł, bo najgłośniej to przy „Diablo 3” był taki znany. Był, ale upadł, bo wszystko padło, wszystko mu psuło dom, córka też. Kolejki do logowania są klasycznym widokiem, żeby żeby żeby wejść do gry. Co do przypadku MMO, jest to poniekąd zrozumiałe. Wiadomo, wymóg bycia online jest oczywistym. W przypadku „Diablo” może mniej, ale to nie to. To wszystko to się zgadza, bo popularność tego tytułu jest po prostu…
Tak, to koszmar wydajnościowo.
Można się zastanowić, na ile funkcje społecznościowe czy online w tego typu grach są konieczne, a to jest inna sprawa. No i ten problem od wielu lat istniał. No i teraz pytanie, jak go rozwiązać? Wiadomo, na każdym forum, z każdym, na każdym portalu, oczywiście wszyscy napiszą to oczywiste: „No przecież dołóżcie więcej serwerów!”. Najprostsza forma, bo bez suwaków nie macie.
Tak, suwaki macie w chmurze.
Kto wie, że coś takiego. Ktoś inny po prostu powie, że tyle kasy zarabiają na naszych grach, które są drogie jak nic i nie mogą dokupić tych dwóch komputerów, bo mogą? Mogą? Oczywiście, że mogą.
Skończą jak z Netfliksem.
Tak, to właśnie jest trochę problem, że jeśli rzeczywiście rozmawiamy o takim flow, który jest generalnie, można powiedzieć, stanowy. Poniekąd to jest kwestia Netflixa, czyli serwowanie statycznego, de facto statycznego contentu. Wideo możemy sobie bardzo fajnie równolegle dać, możemy sobie zoptymalizować przez CDN. Czyli pociąć to na kawałki, pociąć na kawałki. Netflix robi nawet tak, że w dostawców internetowych, w ich dużych data center stawia swoje własne takie wewnętrzne data center i tamten content wrzuca tak, żeby szybciej, żeby bardziej zlokalizować.
Żeby była krótsza droga.
Żeby była krótsza droga.
Bo to nie jest tak, że jak wchodzicie na Netflixa i sobie wasz ulubiony film. Osoby tam ze Stanów czy skądś tam leci po łączach i aż tutaj do Polski, czy do innego kraju. Tego typu kontakt musi być jak najbliżej użytkownika. I właśnie przez systemy Content Delivery Network, albo wręcz data center dostawców internetowych, właśnie minimalizuje ten ruch. I ktoś też zapyta: „No ale po co taki Orange czy UPC? Czy jakaś inna firma zajmująca się internetem miałaby pozwalać sobie na wrzucanie ich rzeczy?”. Dla nich to też jest korzyść, bo to ich sieć jest de facto obciążona tym ruchem Netflixa.
Bo im się płaci, przecież.
To ich łącza są obciążone, więc dla nich to jest też zwrot. Tu jest taka… taka fajna symetria.
Zapewniają klienta.
Po prostu. Dokładnie, dokładnie, więc to się opłaca i dostawcy internetowemu, bo krótsza jest droga i mniejsze obciążenie ich sieci szkieletowej. I uposażenie jest XY, który oczywiście ma szybsze dostarczenie contentu. Natomiast w przypadku takich procesów, jak jest pewna stanowość, gdzie musimy zapisywać dane, tak jak w grach, gdzie się logujemy, gdzie chodzimy po świecie MMO, czy w świecie „Diablo”, tam jednak cały czas musimy coś zapisać, musimy coś przeliczyć. Tego typu procesy nie skalują się aż tak dobrze, jak dostarczanie statycznego contentu, więc okej, możemy sobie dołożyć serwerów i na pewno Blizzard dołożył tych serwerów. Nie oszukujmy się, od premiery „Diablo 3” minęło już… minęła ponad dekadę.
„Trzy” więcej mają.
Tak więc na pewno dołożył, chociaż trzy laptopy. Na pewno też technologia poszła do przodu i na pewno zrobili spory upgrade, może czapkę lepszą, może nawet ściągawkę lepszą, czy co to tam. Natomiast to nie jest to. To nie jest jedyny sposób, a właściwie nie zawsze skuteczny.
Kreatywne rozwiązania problemów wydajnościowych
Natomiast podeszli. W przypadku premiery „Diablo”, która miała miejsce w czerwcu, podeszli do tego troszkę bardziej kreatywnie. Otóż wypuścili dwie wersje gry. Jedna była wersją Deluxe, która była odpowiednio droższa dla VIP-ów, dla VIP-ów, i która dawała cztery dni wcześniej dostęp do gry.
No proszę Cię, nie skusiłbyś się.
No i co to daje? To nam daje oczywiście rozłożenie tego ruchu w czasie. Dzięki temu nie mamy tego piku od tego maksymalnego szczytu obciążenia w jednym momencie, kiedy wszyscy chcą zbadać daną grę i się zalogować tylko na dzień dobry cztery dni wcześniej i przez te cztery dni logują się tylko ci użytkownicy, którzy po przejściach zapłacić więcej.
I ten chce zapłacić więcej.
Ale oni zapłacili więcej i byli tymi pierwszymi na Pudelku, którzy się pochwalić, że „ja już grałem”, bo cztery dni wcześniej już względy takie, że tak powiem, społecznościowe ktoś, kto chce szybciej, ale…
Jaka.
To jest bardzo sprytne podejście, bo po pierwsze rozłożenie tego loadu to jest zawsze sposób na zwiększenie wydajności, uniknięcie kłopotu, uniknięcia kłopotu. Spróbujmy tych użytkowników podzielić, czy to ze względu na lokalizację, czyli tak jak robi Netflix, czy spróbujmy podzielić czasowo. Po prostu podzielmy je w kubełki czasowe. Ale Blizzard zrobił jeszcze tak fajnie, że jeszcze kazał zapłacić tym użytkownikom za to, że optymalizują ich load. Ta edycja była droższa. Oczywiście były głosy niezadowolenia części graczy, ale nie oszukujmy się, ilościowo egzemplarzy zostało sprzedane tyle samo, w sensie ogólnie, ale część z nich została sprzedana po wzroście cen, w większej cenie. Więc to jest perfekcyjne podejście do problemu optymalizacji w takim właśnie szczytowych momentach i technicznego zostało rozwiązane świetnie. Bo użytkownicy podzieleni na pewne grupy. Z punktu biznesowego zostało rozwiązane jeszcze lepiej, bo mieliśmy z tego jeszcze większy zysk, więc to jest perfekcyjna.
Idealnie by to ktoś, kto skopiuje, by następną wersję naszego Clouda wypuszczać też dla VIP-ów, bo zrobimy trochę drożej.
No, to jest pewne, że trzeba zrobić go trochę taniej wcześniej, bo to jest czas na podejście i w przypadku takich pewnie głównie gier, bo ciężko sobie wyobrazić, żeby w przypadku jakiś aplikacji biznesowych coś takiego zrobić, chociaż to też może mieć pewne, pewne walory tutaj właśnie takiego ograniczonego dobra.
Zastanówmy się, jak to w tym. W aktualizacjach systemów działanie. Wychodzi taka łata na dopisanie. A wiadomo, że jest prikaz odgórny, że masz mieć Windę w urzędzie i cierpieć będziesz. I wtedy na przykład wszystkie gdzieś tam korporacje mogą… czy korporacje mogą wygenerować ten ruch? Bo zwykły użytkownik to może sobie zechcieć odwlec taką aktualizację. Ale w korporacjach jest tak, że jest nadrzędne ID, które po prostu ustawia datę takiej aktualizacji. I to się robi dla setek, jeśli nie tysięcy użytkowników naraz. Więc leci sobie jak ja. Tylko że to może być uderzenie, bo do jakiegoś lokalnego repo to się zwykle jakoś tak kasuje. No ale mimo wszystko tu też następuje pewne podzielenie użytkowników.
Czyli znowu została jakaś lokalizacja.
Czyli to ID najpierw dostanie te wszystkie łaty i rozprowadzić po swoim, czyli już nie musimy nękać tego dostawcy głównego o te materiały.
De facto Content Delivery Network.
Tak. Jak partycjonowanie czy to danych, czy użytkowników, czy czasu, czy czasu, to są też sposoby na optymalizację. Czasami warto zastanowić się, czy zamiast, że tak powiem, zasoby, czy nasze dane, czy nie jesteśmy w stanie w naszym produkcie, w naszym systemie, cokolwiek robimy, podzielić składników w ten sposób.
I o ile sztuczne. O ile Greta jest szczęśliwsza, że aż tyle tego złego węgla nie wypala.
Tak, tak.
Antywzorzec optymalizacji: Sklep z zabawkami
Natomiast przeciwną stroną tego rozwiązania, tego problemu, jest sytuacja, którą natknąłem się już kilka lat temu – cztery, pięć lat temu. W każdym bądź razie w takich czasach jeszcze współczesnych to nie było. Rok 2000. Pierwszy to było względnie niedawno. Ani czasy pradziadków. Nie, to było to było względnie dawno, kiedy można powiedzieć. Wydawać by się mogło, że strony internetowe powinny być w miarę zoptymalizowane.
Otóż React był?
Był. Akurat ta strona nie była. Ona wtedy za mała. Miałem potrzebę kupienia zabawki dla jakiegoś dziecka. Może tak być. Może tak być i tego się trzymajmy. I wszedłem. Wszedłem na stronę internetową jednego z większych sklepów z zabawkami w Polsce. To było w godzinach takich późno popołudniowych, czyli wtedy, kiedy ten ruch faktycznie jest największy. I pewnie tych klientów rzeczywiście jest dużo. I zamiast strony tego, tego sklepu rzeczonego, ukazał mi się piękny komunikat, że „W tym momencie nasze serwery są za bardzo obciążone. Proponujemy zrobienie sobie kawy i przyjście co jakieś 15 minut”. I tam był jakiś piękny licznik, jakaś tam graficzka i całe to wszystko. To było tak opracowane. Fajnie. To nie była jakaś taka wygenerowana stronka, gdzie po prostu błąd leciał, jak był timeout. Nie, no, ktoś ewidentnie specjalnie wymyślił taką funkcjonalność, że ja, będąc online, i tak muszę stać w kolejce.
Stać? Może siedzieć, nie? Taki, a mogę zrobić kawę tak, jak autorzy sugerują, mi sugerują. I pewnie część osób sobie pomyślała: „Ale wspaniali ci ludzie z tej strony! Ja jeszcze zdążę, kawę może kawa zrobić, nie muszę stać w kolejce będąc online”. No, no cudo.
Szczerze mówiąc, mnie to kuriozum tej sytuacji mnie tak poraziło i nawet do tej pory, jak sobie o tym myślę, jest to po prostu absurdalne, że ktoś w tej firmie, zamiast pochylić się nad faktycznym problemem wydajnościowym, który gdzieś tam mieli, postanowił wprowadzić…
Ależ partycjonowanie użytkowników!
No tak, tylko że to nie była wersja Deluxe. Ja nie czułem się lepiej, kupując rzeczonego „Diablo 4” w wersji Deluxe. Nie był lepszy od innych. Ja byłem po prostu człowiekiem w kolejce przed laptopem, więc to jest to. To jest antywzorzec totalny rozwiązań rozwiązania tego typu problemów. Zwłaszcza, że widać, że ktoś naprawdę to pomyślał, pomyślał, bo to była graficzka, ładnie zrobiona top world i na całą modłę myśleć. Naprawdę kilka historyjek w grze pewnie na to poszło, zamiast zastanowić się, co.
Podejrzewam, że wiedzieli, co jest przyczyną. Tylko że to nie było takie proste. Naprawienie to jest oczywista sprawa. Tylko co to jest biznes? To zależy tak naprawdę. Odwlekają w czasie moje wejścia na stronę. Po pierwsze odwleka w czasie zysk z tego, a po drugie, w moim przypadku, wolę sobie odwlekać do soboty, bo ja, widząc to, zrezygnowałem zupełnie z zakupu i sobie poszedłem na kawę, bo jednak kolejek w internecie nie zdzierżył.
Ale są pewne kolejki, na przykład u Kawki właśnie. To jest tu kawa, tam Kafka i kolejki. I da się.
No tak, bo to jest zupełnie inna kolejka. To do tego od tego typu można, to można ścierpieć. Więc to są takie dwa przykłady z mojej, z mojej strony.
Podsumowanie i pożegnanie
No fajne, fajne przykłady. Fajny ten przykład, który pokazuje tę cenną cechę, o którą powinniśmy dbać, że optymalizując, my zmniejszamy ten czas i jednocześnie ryzyko, że klient zrezygnuje. Tak, bo jeżeli mu coś zaczyna zamulać już cokolwiek, choć troszkę, to już się zaczyna niepokoić i zaczyna szukać to w sąsiedniej zakładce. A już nie daj Boże na jakiejś ruskiej wyszukiwarce konkurencji.
Wystarczy sobie przytoczyć tę statystykę, która ogólnie raz tam podaje, że każde ileś tam sekund w opóźnieniu generowania strony sprawia, że tracisz tyle i tyle procent klientów. To ma jakieś przełożenie? To rzeczywiście na wielkie ilości danych zostało przeliczone i to się nawet więcej na bieżąco przelicza, przelicza, bo przecież są prowadzone statystyki w ogóle jakiegoś tam z biznesowego punktu widzenia przeżywalności.
Dokładnie to zyskowności. Udowodniona pewna statystyczna prawidłowość, że jednak z dłuższym czasem oczekiwania idzie spadek konwersji, spadek monetyzacji, więc no naprawdę, no tutaj robienie kolejek w internecie nie jest nawet dobrze, ale wyszło jak zawsze.
Dokładnie.
Dobra, fajne przykłady. No i co? I chyba chyba sobie na tym zakończymy ten nasz temat wydajności, bo pewnie już tak długo z bólami.
Ja bym tu jeszcze na koniec chciał taką ciekawostkę, którą wyłapałem od ostatniego odcinka, bo tam się ten obraziłem na sformułowanie „spiętrzenie wątków” w wersji javowej tej książki, którą tam polecali. Notabene pod wszystkimi tymi odcinkami z wydajności tam się ten link powinien znaleźć, link czy tam link nawet bezpośrednio do tej książki, także. Tak czy siak, książkę polecam mimo „spiętrzenia wątków”. A co się okazuje, czym rzecz to jest? Wrzuciłem to w Google, proszę ja Was, i wyskoczyło mi tak: pierwsza strona – „Romanse i erotyka”, i czytamy co następuje: „W środkowym tomie cyklu następuje zarówno intensyfikacja wydarzeń, jak i spiętrzenie wątków. Robi się groźnie i jeszcze bardziej tajemniczo”. Czyli to pewnie o to chodziło.
Najwyraźniej, najwyraźniej jest oczywiste.
Czyli link do tej książki też wrzucimy?
No, może przynajmniej przynajmniej warto zobaczyć, jak to tam się zrobiło tajemniczo.
Dobra, to chyba wypada kończyć na dzisiaj. Mam nadzieję, że ten temat wydał się ciekawy. Omówiliśmy wyczerpująco. To dzięki, Wojtek. I wymyślimy coś na kolejny raz.
Dzięki, Michał.