piątek, 23 kwietnia 2010

Rozproszone testy wydajnościowe z użyciem Apache JMeter

Kolejny wpis o JMeter. Bardzo przydatne narzędzie :)
Dzisiaj chciałem pokazać jak wykonywać rozproszone testy wydajnościowe. Do tego będzie nam potrzebne kilka maszyn z kopią JMeter. Ważne by na każdej maszynie były takie same wersje aplikacji.
Moje środowisko testowe wygląda następująco: (Paint wymiata;)
Zadaniem węzła master jest uruchomienie testów na wszystkich slaveach oraz odbieraniem od nich informacji o wynikach pomiarów. Slave natomiast bombarduje cel żądaniami zdefiniowanymi i przesłanymi przez Master.
Aby uruchomić slave należy na każdej węźle uruchomić Server JMeter -> JMETER_HOME/bin/jmeter-server.bat
Tutaj należy uważać, bo z tego co zauważyłem jest wybierany losowy port a nie jakiś stały przez co mogą wystąpić problemy z firewallem. Jeśli wszystko poszło dobrze coś podobnego do tego powinno się pojawić w konsoli:

To wszystko co należy zrobić na tych węzłach. W węźle master należy otworzyć do edycji plik JMETER_HOME/bin/jmeter.properties. W linii remote_host=127.0.0.1 należy wpisać adresy serwerów slave. W moim przypadku będzie to wyglądać następująco:
remote_hosts=192.168.0.6,192.168.0.7,192.168.0.4,192.168.0.15
Po uruchomieniu JMeter w menu Run-> Remote Start -> powinna pojawić się lista serwerów które dodaliśmy. Możemy odpalać je pojedynczo lub wszystkie jednocześnie za pomocą Remote Start All. Wystarczy już tylko przygotować jakiś plan testów i odpalić. Konsola na węzłach Slave powinna nas informować o rozpoczęciu i zakończeniu testów:
W przypadku jakiś problemów polecam sprawdzić plik jmeter.log w ktalogu /bin/.

Podoba mi się to narzędzie:) Bardzo szybko i prosto można skonfigurować duże źródło ruchu dla testowanego środowiska. Jedna maszyna do zastosować domowych może spokojnie symulować ponad 2000 wirtualnych użytkowników. W zupełności mi to wystarcza dla testowanych przeze mnie projektów.
Postaram się niedługo napisać coś więcej na temat skalowalności JMeter i tego kiedy on może stać się wąskim gardłem, a nie aplikacja lub przepustowość sieci w środowisku.

niedziela, 18 kwietnia 2010

Recenzja - "Wydajne witryny internetowe - Przyspieszanie działania serwisów WWW"

Tematyka optymalizacji oraz skalowania aplikacji WWW jest obiektem moich zainteresowań przez dłuższy już czas. Nie tylko ze względu na temat pracy magisterskiej, ale także ze względu na projekt nad którym aktualnie pracuję, oraz (tak po prostu;) zwykłą ciekawość co i gdzie poprzestawiać by było szybciej, ładniej i wydajniej :)

Przeglądałem parę dni temu zasoby biblioteki na PP i natrafiłem na książkę o interesującym dla mnie tytule "Wydajne witryny internetowe - Przyspieszanie działania serwisów WWW", której autorem jest Steve Souders. Pomyślałem sobie, że to będzie kolejna książka o tym czy używać for czy while oraz, że w php szybsze jest isset od is_null. Jednak zamówiłem, odebrałem i przeczytałem z przyjemnością !
Co ciekawe książka nie zawiera odniesienia do żadnego języka programowania (bezpośrednio przynajmniej) i nie mówi o żadnych wzorcach projektowych. Jej tematem jest optymalizacja bardziej etapu dostarczania strony www do klienta niż logiki biznesowej aplikacji. Jest w niej mowa m.in. o ograniczaniu połączeń TCP, kompresji dokumentów HTML czy buforowaniu zapytań asynchronicznych - prawie nic z programowania.
Wg badań autora sama faza dostarczania dokumentu HTML to około 10-20% całego czasu przesyłania odpowiedzi, reszta czyli ok 80-90% to pobieranie obrazków, cssów jsów i przekierowania. I właśnie te 80-90% jest przez większość lektury przedmiotem optymalizacji.
Książka podzielona została na 17 części z czego 14 to opisane reguły (wraz z przykładami) po których zastosowaniu nasza strona powinna ładować się szybciej - od 25 do 50%, niezły wynik jak na 160 stronicową książkę :)
Oto te reguły posortowane wg autora od najbardziej znaczącej:
  1. Wykonywania mniejszej liczby żądań HTTP
  2. Używanie Content Delivery Network (to rozwiązanie raczej dla bogatszych ;)
  3. Używanie nagłówka Expires
  4. Kompresja gzip
  5. Umieszczanie arkuszy stylów na początku dokumentu
  6. Umieszczanie skryptów na końcu dokumentu
  7. Unikanie wyrażeń CSS
  8. Używanie zewnętrznych plików JavaScript i CSS
  9. Redukcja liczby zapytań DNS
  10. Zmniejszenie objętości kodu JavaScript
  11. Unikanie przekierowań
  12. Usuwanie duplikujących się skryptów
  13. Konfiguracja nagłówka ETag
  14. Buforowanie zapytań Ajax
