Java Hibernate + Oracle. Performance optimization.

Man kann grundsätzlich auf 3 verschiedenen Ebenen das Programm optimieren:

  1. Datenmodell Ebene
  2. Oracle Tuning (Indexes, Hints, Partitioning, Hardware und Betriebssystem Optimierung)
  3. Hibernate Tuning (fetch mode, caching, programm logic)

Das Tuning auf dem Datenmodell Ebene ist eher unwahrscheinlich, wird meistens nur bei besonders hartnäckigen Problemen gemacht, in meisten Fällen wird dabei eine Denormalisierung von Daten durchgeführt. Optimierungen auf Datenmodell Ebene werden oft noch bei der Architektur-Phase gemacht.
Das Tuning auf Oracle Seite ist sehr nützlich und muss auf jeden Fall bei Performance Problemen gemacht werden.
Das Hibernate Tuning ist meist flexibles aber gleichzeitig meist komplexes Prozess.

Um eine effektive Optimierung machen zu können, muss zuerst das Performance-Problem lokalisiert werden. Das erreicht man durch Generierung von realitätsnahen Workloads auf der Applikation, mit gleichzeitigem Logging von allen Hibernate+Datenbank relevanten Events (Caching, SQL’s, timings).

Logging

Hibernate hat einen eingebauten Mechanismus zum loggen von SQL Statements auf Java Konsole, das mit einem Konfigurations-Parameter eingeschaltet werden kann (Spring Beispiel)

<bean id="exampleHibernateProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
 <property name="properties">
  <props>
   <prop key="hibernate.show_sql">true</prop>  <-- show_sql statements on console -->
   <prop key="hibernate.use_sql_comments">true</prop>  <-- generate additional comments for every statement -->
   <prop key="hibernate.format_sql">true</prop>  <-- format_sql_statements -->
   <prop key="hibernate.generate_statistics">true</prop>  <-- generate statistics for sql statements -->
 </property>
</bean>

Alternativer Weg ist das Logging von SQL Statements mit log4j framework.

# Log all SQL Statements
<logger name="org.hibernate.SQL">
    <level value="DEBUG"/>
</logger>
# Logs SQL statements for id generation
<logger name="org.hibernate.id">
    <level value="INFO"/>
</logger>
# Logs the JDBC-Parameter which are passed to a query (very verboose)
<logger name="org.hibernate.type">
    <level value="DEBUG"/>
</logger>
# Logs cache related activities
<logger name="org.hibernate.cache">
    <level value="DEBUG"/>
</logger>

Nächste Alternative wäre die Verwendung von speziellen JDBC logging frameworks wie log4jdbc oder P6Spy, die wesentlich detailliertere Informationen über JDBC Aktivitäten liefern können, wie z.B. „timings“, „result sets“, connection statistics etc. Solche frameworks werden als Proxy für DataSources oder Connection eingeschaltet, was sich leicht im Java Code, oder deklarative (Spring) implementieren lässt.

Noch ein Weg, um Übersicht über Hibernate Aktivitäten zu bekommen, ist die Verwendung von Hibernate Statistiken.
Die Sammlung von Statistiken wird mit „generate_statistics“ Flag eingeschaltet, danach kann man (in regelmäßigen Abständen) die Metriken auf Konsole oder in JMX publizieren. Hier ein Beispiel der JMX Publikation mit Hilfe von Spring:

<bean id="mbeanServer" class="org.springframework.jmx.support.MBeanServerFactoryBean">
	<property name="locateExistingServerIfPossible" value="true"/>
</bean>
<bean id="jmxExporter" class="org.springframework.jmx.export.MBeanExporter">
	<property name="server" ref="mbeanServer"/>
	<property name="beans">
		<map>
			<entry key="Hibernate:application=Statistics" value-ref="jmxStatisticsBean"/>
		</map>
	</property>
</bean>
<bean id="jmxStatisticsBean" class="org.hibernate.jmx.StatisticsService">
	<property name="statisticsEnabled" value="true"/>
	<property name="sessionFactory" ref="ganesha.sessionFactory"/>
