Jak znacznie poprawić wydajność Java?

23

Zespół LMAX przedstawił prezentację na temat tego, w jaki sposób byli w stanie wykonać 100 000 TPS przy opóźnieniu krótszym niż 1 ms . Utworzyli kopię zapasową tej prezentacji za pomocą bloga , artykułu technicznego (PDF) i samego kodu źródłowego .

Niedawno Martin Fowler opublikował doskonały artykuł na temat architektury LMAX i wspomina, że ​​są w stanie obsłużyć sześć milionów zamówień na sekundę, i podkreśla kilka kroków, które zespół podjął, aby podnieść wydajność o kolejny rząd wielkości.

Jak dotąd wyjaśniłem, że kluczem do szybkości procesora logiki biznesowej jest robienie wszystkiego sekwencyjnie, w pamięci. Samo zrobienie tego (i nic naprawdę głupiego) pozwala programistom napisać kod, który może przetwarzać 10K TPS.

Następnie odkryli, że skoncentrowanie się na prostych elementach dobrego kodu może zwiększyć to do zakresu 100 000 TPS. Wymaga to tylko dobrze skonstruowanego kodu i małych metod - w gruncie rzeczy pozwala to Hotspotowi lepiej wykonać optymalizację, a procesory są bardziej wydajne w buforowaniu kodu podczas działania.

Trzeba było nieco sprytniejszego, aby przejść o kolejny rząd wielkości. Zespół LMAX uznał, że jest tam kilka pomocnych rzeczy. Jednym z nich było napisanie niestandardowych implementacji kolekcji Java, które zostały zaprojektowane tak, aby były przyjazne dla pamięci podręcznej i ostrożne w przypadku śmieci.

Inną techniką osiągnięcia tego najwyższego poziomu wydajności jest zwrócenie uwagi na testy wydajności. Od dawna zauważyłem, że ludzie dużo mówią o technikach poprawy wydajności, ale jedyną rzeczą, która naprawdę robi różnicę, jest jej przetestowanie

Fowler wspomniał, że znaleziono kilka rzeczy, ale wspomniał tylko o kilku.

Czy istnieją inne architektury, biblioteki, techniki lub „rzeczy” pomocne w osiągnięciu takiego poziomu wydajności?

Dakotah North
źródło
11
„Jakie inne architektury, biblioteki, techniki lub„ rzeczy ”są pomocne w osiągnięciu takiego poziomu wydajności?” Po co pytać? Że cytat jest ostateczna lista. Istnieje wiele innych rzeczy, z których żadna nie ma podobnego wpływu na przedmioty z tej listy. Cokolwiek innego, co ktokolwiek może wymienić, nie będzie tak pomocne jak ta lista. Po co prosić o złe pomysły, skoro zacytowałeś jedną z najlepszych list optymalizacyjnych, jakie kiedykolwiek stworzono?
S.Lott,
Byłoby miło dowiedzieć się, jakich narzędzi użyli, aby zobaczyć, jak wygenerowany kod działa w systemie.
1
Słyszałem o ludziach przysięgających na wszelkiego rodzaju techniki. Najbardziej skuteczne okazało się profilowanie na poziomie systemu. Może pokazać ci wąskie gardła w sposobie korzystania z systemu przez Twój program i obciążenie pracą. Sugerowałbym stosowanie się do dobrze znanych wytycznych dotyczących wydajności i pisanie kodu modułowego, aby można go było później łatwo dostroić ... Nie sądzę, aby można było pomylić się z profilowaniem systemu.
ritesh

Odpowiedzi:

21

Istnieją wszelkiego rodzaju techniki przetwarzania transakcji o wysokiej wydajności, a ta w artykule Fowlera jest tylko jedną z wielu najnowocześniejszych. Zamiast wymieniać kilka technik, które mogą, ale nie muszą odnosić się do czyjejś sytuacji, myślę, że lepiej jest omówić podstawowe zasady i sposób, w jaki LMAX odnosi się do wielu z nich.

W przypadku systemu przetwarzania transakcji na dużą skalę chcesz wykonać jak najwięcej następujących czynności:

  1. Minimalizuj czas spędzany na najwolniejszych poziomach pamięci. Od najszybszego do najwolniejszego na nowoczesnym serwerze masz: CPU / L1 -> L2 -> L3 -> RAM -> Dysk / LAN -> WAN. Skok z nawet najszybszego współczesnego dysku magnetycznego do najwolniejszej pamięci RAM jest ponad 1000x dla sekwencyjnego dostępu; losowy dostęp jest jeszcze gorszy.

  2. Zminimalizuj lub wyeliminuj czas oczekiwania . Oznacza to współdzielenie jak najmniejszej liczby stanów, a jeśli stan musi być współużytkowany, w miarę możliwości unikaj jawnych blokad.

  3. Rozłóż obciążenie. Procesory nie dostał się znacznie szybciej w ciągu ostatnich kilku lat, ale nie dostał mniejsze, a 8 rdzeni jest dość powszechne na serwerze. Poza tym możesz nawet rozłożyć pracę na wiele komputerów, co jest podejściem Google; wielką zaletą jest to, że skaluje wszystko, łącznie z I / O.

