Przyspieszamy aplikacje w javie

Dwie święte zasady optymalizacji M. Jacksona:

  • Nie optymalizuj.
  • Dla ekspertów – jeszcze nie optymalizuj.

Skoro zatem nie pałą go to kijem. Przyjrzyjmy się metodom przyspieszania działania programów bez dotykania kodu.
W sumie przyjrzyjmy się jednej z metod, a mianowicie metodzie polegającej na instalacji JVM na ramdisku.

Ramdisk – kto zacz?

Dysk w pamięci RAM. Sztuczka, nie mam pewności, ale trop dobry, wymyślona przez M$ w czasach wczesnego DOSa. Mając do dyspozycji tylko napęd floppy i aż 640KB RAM Bill postanowił, że pierwszą rzeczą w trakcie startu systemu będzie utworzenie w pamięci RAM niewielkiego dysku, na którym zostanie umieszczony system operacyjny. Wynikało to nie tylko z problemów z wydajnością odczytu I/O dla dyskietek, ale też z tak prozaicznej przyczyny jaką jest wymiana dyskietki w stacji dysków. W takiej sytuacji program chcąc odwołać się do zasobów systemu operacyjnego trafiał by w nośnik bez tego systemu. Trzeba by było ciągle podmieniać dyskietki. Zatem umieszczenie systemu na takim wirtualnym dysku w RAMie dawało dużo. Minusem jest oczywiście konieczność skopiowania plików co wydłuża czas startu systemu. No bez jaj… na moim pierwszym PC DOS 3.0 z ramdiskiem wstawał tylko 20 sekund dłużej niż bez ramdisku. Co przy starcie rzędu 1 minuty nie robiło większej różnicy. Szczególnie jak później można było swobodnie pracować (czytaj grać).
My wykorzystamy tą samą sztuczkę. Jak wiadomo odczyty z dysku są i dziś powolne. Szczególnie z dysków HDD, a SSD choć szybsze to nie dają takiej samej frajdy (są droższe po prostu). Przy okazji pobawimy się dystrybucją Mint (kolejny klon Debiana).

Pomysł na

Naszą zabawę będziemy prowadzić w oparciu o następujące zasady:

  • Porównujemy czas działania programu napisanego w javie wraz ze startem JVM.
  • Program jest uruchamiany na jre umieszczonym na „zwykłym” HDD oraz jre umieszczonym na ramdisku.
  • Korzystamy z Hot Spot 1.6.0_29.
  • Korzystamy z programu do liczenia całki z e^x przedstawionego w 2009 roku na blogu Przemelka.
  • Całość testów odbywa się na maszynie wirtualnej wyposażonej w 3GB RAM i jeden rdzeń

Tworzymy ramdisk

Zanim przejdziemy do zabawy z pomiarami musimy utworzyć ramdisk. Nie jest to takie banalne jak się wydaje. By wykorzystać tą sztuczkę na produkcji warto wyposażyć się serwer do którego będzie można podpiąć duuuuużoooo RAM. W sensie od 128GB w górę. W ostateczności wykorzystać karty Ramsan (pseudo RAM na PCI-E o pojemności ~900GB – kurewsko drogie ale odczyt na poziomie 2GB/s i zapis 1,5GB/s robi wrażenie o gwarantowanym 330k IOPS nie wspominając).
Wynika to z konieczności zapewnienia systemowi odpowiedniej ilości miejsca w RAMie tak by nie doszło do sytuacji kiedy bieżące działania muszą być zrzucane na swap, bo ramdisk zeżarł miejsce. Jak tak zacznie się dziać to zamiast zysków mamy poważne straty wydajności.
W pierwszej kolejności musimy trochę przekonfigurować parametry uruchomieniowe kernela. W tym celu otwieramy plik /boot/grub/grub.cfg jako root, odnajdujemy wpis:

Listing 1. pierwotna konfiguracja jądra

 linux	/boot/vmlinuz-3.0.0-13-generic root=UUID=704a9627-e6bf-49a1-be34-6edbedcca030 ro   quiet splash vt.handoff=7

i zamieniamy go na

Listing 2. nasza konfiguracja jądra

 linux	/boot/vmlinuz-3.0.0-13-generic root=UUID=704a9627-e6bf-49a1-be34-6edbedcca030 ro   quiet splash vt.handoff=7 ramdisk_size=262144

Jeżeli mamy wiele wpisów to wyszukujemy domyślnego uruchamianego jądra. Po co to? Otóż domyślnie ramdisk w linuxie służy tylko do załadowania podstawowego obrazu jądra przy starcie systemu i nie ma potrzeby by był duży. Ustawione jest zatem coś w okolicach 16MB. My na JRE potrzebujemy około 90MB. Musimy zatem trochę podnieść limity 😀

W kolejnym kroku (nadal jako root), tworzymy dysk na urządzeniu blokowym:

Listing 3. Utworzenie dysku na urządzeniu blokowym

 root$> mkfs -t ext3 -q /dev/ram1 131072

Używamy urządzenie /dev/ram1 bo tak. Znowuż, domyślnie w linuxie jest 16 urządzeń blokowych do utworzenia w RAM. Można podnieść tą ilość, ale na własne ryzyko. Wielkość dysku to 128MB (podana w KB).

Następne kroki to montaż dysku we wskazanym miejscu i skopiowanie JRE na dysk:

Listing 4. Prace dekoracyjno-wykończeniowe

 root$> mkdir jram
root$> cp -r java jram/.
root$> chmod -R 777 jram

Jak ktoś chce to wszystkie kroki z konsoli można wpakować do skryptu wrzucić do /etc/init.d/ i uruchamiać przy każdym starcie kompa automatycznie.

Lecimy z testami

