851 lines
51 KiB
TeX
851 lines
51 KiB
TeX
% !TeX root = ../../thesis.tex
|
|
|
|
\chapter{Performance-Untersuchung der Anwendung}
|
|
\label{ch:performance-investigation-application}
|
|
|
|
Nun werden die unterschiedlichen Schichten betrachtet und möglichen Performance"=Verbesserungen untersucht und deren
|
|
Vor"= und Nachteile herausgearbeitet.
|
|
|
|
Für die Tests wird ein aktuelles Manjaro-System mit frisch installierten Payara als Serverhost und der IntelliJ IDEA
|
|
als Entwicklungsumgebung verwendet. Der Computer ist mit einer Intel CPU i7-12700K, 32 GB Arbeitsspeicher und einer SSD
|
|
als Systemfestplatte ausgestattet.
|
|
|
|
Zur ersten Untersuchung und der Bestimmung der Basis-Linie, wurde das Script ohne eine Änderung an dem Code und der
|
|
Konfiguration mehrfach aufgerufen. Hierbei hat sich gezeigt, dass der erste Aufruf nach dem Deployment circa 1500 ms
|
|
gedauert hat. Die weiteren Aufrufe benötigen im Durchschnitt noch 600 ms. Beim achten Aufruf des Scripts hat der
|
|
Server nicht mehr reagiert und im Log ist ein OutOfMemoryError protokolliert worden.
|
|
|
|
Nach einem Neustart des Servers, konnte das gleiche Verhalten wieder reproduziert werden. Daraufhin wurde das Test-Script
|
|
um die Anzeige der aktuellen Speicherverwendung des Payara-Servers erweitert und diese zeitgleich zu beobachten. Diese
|
|
Auswertung zeigte, dass der Server mit circa 1500 MB RSS Nutzung an seine Grenzen stößt. Diese Grenzen wurde durch die
|
|
Konfigurationsänderung im Payara-Server von \texttt{-Xmx512m} auf \texttt{-Xmx4096m} nach oben verschoben. Nun werden
|
|
circa 60 Aufrufe des Scripts benötigt, damit der Server nicht mehr reagiert. Hierbei wird aber kein OutOfMemoryError
|
|
in der Log-Datei protokolliert und der Server verwendet nun circa 4700 MB RSS. Bei allen Tests war noch mehr als die
|
|
Hälfte des verfügbaren Arbeitsspeichers des Computers ungenutzt.
|
|
|
|
Mit der Konfiguration \texttt{-Xmx} wird der maximal verwendbare Heap"=Speicher in der \ac{JVM} definiert.
|
|
Dies zeigt direkt, dass es ein Problem in der Freigabe der Objekte gibt, da dass erhöhen des verwendbaren
|
|
Arbeitsspeicher das Problem nicht löst, sondern nur verschiebt.
|
|
|
|
Für alle nachfolgenden Messungen wird das Skript im \autoref{ap:calling_script} verwendet, welches die einzelnen
|
|
Aufrufe steuert. Die Ergebnisse werden in eine Tabelle überführt, wie in der \autoref{tbl:measure-without-cache}.
|
|
Hierbei werden die Aufrufzeiten der Webseite aus dem Skript für die Zeitmessung mit Mindest"~, Durchschnitt"~ und
|
|
Maximalzeit aufgenommen, hierbei ist eine kürzere Zeit besser. Zusätzlich wird die Anzahl der aufgerufenen SQL Abfragen
|
|
ermitteln, auch hier gilt, dass weniger Aufrufe besser sind. Als letztes wird noch der verwendete Arbeitsspeicher
|
|
vom \textit{Glassfish}"=Server vor und nach dem Aufruf ermittelt und die Differenz gebildet, hierbei sollte im besten
|
|
Fall die Differenz bei 0 liegen. Dieser Aufbau gilt für alle weiteren Messungen. Zusätzlich werden noch die Laufzeiten
|
|
der \ac{JSF} ermittelt und die durchschnittlichen Zeiten mit in der Tabelle dargestellt, und auch hier ist es besser,
|
|
wenn die Zeiten kürzer sind.
|
|
|
|
Als Grundlage für die Vergleiche wurden eine Messung durchgeführt, bei der alle Caches deaktiviert wurden und keine
|
|
Änderung am Code vorgenommen wurde. Das Ergebnis dieser Messung ist in \autoref{tbl:measure-without-cache} zu finden.
|
|
Diese zeigen auch direkt ein erwartetes Ergebnis, dass der erste Aufruf bedeutend länger dauert als die Nachfolgenden.
|
|
Ebenfalls sieht man eindeutig, dass die Anzahl der Anfragen nach dem ersten Aufruf immer die gleiche Anzahl besitzen.
|
|
Der Speicherbedarf steigt auch relative gleichmässig, was nicht recht ins Bild passt, da hier keine Objekte im Cache
|
|
gehalten werden sollten.
|
|
|
|
\begin{table}[!h]
|
|
\centering
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & & \multicolumn{3}{c|}{RSS (MB)} \\
|
|
\hline
|
|
\# & min & avg & max & Queries & davor & danach & diff \\
|
|
\hline
|
|
1 & 395 & 578 & 1312 & 12237 & 747.15 & 924.88 & 177.73 \\
|
|
2 & 353 & 375 & 464 & 12080 & 924.51 & 1027.75 & 103,24 \\
|
|
3 & 286 & 345 & 535 & 12080 & 1018.21 & 1145.36 & 127.15 \\
|
|
4 & 291 & 307 & 340 & 12080 & 1129.91 & 1239.75 & 109,84 \\
|
|
\hline
|
|
\end{tabular}
|
|
\caption{Messung ohne Caches}
|
|
\label{tbl:measure-without-cache}
|
|
\end{table}
|
|
|
|
Vor jedem weiteren Test-Lauf wurde die Domain beendet und komplett neugestartet, um mit einer frischen Instanz zu
|
|
beginnen. Hierbei ist aufgefallen, dass fast immer 62 Abfragen zur Startup-Phase dazugehört haben, unabhängig von den
|
|
konfigurierten Cache Einstellungen. Einige dieser Abfragen sind durch das Erstellen der Materialisierten Sichten
|
|
\textit{searchreference} und \textit{searchfulltext} erklärbar. Zusätzlich ist noch ein zyklischer Dienst
|
|
\textit{SearchEntityService} vorhanden, der zum Start und alle sechs Stunden den Datenbestand für die Suche aufbereitet
|
|
und entsprechend einige Abfragen an die Datenbank absetzt. Da weder die Sichten noch der Dienst für die Dokumentenliste
|
|
benötigt werden, wurde der Dienst und das Erstellen im Code für die weiteren Tests deaktiviert.
|
|
|
|
Da die Abfragezeiten auf der Datenbank zu gering waren, um eine Verbesserung feststellen zu können, wurde für den
|
|
PostgreSQL und den Payara-Server ein Docker-Container erzeugt und diese limitiert. Die Konfiguration ist im
|
|
\autoref{ap:docker_config} beschrieben.
|
|
|
|
Mit dem neuen Aufbau ergeben sich nun neue Messungen. Für den Speicherbedarf wird nun nicht mehr der benutzte
|
|
Speicher der Anwendung beobachtet, sondern die Speichernutzung des Docker-Containers für den Payara-Server. Auch hier
|
|
ist es besser, wenn es keine oder nur geringe Änderungen vor und nach dem Aufruf der Webseite gibt, ein steigender Wert
|
|
zeigt an, dass der verwendete Speicher nicht sauber freigegeben werden kann.
|
|
|
|
Für die Ausführungszeiten der SQL-Abfragen wurden nur die sechs Abfragen für die Darstellung der Tabelle beachtet.
|
|
Hierzu zählt die Hauptabfrage der Dokumenten"=-Tabelle, die Ermittlung des letzten und ersten Eintrags in der Tabelle,
|
|
die Ermittlung der Adressen des Autors, die Ermittlung der Koautoren, die Ermittlung der Faksimile, sowie die Ermittlung
|
|
der Anzahl aller vorhandenen Dokumente.
|
|
|
|
Zusätzlich wird die Zeit des Rendern der Sicht gemessen. Hierbei wird zum einen die komplette Zeit des Renderns
|
|
ermittelt. Innerhalb des Rendern wird dann noch die Zeit gemessen, wie lange es benötigt, die Daten aus der Datenbank
|
|
zu laden, und in die Java-Objekte umzuformen.
|
|
|
|
\begin{table}[h]
|
|
\centering
|
|
\resizebox{\textwidth}{!}{
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\
|
|
\hline
|
|
\# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\
|
|
\hline
|
|
1 & 451 & 682 & 1931 & 1223.0 & 30.3 & 931.3 & 986.1 & 54.8 & 440 & 666 & 1859 & 290 & 399 & 710 \\ % 12230 - 303 ms (135+ 79+ 39+ 22+17+11) (#2-6,8)
|
|
2 & 341 & 389 & 478 & 1208.0 & 31.2 & 986.5 & 1159.0 & 172.5 & 331 & 378 & 468 & 235 & 282 & 367 \\ % 24310 - 615 ms (270+156+ 78+ 56+34+21) (#2-7)
|
|
3 & 299 & 407 & 682 & 1208.0 & 33.5 & 1163.0 & 1273.0 & 110.0 & 290 & 398 & 672 & 207 & 307 & 579 \\ % 36390 - 950 ms (406+256+118+ 79+55+36) (#2-7)
|
|
4 & 278 & 359 & 424 & 1208.0 & 33.7 & 1272.0 & 1465.0 & 193.0 & 270 & 351 & 415 & 198 & 269 & 342 \\ % 48470 - 1287 ms (564+334+167+105+72+45) (#2-7)
|
|
5 & 264 & 317 & 356 & 1208.0 & 32.9 & 1467.0 & 1574.0 & 107.0 & 256 & 309 & 348 & 184 & 235 & 276 \\ % 60560 - 1616 ms (699+428+210+128+92+59) (#2-7)
|
|
\hline
|
|
\end{tabular}
|
|
}
|
|
\caption{Messung ohne Caches im Docker}
|
|
\label{tbl:measure-without-cache-docker}
|
|
\end{table}
|
|
|
|
\section{Umgestalten der Datenbanktabellen}
|
|
\label{sec:performance-investigation-application:new-table}
|
|
|
|
Hierfür wurde die aktuelle Datenstruktur untersucht um zu prüfen, ob eine Umgestaltung der Tabelle einen Verbesserung
|
|
bringen würden. Die typische Optimierung ist die Normalisierung der Tabellenstruktur. Die Tabellenstruktur ist aktuell
|
|
schon normalisiert, daher kann hier nichts weiter optimiert werden.
|
|
|
|
Eine weitere Optimierungsstrategie besteht in der Denormalisierung, um sich die Verknüpfungen der Tabellen zu sparen.
|
|
Dies ist in diesem Fall nicht anwendbar, da nicht nur 1:n Beziehungen vorhanden sind, sondern auch auch n:m Beziehungen.
|
|
Dadurch würden sich die Anzahl der Dokumentenliste erhöhen. Eine weitere Möglichkeit wäre es, die Duplikate auf der
|
|
Serverseite zusammenzuführen.
|
|
|
|
\section{Caching im OpenJPA}
|
|
\label{sec:performance-investigation-application:caching-openjpa}
|
|
|
|
Die Cache-Einstellung von OpenJPA werden über die zwei Einstellungen \texttt{openjpa.DataCache} und
|
|
\texttt{openjpa.QueryCache} konfiguriert. Bei beiden Einstellungen kann zuerst einmal über ein einfaches Flag
|
|
\textit{true} und \textit{false} entschieden werden ob der Cache aktiv ist. Zusätzlich kann über das Schlüsselwort
|
|
\textit{CacheSize} die Anzahl der Elementen im Cache gesteuert werden. Wird diese Anzahl erreicht, dann werden zufällige
|
|
Objekte aus dem Cache entfernt und in eine SoftReferenceMap übertragen. Bei der Berechnung der Anzahl der Element werden
|
|
angeheftete Objekte nicht beachtet.
|
|
|
|
Die Anzahl der Soft References kann ebenfalls über eine Einstellung gesteuert werden. Hierfür wird die Anzahl der
|
|
Elemente über \textit{SoftReferenceSize} gesetzt, dessen Wert im Standard auf \textit{unbegrenzt} steht. Mit dem Wert
|
|
\textit{0} werden die Soft Referenzen komplett deaktiviert. Über die Attribute an den Entitätsklassen, können diese
|
|
Referenzen ebenfalls gesteuert werden, hierzu muss eine Überwachungszeit angegeben werden. Diese Zeit gibt in ms an,
|
|
wie lange ein Objekt gültig bleibt. Mit dem Wert \textit{-1} wird das Objekt nie ungültig, was ebenfalls der
|
|
Standardwert ist.
|
|
|
|
Zuerst wird mit aktivierten Cache mit einer Cache-Größe von 1000 Elemente getestet. Wie in
|
|
\autoref{tbl:measure-ojpa-active} zu sehen, dauert auch hier der erste Aufruf minimal länger als ohne aktiviertem
|
|
Cache. Alle Nachfolgenden Aufrufe wiederrum sind um 100ms schneller in der Verarbeitung. Auch bei der Anzahl der
|
|
Anfragen an die Datenbank kann der Rückgang der Anfragen sehr gut gesehen werden. Aktuell kann die Verringerung des
|
|
wachsenden Speicherbedarfs nur nicht erklärt werden.
|
|
|
|
\begin{table}[h!]
|
|
\centering
|
|
\resizebox{\textwidth}{!}{
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\
|
|
\hline
|
|
\# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\
|
|
\hline
|
|
1 & 291 & 611 & 2347 & 730.2 & 28.8 & 852.7 & 891.9 & 39.2 & 282 & 595 & 2286 & 172 & 284 & 770 \\ % 7302 - 288 ms (145+ 42+ 40+ 24+18+ 8+ 7+ 4) (#2-8,12)
|
|
2 & 278 & 319 & 422 & 667.3 & 25.8 & 892.7 & 1010.0 & 117.3 & 266 & 309 & 411 & 173 & 195 & 220 \\ % 13975 - 546 ms (282+ 81+ 70+ 47+33+14+11+ 8) (#2-9)
|
|
3 & 229 & 281 & 329 & 680.6 & 27.6 & 1011.0 & 1067.0 & 56.0 & 220 & 271 & 313 & 134 & 180 & 222 \\ % 20781 - 822 ms (430+120+ 99+ 77+49+20+16+11) (#2-9)
|
|
4 & 222 & 280 & 321 & 671.3 & 27.6 & 1067.0 & 1122.0 & 55.0 & 213 & 271 & 310 & 131 & 189 & 238 \\ % 27494 - 1098 ms (569+160+137+ 99+68+26+22+17) (#2-9)
|
|
5 & 206 & 272 & 388 & 683.6 & 27.6 & 1122.0 & 1219.0 & 97.0 & 199 & 264 & 380 & 122 & 175 & 291 \\ % 34330 - 1374 ms (704+202+171+128+86+34+27+22) (#2-9)
|
|
\hline
|
|
\end{tabular}
|
|
}
|
|
\caption{Messung mit OpenJPA-Cache und Größe auf 1000}
|
|
\label{tbl:measure-ojpa-active}
|
|
\end{table}
|
|
|
|
Bei einer erhöhten Cache-Größe, von 1000 auf 10000, zeigt sich auf den ersten Blick ein noch besseres Bild ab, wie in
|
|
\autoref{tbl:measure-ojpa-active-bigger} ersichtlich ist. Der erste Aufruf entspricht der Laufzeit mit geringerer
|
|
Cache"=Größe, aber schon die Anfragen an die Datenbank gehen drastisch zurück. Bei den weiteren Aufrufen werden im
|
|
Schnitt nun nur noch 6 Anfragen pro Seitenaufruf an die Datenbank gestellt, wodurch die Laufzeit im Schnitt nochmal
|
|
um 100 ms beschleunigt werden konnte.
|
|
|
|
\begin{table}[!ht]
|
|
\centering
|
|
\resizebox{\textwidth}{!}{
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\
|
|
\hline
|
|
\# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\
|
|
\hline
|
|
1 & 151 & 368 & 1904 & 141.2 & 20.8 & 906.3 & 936.8 & 30.5 & 164 & 404 & 2232 & 39 & 124 & 847 \\ % 1412 - 208 ms (133+ 40+ 23+9+2+1) (#2,4-6,10,12)
|
|
2 & 133 & 143 & 159 & 6.0 & 20.5 & 935.7 & 939.3 & 3.6 & 121 & 136 & 146 & 32 & 36 & 44 \\ % 1472 - 413 ms (274+ 80+ 47+9+2+1) (#2-3,5,6,10,12)
|
|
3 & 120 & 126 & 132 & 6.0 & 19.9 & 939.4 & 942.7 & 3.3 & 116 & 136 & 256 & 32 & 47 & 167 \\ % 1532 - 612 ms (412+119+ 69+9+2+1) (#2,3,5,6,10,12)
|
|
4 & 120 & 124 & 128 & 6.0 & 21.4 & 944.3 & 945.4 & 1.1 & 105 & 113 & 125 & 30 & 32 & 39 \\ % 1592 - 826 ms (550+168+ 96+9+2+1) (#2-4,6,10,12)
|
|
5 & 109 & 114 & 131 & 6.0 & 19.7 & 945.5 & 946.7 & 1.2 & 101 & 107 & 112 & 30 & 32 & 35 \\ % 1652 - 1023 ms (683+209+119+9+2+1) (#2-4,6,10,12)
|
|
\hline
|
|
\end{tabular}
|
|
}
|
|
\caption{Messung mit OpenJPA-Cache und Größe auf 10000}
|
|
\label{tbl:measure-ojpa-active-bigger}
|
|
\end{table}
|
|
|
|
Bei dem deaktivieren der \textit{SoftReference} und dem kleineren Cache zeigt sich keine große Differenz, somit scheint
|
|
die \textit{SoftReference} nicht das Problem für den steigenden Arbeitsspeicher zu sein, wie in
|
|
\autoref{tbl:measure-ojpa-active-bigger-no-softref} ersichtlich.
|
|
|
|
% document, documentaddresseeperson, first/last, documentcoauthorperson, count und documentfacsimile
|
|
\begin{table}[h!]
|
|
\centering
|
|
\resizebox{\textwidth}{!}{
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\
|
|
\hline
|
|
\# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\
|
|
\hline
|
|
1 & 339 & 659 & 2435 & 880.8 & 33.2 & 909.6 & 960.2 & 50.6 & 330 & 644 & 2375 & 218 & 343 & 815 \\ % 8808 - 332 ms (168+ 63+ 44+ 32+21+ 4) (#1,3-6,10)
|
|
2 & 267 & 332 & 388 & 832.1 & 28.1 & 959.7 & 1000.0 & 40.3 & 259 & 323 & 377 & 178 & 229 & 280 \\ % 17129 - 613 ms (313+111+ 82+ 48+42+17) (#1-3,5-6,8)
|
|
3 & 265 & 397 & 350 & 830.3 & 27.3 & 1001.0 & 1107.0 & 106.0 & 256 & 288 & 241 & 172 & 204 & 252 \\ % 25432 - 886 ms (455+156+125+ 64+63+23) (#1-3,5-6,8)
|
|
4 & 249 & 311 & 401 & 727.8 & 27.1 & 1108.0 & 1234.0 & 126.0 & 240 & 303 & 392 & 165 & 225 & 317 \\ % 32710 - 1157 ms (594+205+163+ 85+80+30) (#1-5,8)
|
|
5 & 268 & 296 & 325 & 931.9 & 28.0 & 1236.0 & 1239.0 & 3.0 & 260 & 288 & 318 & 192 & 217 & 244 \\ % 42029 - 1437 ms (738+254+204+106+97+38) (#1-5,8)
|
|
\hline
|
|
\end{tabular}
|
|
}
|
|
\caption{Messung mit OpenJPA-Cache und Größe auf 1000 und 0 SoftReference}
|
|
\label{tbl:measure-ojpa-active-bigger-no-softref}
|
|
\end{table}
|
|
|
|
Der Vergleich zeigt, dass der Cache eine gute Optimierung bringt, aber dies nur dann gut funktioniert, wenn immer
|
|
wieder die gleichen Objekte ermittelt werden. Sobald die Anfragen im Wechsel gerufen werden oder einfach nur die Menge
|
|
der Objekte den Cache übersteigt, fällt die Verbesserung geringer aus.
|
|
|
|
\section{Cached Queries}
|
|
\label{sec:performance-investigation-application:cached-query}
|
|
|
|
Über die Einstellung \textit{openjpa.""jdbc.""QuerySQLCache} wird der Cache für abfragen aktiviert. Hierbei können
|
|
Abfragen angeben werden, die aus dem Cache ausgeschlossen werden. Der QueryCache wiederrum beachtet aber nur Abfragen
|
|
die keine Parameter verwenden. Das sieht man auch entsprechend der Auswertung der Aufrufe in der
|
|
\autoref{tbl:measure-cached-queries}, dass hier keine Veränderung der Aufrufzeiten stattgefunden hat. Gleich ob man
|
|
mit \ac{JPQL} oder mit der Criteria API abfragt.
|
|
|
|
% document, documentaddresseeperson, first/last, documentcoauthorperson, count und documentfacsimile
|
|
\begin{table}[h!]
|
|
\centering
|
|
\resizebox{\textwidth}{!}{
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\
|
|
\hline
|
|
\# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\
|
|
\hline
|
|
1 & 409 & 771 & 2660 & 1222.4 & xxx & 850.4 & 982.8 & 132.4 & 366 & 633 & 2019 & 254 & 364 & 758 \\ % 12224 - 332 ms (168+ 63+ 44+ 32+21+ 4) (#1,3-6,10)
|
|
2 & 336 & 387 & 504 & 1208.0 & xxx & 982.9 & 1113.0 & 130.1 & 310 & 374 & 433 & 221 & 268 & 345 \\ % 24304 -
|
|
3 & 312 & 373 & 422 & 1208.0 & xxx & 1114.0 & 1221.0 & 107.0 & 295 & 401 & 658 & 216 & 320 & 570 \\ % 36384 -
|
|
4 & 288 & 363 & 471 & 1208.0 & xxx & 1239.0 & 1474.0 & 235.0 & 269 & 356 & 486 & 200 & 279 & 405 \\ % 48464 -
|
|
5 & 325 & 398 & 535 & 1208.0 & xxx & 1474.0 & 1666.0 & 192.0 & 280 & 466 & 804 & 208 & 390 & 725 \\ % 60544 -
|
|
\hline
|
|
\end{tabular}
|
|
}
|
|
\caption{Messung mit aktiviertem Cached Queries}
|
|
\label{tbl:measure-cached-queries}
|
|
\end{table}
|
|
|
|
\mytodos{Queryzeiten fehlen nocht}
|
|
|
|
\section{Caching mit Ehcache}
|
|
\label{sec:performance-investigation-application:caching-ehcache}
|
|
|
|
Der Ehcache ist ein L2"=Cache den man direkt in OpenJPA mit integrieren kann. Hierfür sind einige Punkte zu beachten.
|
|
Zum einen muss die Reference auf das \textit{ehcache} und das \textit{ehcache"=openjpa} Packet hinzugefügt werden.
|
|
Zusätzlich dazu sind die Konfiguration \textit{openjpa.""QueryCache}, \textit{openjpa.""DataCache} und
|
|
\textit{openjpa.""DataCacheManager} auf \textit{ehcache} anzupassen. Anhand der Annotation \textbf{@DataCache} kann
|
|
an jeder Klasse die Benennung des Caches sowie die Verwendung selbst gesteuert werden. Es wird für jede Klasse ein
|
|
eigener Cache angelegt und der Name auf den vollen Klassennamen gesetzt. Die Verwendung ist für alle Klassen
|
|
aktiviert und müssen explizit deaktiviert werden, wenn dies nicht gewünscht ist. Als letztes muss noch der
|
|
Cache"=Manager aktiviert werden, dieser kann entweder durch Code programmiert werden oder über eine Konfiguration
|
|
in der \textit{ehcache.xml}.
|
|
|
|
Anhand der Auswertung von \ref{tbl:measure-ehcache-active} sieht man, dass der Ehcache einen starke Performance
|
|
Verbesserung aufbringt. Über die Performance"=Statistik"=Webseite kann beobachtet werden, dass bei gleichen Aufruf
|
|
der Webseite nur die Treffer in Cache steigen, aber die Misses nicht. Ebenfalls erhöht sich die Anzahl der Objekte
|
|
im Cache nicht. Zusätzlich steigt in diesem Fall der Speicherverbrauch nur gering bis gar nicht.
|
|
|
|
% document, documentaddresseeperson, first/last, documentcoauthorperson, count und documentfacsimile
|
|
\begin{table}[h!]
|
|
\centering
|
|
\resizebox{\textwidth}{!}{
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\
|
|
\hline
|
|
\# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\
|
|
\hline
|
|
%- & 151 & 368 & 1904 & 141.2 & 20.8 & 906.3 & 936.8 & 30.5 & 164 & 404 & 2232 & 39 & 124 & 847 \\ % 1412 - 208 ms (133+ 40+ 23+9+2+1) (#2,4-6,10,12)
|
|
1 & 156 & 488 & 2820 & 135.2 & xxx & 981.6 & 1006.0 & 24.4 & 147 & 490 & 2809 & 39 & 175 & 1186 \\ % 1352 -
|
|
2 & 135 & 144 & 166 & 6.0 & xxx & 1006.0 & 1007.0 & 1.0 & 124 & 136 & 157 & 33 & 38 & 47 \\ % 1412 -
|
|
3 & 121 & 129 & 136 & 6.0 & xxx & 1008.0 & 1009.0 & 1.0 & 113 & 121 & 126 & 32 & 34 & 33 \\ % 1472 -
|
|
4 & 116 & 123 & 133 & 6.0 & xxx & 1008.0 & 1016.0 & 8.0 & 108 & 116 & 125 & 31 & 33 & 34 \\ % 1532 -
|
|
5 & 111 & 118 & 127 & 6.0 & xxx & 1016.0 & 1012.0 & -4.0 & 104 & 111 & 119 & 32 & 34 & 38 \\ % 1592 -
|
|
\hline
|
|
\end{tabular}
|
|
}
|
|
\caption{Messung mit aktiviertem Ehcache}
|
|
\label{tbl:measure-ehcache-active}
|
|
\end{table}
|
|
|
|
|
|
\section{Caching in EJB}
|
|
\label{sec:performance-investigation-application:caching-ejb}
|
|
|
|
Die Cache-Einstellungen des \ac{EJB} sind in der Admin-Oberfläche des Payara-Servers zu erreichen. Unter dem Punkt
|
|
Configurations $\Rightarrow$ server!=config $\Rightarrow$ EJB Container werden zum einem die minimalen und maximalen
|
|
Größen des Pools definiert werden. Ebenso wird an dieser Stelle die maximale Größe des Caches und die Größe der
|
|
Erweiterung definiert.
|
|
|
|
Anhand der Auswertung der \autoref{tbl:measure-ejb-cache-active} ist ersichtlich, dass der \ac{EJB}"=Cache keine
|
|
Auswirkung auf die Performance hat. Und es ist ersichtlich, dass die Anzahl der Datenbankabfragen nicht reduziert
|
|
wurden. Dies ist dadurch zu erklären, dass im \ac{EJB} die Provider gelagert werden, die über Dependency Injection
|
|
den Controller bereitgestellt werden. Die Objekt selbst werden nicht im \ac{EJB}"=Cache hinterlegt.
|
|
|
|
\mytodos{Messzeiten fehlen noch}
|
|
|
|
\begin{table}[h!]
|
|
\centering
|
|
\resizebox{\textwidth}{!}{
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\
|
|
\hline
|
|
\# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\
|
|
\hline
|
|
%- & 151 & 368 & 1904 & 141.2 & 20.8 & 906.3 & 936.8 & 30.5 & 164 & 404 & 2232 & 39 & 124 & 847 \\ % 1412 - 208 ms (133+ 40+ 23+9+2+1) (#2,4-6,10,12)
|
|
1 & 364 & 741 & 2962 & 1222.1 & xxxx & 880.6 & 991.7 & xxx & 353 & 725 & 2902 & 248 & 366 & 689 \\ % 12221 -
|
|
2 & 318 & 378 & 460 & 1208.0 & xxxx & 992.4 & 1099.0 & xxx & 310 & 370 & 451 & 225 & 275 & 362 \\ % 24301 -
|
|
3 & 314 & 397 & 528 & 1208.0 & xxxx & 1109.0 & 1308.0 & xxx & 306 & 388 & 519 & 227 & 307 & 434 \\ % 36381 -
|
|
4 & 334 & 371 & 420 & 1208.0 & xxxx & 1308.0 & 1528.0 & xxx & 326 & 363 & 412 & 246 & 289 & 333 \\ % 48461 -
|
|
5 & 304 & 392 & 562 & 1208.0 & xxxx & 1518.0 & 1662.0 & xxx & 297 & 385 & 555 & 229 & 311 & 478 \\ % 60541 -
|
|
\hline
|
|
\end{tabular}
|
|
}
|
|
\caption{Messung mit \ac{EJB}-Cache}
|
|
\label{tbl:measure-ejb-cache-active}
|
|
\end{table}
|
|
|
|
\section{Abfragen JPQL}
|
|
\label{sec:performance-investigation-application:query-jpql}
|
|
|
|
Für die \ac{JPQL} wird ein \ac{SQL} ähnlicher Syntax verwendet um die Abfragen an die Datenbank durchzuführen. Für die
|
|
Dokumentenliste wird der Code aus dem \autoref{lst:jpql-document-list-jpql} verwendet. Die Namen mit vorangestellten
|
|
Doppelpunkt sind Übergabevariablen.
|
|
|
|
\begin{lstlisting}[language=Java,caption={JPQL Dokumentenliste},label=lst:jpql-document-list-jpql]
|
|
SELECT DISTINCT d FROM Document d
|
|
LEFT JOIN FETCH d.authorPerson
|
|
LEFT JOIN FETCH d.coauthorPersonSet
|
|
LEFT JOIN FETCH d.addresseePersonSet
|
|
WHERE d.validUntil > :now
|
|
AND d.isPublishedInDb = :published
|
|
ORDER BY d.documentId ASC
|
|
\end{lstlisting}
|
|
|
|
In dem dazugehörigen Code am Server wird der JPQL-Code als NamedQuery hinterlegt und über den Name
|
|
\textit{Document.""findAll} referenziert. Eine Veränderung der Abfrage ist hier leider nicht möglich, wie man im Code
|
|
aus \autoref{lst:jpql-document-list} sehen kann.
|
|
|
|
\begin{lstlisting}[language=Java,caption={Java JPQL Dokumentenliste},label=lst:jpql-document-list]
|
|
List<Document> myResultList = createNamedTypedQuery("Document.findAll")
|
|
.setParameter("now", _IncludeDeleted ? new Date(0) : Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant()))
|
|
.setParameter("published", true)
|
|
.setFirstResult(_Start)
|
|
.setMaxResults(_Size)
|
|
.setHint("javax.persistence.query.fetchSize", _Size)
|
|
.getResultList();
|
|
|
|
// Uebergabe der Ergebnisliste
|
|
if(myResultList != null && !myResultList.isEmpty()) {
|
|
myResult.addAll(myResultList);
|
|
}
|
|
\end{lstlisting}
|
|
|
|
Da dieser Code direkt so aus dem Projekt kommt, wird hierfür keine gesonderte Zeitmessung durchgeführt, da diese der
|
|
Messung aus \autoref{tbl:measure-without-cache} entspricht.
|
|
|
|
Für die Optimierung wurden noch zusätzlich die Hints \textit{openjpa.""hint.""OptimizeResultCount},
|
|
\textit{javax.""persistence.""query.""fetchSize} und \textit{openjpa.""FetchPlan.""FetchBatchSize} gesetzt. Hierbei
|
|
konnten je nach gesetzten Wert, keine relevanten Unterschiede festgestellt werden. Hierbei wurde der Wert auf zwei
|
|
gesetzt, welcher viel zu gering ist. Als weiterer Test wurde der Wert auf angefragte Größte gestellt und auf den
|
|
20"=fachen Wert der angefragten Größe.
|
|
|
|
Ebenso bringt der Hint \textit{openjpa.""FetchPlan.""ReadLockMode} auch keinen Unterschied bei der Geschwindigkeit.
|
|
Hierbei ist erklärbar, da im Standard bei einer reinen Selektion eine Lesesperre aktiv sein muss.
|
|
Bei \textit{openjpa.""FetchPlan.""Isolation} wird gesteuert, auf welche Sperren beim laden geachtet wird. Damit könnte
|
|
man zwar schreibsperren umgehen, und würde damit die Anfrage nicht mehr blockieren lassen, aber es führt unweigerlich
|
|
zu sogenannten \glqq Dirty"=Reads\grqq, wodurch die Ausgabe verfälscht werden könnte. Daher ist diese Einstellung sehr
|
|
mit Vorsicht zu verwenden.
|
|
|
|
Mit dem Hint \textit{openjpa.""FetchPlan.""EagerFetchMode} wird definiert, wie zusammengehörige Objekte abgefragt werden.
|
|
Bei dem Wert \textit{none} werden nur die Basis"=Daten abgefragt und jedes weitere Objekt wird in einem eigenen
|
|
Statement abgefragt. Mit \textit{join} wird definiert, dass abhängige Objekte die als \glqq to-one\grqq"=Relation
|
|
definiert sind, in der Abfrage über einen Join verknüpft und damit direkt mitgeladen werden. Bei reinen
|
|
\glqq to-one\grqq"=Relation funktioniert das rekursive und spart sich damit einige einzelne Abfragen.
|
|
Bei der Einstellung \textit{parallel} wird für zwar für jedes abhängigen Objektdefinition eine Abfrage durchgeführt,
|
|
aber bei dieser wird der Einstieg über das Hauptobjekt durchgeführt. Somit muss in unserem Beispiel nicht für jedes
|
|
Dokument eine einzelne abfrage für die Koautoren durchgeführt werden, sondern es wird nur eine Abfrage abgesetzt für
|
|
alle Dokumente die ermittelt wurden. Technisch gesehen wird, die gleiche WHERE"=Abfrage nochmal durchgeführt und um
|
|
die JOINS ergänzt, um die Daten der Unterobjekte zu ermitteln.
|
|
Mit dem Hint \textit{openjpa.""FetchPlan.""SubclassFetchMode} ist die Konfiguration für Unterklassen definiert. Die
|
|
Möglichkeiten entsprechen der vom \textit{openjpa.""FetchPlan.""EagerFetchMode}.
|
|
|
|
Beim Umstellen der 2 Hints auf \textit{parallel} wird die Bearbeitungszeit fast halbiert und Anzahl der Datenbankaufrufe
|
|
wurde fast geviertelt. Dies zeigt, dass die einzelnen Aufrufe je Dokument aufwendiger sind, als eine komplette Abfrage
|
|
der abhängigen Daten und das zusammensetzen in der OpenJPA-Schicht.
|
|
|
|
Der letzte Hint \textit{openjpa.""FetchPlan.""MaxFetchDepth} schränkt die rekursive Tiefe ein, für die abhängige
|
|
Objekte mitgeladen werden. Lediglich auf Grund fehlender Datenbestände wird die Abfrage beschleunigt.
|
|
|
|
\section{Abfragen Criteria API}
|
|
\label{sec:performance-investigation-application:query-criteria-api}
|
|
|
|
Für die Criteria API wird die Abfrage nicht in einem SQL-Dialekt beschreiben. Hierbei werden über Attribute die
|
|
Verlinkung zur Datenbank durchgeführt. An der Klasse selbst wird der Tabellenname definiert und an den Attributen die
|
|
Spaltennamen. Um die Anfrage durchführen muss nun nur noch Datenklasse angegeben werden und mit den Parametern
|
|
versorgt werden, wie es in \autoref{lst:criteria-api} gezeigt wird.
|
|
|
|
\begin{lstlisting}[language=Java,caption={Criteria API Dokumentenliste},label=lst:criteria-api]
|
|
CriteriaBuilder cb = getEntityManager().getCriteriaBuilder();
|
|
CriteriaQuery<Document> cq = cb.createQuery(Document.class);
|
|
Root<Document> from = cq.from(Document.class);
|
|
ParameterExpression<Boolean> includedPara = cb.parameter(Boolean.class, "published");
|
|
ParameterExpression<Date> validPart = cb.parameter(Date.class, "now");
|
|
|
|
CriteriaQuery<Document> select = cq.select(from)
|
|
.where(cb.and(
|
|
cb.equal(from.get("isPublishedInDb"), includedPara),
|
|
cb.greaterThan(from.get("validUntil"), validPart)
|
|
));
|
|
TypedQuery<Document> typedQuery = getEntityManager().createQuery(select)
|
|
.setParameter("now", _IncludeDeleted ? new Date(0) : Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant()))
|
|
.setParameter("published", true)
|
|
.setFirstResult(_Start)
|
|
.setMaxResults(_Size)
|
|
.setHint("javax.persistence.query.fetchSize", _Size);
|
|
List<Document> myResultList = typedQuery.getResultList();
|
|
|
|
// Uebergabe der Ergebnisliste
|
|
if (myResultList != null && !myResultList.isEmpty()) {
|
|
myResult.addAll(myResultList);
|
|
}
|
|
\end{lstlisting}
|
|
|
|
Wie in der Messung in \autoref{tbl:measure-criteria-api} zu sehen, unterscheiden sich die Abfragezeiten nur marginal
|
|
von denen mit \ac{JPQL}. Wenn man sich den Code im Debugger anschaut, sieht man auch, dass die zusammengesetzten
|
|
Abfragen in den Java-Objekten fast identisch sind. Und in der Datenbank sind die Anfragen identisch zu denen über JPQL.
|
|
|
|
\begin{table}[h!]
|
|
\centering
|
|
\resizebox{\textwidth}{!}{
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\
|
|
\hline
|
|
\# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\
|
|
\hline
|
|
1 & 396 & 572 & 1535 & 12173 & 796.59 & 970.10 & 173.51 \\
|
|
2 & 333 & 366 & 397 & 12080 & 982.28 & 1064.12 & 81.84 \\
|
|
3 & 286 & 339 & 554 & 12080 & 1048.12 & 1162.92 & 114.80 \\
|
|
4 & 293 & 317 & 388 & 12080 & 1150.43 & 1263.77 & 113.34 \\
|
|
\hline
|
|
\end{tabular}
|
|
}
|
|
\caption{Messung mit Criteria-API ohne Cache}
|
|
\label{tbl:measure-criteria-api}
|
|
\end{table}
|
|
|
|
Daher bringt die Criteria API keinen performance Vorteil gegenüber der JPQL"=Implementierung. Somit können beide
|
|
Implementierung ohne bedenken gegeneinander ausgetauscht werden, und die verwendet werden, die für den Anwendungsfall
|
|
einfacher umzusetzen ist.
|
|
|
|
Bei den Hints ist es das gleiche wie bei \ac{JPQL}. Auch hier haben die meisten Hints keinen merkbaren Einfluss. Die
|
|
Einstellung \textit{openjpa.""FetchPlan.""EagerFetchMode} liefert auch hier Optimierungen, wenn der Wert auf
|
|
\textit{parallel} gestellt wird. Hier wird ebenfalls die Anzahl der Anfragen reduziert und damit auch die
|
|
Geschwindigkeit optimiert.
|
|
|
|
\section{Materialized Views}
|
|
\label{sec:performance-investigation-application:materialized-views}
|
|
|
|
Materialized Views sind Sichten in der Datenbank, die beim erstellen der Sicht den aktuellen Zustand ermitteln und
|
|
Zwischenspeichern. Somit wird beim Zugriff auf diese Sichten, nicht die hinterlegte Abfrage ausgeführt, sondern auf
|
|
die gespeicherten Daten zugegriffen. Dies ist gerade bei vielen Joins von Vorteil. Zusätzlich können auf solchen
|
|
Sichten auch Indexe erstellt werden, um noch effektiver die Abfragen bearbeiten zu können.
|
|
|
|
Der größte Nachteil dieser Sichten ist, dass sie zyklisch oder bei Datenänderungen aktualisiert werden müssen, sonst
|
|
läuft der Datenbestand der Sicht und der zugrundeliegenden Abfrage auseinander. Da die Hauptarbeiten auf der Webseite
|
|
die Abfrage der Daten ist, und nicht das editieren, kann dieser Nachteil bei entsprechender Optimierung ignoriert werden.
|
|
|
|
\mytodos{hier nochmal die referenz auf wedekind-repo einbauen}
|
|
|
|
In diesem Test wurde die aktuelle Implementierung aus dem Wedekind"=Projekt der Materialized View inklusive der Trigger
|
|
und der \textit{SearchDocument}"=Klasse übernommen. Wie in \autoref{lst:sql-materialized-view} zu sehen, wurden zur
|
|
Standard"=Abfrage, die sonst zusätzlichen Abfragen als direkte Sub"=Selects mit integriert. Der Datenbestand dieser
|
|
Sub"=Selects, wird im Json"=Format angegeben, damit bei den Koautoren und den Adressen mehrere Datensätze in einer
|
|
Zeile zurückgegeben werden können. Ohne diese Technik würde sich die Anzahl der Dokumente vervielfachen.
|
|
|
|
\begin{lstlisting}[language=SQL,caption={SQL Materialized View},label=lst:sql-materialized-view]
|
|
CREATE MATERIALIZED VIEW searchdocument AS
|
|
SELECT
|
|
d.id, d.documentid, d.datetype, d.startdatestatus, d.startyear,
|
|
d.startmonth, d.startday, d.enddatestatus, d.endyear, d.endmonth,
|
|
d.endday,
|
|
(
|
|
SELECT
|
|
jsonb_build_object(
|
|
'personId', hp.personid,
|
|
'surname', hp.surname,
|
|
'firstname', hp.firstname,
|
|
'dateBirth', json_build_object(
|
|
'year', hp.birthstartyear,
|
|
'month', hp.birthstartmonth,
|
|
'day', hp.birthstartday
|
|
),
|
|
'dateDeath', json_build_object(
|
|
'year', hp.deathstartyear,
|
|
'month', hp.deathstartmonth,
|
|
'day', hp.deathstartday
|
|
)
|
|
)
|
|
FROM historicalperson hp
|
|
WHERE hp.id = d.authorperson_id
|
|
AND hp.validuntil > NOW()
|
|
) as author,
|
|
(
|
|
SELECT
|
|
jsonb_agg(jsonb_build_object(
|
|
'personId', hcap.personid,
|
|
'surname', hcap.surname,
|
|
'firstname', hcap.firstname,
|
|
'dateBirth', json_build_object(
|
|
'year', hcap.birthstartyear,
|
|
'month', hcap.birthstartmonth,
|
|
'day', hcap.birthstartday
|
|
),
|
|
'dateDeath', json_build_object(
|
|
'year', hcap.deathstartyear,
|
|
'month', hcap.deathstartmonth,
|
|
'day', hcap.deathstartday
|
|
)
|
|
))
|
|
FROM documentcoauthorperson dcap
|
|
JOIN historicalperson hcap
|
|
ON hcap.id = dcap.authorperson_id
|
|
AND dcap.validuntil > NOW()
|
|
AND hcap.validuntil > NOW()
|
|
WHERE dcap.document_id = d.id
|
|
) AS coauthors,
|
|
(
|
|
SELECT
|
|
jsonb_agg(jsonb_build_object(
|
|
'personId', hap.personid,
|
|
'surname', hap.surname,
|
|
'firstname', hap.firstname,
|
|
'dateBirth', json_build_object(
|
|
'year', hap.birthstartyear,
|
|
'month', hap.birthstartmonth,
|
|
'day', hap.birthstartday
|
|
),
|
|
'dateDeath', json_build_object(
|
|
'year', hap.deathstartyear,
|
|
'month', hap.deathstartmonth,
|
|
'day', hap.deathstartday
|
|
)
|
|
))
|
|
FROM documentaddresseeperson dap
|
|
JOIN historicalperson hap
|
|
ON hap.id = dap.addresseeperson_id
|
|
AND dap.validuntil > NOW()
|
|
AND hap.validuntil > NOW()
|
|
WHERE dap.document_id = d.id
|
|
) AS addressees,
|
|
sc.city, d.documentcategory, d.ispublishedindb, d.createdat,
|
|
d.modifiedat, d.validuntil
|
|
FROM document d
|
|
LEFT JOIN sitecity sc ON sc.id = d.city_id;
|
|
\end{lstlisting}
|
|
|
|
Zusätzlich zur View, werden noch die Indexe aus \autoref{lst:sql-materialized-view-index} erstellt. Diese werden für
|
|
eine bessere Performance der Abfrage benötigt.
|
|
|
|
\begin{lstlisting}[language=SQL,caption={SQL Materialized View},label=lst:sql-materialized-view-index]
|
|
CREATE INDEX idx_searchdocument_documentid
|
|
ON searchdocument (documentid);
|
|
|
|
CREATE INDEX idx_searchdocument_author_surname_firstname
|
|
ON searchdocument ((author->>'surname'), (author->>'firstname'));
|
|
|
|
CREATE INDEX idx_searchdocument_startdate
|
|
ON searchdocument (startyear, startmonth, startday);
|
|
|
|
CREATE INDEX idx_searchdocument_addressees_first_entry
|
|
ON searchdocument
|
|
( ((addressees->0->>'surname')::text)
|
|
, ((addressees->0->>'firstname')::text));
|
|
|
|
CREATE INDEX idx_searchdocument_city
|
|
ON searchdocument (city);
|
|
|
|
CREATE INDEX idx_searchdocument_documentcategory
|
|
ON searchdocument (documentcategory);
|
|
\end{lstlisting}
|
|
|
|
Für die Datenermittlung wurden die notwendigen Teile aus dem Wedekind"=Projekt kopiert. Bei der Darstellung wurden die
|
|
vorhanden Elemente die die Liste der Dokumente anzeigt kopiert und auf die \textit{SearchDocument}"=Klasse angepasst.
|
|
|
|
% document, first/last, documentaddresseeperson, documentcoauthorperson, documentfacsimile und count
|
|
% document, count, first/last
|
|
\begin{table}[h!]
|
|
\centering
|
|
\resizebox{\textwidth}{!}{
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\
|
|
\hline
|
|
\# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\
|
|
\hline
|
|
1 & 203 & 315 & 808 & 17.8 & 3.0 & 851.4 & 883.9 & 32.5 \\ % 178 - 30 ms (19+11+0) (#2,4,8)
|
|
2 & 154 & 172 & 187 & 9.0 & 2.2 & 883.2 & 887.0 & 3.8 \\ % 268 - 52 ms (33+18+1) (#2,3,8)
|
|
3 & 145 & 151 & 163 & 9.0 & 2.8 & 887.7 & 895.3 & 7.6 \\ % 358 - 80 ms (52+27+1) (#2,3,8)
|
|
4 & 132 & 143 & 152 & 9.0 & 2.8 & 896.0 & 900.0 & 4.0 \\ % 448 - 108 ms (70+37+1) (#2,3,8)
|
|
5 & 121 & 125 & 132 & 9.0 & 2.4 & 900.6 & 901.0 & 0.4 \\ % 538 - 132 ms (85+46+1) (#2,3,8)
|
|
\hline
|
|
\end{tabular}
|
|
}
|
|
\caption{Messung mit Materialized View}
|
|
\label{tbl:measure-materialized-view}
|
|
\end{table}
|
|
|
|
Wie in \autoref{tbl:measure-materialized-view} zu sehen, bringt die Verwendung der Materialized View eine Verbesserung
|
|
in verschiedenen Punkten. Zum einen ist eine Verbesserung der Aufrufzeiten zu erkennen, zusätzlich fällt der
|
|
Speicheranstieg weniger stark aus. Die Verbesserung der Aufrufzeiten lässt sich zusätzlich erklären, dass hier nun
|
|
nur noch vier statt der 6 Abfragen an die Datenbank gestellt werden, da die Einzelabfragen für die Adressen der
|
|
Personen und der Koautoren komplett entfallen.
|
|
|
|
Nach dem der Quellcode nochmal untersucht wurde, konnte man festellen, dass bei jeder Anfrage die gleiche Bedingung
|
|
benötigt wurde. Da die Sicht nun explizit für dies Anfrage geschaffen wurde, wurde die Bedingungen nun direkt in die
|
|
Sicht mit integriert. Dies bedeutet eine Erweiterung der Sicht aus \autoref{lst:sql-materialized-view} um
|
|
\autoref{lst:sql-materialized-view-ext} und das entfernen der Parameter aus dem SQL-Anfragen im Java-Code.
|
|
|
|
\begin{lstlisting}[language=SQL,caption={SQL Materialized View Erweiterung},label=lst:sql-materialized-view-ext]
|
|
WHERE d.validuntil > NOW()
|
|
AND d.ispublishedindb = true;
|
|
\end{lstlisting}
|
|
|
|
Nach dem Anpassungen haben sich dann die Werte aus \autoref{tbl:measure-materialized-view-ext} ergeben.
|
|
|
|
\begin{table}[h!]
|
|
\centering
|
|
\resizebox{\textwidth}{!}{
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\
|
|
\hline
|
|
\# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\
|
|
\hline
|
|
1 & 241 & 348 & 859 & 16.8 & xxx & 896.0 & 932.4 & 36.4 & 232 & 331 & 803 & 132 & 174 & 334 \\ % 168 -
|
|
2 & 164 & 194 & 225 & 9.0 & xxx & 933.3 & 935.9 & 2.6 & 154 & 185 & 215 & 79 & 99 & 117 \\ % 258 -
|
|
3 & 147 & 161 & 179 & 9.0 & xxx & 935.8 & 938.8 & 3.0 & 139 & 152 & 167 & 68 & 77 & 86 \\ % 348 -
|
|
4 & 135 & 145 & 183 & 9.0 & xxx & 939.4 & 936.0 & -3.4 & 127 & 137 & 174 & 70 & 73 & 75 \\ % 438 -
|
|
5 & 126 & 137 & 154 & 9.0 & xxx & 936.1 & 939.1 & 3.0 & 118 & 129 & 143 & 66 & 72 & 79 \\ % 528 -
|
|
\hline
|
|
\end{tabular}
|
|
}
|
|
\caption{Messung mit erweiterter Materialized View}
|
|
\label{tbl:measure-materialized-view-ext}
|
|
\end{table}
|
|
|
|
Da bei der Materialized View das laden der Daten und das wandeln in die Java"=Objekte getrennt programmiert wurde,
|
|
können hier eigene Zeitmessungen für die zwei Schritte eingebaut werden. Hierfür wird die Zeit vor dem
|
|
\textit{map}"=Aufruf und der \textit{map}"=Aufruf gemessen. Für den ersten Aufruf, wurde ein \textit{SearchDocument}
|
|
Objekt erzeugt und immer diese Objekt zurückgegeben. Damit wurde erst mal überprüft, wie lange das ermitteln der Daten
|
|
und das durcharbeiten der Ergebnisse bestimmt. Hierbei lagen die Zeiten bei circa 1 ms für das reine Datenladen und 3 ms
|
|
für den Aufruf der \textit{map}"=Funktion. Sowie innerhalb der \textit{map}"=Funktion pro Eintrag ein Objekt
|
|
erzeugt, noch ohne eine Konvertierung der ermittelten Daten in das Objekt, steigt die Laufzeit schon auf 54 ms.
|
|
Wenn man nun noch die Konvertierung der Daten wieder einbaut, steigt die Laufzeit nochmal auf nun 82 ms.
|
|
Dies zeigt, alleine das erzeugen der Objekt und der Json"=Parse Aufruf kostet die meiste Zeit.
|
|
|
|
Bei der Verwendung des Hints \textit{openjpa.""FetchPlan.""FetchBatchSize} kann die Abfrage enorm verschlechtern. Wenn
|
|
dieser Wert zu klein oder groß definiert ist, wird die Laufzeit verschlechtert. Bei einem zu großen Wert wird die
|
|
Laufzeit der Datenbankanfrage auf circa 20 ms verlängert. Wenn der Wert zu gering gewählt ist, dann wird zwar die
|
|
Laufzeit der Datenbankanfrage minimal verkürzt, aber die \textit{map}"=Funktion wird dadurch verlängert.
|
|
|
|
Das aktivieren der Cache"=Optionen wie in \autoref{sec:performance-investigation-application:caching-openjpa} oder in
|
|
\autoref{sec:performance-investigation-application:cached-query} dargestellt, haben keine Auswirkung auf die Performance.
|
|
Dies ist dadurch erklärbar, da keine Objekte durch das OpenJPA"=Framework erstellt werden, sondern erst in der
|
|
\textit{map}"=Funktion des eigenen Codes.
|
|
|
|
Wie schon ermittelt, benötigt das erstellen der Objekte den Großteil der Zeit für die Datenermittlung. Daher wurde die
|
|
bereitgestellt Klasse betrachtet und die wie mit Herrn Tobias Holstein ausgemacht, der JsonParser nochmal genauer
|
|
untersucht. Im ersten Schritt wird die Parse"=Funktion entfernt und die Seite nochmals aufgerufen. Durch diese
|
|
Umstellung fällt die Laufzeit der Datenermittlung auf circa 4 ms ab. Nun muss noch geprüft werden, welche Zeit nun der
|
|
Client zum parsen der \textit{Json}"=Daten benötigt. Hierfür werden die Daten in einem versteckten
|
|
\textbf{span}"=Element hinterlegt, wie es im \autoref{lst:jsf-datatable-json} zu sehen ist. Die hinterlegte
|
|
\textit{CSS}"=Klasse ist zum auffinden der Elemente für den späteren Javascript. Das \textbf{ajax}"=Element im Beispiel
|
|
ist notwendig, damit bei einem Seitenwechsel die gleiche Interpreter"=Funktion für die \textit{Json}"=Daten aufgerufen
|
|
wird, wie beim laden der Webseite.
|
|
|
|
\begin{lstlisting}[language=xml,caption={DataTable mit Json},label=lst:jsf-datatable-json]
|
|
<p:ajax event="page" oncomplete="convertJsonData()"/>
|
|
<p:column id="author" headerText="#{lang.List_Docs_Author}" sortable="true" sortBy="#{myObj.surname}">
|
|
<h:outputText styleClass="json-convert" style="display: none;" value="#{myObj.authorJson}"/>
|
|
</p:column>
|
|
<p:column id="Addressee"
|
|
headerText="#{lang.List_Docs_Addressee}"
|
|
sortable="true"
|
|
sortBy="#{(myObj.addresseePersonSet!=null and myObj.addresseePersonSet.size() > 0)?myObj.addresseePersonSet[0].addresseePerson:''}">
|
|
<h:outputText styleClass="json-convert" style="display: none;" value="#{myObj.adresseeJson}" data="abc"/>
|
|
</p:column>
|
|
\end{lstlisting}
|
|
|
|
Die Interpreter"=Funktion, welche in JavaScript geschrieben ist, wird benötigt um die ie übertragenen
|
|
\textit{Json}"=Daten in eine darstelle Form zu bringen. Die Funktion aus dem \autoref{lst:jsf-datatable-json-convert}
|
|
ermittelt erst alle versteckten Elemente, parsed den Inhalt und erstellt neue \textit{HTML}"=Elemente mit dem
|
|
darzustellenden Inhalt. Zusätzlich wird noch eine Zeitmessung mit eingebaut, um die Laufzeit am Client für das Rendern
|
|
in der Konsole anzuzeigen. Die Funktion wird nun direkt nach dem die Webseite fertig geladen wurde aufgerufen.
|
|
|
|
\begin{lstlisting}[language=javascript,caption={Wandeln von Json nach Html},label=lst:jsf-datatable-json-convert]
|
|
function isEmpty(str) {
|
|
return (str === null) || (str === undefined) || (typeof str === "string" && str.length === 0);
|
|
}
|
|
function convertJsonData() {
|
|
let $jsonObj = $(".json-convert")
|
|
, start = new Date()
|
|
;
|
|
|
|
$.each($jsonObj, function() {
|
|
let json = this.innerHTML
|
|
, strEmpty = (json === null) || (typeof json === "string" && json.length === 0)
|
|
, jsonDat = strEmpty ? null : JSON.parse(json)
|
|
;
|
|
if(!strEmpty) {
|
|
let res = ""
|
|
, $that = $(this)
|
|
, $par = $that.parent()
|
|
;
|
|
$.each(jsonDat, function() {
|
|
let hasOnlyOne = isEmpty(this.surname) || isEmpty(this.firstname)
|
|
, pseudonymExists = !isEmpty(this.pseudonym)
|
|
, namePart = "<span>" + (hasOnlyOne ? this.surname + this.firstname : this.surname + ", " + this.firstname) + "</span><br/>"
|
|
, pseudoPart = pseudonymExists ? "<span class='w3-small w3-text-dark-gray'>" + this.pseudonym + "</span><br/>" : ""
|
|
;
|
|
res += namePart + pseudoPart;
|
|
});
|
|
$par.append(res);
|
|
}
|
|
});
|
|
let end = new Date()
|
|
, diff = (end - start)
|
|
;
|
|
console.log(Math.round(diff) + " ms");
|
|
}
|
|
|
|
$(document).ready(function() {
|
|
convertJsonData();
|
|
});
|
|
\end{lstlisting}
|
|
|
|
Da nun am Client Code ausgeführt wird, nachdem die Daten übertragen wurden, kann nicht mehr alles über das Script
|
|
durchgeführt werden. Daher werden nun die Laufzeiten am Server und am Client zusammenaddiert. Im Schnitt benötigt der
|
|
Aufruf auf der Serverseite nun 70 ms und am Client sind es circa 13 ms. Dies bedeutet addiert kommt man mit dieser
|
|
Lösung auf eine kürzere Laufzeit und weniger Last am Server.
|
|
|
|
\section{Optimierung der Abfrage}
|
|
\label{sec:performance-investigation-application:optimizing-query}
|
|
|
|
Für die Optimierung der Abfrage werden diese zuerst mit \textit{explain}, wie in \autoref{lst:explain-diagnostic}
|
|
dargestellt, untersuchen. Für die einfachere Diagnose, wird der erstellte Plan Mithilfe von pev2
|
|
\citep{GitHubda51:online} visualisiert.
|
|
|
|
\begin{lstlisting}[language=SQL,caption={Explain für Diagnose},label=lst:explain-diagnostic]
|
|
explain (analyze, verbose, buffers, summary, format json)
|
|
select <Spalten>
|
|
from
|
|
public.document t0
|
|
left outer join public.historicalperson t1 on t0.authorperson_id = t1.id
|
|
left outer join public.sitecity t5 on t0.city_id = t5.id
|
|
left outer join public.appuser t6 on t0.editor_id = t6.id
|
|
left outer join public.extendedbiography t2 on t1.extendedbiography_id = t2.id
|
|
left outer join public.sitecity t3 on t1.sitecity_birth_id = t3.id
|
|
left outer join public.sitecity t4 on t1.sitecity_death_id = t4.id
|
|
left outer join public.appuserrole t7 on t6.appuserrole_id = t7.id
|
|
where (t0.validuntil > NOW()
|
|
and t0.ispublishedindb = true)
|
|
order by startyear DESC, startmonth DESC, startday DESC
|
|
limit 400;
|
|
\end{lstlisting}
|
|
|
|
Die erstellte Visualisierung der Abfrage ist in \autoref{fig:explain-visualize} zu sehen. In der Visualisierung wurde
|
|
die Darstellung der Kosten gewählt, da ein Vergleich auf Basis der Zeit sehr schwierig ist und von äußeren Faktoren
|
|
abhängt, wie zum Beispiel dem Cache. Die Kosten sind stabiler und hängen in erster Linie vom Datenbestand ab.
|
|
|
|
\begin{figure}[h!]
|
|
\includegraphics[width=\linewidth]{gfx/chapter05_ExplainVisualize.png}
|
|
\caption{Visualisierung EXPLAIN}
|
|
\label{fig:explain-visualize}
|
|
\end{figure}
|
|
|
|
In der Graphik ist zu sehen, dass zum einen die Hauptkosten im untersten Knoten \textit{Seq Scan} und einen der
|
|
obersten Knoten dem \textit{HashAggregate} liegen. Zusätzlich sieht man anhand der stärke von den Verbindungslinien der
|
|
Knoten, dass die Menge der Datensätze enorm hoch ist und dieser sich bis zum obersten Knoten durchzieht. Dies
|
|
bedeutet, dass die Einschränkung des Datenbestandes erst am Ende der Abfrage durchgeführt wird und diesbezüglich die
|
|
Dauer der Abfrage linear mit den Inhalt der \textit{document}"=Tabelle zusammenhängt. Des Weiteren wird für keine
|
|
Tabelle ein \textit{Index Scan} verwendet, sondern immer mit einem \textit{Seq Scan} gearbeitet, da durch das ermitteln
|
|
des kompletten Datenbestandes der Optimizer entscheidet, dass der komplette Scan der Tabelle kostengünstiger ist, als
|
|
die Verwendung eines der vorhandenen Indexe. Dies kann durch den Befehl \lstinline[language=SQL]|SET enable_seqscan = off|
|
|
sehr einfach verifiziert werden. Damit wird die Verwendung von \textit{Seq Scan} deaktiviert und es wird dann ein
|
|
\textit{Index Scan} verwendet. Wenn man nun beide Pläne vergleicht sieht man die Erhöhung der Kosten bei der Verwendung
|
|
von \textit{Index Scan}.
|
|
|
|
Die beste Optimierung hierbei ist, die Menge der Datensätze so früh wie möglich einzuschränken. Da die Verwendung von
|
|
\textit{order by} innerhalb eines Sub"=Selects nicht erlaubt ist, verwenden wir hierfür eine \textit{Common Table
|
|
Expression}, wie es in \autoref{lst:explain-optimize-cte} zu sehen ist. Zusätzlich wurde noch ein Index auf der
|
|
\textit{document}"=Tabelle für die Spalten der Bedingung und der Sortierung gesetzt, wie in
|
|
\autoref{lst:explain-optimize-cte-idx} zur sehen, damit in der \textit{Common Table Expression} nur der Index verwendet
|
|
werden kann und kein zusätzlicher Zugriff in die Tabelle notwendig ist.
|
|
|
|
\begin{lstlisting}[language=SQL,caption={Optimierung mit Common Table Expression},label=lst:explain-optimize-cte]
|
|
with doc_limit as (
|
|
select id
|
|
from public.document
|
|
where validuntil > now()
|
|
and ispublishedindb = true
|
|
order by startyear desc, startmonth desc, startday desc
|
|
limit 400
|
|
)
|
|
select *
|
|
from doc_limit t
|
|
join public.document t0 on t0.id = t.id
|
|
order by t0.startyear desc, t0.startmonth desc, t0.startday desc
|
|
\end{lstlisting}
|
|
|
|
\begin{lstlisting}[language=SQL,caption={Index für Common Table Expression},label=lst:explain-optimize-cte-idx]
|
|
create index idx_document_with_stmt on document using btree
|
|
( ispublishedindb, validuntil, startyear desc, startmonth desc
|
|
, startday desc, id );
|
|
\end{lstlisting}
|
|
|
|
Mit diesen Umstellungen erkennt man nun, dass die Kosten entsprechend gefallen sind. Ebenfalls konnten die Laufzeit um
|
|
mehr als den Faktor drei reduziert werden. Die Optimierung ist in \autoref{fig:explain-visualize-with} sehr deutlich
|
|
an dein dünneren Verbindungslinien zwischen den Knoten und der Umstellung von einigen \textit{Seq Scan} zu
|
|
\textit{Index Scan} ersichtlich. Zeitgleich ist auch der teure \textit{HashAggregate} nicht mehr im Abfrageplan
|
|
vorhanden.
|
|
|
|
\begin{figure}[h!]
|
|
\includegraphics[width=\linewidth]{gfx/chapter05_ExplainVisualize_with.png}
|
|
\caption{Visualisierung EXPLAIN with}
|
|
\label{fig:explain-visualize-with}
|
|
\end{figure}
|
|
|
|
Bei der Untersuchung der Abfrage zur Materialized View ist direkt herausgekommen, dass hier keine Optimierung mehr
|
|
möglich ist, da durch die definierten Index bei den aktuell möglichen Sortierkriterien direkt ein \textit{Index Scan}
|
|
verwendet wird. Damit ist der schnellstmögliche Zugriff gegeben.
|