Według Fowler, LMAX stosuje następujące podejście do każdego z nich:

  1. Zachowaj cały stan w pamięci przez cały czas. Większość silników baz danych i tak to zrobi, jeśli cała baza danych zmieści się w pamięci, ale nie chcą pozostawić niczego przypadkowi, co jest zrozumiałe na platformie transakcyjnej w czasie rzeczywistym. Aby to zrobić bez dodawania mnóstwa ryzyka, musieli zbudować zestaw lekkiej infrastruktury do tworzenia kopii zapasowych i przełączania awaryjnego.

  2. Użyj kolejki bez blokady („disruptor”) dla strumienia zdarzeń wejściowych. W przeciwieństwie do tradycyjnych trwałych kolejek wiadomości, które definitywnie nie blokują się i w rzeczywistości zwykle wiążą się z boleśnie powolnymi rozproszonymi transakcjami .

  3. Niewiele. LMAX rzuca to pod magistralę na tej podstawie, że obciążenia są współzależne; wynik jednego zmienia parametry dla innych. Jest to krytyczne zastrzeżenie, które Fowler wyraźnie wzywa. Oni robią jakieś zastosowanie współbieżności w celu zapewnienia możliwości przełączania awaryjnego, ale wszystkie logiki biznesowej jest przetwarzany na pojedynczym wątku .

LMAX to nie jedyne podejście do OLTP na dużą skalę. I chociaż jest to całkiem genialne samo w sobie, nie musisz używać najnowocześniejszych technik, aby osiągnąć ten poziom wydajności.

Ze wszystkich powyższych zasad nr 3 jest prawdopodobnie najważniejszy i najbardziej skuteczny, ponieważ, szczerze mówiąc, sprzęt jest tani. Jeśli potrafisz właściwie podzielić obciążenie pracą na pół tuzina rdzeni i kilkadziesiąt maszyn, to niebo jest granicą dla konwencjonalnych technik obliczeń równoległych . Zdziwiłbyś się, ile przepustowości możesz osiągnąć, korzystając tylko z szeregu kolejek wiadomości i dystrybutora działającego w trybie round-robin. Oczywiście nie jest tak wydajny jak LMAX - w rzeczywistości nie jest nawet bliski - ale przepustowość, opóźnienia i opłacalność to osobne problemy, a tutaj mówimy konkretnie o przepustowości.

Jeśli masz te same specjalne potrzeby, co LMAX - w szczególności stan współdzielony, który odpowiada rzeczywistości biznesowej w przeciwieństwie do pochopnego wyboru projektu - sugeruję wypróbowanie ich komponentu, ponieważ nie widziałem zbyt wiele w innym przypadku jest to dostosowane do tych wymagań. Ale jeśli mówimy po prostu o wysokiej skalowalności, zachęcam do dalszych badań systemów rozproszonych, ponieważ są one kanonicznym podejściem stosowanym przez większość organizacji (Hadoop i powiązane projekty, ESB i powiązane architektury, CQRS, które Fowler również wzmianki i tak dalej).

Dyski SSD również staną się przełomem; zapewne już są. Możesz teraz mieć stałe miejsce do przechowywania o podobnych czasach dostępu do pamięci RAM, i chociaż dyski SSD klasy serwerowej są nadal strasznie drogie, ostatecznie spadną w cenie, gdy wzrośnie współczynnik adopcji. Został on dogłębnie zbadany, a wyniki są dość zadziwiające i z czasem będą się poprawiać, więc cała koncepcja „zachowaj wszystko w pamięci” jest o wiele mniej ważna niż kiedyś. Więc jeszcze raz staram się skupić na współbieżności, gdy tylko jest to możliwe.

