Grails a pamięć podręczna drugiego poziomu – instancje klasy

11/05/2010

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):
  • Thread Group

  • kontroler pętli (Loop Controller):
  • 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.