Trzeba przyznać, że Pan Souders posiada bardzo dużą wiedzę w tej dziedzinie, za każdym razem dane zagadnienie przedstawione jest z dokładnym uzasadnieniem, stosownym przykładem oraz wynikami pomiarów jakie otrzymał. Bardzo dużo wspominane jest o portalu Yahoo! gdzie pracował przy jego optymalizacji, więc przykłady trafiają się także z "dużych" witryn. W ostatnim rozdziale ponadto poddano analizie 10 jednych z największych serwisów internetowych (YouTube, Google, MSN, Amazon, CNN, Wikipedia, AOL, Amazon, Yahoo!, eBay), gdzie zostają wytknięte wszystkie ich słabości. Swoją drogą ten rozdział mógłby być dedykowany administratorom tych serwisów:)
Z czystym sercem polecam tę książkę każdemu webmasterowi, administratorowi, programiście witryn i komukolwiek kto ma do czynienia z WWW. Te 160 stron może przynieść bardzo duże oszczędności finansowe, gdy serwis zacznie się rozrastać. A co najlepsze - książka kosztuje tylko 29,00 zł !

sobota, 17 kwietnia 2010

Testy wydajnościowe bazy danych z Apache JMeter

Dzisiaj na tablicy od rana mam Apache JMeter - świetne i darmowe narzędzie do testowania obciążenia. Kto by chciał się zapoznać z tym jak testować swoje witryny za pomocą tego narzędzia zapraszam (który to już raz?;]) na blog Mateusza Zięby -> Na Jawie - perwszy test z Apache JMeter.
U mnie potestujemy za to jak sprawuje się silnik bazy danych dla zapytań używanych w tworzonej przez nas aplikacji.
Pobrać JMeter można tutaj. Po ściągnięciu, ale przed uruchomieniem należy przegrać sterownik JDBC bazy danych której będziemy używać do katalogu /lib/ znajdujący się w katalogu głównym aplikacji. W moim przypadku bazą danych będzie DB2, więc kopiuję tam plik db2jcc4.jar.
 Po uruchomieniu programu zabieramy się za tworzenie testu. Do węzła "Test Plan" po prawej stronie dodajemy Add -> Thread Group. Grupa wątków definiuje ilu użytkowników będzie symulować test, ile razy każdy z nich będzie wykonywać wszystkie zdefiniowane żądania oraz co jaki czas powinien startować kolejny użytkownik. Dla przykładowego testu ustawiłem następujące wartości:
Number of threads: 8
Ramp-Up Period: 0 (startują wszyscy od razu)
Loop count: 20