Aaronaught
źródło
Dyskusja na temat zasad leżących u podstaw zasad jest świetna, a twój komentarz jest świetny i ... chyba że pismo Fowlera nie zawierało wzmianki w notatce o buforowaniu nieprzewidzianych algorytmów en.wikipedia.org/wiki/Cache-oblivious_alameterm (który ładnie pasuje do kategoria nr 1, którą masz powyżej) Nigdy bym się na nich nie natknęła. Więc ... jeśli chodzi o każdą z powyższych kategorii, czy znasz 3 najważniejsze rzeczy, które dana osoba powinna wiedzieć?
Dakotah North
@Dakotah: Ja nawet nie zaczynają się martwić o cache miejscowości dopóki ja całkowicie wyeliminowane dysk I / O, czyli tam, gdzie większość czasu spędza czekając w większości zastosowań. Poza tym, co rozumiesz przez „3 najważniejsze rzeczy, które dana osoba powinna wiedzieć”? Top 3 co, aby wiedzieć o czym?
Aaronaught
Skok z opóźnienia dostępu do pamięci RAM (~ 10 ^ -9s) na opóźnienie dysku magnetycznego (~ 10 ^ -3s średni przypadek) to kolejne kilka rzędów wielkości większych niż 1000x. Nawet dyski SSD nadal mają czasy dostępu mierzone w setkach mikrosekund.
Sedate Alien
@Sateate: Opóźnienie tak, ale jest to bardziej kwestia przepustowości niż surowego opóźnienia, a kiedy miniesz czasy dostępu i osiągniesz całkowitą prędkość transferu, dyski nie są wcale takie złe. Dlatego dokonałem rozróżnienia między dostępem losowym a sekwencyjnym; w przypadku scenariuszy dostępu swobodnego staje się to przede wszystkim problemem z opóźnieniem.
Aaronaught
@Aaronaught: Po ponownym przeczytaniu przypuszczam, że masz rację. Być może należy podkreślić, że cały dostęp do danych powinien być możliwie jak najbardziej sekwencyjny; znaczące korzyści można również uzyskać, uzyskując dostęp do danych w kolejności z pamięci RAM.
Sedate Alien
10

Myślę, że największą lekcją, jaką można się z tego nauczyć, jest to, że musisz zacząć od podstaw:

  • Dobre algorytmy, odpowiednie struktury danych i nie robienie niczego „naprawdę głupiego”
  • Dobrze przemyślany kod
  • Test wydajności

Podczas testowania wydajności profilujesz swój kod, znajdujesz wąskie gardła i naprawiasz je jeden po drugim.

Zbyt wiele osób przeskakuje do części „napraw je jeden po drugim”. Spędzają dużo czasu, pisząc „niestandardowe implementacje kolekcji java”, ponieważ po prostu wiedzą, że cały powód, dla którego ich system jest wolny, wynika z braków pamięci podręcznej. Może to być czynnikiem przyczyniającym się, ale jeśli przejdziesz do poprawiania kodu niskiego poziomu, prawdopodobnie przegapisz większy problem korzystania z ArrayList, gdy powinieneś używać LinkedList lub że prawdziwy powód, dla którego twój system jest powolne, ponieważ ORM leniwie ładuje dzieci encji, a zatem wykonuje 400 oddzielnych podróży do bazy danych dla każdego żądania.

Adam Jaskiewicz
źródło
7

Nie będę specjalnie komentować kodu LMAX, ponieważ uważam, że jest on obszernie opisany, ale oto kilka przykładów rzeczy, które zrobiłem, które doprowadziły do ​​znacznej wymiernej poprawy wydajności.