Do testów posłuży nam program time któremu zapodamy jako argument polecenie uruchamiające program. Cały test w pełnej krasie:

Listing 5. Skrypt uruchamiający test

#!/bin/bash

echo 'uruchomienie z lokalizacji na HDD'
cd ~/calka/Java
time ~/java/bin/java Speed

echo 'Uruchomienie z lokalizacji w RAM'
cd ~/jram/calka/Java
time ~/jram/java/bin/java Speed

i…

Listing 5. Skrypt uruchamiający test

uruchomienie z lokalizacji na HDD
4

real    0m4.334s
user    0m4.100s
sys     0m0.056s
Uruchomienie z lokalizacji w RAM
4

real    0m4.257s
user    0m4.100s
sys     0m0.020s

co zaskakujące różnice nie są duże. Polecam jednak wykonanie dwóch sztuczek. Zamianę kolejności testów. Uruchomienie ich w „czwórkach” np. HDD, HDD, RAM, RAM albo HDD, RAM, RAM, HDD. Zobaczycie, że czasy wykonania zaczynają się bardziej rozjeżdżać na korzyść RAM.

Podsumowanie

To co tutaj przedstawiłem nie jest nowością. Tak jak w latach 80tych ramdisk ratował DOS tak i dziś może uratować przymuloną aplikację. Wynika to z faktu, że współczesne komputery są w sytuacji podobnej jak PC w latach 80tych. Ilość maksymalnego dostępnego RAM jest na tyle duża, że można jego część poświęcić na rzecz utworzenia ramdisku.
Zyskiem z tej zabawy jest przyspieszenie operacji I/O co będzie szczególnie korzystne jeżeli nasza aplikacja intensywnie korzysta z dysków.
Należy pamiętać też o wadach tego rozwiązania. Najpoważniejszą jest utrata danych z dysku w momencie awarii maszyny. Kolejną jest dłuższy czas startu systemu. Na koniec warto też pamiętać, że koszty wprowadzenia takiego rozwiązania (zakupu RAM) mogą być wysokie.
Co zatem warto umieścić w RAMie? Na pewno JVM, serwer aplikacyjny i paczki z aplikacjami. Jest to też dobre miejsce dla wszelkiej maści szablonów czy to JasperReports czy to Velocity. Generalnie do RAM można wepchnąć w praktyce cały nasz „Static content”, ale trzeba pamiętać, żeby mieć jego kopie na jakimś pewnym HDD. Jeżeli mamy bardzo dużo RAM to jest to też świetne miejsce na /tmp.
Na koniec warto jeszcze poczytać sobie o tworzeniu obrazów dysków i ich wgrywaniu. Czasami zrzucenie obrazu ramdisku przed wyłączeniem maszyny i następnie wgranie obrazu po włączeniu jest całkiem dobrym pomysłem.

8 myśli na temat “Przyspieszamy aplikacje w javie

  1. Czy ja wiem, czy takie drogie? 12GB ramu DDR III z ECC Kingstona (KVR1333D3E9SK3/12GI) kosztuje 358 PLN Brutto. (Bez ECC KHX1600C9D3K3/12GX to wydatek 249 Netto).

  2. @Qyon, drogie w ujęciu np. Ramsan-70 kosztującego tyle co Ferrari. Zresztą jak nie masz własnego-własnego serwera, a pracujesz na wynajętym dedyku. Biorąc pod uwagę ceny jakie ma np. Hostit (nasz kochany dostawca) to już zakup 12GB RAM to wydatek rzędu 160-200PLN/mies. a my mówimy tu o zapotrzebowaniu rzędu 128GB+.

  3. Po się tak męczyć z ramdysk’iem skoro można w dowolnym miejscu podmontować tmpfs? Nawet mając SSD można śmiało montować wszystkie katalogi target jako tmpfs, teoretycznie powinno to wydłużyć życie naszego dysku przez zmniejszenie operacji zapisu, oraz przyśpieszyć zapisywanie i odczyt wyników kompilacji. Jedynym minusem jest to że `mvn clean` się nam wywali gdyż nie będzie mógł usunąć katalogu target …

    Żeby się nie męczyć w ręczne montowanie wszystkich target we wszystkich modułach wystarczy wywołać coś takiego (oczywiscie z uprawnieniami root’a):

    for i in `find . -name „target”`; do
    rm -rf $i/* ; mount -t tmpfs tmpfs $i done

    dodatkowo zostanie wyczyszona zawartość katalogu target przed potmontownaiem go jako tmpfs

  4. @Dariusz, tmpfs ma jedną poważną wadę. Jest swapowalny. Co w połączeniu z jego dynamiczną zmianą wielkości może zakończyć się radosnym mieleniem swapu. Ramdisk nie będzie nam tworzył tego typu problemów oczywiście do czasu, aż nie zabraknie pamięci. Tyle tyko, że wtedy swapowane będą dane z RAM, a nie z ramdisku. Różnica jest bardzo subtelna, ale istotna w niektórych sytuacjach.

  5. @Kozilek, cóż w mojej sytuacji swap’owanie nigdy nie nastąpi … bo nie mam swap’u również dlatego żeby SSD nie zajeżdżać 😉

  6. Można i tak. Ja niestety mam w fabryczce zwykłe HDD na macierzy i swapowanie nie następuje tylko dlatego, że mamy dużo RAM. Teraz dążymy do ograniczenia I/O na macierzy.

  7. Ja mam taką moją prywatną zasadę, że jeżeli serwer zaczyna używać aktywnie swapa, to przegrałeś.

  8. @Qyon, dużo zależy od aplikacji. Czasami swapowanie choć spowalnia jest jedyna drogą. Szczególnie na starszych 32bitowych maszynach gdy obrabiasz dużo plików z danymi.

Napisz odpowiedź

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax