TLDR; Jeśli nadal używasz JMetera to polecam przesiadkę na coś wygodniejszego i przyjemniejszego w użyciu dla programisty.

Gatling vs JMeter - czego użyć do testowania wydajności

JMeter i Gatling to najbardziej popularne narzędzia do testowania wydajności. Istnieje już sporo tekstów porównujących te dwa projekty. W takim razie po co kolejny wpis na ten temat? Postaram się porównać te dwa narzędzia trochę pod innym kątem. Używałem obu dość długo i myślę, że przyszedł czas na podsumowanie moich doświadczeń.

Chcesz wydajniej testować aplikacje webowe, otrzymywać dedykowane materiały i informacje o planowanych szkoleniach?
Dołącz do listy mailingowej SoftwareMill Academy!
https://sml.io/gatling

Jak powinno wyglądać testowanie wydajności w idealnym świecie?

Jestem deweloperem i z mojej perspektywy testowanie wydajności aplikacji jest odpowiedzialnością dewelopera, który ją napisał. Tym samym podejście, w którym dedykowany zespół testerów, który nigdy nie dotknął kodu źródłowego testowanego systemu musi sprawdzić jego wydajność nie zadziała. Nie chodzi mi o to, że testerzy nie są w stanie stworzyć dobrych testów wydajności. Chcę raczej podkreślić, że jeśli mamy do dyspozycji testerów wydajności, to proces testowania aplikacji musi być przeprowadzony w ścisłej współpracy pomiędzy testerami i deweloperami. Szczególnie na samym początku.

Z czasem coraz więcej odpowiedzialności może być przerzucone na zespół testerski, ale deweloperzy i tak będą potrzebni chociażby do analizy rezultatów. Szczególnie jeśli nie będą one takie, jak zakładaliśmy na początku. Jest to oczywiście bardzo dobre dla samym deweloperów, ponieważ tworzy sprzężenie zwrotne dotyczące jakości ich rozwiązań, analogicznie jak w przypadku pisania testów jednostkowych, integracyjnych czy e2e.

Testy wydajności bez wątpienia są jednymi z najdroższych testów przy tworzeniu oprogramowania. Oprócz tego, że wymagają cennego czasu testera lub dewelopera do ich utworzenia, w dodatku wymagają dedykowanego (jeśli to możliwe) środowiska do przeprowadzenia takich testów. Aplikacje zmieniają się bardzo dynamicznie, a przez to również testy wydajności powinny śledzić te zmiany i być na bieżąco utrzymywane. To czasami bywa nawet większym kosztem niż pierwotne utworzenie takich testów.

Z powyższych powodów moja pierwsza rada jeśli chodzi o testy wydajności to żeby na spokojnie przemyśleć ten cały proces w kontekście długoterminowym. Idealnie gdyby testy wydajności były częścią naszego cyklu Continuous Deployment (CD). Nie zawsze jest to możliwe i nie zawsze ma sens. Jednak z moich obserwacji wynika, że pójście na skróty na początku zabawy z testowaniem wydajności może w przyszłości nas drogo kosztować.

Jak wybrać narzędzia do testów wydajności?

Wybór narzędzia do testów wydajności będzie jednym z pierwszych dylematów i mam nadzieję, że ten wpis pomoże Ci odrobinę z tym problemem.

Ponownie, z perspektywy dewelopera, od dobrego narzędzia do testów wydajności powinieneś oczekiwać 4 głównych atrybutów:

  1. czytelność
  2. komponowalność
  3. poprawna matematyka
  4. możliwość rozproszonego testowania.

Tylko tyle? Szczerze powiedziawszy - tak. To są najistotniejsze aspekty przy wyborze narzędzia. Oczywiście, jest całe mnóstwo specyficznych i przydatnych funkcjonalności, aczkolwiek większość narzędzi dostarcza mniej więcej podobny zbiór opcji jeśli chodzi o tworzenie scenariuszy testowych, symulowanie ruchu produkcyjnego, itp. Dlatego chcę się skupić na tych punktach, by pokazać to, co jest tak naprawdę istotne przy wyborze narzędzia, które będzie zarówno wygodne jak i utrzymywalne.

