Wydajność

Autor: Marcin Kasiński
21.01.2011 13:24:00 +0200

Wiadomo jest, że wydajność aplikacji napisanych w języku JAVA jest gorsza niż wydajność aplikacji napisanych np. w języku C/C++. Dzieje się tak dlatego, że aplikacje napisane w języku C są kompilowane do kodu maszynowego, natomiast aplikacje JAVA są kompilowane do pseudokodu, który jest potem interpretowany przez wirtualna maszynę JAVA. Plusem jednak programowania w języku JAVA jest, wydaje się, szybszy development. Należy tu również pamiętać, że nawet najszybszy język programowanie nie pomoże, jeżeli programista zastosuje niewydajny algorytm. Dlatego ja osobiście skłaniałbym się ku poprawie wydajności wykorzystywanych algorytmów i zostałbym przy języku JAVA niż zastanawiałbym się nad przejściem na inny język programowania celem poprawy wydajności aplikacji. w tej części postaram się zwrócić uwagę na kilka aspektów programowania w języku JAVA. Umieszczę tu również kilka uwag dotyczących ogólnych porad dotyczących programowania niezależnie od wykorzystywanego języka.

Tablice Tworzenie obiektów Wątki Wyjątki i obsługa błędów EJB Operacje na łańcuchach Operacje na plikach Pętle WebServices Sortowanie Algorytmy Inne

Wydajność a serwlety i JSP

Kompresja

Standardowo treść stron WWW klientowi przez serwer przesyłanych jest w postaci pełnego tekstu opisującego daną stronę. Jako, że dane te są przesyłane przez sieć, która w tej komunikacji zdecydowanie stanowi wąskie gardło wskazane jest aby zmniejszyć ilość danych przesyłanych przez sieć w komunikacji klient - serwer WWW. Wiadomo, że wszystkie dane opisujące stronę są istotne dla poprawnego jej wyświetlania nie można tu obciąć pewna część strony HTML. Z pomocą przychodzi nam tu kompresja stron WWW wysyłanych klientowi. Funkcjonalność ta polega na tym, że treść strony WWW przed wysłaniem je jest kompresowana i w takiej postaci trafia do klienta. Po stronie klienta przed wyświetleniem strona jest rozpakowowana i dopiero wtedy jest wyświetlana. Mimo że w komunikacji tracimy czas na kompresje i rozpakowywanie danych wydajność wzrasta. Dzieje się tak dlatego, że dane tekstowe, jakim z pewnością jest Ľródło strony WWW, bardzo dobrze się kompresują w związku z czym znacznie zmniejszamy tu ilość danych przesyłanych przez sieć. Należy tu jednak dodać , że to czy dane serwer będzie wysyłał w formie oryginalnej, czy skompresowanej nie zależy tylko i wyłącznie od serwera i jego konfiguracji, ale w większym stopniu od klienta. Jeśli serwer posiada funkcjonalność i klient wysyła do tego serwera żądanie o konkretną stronę WWW, serwer analizuje nagłówki tego żądania w poszukiwaniu nagłówka o nazwie Accept-Encoding. Jeśli ten nagłówek zawiera w sobie m.in. łańcuch 'gzip' oznacza to, że klient będzie potrafił obsłużyć skompresowaną treść strony. W przeciwnym razie niezależnie, czy serwer potrafi wysłać skompresowane dane, czy nie, do klienta zawsze wysyłane są strony w formie nieskompresowanej. Fragment kodu serwleta analizującego nagłówki żądania klienta i wysyłającego skompresowane lub nieskompresowane dane może mieć postać:


