Wstęp
Postanowiłem sprawdzić w jakim stopniu zastosowanie pamięci podręcznej drugiego poziomu (second level cache – slc) wpływa na poprawę wydajności aplikacji. Ponieważ temat jest obszerny, więc w tym wpisie zająłem się wykorzystaniem slc dla instancji klas. Kod źródłowy zarówno aplikacji jak i testów jest dostępny w repozytorium git pod adresem http://github.com/tlempart/grails-slc.
Wersja bez slc
Aplikacja
Zacząłem od utworzenia aplikacji:
grails create-app grails-slc
i klasy domenowej Book
, której instancje nie będą umieszczane w slc:
grails create-domain-class com.example.Book
z jednym atrybutem tytuł:
package com.example class Book { String title }
Aby umożliwić testy wydajnościowe utworzyłem kontroler:
grails create-controller com.example.Book
z standardową akcją list
:
package com.example class BookController { def list = { render { 1000.times { Book.get(it) } } } }
Uwaga w kontrolerze wykorzystałem metodę get
, gdyż jest to jedna z kilku metod GORM, która potrafi pobrać instancje klas z slc, inne metody to:
Metody find*
lub list*
nie korzystają z slc tylko z query cache.
Pozostało tylko uzupełnić Bootstrap
:
class BootStrap { def init = { servletContext -> 1000.times { new com.example.Book(title: "Tytuł {it}").save() } } def destroy = { } }
Testy
Aby zmierzyć wydajność akcji list
użyłem narzędzia jmeter.
Po uruchomieniu jmeter utworzyłem:
- grupę wątków (Thread Group):
- kontroler pętli (Loop Controller):
- żądanie HTTP (HTTP Request):
- oraz wyniki w postaci wykresu graficznego (Graph Results).
Projekt zapisałem w pliku test/jmeter/book.jmx
Przed uruchomieniem testu wystartowałem aplikację:
grails run-app
Wynik uruchomienia testu dał średnią 208 a medianę 184.
Wersja z slc
Aplikacja
Dodałem klasę domenową CachedBook
podobną do Book
, ale z domknięciem statycznym mapping
zawierającym aktywację pamięci podręcznej drugiego poziomu dla instancji tejże klasy:
package com.example class CachedBook { String title static mapping = { cache true } }
co według dokumentacji grails (sekcja 5.5.2.2. Caching Strategy) powinno skutkować umieszczaniem instancji klasy CachedBook
w slc.
Uwaga, zapis
static mapping = { cache: true }
jest nieprawidłowy. Instancje klasy domenowej nie będą zapisywane w pamięci podręcznej drugiego poziomu a co więcej grails nie wypisze żadnego ostrzeżenia.
Do kontrolera BookController
dodałem nową akcję, która pozwoli na test wydajności odczytu instancji klasy CachedBook
:
def list = { render { 1000.times { CachedBook.get(it) } } }
Na koniec uzupełniłem Bootstrap
:
class BootStrap { def init = { servletContext -> 1000.times { new com.example.Book(title: "Tytuł {it}").save() } 1000.times { new com.example.CachedBook(title: "Tytuł {it}").save() } } def destroy = { } }
Pamięć podręczna jest domyślnie skonfigurowana i włączona w aplikacji grails, więc nie należało nic więcej robić.
Testy
Skopiowałem projekt book.jmx do bookCached.jmx i zmieniłem żądanie HTTP (HTTP Request) na następujące:
Ponowne uruchomienie testu dało średnią 140 a medianę 124:
Baza danych w osobnym procesie
Ponieważ powyższe testy były przeprowadzane dla bazy HSQLDB w trybie mem, więc postanowiłem przeprowadzić je dla dwóch różnych baz danych uruchomionych w osobnym procesie. Pierwszą z nich była wcześniej już użyta HSQLDB a drugą PostgreSQL. Rozpocząłem od uzupełnienia konfiguracji dostępu do bazy danych w pliku grails-app/conf/DataSource.groovy:
environments { development { dataSource { dbCreate = "create-drop" // one of 'create', 'create-drop','update' url = "jdbc:hsqldb:mem:devDB" } } development2 { dataSource { dbCreate = "create-drop" // one of 'create', 'create-drop','update' url = "jdbc:hsqldb:hsql://localhost/test;shutdown=true" } } development3 { dataSource { pooled = true driverClassName = "org.postgresql.Driver" username = "test" password = "test" dbCreate = "create-drop" // one of 'create', 'create-drop','update' url = "jdbc:postgresql:test" } } ...
Testy dla HSQLDB wymagały uruchomienia aplikacji dla środowiska development2 :
grails -Dgrails.env=development2 run-app
Uruchomienie testów dla aplikacji bez slc dało średnią 893 medianę 861, a z slc średnią 406 medianę 379.
Analogicznie testy dla PostgreSQL wymagały uruchomienia aplikacji dla środowiska development3:
grails -Dgrails.env=development3 run-app
Uruchomienie testów dla aplikacji bez slc dało średnią 603 medianę 576, a z slc średnią 398 medianę 369.
Wnioski
W poniższej tabeli zebrałem wyniki wszystkich przeprowadzonych testów:
HSQLDB (mem) | HSQLDB (standalone) | PostgreSQL | |
---|---|---|---|
bez slc | 208/184 | 893/861 | 603/576 |
z slc | 140/124 | 406/379 | 398/369 |
Analizując wyniki testów dla aplikacji bez użycia slc można zauważyć dwie rzeczy:
- brak narzutów na połączenia sieciowe w przypadku bazy danych HSQLDB (mem) robi bardzo dużą różnice,
- silnik bazy danych PostgreSQL jest wydajniejszy niż silnik bazy danych HSQLDB (standalone)
Analizując wyniki testów dla aplikacji z użyciem slc można zauważyć również dwie rzeczy:
- brak narzutów na połączenia sieciowe w przypadku bazy danych HSQLDB (mem) robi mniejszą różnicę niż w przypadku braku slc, ale nadal dużą
- silnik bazy danych w przypadku baz danych uruchamianych w osobnym procesie nie ma wielkiego znaczenia, wyniki w przypadku baz danych HSQLDB (standalone) i PostgreSQL są zbliżone
Ze względu na obszerność tematu nie rozbijałem testów na poszczególne strategie slc (read-write, read-only, lazy, non-lazy). Świadomie pominąłem również wpływ aktualizacji slc na wydajność aplikacji.