Jeśli jakieś narzędzie spełnia te 4 podstawowe wymogi, dopiero wtedy możemy przejść do analizy pełnego spektrum jego funkcjonalności. W przeciwnym wypadku, skuszeni jakąś ciekawą funkcją, która w dłuższej perspektywie nie będzie ani taka interesująca, ani taka przydatna, skończymy z nieoptymalnym narzędziem do testów.

Czytelność testów

Ok, zaczynamy od pierwszego punktu. Porównywanie czytelności aplikacji z GUI vs kod źródłowy to dość nietypowe podejście, ale zobaczmy co z tego wyjdzie. Bardzo prosty przepływ biznesowy w JMeter może wyglądać jak poniżej.

Na pierwszy rzut oka - całkiem nieźle. Wszystko jest czytelne i proste do zrozumienia co dany test testuje. Niestety, jeśli zaczniemy rozszerzać nasz scenariusz i dodawać nowe kroki, parametryzować obecne lub zmieniać ich zachowanie, to bardzo szybko dojdziemy do wniosku, że jest to bardzo żmudna praca. Trzeba dużo używać dużo myszki, wiedzieć co gdzie jest schowane, a co gorsze, pamiętać o wszystkich niejawnych powiązaniach (np. współdzielone zmienne) między poszczególnymi zapytaniami.

Z mojego doświadczenia, prędzej czy później edycja z GUI przestanie być przyjemna i przerzucimy się do kodu źródłowego, który jest w XMLu i (jak to XML) jest kompletnie nieczytelny. Mimo wszystko w XMLu możemy chociaż używać podstawowych technik refaktoryzacji opartych na stringach, jak “zamień na”, itp.

<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.3">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true">
      <stringProp name="TestPlan.comments"></stringProp>
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
      <stringProp name="TestPlan.user_define_classpath"></stringProp>
      ...
      bardzo, bardzo długi xml
      ...

Całość dostępna tutaj.

Dokładnie ten sam scenariusz w Gatlingu wygląda następująco:

  val scn =
    scenario("Example scenario")
      .exec(http("go to main page").get("/"))
      .exec(http("find computer").get("/computers?f=macbook"))
      .exec(http("edit computer").get("/computers/6"))
      .exec(http("go to main page").get("/"))
      .repeat(4, "page") {
        exec(http("go to page").get("/computers?p=${page}"))
      }
      .exec(http("go to create new computer page").get("/computers/new"))
      .exec(
        http("create new computer")
          .post("/computers")
          .formParam("name", "Beautiful Computer")
          .formParam("introduced", "2012-05-30")
          .formParam("discontinued", "")
          .formParam("company", "37")
      )

Wygląda to bardzo podobnie jak w przypadku JMetera, tzn. widać co jest testowane, jaki jest ogólny flow. Nie zapominajmy, że jest to jednak kod źródłowy, który jest czytelny prawie jak normalne zdania. Święty Graal wszystkich programistów. To, co od razu powinno nam przyjść do głowy, to fakt, że jeśli mamy do czynienia z kodem źródłowym, to możemy używać wszystkich znanych nam metod refaktoryzacji do rozbudowy scenariusza lub poprawy jego czytelności.

Nie wspominając o tym, że język Scala (używany w Gatlingu) jest językiem mocno typowanym, dzięki czemu większość problemów z prawidłową konstrukcją scenariuszy będzie wyłapana już na etapie kompilacji kodu. W JMeter błędy pojawią się dopiero podczas uruchomienia scenariusza, co zdecydowanie spowalnia pętlę zwrotną z wynikami.

Kolejnym argumentem przemawiającym za kodem źródłowym jest to, że bardzo łatwo można takie testy wersjonować, recenzować przez wielu programistów, nawet z różnych zespołów. Powodzenia jeśli musimy to robić z XML liczącymi tysiące linii.

Komponowalność

Komponowalność jest kluczowa jeśli musimy stworzyć wiele testów wydajności, które będą współdzielić jakąś logikę, np. autentykację, tworzenie usera, itp. W JMeterze możemy bardzo szybko stworzyć katastrofę copy-paste. Nawet w tym prostym planie testowym mamy fragmenty, które się powtarzają:

Z czasem takich miejsc będzie więcej. Nie tylko pojedyncze requesty, ale całe sekcje logiki biznesowej będą zduplikowane. Można ten problem zaadresować przez użycie Module Controller, lub tworzenie swoich własnych rozszerzeń w Groovy lub BeanShell, ale uwierzcie mi, to nie jest zbyt przyjemne ani wygodne.

W Gatlingu, budowanie reużywalnych fragmentów jest w zasadzie ograniczone tylko naszymi umiejętnościami programistycznymi. Pierwszym krokiem może być wyekstrahowanie niektórych metod, tak żeby można ich było użyć wiele razy.

  private val goToMainPage = http("go to main page").get("/")

  private def findComputer(name: String) = http("find computer").get(s"/computers?f=${name}")

  private def editComputer(id: Int) = http("edit computer").get(s"/computers/${id}")

  private def goToPage(page: Int) = http("go to page").get(s"/computers?p=${page}")

  private val goToCreateNewComputerPage = http("go to create new computer page").get("/computers/new")

  private def createNewComputer(name: String) =
    http("create new computer")
      .post("/computers")
      .formParam("name", name)
      .formParam("introduced", "2012-05-30")
      .formParam("discontinued", "")
      .formParam("company", "37")

  val scn =
    scenario("Example scenario")
      .exec(goToMainPage)
      .exec(findComputer("macbook"))
      .exec(editComputer(6))
      .exec(goToMainPage)
      .exec(goToPage(1))
      .exec(goToPage(2))
      .exec(goToPage(3))
      .exec(goToPage(10))
      .exec(goToCreateNewComputerPage)
      .exec(createNewComputer("Awesome computer"))

Następnie możemy podzielić nasz scenariusz na mniejsze fragmenty, które po połączeniu tworzą bardziej skomplikowany przepływ biznesowy.

  val search = exec(goToMainPage)
    .exec(findComputer("macbook"))
    .exec(editComputer(6))

  val jumpBetweenPages = exec(goToPage(1))
    .exec(goToPage(2))
    .exec(goToPage(3))
    .exec(goToPage(10))

  val addComputer = exec(goToMainPage)
    .exec(goToCreateNewComputerPage)
    .exec(createNewComputer("Awesome computer"))

  val scn =
    scenario("Example scenario")
      .exec(search, jumpBetweenPages, addComputer)

Jeśli musimy utrzymywać testy wydajności w dłuższej perspektywie czasowej, to niewątpliwie wysoki poziom komponowalności będzie zdecydowaną przewagą nad innymi narzędziami. Z moich obserwacji wynika, że jedynie narzędzia, które umożliwiają pisanie testów w kodzie źródłowym, np. Gatling i Scala, Locust i Python, WRK2 i Lua, spełniają to kryterium. Jeśli testy zapisywane są w postaci formatów tekstowych takich jak XML, JSON, itd, zawszę będziemy w pewnym sensie ograniczeni możliwościami komponowalności tych formatów.

Poprawna matematyka

Jest takie powiedzenie, które powinna znać każda osoba zajmująca się testami wydajności: “kłamstwa, cholerne kłamstwa i statystyka”. Jeśli nie zna to na pewno pozna w bardzo bolesny sposób. O tym dlaczego to zdanie powinno być mantrą każdego testera wydajności można by napisać osobny wpis. W dużym skrócie: mediana, średnia arytmetyczna, odchylenie standardowe są kompletnie bezużytecznymi metrykami w tej dziedzinie. Trochę więcej szczegółów można posłuchać na świetnej prezentacji Gila Tene. Tym samym, jeśli narzędzie do testów wydajności dostarcza tylko tych danych statycznych, można je już teraz wyrzucić do kosza.

Jedyną sensowną metryką do mierzenia i porównywania wydajności są percentyle. Należy jednak używać ich również z zachowaniem pewnej podejrzliwości dotyczącej tego jak zostały one zaimplementowane. Bardzo często implementacja bazuje na średniej arytmetycznej i odchyleniu standardowym, co oczywiście powoduje, że są równie bezużyteczne.

Sposobem na weryfikację poprawności percentyli są sposoby wymienione w prezentacji wyżej lub samodzielne sprawdzenie kodu źródłowego ich implementacji. Ubolewam nad tym, że bardzo rzadko sposób liczenia percentyli jest odpowiednio udokumentowany w danym narzędziu do testów wydajności. Nawet jeśli taka dokumentacja istnieje, to niewiele osób z niej skorzysta i przez to może np. wpaść w pułapkę jaką zostawia biblioteka Dropwizard Metrics.

Bez prawidłowej matematyki/statystyki cała nasza praca w kontekście testów wydajności może być kompletnie bezwartościowa, ponieważ nie będziemy w stanie wnioskować na temat wyników pojedynczego testu czy też porównywać wyników ze sobą.

W swoim testach bardzo często bazuję na wykresach z percentylami zmieniającymi się w czasie, co jest możliwe do uzyskania zarówno w Gatlingu jak i JMeterze. Dzięki temu jesteśmy w stanie powiedzieć czy testowany system nie ma jakichś czkawek wydajnościowych podczas trwania całego testu.

Do porównywania wyników poszczególnych testów potrzebne są globalne percentyle, które dostarczą oba porównywane narzędzia. Niemniej, odbiłem się kiedyś od dość ciekawego problemu z dokładności globalnych percentyli w JMeterze. Gatling w swojej implementacji używa biblioteki HdrHistogram do obliczania percentyli, która oferuje bardzo rozsądny kompromis pomiędzy dokładnością a zapotrzebowaniem na pamięć.

Rozproszone testy

Istnieje sporo artykułów dotyczących wydajności samych narzędzi do testowania wydajności. To może być istotne do pewnego poziomu, bo niewątpliwie chcemy wygenerować olbrzymi ruch, tak żeby odpowiednio “docisnąć” testowany system. Problem polega na tym, że obecne aplikacje to bardzo rzadko pojedyncze instancje uruchomione na jednej maszynie. Z reguły są to rozproszone systemy działające na wielu instancjach i wielu maszynach (często w dynamicznie skalowalnych rozwiązaniach chmurowych). Pojedyncza maszyna z uruchomionym testem wydajności nie będzie w stanie odpowiednio obciążyć takiego środowiska. Dlatego zamiast skupiać się na tym, które narzędzie wygeneruje mi większy ruch z mojego laptopa, lepiej sprawdzić czy istnieje uruchomienie rozproszonych testów na wielu maszynach jednocześnie.

W tym przypadku mamy jednoznaczny remis. Możemy rozproszyć nasze testy manualnie w Gatlingu jak i w JMeterze. Dodatkowo możemy użyć istniejących rozwiązań, które zrobią to za nas automatycznie jak Flood. Zdecydowanie polecam tę drugą opcję, ponieważ zaoszczędzimy dzięki niej wiele cennego czasu.

Podsumowanie

Mimo że ogólny wydźwięk tego artykułu mógł być odebrany jako roast na JMeterze, to nie taka była moja intencja. Używałem obu tych narzędzi, JMeter wydawał mi się kiedyś jedynym sensownym narzędziem do testowania wydajności ale w momencie gdy zacząłem używać Gatlinga, to nie widzę już powrotu.

Narzędzie z graficznym interfejsem będzie prawdopodobnie łatwiejsze do użycia na samym początku, ale jednak idea, że kod źródłowy jest moim testem - zdecydowanie bardziej do mnie przemawia. Nie wspominając o tym, że DSL Gatlinga jest naprawdę przyjemny, wygodny w użyciu. Testy są czytelne i dużo łatwiej o ich utrzymanie.

Wiele ludzi podchodzi do Gatlinga sceptycznie, ponieważ wymaga on nauczenia się nowego języka programowania jakim jest Scala. Sama Scala ma dość ciężki PR, który może wynikać z tego, że jest to trudny język i ciężko się go używa. Nic bardziej mylnego, język jak język, ma swoje plusy i minusy, natomiast w kontekście samego Gatlinga wymagana jest jedynie podstawowa znajomość składni. Jeśli natomiast zawsze chciałeś użyć Scali w swojej pracy, ale nie do końca było to możliwe z różnych przyczyn, to być może testy wydajnościowe (i automatyczne) będą dobrym sposobem na delikatne włączenie tego języka do Twojego ekosystemu.

Polecam również śledzić nasze najnowsze posty, ponieważ mam w planach porównanie Gatlinga i Locusta, który również jest ciekawą alternatywą dla JMetera.

Na koniec zapraszam Cię do SoftwareMill Academy, gdzie niedługo uruchamiamy dedykowany kurs Gatlinga.

Szukasz nowych lub alternatywnych narzędzi do testów wydajnościowych?
Chcesz wygodniej testować?

Dołącz do nas, by co kilka tygodni otrzymywać mailowo dedykowane materiały o Gatlingu.

Blog Comments powered by Disqus.
Find more articles like this in Blog section