...
Enumeration e = ((HttpServletRequest)request).
getHeaders("Accept-Encoding");
while (e.hasMoreElements())
{
String header=(String)e.nextElement();
if (header!=null && (header.toUpperCase().indexOf("GZIP")>-1))
{
//Klient obsługuje skompresowany kontent
...
setGZIPContent(response)
out = new GZIPoutputStream(response.getOutputStream());
}
else
{
//Klient NIE obsługuje skompresowany kontent
...
out= response.getOutputStream();
}

Automatyczne przeładowywanie serwletów

W większości kontenerach serwletów mamy możliwość ustawienie parametru konkretnego serwleta określającego co jaki czas serwlet ten będzie przeładowywany. Niska wartość tego parametru jest przydatna tylko i wyłącznie w środowiskach rozwojowych, gdzie cały czas pracujemy nad serwletem i wskazane jest aby jak najszybciej zmiany poczynione w serwlecie były uwzględnione w kontenerze bez potrzeby resetowania kontenera. W przypadku środowiska produkcyjnego tą funkcjonalność zaleca się wyłączyć lub jeśli nie jest to możliwe zaleca się ustawić ten parametru na bardzo dużą wartość. W środowisku produkcyjnym nie występują zmiany serwletów, więc cykliczne przekompilowania serwletów nie będą miały sensu. Kompilacja serwleta zajmuje czas i zużycie procesora może to spowodować tylko spadek wydajności co w środowisku produkcyjnym nie jest wskazane.

Sesje HTTP

Sesje HTTP służą do przechowywania po stronie serwera stanów klienta potrzebnych do poprawnej pracy aplikacji. Dane te są zapisywane w jakiś trwały sposób po stronie serwera. Pierwsze załadowanie przez klienta strony będącej aplikacją internetową powoduje wygenerowanie przez serwer specjalnego unikalnego identyfikatora poprzez który dane klienta będą identyfikowane. Po tej operacji za każdym razem, kiedy klient odwołuje się do stron aplikacji, przekazuje on wcześniej wygenerowany przez serwer identyfikator. Może to nastąpić na dwa sposoby. Jednym sposobem jest przekazywanie identyfikatora sesji w nagłówkach HTTP. Drugim sposobem jest przekazywanie tego identyfikatora jako kolejnego argumentu wywołania strony. Dokleja się go na koniec zmiennej QUERY_STRING, określającej argumenty wywołania. Przykładem takiej aplikacji wykorzystującej sesje HTTP może być sklep internetowy, gdzie w sesji każdego klienta znajdują się dane jego bieżącego koszyka na zakupy.

Wielkość sesje HTTP

W sesji można umieścić dowolny obiekt, na którym można dokonać operacji serializacji oraz deserializacji. ważne jest natomiast aby bardzo uważnie rozważyć wielkość tych obiektów umieszczanych w sesji. Ważne jest to ze względu na wydajność. Wiadomo, że im więcej danych , tym dłużej trwa zapisanie ich w jakiejś trwałej pamięci.

Usunięcie obiektów sesje HTTP

Wskazane jest aby niepotrzebne już obiekty z sesji, czy też całych sesji HTTP usuwać zaraz po tym jak już nie będą potrzebne. przyspiesza to proces wyszukiwania danych związanych z daną sesją ponieważ pętla wyszukująca nie musi przeszukiwać danych, które już nie są potrzebne do poprawnej pracy aplikacji. Aby ustawić czas trwania sesji wykorzystujemy metodę: setMaxinactiveInterval(int interval) .Aby całkowicie usunąć obiekty danej sesji z pamięci trwałej wykorzystujemy metodę: invalidate()

Metody redirect i forward

Porównanie tych dwóch metod nie wymaga zbyt skomplikowanej analizy aby dowieĽć, że bardziej wydajne jest użycie metody forward niż redirect. Dzieje się tak, ponieważ w metodzie redirect, jak sama nazwa wskazuje, występuje całkowite przekierowanie żądania, gdzie jeszcze raz przeglądarka musi wygenerować żądanie do przekierowanego zasobu. W przypadku metody forward mamy do czynienia z wewnętrznym dla serwleta przekierowaniem, którego czas obsługi trwa zdecydowanie krócej.

HTTPS

Protokół HTTPS jest rozszerzeniem protokołu HTTP o szyfrowanie przesyłanych danych pomiędzy klientem a serwerem. Szyfrowanie to ma na celu niedopuszczenie do podsłuchania przesyłanych danych przez osoby trzecie. Dodatkowo poprzez analizę certyfikatów klient ma pewność, że serwer, z którym się połączył nie jest fałszywy. Dodatkowo serwer poprzez analizę certyfikatu klienta może zdecydować o przesłaniu klientowi stronę, o którą prosił, bądĽ nie. Ma to szczególne znaczenie w przypadku aplikacji biznesowych (banki oraz sklepy internetowe itp.), gdzie bardzo ważna jest poufność przesyłanych danych

Wydajność a bazy danych

W przypadku aplikacji współpracujących z bazą danych należy rozważyć kwestie wydajności zapytań wysyłanych do bazy danych, odpowiedniego projektu bazy danych oraz odpowiedniego projektu aplikacji.

Pula połączeń

W przypadku aplikacji standalone pula połączeń nie jest nam potrzebna i nie poprawi wydajności naszej aplikacji. W Przypadku aplikacji internetowej natomiast użycie puli połączeń niekiedy może drastycznie polepszyć wydajność. Pula połączeń jest magazynem kilku połączeń do bazy danych z których aplikacji korzysta podczas swojej pracy. Aby zrozumieć zalety użycia puli połączeń postaram się tu przedstawić dwie wersje aplikacji, jedna bez puli, a drugą z pulą połączeń. WyobraĽmy sobie aplikacje internetowa, która po kliknięciu na link w przeglądarce wykonuje jakieś operacje na bazie danych. W wersji bez puli połączeń po kliknięciu aplikacja łączy się z bazą danych, a dopiero następnie wykonuje operacje na bazie danych. W przypadku aplikacji z pulą połączeń po kliknięciu aplikacja nie łączy się z bazą danych, tylko z puli połączeń pobiera już istniejące połączenie do bazy danych i na tym połączeniu wykonuje operacje na bazie danych. Pula połączeń najczęściej inicjowana jest na starcie aplikacji i wtedy następuje taka ilość połączeń na jaką skonfigurowana jest pula połączeń. Standardowa implementacja puli połączeń polega na uruchamianiu podczas startu pewnej ilości połączeń (wartość startowa). Jeśli aplikacja podczas pracy zażąda połączenia z puli połączeń połączenie z puli do aplikacji jest zwracane, następnie aplikacja na tym połączeniu wykonuje operacje na bazie i kiedy połączenie nie jest już potrzebne połączenie to zwraca do puli połączeń, a wielkość puli zmniejsza się o jeden. W przypadku jeśli pula połączeń jest pusta aplikacja zazwyczaj dokonuje jeszcze kilku prób pobrania połączenia z puli i jeśli to nie pomoże, to zwraca błąd. Pula może być pusta ponieważ może tak się stać, że jednocześnie będziemy mieli wiele żądań operacji na bazie danych i przy kolejnym żądaniu może się okazać iż w danej chwili wszystkie połączenia są używane.

Klasa PreparedStatement

W większości dokumentów opisujących dobre praktyki w aplikacjach wykorzystujących bazy danych widzimy podpowiedĽ "używaj klas PreparedStatement". W przypadku zwykłej klasy Statement za każdym razem, kiedy chcemy wykonać dane polecenie SQL, przed jego wykonaniem następuje parsowanie oraz sprawdzenie poprawności składni zapytania. W przypadku klasy PreparedStatement wygląda to zupełnie inaczej. W pierwszej fazie tworzymy obiekt PreparedStatement, który jako parametr przyjmuje zapytanie SQL, w którym zamiast istotnych dynamicznych parametrów zapytania podajemy znak '?'.


PreparedStatement myPreparedStmt=
conn.prepareStatement(
"select position, name from tab1 where id=? and code=?");

W ten sposób stworzyliśmy zapytanie, które tylko jeden raz będzie sparsowane w celu zbadania poprawności zapytania. Każde wykonanie zapytania na bazie danych będzie polegało na przekazaniu do obiektu typu PreparedStatement parametrów zapytania oraz wykonania takie go zapytania bez powtarzania operacji parsowania i weryfikacji poprawności oraz do pobrania odpowiedzi bazy danych.



myPreparedStmt.setInt(1, id);
myPreparedStmt.setString(2, code);
ResultSet rs = myPreparedStmt.executeQuery();
while (rs.next()) {
position = rs.getLong(1);
name = rs.getString(2);


Warto tu zauważyć, iż do przekazania parametrów zapytania używamy odpowiednich metod setXXX klasy PreparedStatement, do operacji pobrania informacji z bazy danych metod getXXX klasy ResultSet, gdzie XXX określa jaki typ obiektów przekazujemy jako parametr lub pobieramy z bazy danych.

Operacje wsadowe

Aby omówić termin operacje wsadowe podzielę go na dwa w zasadzie oddzielne części, operacje wsadowe przy czytaniu oraz operacje wsadowe przy zmianach.

Operacje wsadowe przy czytaniu

Kiedy wysyłamy zapytanie do bazy danych zwracające wiersze baza do sterownika JDBC nie zwraca od razy wszystkie wiersze, tylko taką ilość wierszy, jaką ustawimy w odpowiednim parametrze sterownika JDBC, klasy Statement lub ResultSet określającym wielkość bufora rekordów zwracanych w jednej paczce wysyłanej od bazy do klienta. Nazwa tego parametru zależy od odpowiedniego sterownika JDBC. Za przykład opisania zasada działania sterownika w przypadku przyjmijmy zapytanie SQL zwracające 1000 rekordów oraz bufor rekordów ustawiony na 50. W takim przypadku pierwsza odpowiedz z bazy będzie zawierała pierwsze 50 rekordów odpowiedzi dopóki będziemy operowali tylko na tych pierwszych 50 rekordach. Jeśli odwołamy się do 51 rekordu (rekord spoza bieżącej paczki) sterownik wyśle żądanie do bazy danych żądanie przesłanie kolejnej paczki zawierającej rekordy od 51 do 100. Analogicznie będzie to wyglądało przy kolejnych rekordach aż do osiągnięcie końca rekordów z odpowiedzi. Widzimy tu, żę przy takiej konfiguracji jak tu przedstawionej odczytanie z bazy wszystkich rekordów odpowiedzi wymaga wysłania do bazy 20 żądań (1000/50). Operacje wysyłające te wewnętrzne żądania kolejnych bloków odpowiedzi do bazy dla programisty są przeĽroczyste. Ważne jest, aby mieć na uwadze fakt istnienia tego parametru oraz zasady pobierania odpowiedzi z bazy danych. W naszym przypadku zmieniając wartość bufora rekordów na 512 takich zmniejszymy ilość takich wewnętrznych żądań do bazy do 2. Należy wziąć pod uwagę, że wielkości tego bufora nie można zwiększać do woli. Musimy tu znaleĽć złoty środek pomiędzy ilością operacji sieciowych, a ilością pamięci wykorzystywanej do buforowania bloku zwracanych rekordów.

Aby ustawić wielkość bufora rekordów zwracanych podczas łączenia się z bazą danych np. w przypadku bazy danych Oracle może mieć postać:

Properties props= new Properties();
props.put("defaultRowPrefetch",256);

W przypadku ustawienia tego parametru za pomocą klasy Statement lub ResultSet operacja ta ma postać:

Statement.setFetchSize(int size);
ResultSet.setFetchSize(int size);
Operacje wsadowe przy zmianach

Standardowo wysyłając kilka zmieniających instrukcji SQL do bazy danych jedna instrukcja to jedno odwołanie się do bazy danych. Przy operacjach wsadowych przy zmianach celem zwiększenie wydajności aplikacji instrukcje te wysyłamy za pomocą jednego odwołania się do bazy danych. Przykład takich operacji wsadowych może mieć postać:


...
con.setAutoCommit(false);
Statement stm= con.createStatement();
stm.addBatch("INSERT INTO TAB1 VALUES('Jan','Nowak',6)");
stm.addBatch("INSERT INTO TAB1 VALUES('Adam','Kowalski',12)");
stm.addBatch("INSERT INTO TAB1 VALUES('Jacek','Szczepański',15)");
int[] ret=stm.executeBatch();
...
con.commit();

Te same operacje używając klasę PreparedStatement:


...
con.setAutoCommit(false);
PreparedStatement pstm= con.prepareStatement("INSERT INTO TAB1 VALUES(?,?,?)");
pstm.setString(1,"Jan");
pstm.setString(2,"Nowak");
pstm.setInt(3,6);
pstm.addBatch();
pstm.setString(1,"Adam");
pstm.setString(2,"Kowalski");
pstm.setInt(3,12);
pstm.addBatch();
pstm.setString(1,"Jacek");
pstm.setString(2,"Szczepański");
pstm.setInt(3,15);
pstm.addBatch();
int[] ret=pstm.executeBatch();
...
con.commit();

Minimalizacja ilości zwracanych danych

Podczas projektowania zapytań SQL należy zwrócić uwagę na to aby pobierać z bazy tylko te informacje, które potem w aplikacji będziemy wykorzystywać. I tak jeśli w tabeli tab1 znajduje się 10 kolumn, a my w aplikacji zamierzamy wykorzystywać tylko dwie kolumny zapytanie select * from tab1 wydaje się być zdecydowanie nadmiarowe. Zaleca się w takiej sytuacji użycie zapytania precyzującego zwracane dane: select col1,col2 from tab1

Dla niektórych wydaje się to może mało istotne jednak w przypadku, kiedy zwracana jest dośc duża liczba rekordów polepszenie wydajności spowodowane zmniejszeniem ilości zwracanych danych będzie z pewnością zauważalne.

Sterowniki JDBC

Wydajność a operacje sieciowe

Niezależne przetwarzanie żądań

Aby omówić ten temat posłużę się przykładem aplikacji serwerowej. Niech będzie to aplikacja serwerowa, która w odpowiedzi na żądanie klienta wykonuje jakieś długotrwałe operacje i zwraca odpowiedĽ klientowi. Jedną z możliwości realizacji takiej aplikacji serwerowej jest iteracyjna obsługa żądań. polega ona na obsłudze w danej chwili tylko jednego żądania. W przypadku, kiedy przychodzi kolejne żądanie i jeśli aplikacja w danym momencie już inne żądanie jest obsługiwane, każde kolejne muszą czekać na koniec obsługi żądania poprzedniego. Może to doprowadzić do niepotrzebnego kolejkowania żądań i wydłużenia czasu odpowiedzi serwera.

Innym rozwiązaniem jest utworzenie tej aplikacji jako serwer wielowątkowy. W przypadku takiej architektury serwer, tak jak w poprzedniej opcji oczekuje na żądania od klienta. Różnica polega na obsłudze nadchodzących żądań. Kiedy nadchodzi nowe żądanie od klienta serwer przekazuje obsługę tego żądania do jakiegoś pobocznego wątku pracującego równolegle do głównego wątku nasłuchującego i powraca do oczekiwania na nowe połączenia. Wątki na potrzeby obsługi żądań klienta najczęściej tworzone są podczas startu aplikacji i najczęściej przechowywane w jakiejś puli wątków. Odpowiednio dobierając wielkość tej puli mamy wpływ na wydajność aplikacji. W przypadku, kiedy pula jest pusta i przychodzi kolejne żądanie aplikacja zwraca komunikat o błędzie, co zapobiega powstawaniu niepotrzebnego kolejkowania. w takiej sytuacji klient może spróbować jeszcze raz połączyć się z serwerem po jakimś czasie.

XML

W przypadku wykorzystania XML we własnych aplikacjach poza niezaprzeczalnymi plusami standard ten dostarcza również pewne niedogodności. Należy zwrócić uwagę na fakt, że dokument XML nie jest najprostszy do przeanalizowania. Zawiera on zagnieżdżenia, atrybuty oraz reguły, które muszą być sprawdzone. W związku z tym wydajność pozostawia tutaj wiele do życzenia. Oczywiście najbardziej to widać przy dużym dokumencie XML i przy częstym jego analizowaniu. Wydajność ta jest różna dla metody SAX i DOM (zdecydowanie na korzyść tej pierwszej). Drugi aspekt, to konsumowanie zasobów w przypadku metody DOM, gdzie cały dokument musi być wczytany do pamięci. Zaleca się w związku z tym zawsze przeanalizowanie, czy XML jest dla aplikacji dobrym rozwiązaniem. Dla przykładu przesyłanie dużej ilości danych pomiędzy systemami można zrealizować nie za pomocą XML, tylko np. FTP.


powrót
Zachęcam do przedstawienia swoich uwag i opinii w polu komentarzy.

Komentarze

Dodaj Komentarz