Teraz do grupy wątków należy dodać konfigurację połączenia z bazą danych: Add -> Config Element -> JDBC Connection Configuration.W tym oknie uzupełniamy:
Variable Name: (tutaj dowolna nazwa, u mnie: DB2_SAMPLE)
Database URL: (ścieżka połączenia do bazy danych, u mnie: jdbc:db2://localhost:5000/SAMPLE)
JDBC Driver class: (klasa sterownika, u mnie  com.ibm.db2.jcc.DB2Driver
Username: użytkownik
Password: hasło
Bardzo ważne jest uzupełnione przez nas pole Variable Name ponieważ to na podstawie tej nazwy będziemy się odwoływać do tego połączenia przy tworzeniu zapytań. Możliwe jest utworzenie wielu połączeń i korzystanie z nich w różnych zapytaniach, właśnie na podstawie tej nazwy (musi być ona unikalna!).
Teraz należy dodać zapytania które będą wykonywane: Add -> Sampler -> JDBC Request. Ja do testów podałem dwa bardzo proste zapytania zwracające listę pracowników oraz listę departamentów z przykładowej bazy w DB2.
EmpoyeeList:
DepartmentList:
Należy pamiętać by w obu zapytaniach pole Variable Name miało wartość taką jak nazwa puli połączeń z konfiguracji połączenia. W moim wypadku jest to DB2_SAMPLE.
No to już praktycznie wszystko gotowe. Dodajmy jeszcze Add -> Listeners -> Summary Report do wyświetlenia wyników podsumowujących testy.
I można startować z testem: Run -> Start :) Podczas symulacji łącznie powinno zostać wykonanych: liczba użytkowników * liczba powtórzeń * liczba zapytań SQL żądań.
Miłego testowania !

piątek, 16 kwietnia 2010

Mapowanie parametrów GET w JSF

Jak miło można "pobierać" wartości GET z URL w JSF - praktycznie samo się robi. Wystarczy powiązać w faces-config.xml nazwę pola w GET z polem w zarządzanym ziarnie
Dla przykładowego linka: /employeeView.faces?employeeId=000010
faces-config.xml:
<managed-bean>
        <managed-bean-name>employeeBean</managed-bean-name>
        <managed-bean-class>com.sampleproject.jsf.EmployeeBean</managed-bean-class>
        <managed-bean-scope>request</managed-bean-scope>
        <managed-property>
            <property-name>employeeId</property-name>
            <value>#{param.employeeId}</value>
        </managed-property>
</managed-bean>

I nie trzeba się o nic martwić, bo zrobi się samo. Co jest bardzo dobre JSF nie rzuca wyjątkami gdy brakuje wartości w GET tylko wstawia null.

środa, 7 kwietnia 2010

Karmimy bazę danych - dbMonster

Przez ostatni czas szukałem narzędzia do generowania dużej ilości danych dla baz danych.
Natrafiłem na dość sporo ich w sieci ale niestety znaczna większość była komercyjna a ich wersje trialowe umożliwiały generowanie np. tylko 40 rekordów. Ale na szczęście udało mi się także napotkać na miły program autorstwa Polaka Piotra Maja - dbMonster. Narzędzie to obsługuje się z poziomu (niestety) konsoli i wymaga stworzenia mapowania w pliku XML oraz prostej konfiguracji.
Zacznijmy od stworzenia tabeli w bazie danych którą później zapchamy danymi:
CONNECT TO DBMNTEST;
CREATE TABLE WOOKASZ.PRODUKTY ( 
ID_PRODUKTU BIGINT  NOT NULL  GENERATED ALWAYS AS IDENTITY (START WITH 0, INCREMENT BY 1, NO CACHE ), 
NAZWA VARCHAR (80)  NOT NULL, 
CENA DECIMAL (9, 2)  NOT NULL, 
CONSTRAINT CC1270576608403 PRIMARY KEY (ID_PRODUKTU)
);
CONNECT RESET;
W konfiguracji podajemy podstawowe dane do połączenia i kilka parametrów potrzebnych do pracy dbMonster.
test.properties:
# podstawowe dane do połączenia
dbmonster.jdbc.driver=com.ibm.db2.jcc.DB2Driver
dbmonster.jdbc.url=jdbc:db2://127.0.0.1:50000/dbmntest
dbmonster.jdbc.username=WOOKASZ
dbmonster.jdbc.password=password
# po ilu insertach wykonywać commit
dbmonster.jdbc.transaction.size=1000

# nazwa schematu dla takich baz jak Oracle lub DB2
dbmonster.jdbc.schema=WOOKASZ

# ile razy próbować wygenerować unikalny klucz
dbmonster.max-tries=1000

# domyślna liczba wierszy dla SchemaGrabber (o tym innym razem;)
dbmonster.rows=1000

# klasa wizualizująca progress bar
dbmonster.progress.monitor=pl.kernelpanic.dbmonster.ProgressMonitorAdapter
Myślę, że tak konfiguracja jest dość prosta i nie trzeba jej głębiej omawiać.
Teraz czas dla schematu w którym opisana jest struktura tabel dla których generowane będą dane. W naszym przypadku jest to tylko jedna tabela PRODUKTY.
testSchema.xml:
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE dbmonster-schema PUBLIC
    "-//kernelpanic.pl//DBMonster Database Schema DTD 1.1//EN"
    "http://dbmonster.kernelpanic.pl/dtd/dbmonster-schema-1.1.dtd">
<dbmonster-schema>
   <name>DB Monster Test Schema ! ^^</name>
    <table name="PRODUKTY" rows="15000">
        <key databaseDefault="true">
           <generator type="pl.kernelpanic.dbmonster.generator.MaxKeyGenerator">
               <property name="columnName" value="ID_PRODUKTU"/>
           </generator>
        </key>
        <column name="nazwa">
            <generator type="pl.kernelpanic.dbmonster.generator.StringGenerator">
                <property name="nulls" value="0"/>
                <property name="minLength" value="4"/>
                <property name="maxLength" value="80"/>
                <property name="allowSpaces" value="true"/>
            </generator>
        </column>
        <column name="cena">
            <generator type="pl.kernelpanic.dbmonster.generator.NumberGenerator">
                <property name="nulls" value="0"/>
                <property name="minValue" value="10"/>
                <property name="maxValue" value="30000"/>
                <property name="returnedType" value="numeric"/>
    <property name="scale" value="2"/>
            </generator>
        </column>
    </table>
</dbmonster-schema>
Opis struktury tabeli zawiera się w znaczniku table którego pierwszy atrubuty name musi zawierać poprawną nazwę tabeli, a drugi rows liczbę wierszy ile ma być wygenerowanych dla tej tabeli. dbMonster obsługuje klucze główne tabel, informacje o nich zawiera się w znaczniku key. W tym przypadku nie używamy generatora udostępnianego przez dbMonster do generowania wartości dla kluczy (używane są identity columns) dlatego atrybut databaseDefault ma wartość true. Dalej umieszczone zostały definicje kolumn. Każda kolumna zawiera parametry do generowania, zależne są one od tego jakiego typu generatora używamy. Listą dostępnych generatorów wraz z opisem parametrów dostępna jest tutaj. Co jest bardzo miłe dostępne jest pełne API który pozwala na tworzenie własnych generatorów.
Co mi się bardzo spodobało StringGenerator nie generuje dziwnego zlepku losowych liter tylko normalne słowa z dostępnego dla niego słownika:) Dlatego nazwą produktu nie będzie np. "AhT8ddnoJuuTQpp" ale np. "keypad's tingle". Wiem, że to i tak bez sensu (a może to coś znaczy ?) ale lepiej to wygląda gdy prezentujemy nasz system.
Zanim będzie można uruchomić generację należy przekopiować do katalogu /lib/ sterownik JDBC dla używanej bazy danych.
Teraz możemy użyć gotowego skryptu bat z katalogu /bin/ aby uruchomić generację:
dbmonster -c test.properties -s testSchema.xml

