Sytuacja kobiet w IT w 2024 roku
6.07.20218 min
Jad Sarmo

Jad SarmoHead of Trading TechnologyB2C2

Dlaczego wybrałem Javę do tworzenia hiper wydajnych aplikacji

Sprawdź, dlaczego Java może być dobrym wyborem, jeśli chodzi o tworzenie aplikacji, które wymagają bardzo niskich opóźnień.

Dlaczego wybrałem Javę do tworzenia hiper wydajnych aplikacji

W świecie handlu wysoką częstotliwością automatyzowane aplikacje przetwarzają setki milionów sygnałów każdego dnia i składają tysiące zamówień na całym świecie. Aby pozostać konkurencyjnym, czas reakcji musi być ciągle w mikrosekundach, zwłaszcza podczas niespodziewanych godzin szczytu, tak jak w przypadku teorii o nazwie “Black Swan”. 

W typowej architekturze sygnały związane z handlem instrumentami finansowymi zostaną przekonwertowane na pojedynczy format danych (giełdy wykorzystują wiele protokołów, np. TCP/IP, UDP Multicast, oraz wiele formatów, takich jak protokół binarny, SBE, JSON, FIX itd.).

Takie znormalizowane wiadomości są wysyłane do algorytmicznych serwisów, silników statycznych, UI, serwerów logowania oraz baz danych wszystkich rodzajów (in-memory, fizycznych baz danych i rozproszonych baz danych). 

Jakakolwiek latencja może być kosztowna w skutkach, spowodowana np., decyzjami opartymi o stare ceny lub to, że zamówienie wejdzie za późno na rynek. 

Aby nadrobić te kilka istotnych milisekund, większość organizacji inwestuje w drogi sprzęt: serwery z podkręconymi procesorami chłodzonymi cieczą (w 2020 można już było kupić serwer z 56 rdzeniami i 5.6 GHz oraz 1 TB RAMu), kolokację w wielu centrach danych, przełączniki sieciowe typu “high-end”, dedykowane linie podmorskie (Hibernian Express jest tutaj głównym dostawcą), a nawet radiolinie. 

Często można zobaczyć niestandardowy kernel Linuksa, który pomija system operacyjny, co jest potrzebne do wykonania przeskoku danych bezpośrednio z karty sieciowej do aplikacji, komunikacji międzyprocesowej (ang. Interprocess communication), czy nawet do programowanych układów (FPGA). 

Jeśli chodzi o języki programowania, to C++ byłby pierwszym kandydatem do tworzenia aplikacji po stronie serwera: jest szybki, chyba najbliższy kodowi maszynowemu i jak już się go skompiluje na docelową platformę, to daje nam powtarzalny czas przetwarzania. 

Stwierdziliśmy jednak, że chcemy czegoś innego. 

Przez poprzednie 14 lat byliśmy konkurencyjni w przestrzeni algorytmicznej FX w Javie i korzystaliśmy z budżetowego, lecz świetnego sprzętu.


Mając mały zespół i ograniczone zasoby oraz niedobór dobrych developerów na rynku, z Javą mogliśmy szybko dodać usprawnienia do programów, ponieważ ekosystem tego języka pozwala na szybszy czas wprowadzania na rynek, niż wszystkie języki pochodne od C. Usprawnienie, które wymyślono rano, może być już na produkcji po południu. 

W porównaniu do wielkich korporacji, które potrzebują tygodni, czy nawet miesięcy na nawet najmniejszą aktualizację oprogramowania, ta cecha Javy naprawdę dużo nam tutaj daje. W obszarze, w którym jeden błąd może spowodować utratę dochodów na poziomie całego roku w przeciągu sekund, nie byliśmy gotowi na poświęcenie jakości. Implementowaliśmy rygorystyczne środowisko Agile, włączając w to Jenkinsa, Maven, testy jednostkowe, nocne budowanie oraz Jirę, która korzystała z wielu bibliotek i projektów open source.

Dzięki Javie developerzy mogą się skupić na intuicyjnej i obiektowej logice biznesowej, zamiast na debugowaniu niejasnych zrzutów pamięci, czy zarządzaniu wskaźnikami, jak ma to miejsce w C++. Co więcej, dzięki solidnemu zarządzaniu pamięcią w Javie, juniorzy mogą już robić coś wartościowego bez większego ryzyka i to pierwszego dnia pracy.

Dzięki dobrym wzorcom projektowym i porządnemu podejściu do kodowania możliwe jest osiągnięcie poziomu latencji C++ z Javą. 

Na przykład, Java zoptymalizuje i skompiluje najlepszą ścieżkę, jaką można zaobserwować podczas uruchomienia aplikacji, ale C++ kompiluje wszystko wcześniej, więc nawet niewykorzystane metody nadal będą częścią finalnej i wykonywanej binarki. 

Jest tutaj jednak jeden spory problem. To, co sprawia, że Java jest tak mocnym i fajnym językiem, jest również jej największą wadą (przynajmniej dla aplikacji, które muszą bardzo szybko działać): jest to JVM.

  1. Java kompiluje kod, jak leci (JIT - kompilator Just in Time), co oznacza, że wywołuje opóźnienie kompilacji, w momencie, w którym spotyka jakiś kod. 
  2. Sposób, w jaki Java zarządza pamięcią, to alokacja części pamięci w swojej stercie. Często bywa i tak, że Java wyczyści miejsce i usunie stare obiekty, aby zrobić miejsce na nowe. Głównym problemem jest to, że aby coś dokładnie policzyć, to wątki aplikacji muszą zostać na chwilę zamrożone - proces ten jest znany jako odśmiecanie (ang. Garbage Collection). 


Garbage Collection to główny powód, dla którego deweloperzy aplikacji o niskim poziomie latencji mogą odrzucić Javę. Mamy jednak trochę JVMów dostępnych na rynku. 

Najpowszechniejszym jest Oracle Hotspot JVM, który jest dość szeroko używany w społeczności Javy, głównie ze względów historycznych. 

Istnieje jednak alternatywa dla bardzo wymagających aplikacji - nazywa się Zing. W świetny sposób zastępuje Oracle Hotspot JVM i adresuje jednakowo pauzy GC oraz problemy kompilacji JIT. 

Przyjrzyjmy się niektórym problemom, które są tożsame z używaniem Javy i innych rozwiązań. 

Czym jest kompilator Just-In-Time Javy 

Języki takie jak C++ nazywamy językami kompilowanymi, ponieważ dostarczany kod jest całkowicie binarny i możliwy do wykonania bezpośrednio w procesorze. 

PHP, czy Perl to języki interpretowane, ponieważ tłumacz, którego instalujemy na maszynie docelowej, kompiluje każdą linijkę kodu, w miarę jego wykonywania. 

A Java jest gdzieś pomiędzy: kompiluje kod do Java bytecode, który może być z kolei kompilowany do kodu binarnego, kiedy zachodzi taka potrzeba. 

Powód, dla którego Java nie kompiluje kodu w czasie uruchomienia, ma coś wspólnego z długofalową optymalizacją. Obserwując aplikację i analizując wywołania metod w czasie rzeczywistym, Java kompiluje często wywoływane części kodu. Może nawet robić założenia, opierając się na doświadczeniu (np. że ta część kodu nigdy się nie wywołuje, albo że ten obiekt to zawsze String). 

Nasz skompilowany już kod jest zatem bardzo szybki. Ale mamy trzy minusy: 

  1. Dana metoda musi zostać wywołana odpowiednią liczbę razy, aby mogła przekroczyć próg zanim będziemy mogli ją zoptymalizować i skompilować (limit można konfigurować, ale w okolicach 1000 wywołań). Do tego czasu kod, który nie jest zoptymalizowany, wykonywany jest wolniej. Istnieje kompromis między szybszą kompilacją i kompilacją o wysokiej jakości (jeśli założenia byłyby błędne, można ponieść koszty rekompilacji).  
  2. Kiedy restartujemy aplikację Javy, to wracamy do początku i czekamy na ponowne osiągnięcie progu. 
  3. Niektóre aplikacje (jak te nasze) mają czasem trochę rzadkich, ale niezwykle istotnych metod, które można wywołać tylko kilka razy, ale trzeba być niezwykle szybkim, kiedy się to robi (pomyśl o ryzyku albo procesie stop-loss, który wywołuje się tylko w przypadkach kryzysowych). 


Zing radzi sobie z tymi problemami dzięki temu, że JVM zapisuje stan kompilowanych metod i klas, w czymś, co tam się nazywa profil. Ta unikalna funkcja o nazwie ReadyNow!® oznacza, że aplikacje Javy zawsze działają w optymalnej prędkości, nawet po restarcie. 

Gdy restartujesz swoją aplikację za pomocą obecnego profilu, Azul JVM natychmiast przypomina sobie ponownie swoje poprzednie decyzje i kompiluje metody bezpośrednio, rozwiązując problem rozgrzewania się Javy. 

Co więcej, można zbudować profil w środowisku developerskim, aby naśladować zachowanie produkcyjne. Zoptymalizowany profil może być poddany deploymentowi na produkcji, jeśli wiemy, że wszystkie ścieżki kryteriów są kompilowane i optymalizowane. 

Poniższy graf pokazuje maksymalną latencję aplikacji handlowej (w symulowanym środowisku). 

Wygenerowano przy pomocy HdrHistogram


Duża maksymalna latencja Hotspot JVM jest wyraźnie widoczna podczas gdy latencja Zing pozostaje w miarę równa. Percentyl dystrybucji wskazuje na to, że w 1% procencie przypadków, Hotspot JVM powoduje latencję 16 razy gorszą od Zing JVM. 

Rozwiązywanie pauz w Garbage Collection

Kolejnym problemem jest to, że podczas odśmiecania cała aplikacja może się zamrozić na kilka milisekund, czy nawet sekund (opóźnienie wzrasta wraz ze złożonością kodu i rozmiarem sterty), a co gorsza, nie ma się nad tym w ogóle kontroli. 

Gdy zatrzymywanie aplikacji na kilka milisekund, czy nawet sekund może być do zaakceptowania dla wielu aplikacji Javy, to jest to niestety katastrofa dla aplikacji, gdzie potrzeba niskich opóźnień - obojętnie, czy mówimy o branży motoryzacyjnej, medycznej, czy sektorze finansowym. 

Wpływ Garbage Collectora jest niezwykle ważny dla developerów Javy - pełne odśmiecanie często określa się jako “pauzą, podczas której zatrzymuje się cały świat” - zamraża ona całą aplikację.  

Na przestrzeni lat, wiele algorytmów GC próbowało iść na kompromis, jeśli chodzi o przepustowość (czyli ile pamięci zużywa się na logikę aplikacji bez odśmiecania) i pauzy dla Garbage Collectora (na jak długo mogę zatrzymać aplikację?)

Od czasu Javy 9, G1 collector był domyślnym Garbage Collectorem, a jego praca polegała głównie na dzieleniu pauz w odniesieniu do docelowego czasu użytkownika. Zazwyczaj mamy krótsze pauzy, ale kosztem mniejszej wydajności procesora. Co więcej, czas pauzy zwiększa się wraz ze zwiększaniem się wielkości sterty. 

Java oferuje dużo funkcji, aby dostroić odśmiecanie (oraz JVM jako takie) - od rozmiaru sterty do algorytmu odśmiecania oraz liczby wątków przypisanych do Garbage Colletora. Często można zobaczyć skonfigurowane aplikacje Javy z mnóstwem niestandardowych opcji: 


Przykład opcji wiersza poleceń w Javie


Wielu developerów (w tym nasi) zwróciło się ku wielu technikom, tylko po to, żeby całkowicie uniknąć GC. Głównie chodzi o to, że jeśli stworzymy mniej opcji, to będziemy mieli mniej obiektów do odśmiecania. 

Jest pewna stara technika (której ciągle się używa), polegająca na korzystaniu z puli obiektów, które są gotowe do użycia. Connection pool bazy danych zatrzyma referencję dla 10 otwartych połączeń, które są gotowe do użycia, kiedy tylko będziemy musieli ich użyć. 

Wielowątkowość często wymaga blokad, które powodują opóźnienie synchronizacji oraz pauzy (zwłaszcza w przypadku, w którym dzielimy się zasobami). Przykładowo, dość często używa się tu bufora cyklicznego wraz z wieloma wątkami, które piszą i czytają bez konieczności używania blokad. 

Frustracja powoduje, że wielu ekspertów zdecydowało się na całkowite nadpisywanie zarządzania pamięcią w Javie i zarządzanie alokacją pamięci samodzielnie, co, podczas rozwiązywania jakiegoś problemu, stwarza większą złożoność i ryzyko. 

W tym kontekście staje się oczywiste, że powinniśmy rozważyć inne JVMy - decydujemy się na Azul Zing JVM. 

Szybko jesteśmy w stanie osiągnąć bardzo wysoką wydajność z małymi pauzami. 

Dzieje się tak, ponieważ Zing wykorzystuje unikalny collector, który nazywa się C4 (Continuously Concurrent Compacting Collector) - pozwala on na odśmiecanie bez pauz, niezależnie od rozmiaru sterty (aż do 8 TB). 

Można to osiągnąć przez współbieżne mapowanie i kondensowanie pamięci, gdy aplikacja jest cały czas uruchomiona. 

Co więcej, nie wymaga to żadnych zmian w kodzie i zarówno latencja jak i usprawnienia w prędkości są od razu widoczne, bez potrzeby długiej konfiguracji. 

W takim kontekście programiści Javy mogą się cieszyć najlepszymi rzeczami z tych dwóch światów - prostotą Javy (nie ma co panikować przy tworzeniu nowych obiektów) i podstawową wydajnością Zing pozwalającą na super przewidywalną latencję w całym systemie. 

Dzięki GC easy, uniwersalnemu narzędziu do analizy logów odśmiecanie, możemy szybko porównać z JVM w prawdziwej zautomatyzowanej aplikacji handlowej (w symulowanym środowisku). 
 


W naszej aplikacji przerwy GC są o 180 razy mniejsze z Zing, niż byłyby ze standardowym Oracle Hotspot JVM.

Jeszcze bardziej imponujące jest to, że podczas gdy pauzy GC zwykle odpowiadają rzeczywistym czasie pauz aplikacji, Zing Smart GC zwykle pracuje równolegle z minimalną pauzą lub jej brakiem.

Podsumowanie

Podsumowując, nadal można osiągnąć wysoką wydajność i niską latencję, jednocześnie ciesząc się prostotą i zorientowanym na biznes charakterem Javy.

Podczas gdy C++ jest używany do konkretnych komponentów niskiego poziomu, takich jak sterowniki, bazy danych, kompilatory i systemy operacyjne, większość rzeczywistych aplikacji można napisać w Javie, nawet jeśli mówimy o tych najbardziej wymagających.

To właśnie dlatego, według Oracle, Java jest językiem programowania numer 1, z milionami programistów i ponad 51 miliardami JVM-ów na całym świecie.

Dziękuję za uwagę!


Oryginał tekstu w języku angielskim możesz przeczytać tutaj

<p>Loading...</p>