Jak zawsze są to techniki, które należy zastosować, gdy wiesz, że masz problem i potrzebujesz poprawić wydajność - w przeciwnym razie prawdopodobnie przedwcześnie przeprowadzisz optymalizację.

  • Użyj odpowiedniej struktury danych i w razie potrzeby utwórz niestandardową - poprawny projekt struktury danych przewyższa ulepszenia, jakie kiedykolwiek uzyskasz dzięki mikrooptymalizacjom, więc zrób to najpierw. Jeśli twój algorytm zależy od wydajności w wielu szybkich odczytach losowego dostępu O (1), upewnij się, że masz strukturę danych, która to obsługuje! Warto przeskoczyć przez niektóre obręcze, aby to zrobić, np. Znaleźć sposób, w jaki możesz przedstawić swoje dane w tablicy, aby wykorzystać bardzo szybkie odczyty indeksowane O (1).
  • Procesor jest szybszy niż dostęp do pamięci - możesz wykonać sporo obliczeń w czasie potrzebnym do odczytania jednej losowej pamięci, jeśli pamięć nie znajduje się w pamięci podręcznej L1 / L2. Zwykle warto wykonać obliczenia, jeśli oszczędza to odczyt pamięci.
  • Pomóż kompilatorowi JIT w tworzeniu ostatecznych pól, metod i klas umożliwia określone optymalizacje, które naprawdę pomagają kompilatorowi JIT. Konkretne przykłady:

    • Kompilator może założyć, że klasa końcowa nie ma podklas, więc może przekształcić wirtualne wywołania metod w statyczne wywołania metod
    • Kompilator może traktować statyczne pola końcowe jako stałą w celu poprawienia wydajności, szczególnie jeśli stała jest następnie używana w obliczeniach, które można obliczać w czasie kompilacji.
    • Jeśli pole zawierające obiekt Java zostanie zainicjowane jako ostateczne, wówczas optymalizator może wyeliminować zarówno kontrolę zerową, jak i wysyłanie metody wirtualnej. Miły.
  • Zastąp klasy kolekcji tablicami - skutkuje to mniej czytelnym kodem i jest trudniejsze w utrzymaniu, ale prawie zawsze jest szybsze, ponieważ usuwa warstwę pośrednią i korzysta z wielu fajnych optymalizacji dostępu do tablicy. Zwykle dobry pomysł w wewnętrznych pętlach / kodzie wrażliwym na wydajność po zidentyfikowaniu go jako wąskiego gardła, ale unikaj inaczej ze względu na czytelność!

  • Używaj prymitywów tam, gdzie to możliwe - prymitywy są zasadniczo szybsze niż ich obiektowe odpowiedniki. W szczególności boks powoduje ogromne obciążenie i może powodować nieprzyjemne przerwy w GC. Nie zezwalaj na zapakowanie żadnych prymitywów, jeśli zależy Ci na wydajności / opóźnieniu.

  • Zminimalizuj blokowanie niskiego poziomu - zamki są bardzo drogie na niskim poziomie. Znajdź sposoby, aby całkowicie uniknąć blokowania lub zablokować na poziomie zgrubnym, abyś tylko musiał blokować sporadycznie duże bloki danych, a kod niskiego poziomu może kontynuować bez martwienia się o problemy z blokowaniem lub współbieżnością.

  • Unikaj przydzielania pamięci - może to faktycznie spowolnić ogólnie, ponieważ zbieranie śmieci JVM jest niezwykle wydajne, ale jest bardzo pomocne, jeśli próbujesz uzyskać bardzo małe opóźnienia i musisz zminimalizować przerwy GC. Istnieją specjalne struktury danych, których można użyć, aby uniknąć przydziału - w szczególności biblioteka http://javolution.org/ jest dla nich doskonała i godna uwagi.
mikera
źródło
Nie zgadzam się z ostatecznymi metodami . JIT jest w stanie dowiedzieć się, że metoda nigdy nie jest nadpisywana. Ponadto, jeśli podklasa zostanie załadowana później, może cofnąć optymalizację. Zauważ też, że „unikanie przydzielania pamięci” może również utrudnić pracę GC, a tym samym spowolnić - więc używaj go ostrożnie.
maaartinus,
@maaartinus: jeśli chodzi o finalniektóre zespoły JIT, mogą to rozgryźć , inne mogą nie. Jest to zależne od implementacji (podobnie jak wiele porad dotyczących strojenia wydajności). Zgadzam się na przydziały - musisz to porównać. Zwykle uważam, że lepiej jest wyeliminować przydziały, ale YMMV.
mikera
4

Poza tym, co już zostało podane w doskonałej odpowiedzi Aaronaught , chciałbym zauważyć, że taki kod może być trudny do opracowania, zrozumienia i debugowania. „Chociaż jest bardzo wydajny ... bardzo łatwo go spieprzyć ...”, jak wspomniał jeden z ich facetów na blogu LMAX .

  • Dla programisty przyzwyczajonego do tradycyjnych zapytań i blokad kodowanie nowego podejścia może przypominać jazdę na dzikim koniu. Przynajmniej takie było moje własne doświadczenie podczas eksperymentowania z Phaser, której koncepcji wspomniano w dokumencie technicznym LMAX. W tym sensie powiedziałbym, że to podejście zamienia rywalizację o blokadę na rywalizację mózgów programistów .

Biorąc pod uwagę powyższe, uważam, że osoby wybierające Disruptor i podobne podejścia lepiej upewniają się, że dysponują zasobami programistycznymi wystarczającymi do utrzymania rozwiązania.

Ogólnie rzecz biorąc, podejście Disruptor wydaje mi się dość obiecujące. Nawet jeśli Twoja firma nie może sobie pozwolić na wykorzystanie go, np. Z wyżej wymienionych powodów, rozważ przekonanie swojego kierownictwa do „zainwestowania” wysiłku w jego badanie (i ogólnie SEDA ) - ponieważ jeśli tego nie zrobi, jest szansa, że ​​któregoś dnia klienci zostawią ich na korzyść bardziej konkurencyjnego rozwiązania wymagającego 4x, 8x itp. mniej serwerów.

komar
źródło