rem Batch file to run dbmonster under Windows

rem Contributed by Peter De Bruycker
2010-04-07 12:21:55,776 INFO  DBMonster - Let's feed this hungry database.
2010-04-07 12:21:56,257 INFO  DBCPConnectionProvider - Today we are feeding: DB2/NT SQL09070
2010-04-07 12:21:56,465 INFO  Schema - Generating schema .
2010-04-07 12:21:56,473 INFO  Table - Generating table <PRODUKTY>.
2010-04-07 12:22:28,570 INFO  Table - Generation of table <PRODUKTY> finished.
2010-04-07 12:22:28,574 INFO  Schema - Generation of schema <DB Monster Test Schema ! ^^> finished.
2010-04-07 12:22:28,579 INFO  DBMonster - Finished in 32 sec. 804 ms.
I mamy tabelę pełną danych:)
Dodam jeszcze, że tworzenie schematu nie jest konieczne, gdy w tabelach znajdują się jakieś dane. Można użyć tzw. SchemaGrabber aby wygenerować schemat xml. Jednak jeszcze nie udało mi się tego dokonać, ciągle otrzymuję pustą definicję tabeli. Jak mi się to uda to postaram się opisać.

Narzędzie jest bardzo dobre gdy nie mamy jakiś skomplikowanych warunków do tego jakie dane mają być składowane w bazie danych, do prostych zastosować idealne. Szkoda tylko, że projekt ten już od 2006 roku nie jest rozwijany, według mnie ma solidne podstawy do tego by konkurować z komercyjnymi rozwiązaniami. Może ktoś przejmie pałeczkę i zajmie się rozbudową dbMonster ?