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.