</bean>

Es gibt mehrere Tools für Visualisierung von Hibernate JMX Statistiken, ich kann z.B. Hibernate-JConsole Tool empfehlen, das eine sehr übersichtliche Darstellung von Metriken liefert. Hibernate-jconsole ist ein JConsole/VisualVM plugin, das beim Ausführen aber unbedingt das Hibernate JAR braucht.
Beispiel der Verwendung: java -jar hibernate-jconsole-1.0.4.jar -J-Dhibernate.classpath=/home/user/hibernate-core-3.6.2.Final.jar

Hibernate Statistiken haben in der jetzigen Version zwei kleine Macken. Erstens – werden die SQL Abfragen die mit Criteria API generiert wurden nicht berücksichtigt (es gibt dazu ein Bug/Workarround. Zweitens – werden keine impliziten Queries in die Statistiken eingenommen (z.B. Lazzy-Load Queries oder Queries die durch FetchMode.SELECT generiert wurden). Um auch die implizite Queries analysieren zu können (und überhaupt sehen zu können), werden SQL logging von Hibernate oder spezielle JDBC logging frameworks benötigt.
Ein weiteres Problem von Hibernate-Statistiken, ist die Detaillierung von Cache Information. Da Hibernate sich von Caching-Technologie abstrahiert, kann es nur rudimentäre Informationen darüber liefern wie z.B. (Hits, Misses, Puts). Um mehr Daten über die Caches zu sehen, werden spezifische Tools verwendet abhängig von eingesetztem Cache-Provider. Zum Beispiel, für EHCache gibt es auch ein JMX Monitoring Mechanismus (kann parallel zum Hibernate-Statistik verwendet werden), das zusätzliche Informationen liefert wie: Anzahl Objekte im Speicher und auf der Festplatte, Speicherverbrauch, Evicts.

Ein komplett anderer Logging-Ansatz wäre die Nutzung von Oracle Datenbank-Tools (trace, „Top Statements“ in Enterprise Manger, AWR). Solche Instrumenten erfordern leider oft Administrations-Rechte, was ein üblicher Java-Developer selten bekommt.

Oracle Tuning

Auf der Oracle Seite sollte man sich als Erstes den Execution Plan von SQL Statements anschauen. Das häufigste und einfachste Performance-Problem, sind die fehlenden Indexe. Oracle hat allerdings viele weitere Optimierungs-Möglichkeiten, die aber nur durch einen erfahrenen Oracle Spezialist geplant und implementiert werden sollen:

  • Caching
  • Partitionierung
  • SQL Performance Analyzer Tool
  • SQL Profiles (static execution plans)
  • Optimierungen auf Hardware und Betriebsystem-Ebene

Oracle hat interne Optimierungs-Mechanismen (Statistiken, Optimizer, Caches), die dynamisch versuchen den Durchsatz von der Datenbank zu verbessern. Aus diesem Grund, beim Performance-Tests, müssen folgende Bedingungen maximal erfühlt werden:

  • Hardware und Software vom Oracle Test-Server soll identisch zum Produktiv-Server sein.
  • Der Datenbank-Inhalt soll identisch zum Produktive-Server sein.
  • Auf dem Test-Server während des Tests müssen gleiche Prozesse laufen, wie auf dem Produktive Server.

Idealerweise sollte man direkt auf Produktive-Server testen, was leider nur sehr selten möglich ist.
Alternative erzeugt man eine vollständige Kopie von Produktive-System (z.B. mit FileSystem snapshots, oder RMAN). Mit Hilfe von realitätsnahen Test-Workloads, oder speziellen Mechanismen zum Workload-Simulation, wie Oracle Database Replay, wird auf dem Test-System das Produktive-Workload simuliert. Erst dann kann man mit den Tests und Optimierungen beginnen, um die beste Ergebnisse zu bekommen (bzw. überhaupt das Performance-Problem zu lokalisieren).

Hibernate Tuning

Im Hibernate gibt es grundsätzlich zwei Optimierungs-Möglichkeiten:

  1. Optimierung von Assoziationen (Eager vs Lazy loading, FetchMode, BatchSize etc.)
  2. Caching (Query und Second-Level caching)

Mit Caching, versucht man die Datenbank Abfragen vollständig zu vermeiden.
Hibernate hat zwei Ebenen von Entity Caches – First Level Cache (gehört zur Hibernate Session) und ein Second-Level-Cache (wird von mehreren Hibernate Sessions zusammenverwendet). Mit dem Second-Level-Cache (und zusätzlich einem Query-Cache) erreicht man den maximalem Performance-Schub. In dem First und Second-Level-Caches befinden sich die Entities. Das Query-Cache enthält die Ergebnisse von Query Abfragen, und zwar nur die ID’s von den Entities, die von der Abfrage gelesen wurden, die Entities selbst werden in dem Second-Level-Cache gehalten. Dadurch ist ein Query-Cache ohne aktiviertem Second-Level-Cache sinnlos!
Beim Konfigurieren von Caches muss man 3 Sachen beachten:

  1. Cache Provider und seine Features wie Speicher-Typ (Memory, Festplatte, Cluster), Unterstützung für Concurency-Strategien und Query-Cache
  2. Concurency-Strategy für jeden einzelnen Entity-Typ (read-only, nonstrict-read-write, read-write, transactional)
  3. Gute Cache-Kandidaten – nicht alle Entities eignen sich für das Caching

Für unveränderliche Objekte kann Caching nahezu sorglos definiert werden, beim veränderlichen Objekten muss man Eigenschaften betrachten wie: Entity-Speicherverbrauch, Verhältnis zwischen Lese/Schreib-Operationen, Concurency und Invalidierungs-Strategie.
Objekte in First und Second-Level Caches werden automatisch invalidiert, sobald ein Entity geändert wird. Für verteilte Anwendungen, mit Caching-Provider ohne Cluster-Unterstützung, muss man sich um die Invalidierung von Objekten selbst kümmern, dafür gibt es im Hibernate spezielle „evict“ Funktionen am SessionFactory für einzelne Entities und ganze Cache-Regionen invalidieren.
Beim Aktivieren von Query-Cache, wird gleichzeitig noch ein zusätzlicher UpdateTimestampsRegion erstellt, in dem, die Zeiten der letzten Änderung einer (für Hibernate bekannter) Datenbank Tabelle gespeichert werden. Jeder QueryCahe kennt die Liste aller Tabellen, die in der Query verwendet wurden. Sobald Hibernate merkt, das eine der Tabellen in der Zwischenzeit geändert wurde, wird der gesamter Query-Cache Region invalidiert.

Beim Optimieren von Assoziationen geht es darum, eine möglichst effiziente Balance zwischen der Anzahl und Dauer von SQL Abfragen zu finden (Ausführungsdauer und Anzahl/Grösse der gelesenen Datensätze). Generell ist Lazy loading empfohlen (ist standard FetchMode in Hibernate), man kann das aber nicht pauschal für alle Abfragen verwenden. Eager loading und andere Strategien können in bestimmten Situationen bessere Ergebnisse liefern.
Typische Problemen sind:

  • N+1 Problem – kann mit FetchMode.JOIN, FetchMode.SELECT, FetchMode.SUBSELECT und BatchSize verbessert werden
  • Monster SQL (kartesischer Produkt bei Eager loading – zu viele JOIN’s + ein sehr langer ResultSet + Ausführungsdauer) – man vermeidet das mit FetchModel.SELECT oder BatchSize bzw. Lazzy loading
  • Zu viele Abfragen – suboptimale Applikation oder DAO Logik
  • Verwendung von Native SQL mit Parametern in SQL Code – stattdessen sollte man Parameter binding verwenden

Links

Performance tuning tips for Hibernate and Java Persistence
Trully understanding the second level and query caches
Hibernate Improving performance
Hibernate Fetching Strategies examples

log4jdbc-remix
log4jdbc

Oracle Performance Tuning Steps
Oracle Database Performance Tuning Guide

Veröffentlicht in Java