BASEL BERN BRUGG DÜSSELDORF FRANKFURT A.M. FREIBURG I.BR. GENEVA
HAMBURG COPENHAGEN LAUSANNE MUNICH STUTTGART VIENNA ZURICH
Mastering JPA Performance
Thomas BröllPrincipal ConsultantTrivadis GmbH, Stuttgart
Das Problem
Java Forum Stuttgart - Mastering JPA Performance2 6. Juli 2017
Langsame Anwendung, Java, Datenbankzugriffe mit JPA/Hibernate
Entwickler: Geringe Auslastung am Applikationsserver
– Die Datenbank ist das Problem!
DBA: Geringe Auslastung der Datenbank
– Die Datenbank ist nicht das Problem!
Netzwerk auch nicht am Limit!
DatabaseApplication
Server
Clients
(Web)
Test Szenario
Java Forum Stuttgart - Mastering JPA Performance3 6. Juli 2017
Schreibe Daten aus CSV
Kleine Objekte, 3 Ebenen
Keine Optimierungen
Lokale in-Memory-Datenbank (javadb)
Spring Boot
State
City
Postal
Code
1
1
n
n
13381
10585
16
Java Code
Java Forum Stuttgart - Mastering JPA Performance5 6. Juli 2017
City c = …em.persist(c);
Service
@Entitypublic class City {@GeneratedValue @Id long id; …
Entity
Code – ?? Sekunden
Java Forum Stuttgart - Mastering JPA Performance6 6. Juli 2017
StreamSupport.stream(CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(fr).spliterator(), false).map(record -> {
State state = intern (new State(record.get("bundesland")), states, State::getName, s -> {em.persist(s);
});City city = intern (new City(record.get("ort")), cities, City::getName, c -> {
c.setState(state);em.persist(c);
});
PostalCode postalCode = new PostalCode(record.get("plz"));postalCode.setCity(city);return new ImmutableTriple<>(state, city, postalCode);
})//.forEach(triple -> {
em.persist(triple.getRight());});
Aus Sicht der Datenbank
Java Forum Stuttgart - Mastering JPA Performance8 6. Juli 2017
DB Monitoring
SQL Statements & Transaktionen
CPU
I/O
Top 10 statements (Anzahl)
Top 10 statements (Ausführungszeit)
Aus Sicht der Java VM
Java Forum Stuttgart - Mastering JPA Performance10 6. Juli 2017
Java VM
JVisualVM / jstack
JDBC Monitoring
JVM Profiling
Total/used memory
Methoden hotspots (Ausführungszeit)
Logging
JVM Analyse 1
Java Forum Stuttgart - Mastering JPA Performance11 6. Juli 2017
"main" #1 prio=5 os_prio=0 tid=0x00000000023ce800 nid=0x1534 runnable [0x000000000266c000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:170)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at com.mysql.cj.core.io.ReadAheadInputStream.fill(ReadAheadInputStream.java:101)at com.mysql.cj.core.io.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:144)
at com.mysql.cj.core.io.ReadAheadInputStream.read(ReadAheadInputStream.java:174)
- locked <0x00000006c5bb1b58> (a com.mysql.cj.core.io.ReadAheadInputStream)
at java.io.FilterInputStream.read(FilterInputStream.java:133)
at com.mysql.cj.core.io.FullReadInputStream.readFully(FullReadInputStream.java:58)
at com.mysql.cj.mysqla.io.SimplePacketReader.readHeader(SimplePacketReader.java:60)
at com.mysql.cj.mysqla.io.TimeTrackingPacketReader.readHeader(TimeTrackingPacketReader.java:48)
at com.mysql.cj.mysqla.io.MultiPacketReader.readHeader(MultiPacketReader.java:51)
at com.mysql.cj.mysqla.io.MysqlaProtocol.readPacket(MysqlaProtocol.java:521)
at com.mysql.cj.mysqla.io.MysqlaProtocol.checkErrorPacket(MysqlaProtocol.java:723)
at com.mysql.cj.mysqla.io.MysqlaProtocol.sendCommand(MysqlaProtocol.java:662)
at com.mysql.cj.mysqla.io.MysqlaProtocol.sqlQueryDirect(MysqlaProtocol.java:950)
at com.mysql.cj.mysqla.MysqlaSession.sqlQueryDirect(MysqlaSession.java:431)
at com.mysql.cj.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:1974)
- locked <0x00000006c5b76500> (a com.mysql.cj.jdbc.ConnectionImpl)
at com.mysql.cj.jdbc.ConnectionImpl.commit(ConnectionImpl.java:1196)
- locked <0x00000006c5b76500> (a com.mysql.cj.jdbc.ConnectionImpl)
at sun.reflect.GeneratedMethodAccessor38.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.tomcat.jdbc.pool.ProxyConnection.invoke(ProxyConnection.java:126)
at org.apache.tomcat.jdbc.pool.JdbcInterceptor.invoke(JdbcInterceptor.java:108)
at org.apache.tomcat.jdbc.pool.DisposableConnectionFacade.invoke(DisposableConnectionFacade.java:81)
at com.sun.proxy.$Proxy86.commit(Unknown Source)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcIsolationDelegate.delegateWork(JdbcIsolationDelegate.java:59)
at
org.hibernate.id.enhanced.TableGenerator$1.getNextValue(TableGenerator.java:530)at org.hibernate.id.enhanced.NoopOptimizer.generate(NoopOptimizer.java:40)
at org.hibernate.id.enhanced.TableGenerator.generate(TableGenerator.java:526)
at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:101)
at org.hibernate.jpa.event.internal.core.JpaPersistEventListener.saveWithGeneratedId(JpaPersistEventListener.java:67)
at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:189)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:132)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:58)
at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:775)
at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:748)
at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:753)
at org.hibernate.jpa.spi.AbstractEntityManagerImpl.persist(AbstractEntityManagerImpl.java:1146)
at sun.reflect.GeneratedMethodAccessor37.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:298)
JVM stack (7 von 10)
Lösung: * Multi ID Generator
Java Forum Stuttgart - Mastering JPA Performance12 6. Juli 2017
Auto-ID Generator erzeugt viele Abfragen
Transaktion wartet synchron
Databank-generierte ID-Spalten
– Sind keine Lösung (JDBC Limit)
Multi ID Generator!
– Ungenutzte IDs
– Keine streng monotone Vergabe der Ids mehr
UUID-Generator ?
@TableGenerator(name = "city", initialValue = 1000, allocationSize = Constants.ALLOCATION_SIZE, table = "sequences", pkColumnName = "name", valueColumnName = "value")@GeneratedValue(generator="city")@Id long id;
Table Generator mit AllocationSize!
Java Forum Stuttgart - Mastering JPA Performance13 6. Juli 2017
0
2000
4000
6000
8000
10000
12000
14000
0
10000
20000
30000
40000
50000
60000
70000
80000
1 2 4 8 16
Allocation vs. time
Statements
time
Der gleiche Code – 6.2 Sekunden
Java Forum Stuttgart - Mastering JPA Performance14 6. Juli 2017
StreamSupport.stream(CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(fr).spliterator(), false).map(record -> {
State state = intern (new State(record.get("bundesland")), states, State::getName, s -> {em.persist(s);
});City city = intern (new City(record.get("ort")), cities, City::getName, c -> {
c.setState(state);em.persist(c);
});
PostalCode postalCode = new PostalCode(record.get("plz"));postalCode.setCity(city);return new ImmutableTriple<>(state, city, postalCode);
})//.forEach(triple -> {
em.persist(triple.getRight());});
Analyse 2
Java Forum Stuttgart - Mastering JPA Performance15 6. Juli 2017
"main" #1 prio=5 os_prio=0 tid=0x00000000017be800 nid=0x29fc runnable [0x000000000315c000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:170)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at com.mysql.cj.core.io.ReadAheadInputStream.fill(ReadAheadInputStream.java:101)
at com.mysql.cj.core.io.ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(ReadAheadInputStream.java:144)
at com.mysql.cj.core.io.ReadAheadInputStream.read(ReadAheadInputStream.java:174)
- locked <0x00000006c5a99ea8> (a com.mysql.cj.core.io.ReadAheadInputStream)
at java.io.FilterInputStream.read(FilterInputStream.java:133)
at com.mysql.cj.core.io.FullReadInputStream.readFully(FullReadInputStream.java:58)at com.mysql.cj.mysqla.io.SimplePacketReader.readHeader(SimplePacketReader.java:60)
at com.mysql.cj.mysqla.io.TimeTrackingPacketReader.readHeader(TimeTrackingPacketReader.java:48)
at com.mysql.cj.mysqla.io.MultiPacketReader.readHeader(MultiPacketReader.java:51)
at com.mysql.cj.mysqla.io.MysqlaProtocol.readPacket(MysqlaProtocol.java:521)
at com.mysql.cj.mysqla.io.MysqlaProtocol.checkErrorPacket(MysqlaProtocol.java:723)
at com.mysql.cj.mysqla.io.MysqlaProtocol.sendCommand(MysqlaProtocol.java:662)
at com.mysql.cj.mysqla.io.MysqlaProtocol.sqlQueryDirect(MysqlaProtocol.java:950)
at com.mysql.cj.mysqla.MysqlaSession.sqlQueryDirect(MysqlaSession.java:431)
at com.mysql.cj.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:1978)
- locked <0x00000006c59fd458> (a com.mysql.cj.jdbc.ConnectionImpl)
at com.mysql.cj.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1826)
- locked <0x00000006c59fd458> (a com.mysql.cj.jdbc.ConnectionImpl)
at com.mysql.cj.jdbc.PreparedStatement.executeUpdateInternal(PreparedStatement.java:2034)
- locked <0x00000006c59fd458> (a com.mysql.cj.jdbc.ConnectionImpl)
at com.mysql.cj.jdbc.PreparedStatement.executeUpdateInternal(PreparedStatement.java:1970)
- locked <0x00000006c59fd458> (a com.mysql.cj.jdbc.ConnectionImpl)
at com.mysql.cj.jdbc.PreparedStatement.executeLargeUpdate(PreparedStatement.java:4984)
at com.mysql.cj.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1955)
at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:204)
at
org.hibernate.engine.jdbc.batch.internal.NonBatchingBatch.addToBatch(NonB
atchingBatch.java:45)at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2897)
at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3397)
at org.hibernate.action.internal.EntityInsertAction.execute(EntityInsertAction.java:89)
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:582)
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:456)
at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:337)
at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:39)
JVM stack
Batch Optimierung
Java Forum Stuttgart - Mastering JPA Performance16 6. Juli 2017
spring.jpa.properties.hibernate.jdbc.batch_size=100
Aktiviere JDBC Batch
0
1000
2000
3000
4000
5000
1 2 4 8 16
Batch size vs. time
Time
Batch optimierter Code – 1.9 Sekunden!!
Java Forum Stuttgart - Mastering JPA Performance17 6. Juli 2017
List<ImmutableTriple<State,City,PostalCode>> triples = StreamSupport.stream(CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(fr).spliterator(), false).map(record -> {
State state = intern (new State(record.get("bundesland")), states, State::getName, s -> {em.persist(s);
});City city = intern (new City(record.get("ort")), cities, City::getName, c -> {
c.setState(state);em.persist(c);
});
PostalCode postalCode = new PostalCode(record.get("plz"));postalCode.setCity(city);return new ImmutableTriple<>(state, city, postalCode);
})//.collect(Collectors.toList());
triples.stream().forEach(triple -> em.persist(triple.getRight()));
Gebrochene JDBC batches
Java Forum Stuttgart - Mastering JPA Performance18 6. Juli 2017
Bester Fall: 3 JDBC batches
States brechen Batch für City 16 *
Cities brechen PostalCode 10585 *
16 Unterbrechungen haben keinen
signifikanten Einfluss, aber die 10585!
1:n Beziehung ist nahezu 1:1
State
City
Postal
Code
1
1
n
n
13381
10585
16
Lösung: Batch Sortierung
Java Forum Stuttgart - Mastering JPA Performance20 6. Juli 2017
Sortiere (INSERT) Operationen für JDBC batch
Batch wird gebrochen durch
– Queries
– Flush()
– Persistieren einer anderen Entität
– Identity columns (database level)
Entity Lifecycle
Java Forum Stuttgart - Mastering JPA Performance22 6. Juli 2017
...
city.setName(„newName“)
} // update & commit on exit
Automagisch? Optimiert?
Managed
RemovedDetached
New
Merge (Anti-Pattern)
Java Forum Stuttgart - Mastering JPA Performance23 6. Juli 2017
TestEntity te = …
TestEntity merged = em.merge(te);
Merge führt zuerst SELECT, dann UPDATE aus
SELECT – COPY – RETURN
https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Gui
de.html#pc-cascade-merge
Großer Persistence-Context
Java Forum Stuttgart - Mastering JPA Performance24 6. Juli 2017
Große Anzahl Instanzen/Attribute
EntityManager führt „dirty check“ für jeden flush() aus
Performance hängt von Implementierung ab
– Equals
– „Dirty“ Flag
(Anti-)Pattern: flush & clear (ok für große Mengen)
Java Forum Stuttgart - Mastering JPA Performance25 6. Juli 2017
em.clear()
Aufräumen – verwirft offene Änderungen
em.flush()
Schreibe alle Änderungen
Fremdschlüssel
Java Forum Stuttgart - Mastering JPA Performance26 6. Juli 2017
State s = em.getReference(State.class, 4711L);
City c = new City("Test getReference");c.setState(s);...
getReference führt keinen SELECT aus (verlässt sich auf referentielle Integrität)
Jeder Zugriff auf die Properties dieses Objekts führt zu einem SELECT auf die DB!
Eine Entität finden/laden
Java Forum Stuttgart - Mastering JPA Performance28 6. Juli 2017
City st = em.createNamedQuery("City.byName", City.class).setParameter("name", "Stuttgart").getSingleResult();
Query Ausführung
@Entity
@NamedQueries({@NamedQuery(name="City.byName", query="SELECT e FROM City e
WHERE e.name=:name")})
public class City { …
Query Definition
SQLs
Java Forum Stuttgart - Mastering JPA Performance29 6. Juli 2017
Hibernate: /* City.byName */ select city0_.id as id1_0_,
city0_.name as name2_0_, city0_.state_id as state_id3_0_ from city city0_ where
city0_.name=?Hibernate:
select state0_.id as id1_6_0_, state0_.name as name2_6_0_ from
state state0_ where
state0_.id=?
„1+n“-Problem
Java Forum Stuttgart - Mastering JPA Performance30 6. Juli 2017
EIN Statement aus der Anwendung
N Statements durch Hibernate
Objekte werden nachgeladen (n:1, 1:1)
Hierdurch entsteht indeterministisches Verhalten bzgl. der Laufzeit!
Wird teilweise auch durch das UI ausgelöst!
Lesezugriffe
Java Forum Stuttgart - Mastering JPA Performance31 6. Juli 2017
Sicht der Datenbank:
– „einfache“ Queries auf Tabellen, in der Regel alle Spalten
– Viele Abfragen auf Primärschlüssel
– Strukturell einfache Abfragen, viele Joins möglich
Collections (@OneToMany) sind Lazy
Referenzen auf Objekte sind (@ManyToOne)
– … lazy ist nur ein Hint!
Lazy (nur 1 Statement)
Java Forum Stuttgart - Mastering JPA Performance32 6. Juli 2017
/* City.byName */ select
city0_.id as id1_0_,
city0_.name as name2_0_,
city0_.state_id as state_id3_0_
from city city0_
where
city0_.name=?
@ManyToOne(fetch=FetchType.LAZY)State state;
In City
Eager (nur 1 Statement) – nicht für Abfragen!
Java Forum Stuttgart - Mastering JPA Performance33 6. Juli 2017
selectcity0_.id as id1_0_0_, city0_.name as name2_0_0_,city0_.state_id as state_id3_0_0_, state1_.id as id1_6_1_,state1_.name as name2_6_1_
from city city0_ left outer join state state1_ on city0_.state_id=state1_.id where city0_.id=?
@ManyToOne(fetch=FetchType.EAGER)State state;
In City
Lösung: Prefetch Query
Java Forum Stuttgart - Mastering JPA Performance34 6. Juli 2017
Eager & Lazy sind evtl. nicht ausreichend, Definition im Query möglich
Eager erzeugt große Ergebnismengen
Prefetch Query lädt abhängige Entitäten, nutzt den PersistenceContext als Cache
– Deterministische Anzahl SQL-Abfragen
– Kleinere Gesamtmenge (Zeilen & Spalten)
Beispiel: Lade mehrere Postleitzahlen mit Städten und BundesländernPrefetch: SELECT e.city.state FROM PostalCode e WHERE e.code IN (:codes)
Query: SELECT e FROM PostalCode e
JOIN FETCH e.city WHERE e.code IN (:codes)
Unabhängig von der Datenkonstellation immer exakt zwei SQL-Statements!
Lösung: Pagination
Java Forum Stuttgart - Mastering JPA Performance35 6. Juli 2017
Abhängig von der Implementierung (Datenbank-Dialekt)
Kleine Performance-Verbesserung
query.setFirstResult(0);
query.setMaxResults(10);
Reduziere die Gesamtergebnismenge
AppEngine: US, Database: US / Europe
Java Forum Stuttgart - Mastering JPA Performance38 6. Juli 2017
Database „ping“ - optimierter Code
Java Forum Stuttgart - Mastering JPA Performance39 6. Juli 2017
SELECT ohne Ergebnis, Roundtrip-Zeit
App Engine US-Central, MySQL Europe-West: 120ms (max: 215ms)
App Engine US-Central, MySQL US-Central: 1-2ms (max: 4-5ms)
Physikalisches Limit: 80ms (Lichtgeschwindigkeit in Glasfaser: 200.000 km/s)
Empfehlung: Mehr als eine Zone nutzen, aber in der gleichen Region bleiben
Cloud Messungen
Java Forum Stuttgart - Mastering JPA Performance40 6. Juli 2017
Scenario 10% (2456 rows) 100% (23.978 rows)
US->US 2.59s 15.9s
US->EU 264s = 4:24 ~43min
Der JDBC Treiber ...
Java Forum Stuttgart - Mastering JPA Performance42 6. Juli 2017
... Macht nicht unbedingt was wir ihm sagen
Optionen in der Dokumentation prüfen (herstellerspezifisch)
Konfigurationen bzgl. Performance testen
Der JDBC Treiber …
Java Forum Stuttgart - Mastering JPA Performance43 6. Juli 2017
Batch Update (mysql/mariadb)
Default:
mysql: rewriteBatchedStatements
mariadb: useBatchMultiSend (default=100)
JDBC Driver fetch size
Oracle: 10
mysql: *
Top-Down in die Datenbank
Java Forum Stuttgart - Mastering JPA Performance45 6. Juli 2017
Queries erhalten nicht automatisch den benötigten Index
Fremdschlüssel-Index für Löschungen
Index-Optimierung (Fetch from Index) nur bedingt möglich
Anti-Pattern: CASCADE
Java Forum Stuttgart - Mastering JPA Performance46 6. Juli 2017
Bietet das automatische Kaskadieren von Operationen im EntityManager
Vermeiden oder nur begrenzt nutzen!
Rekursives Problem
Keine Kontrolle der Anwendung
Keine Optimierungen möglich (z.B. Batch-Optimierung)
Langfristig nicht kontrollierbar
Caching (Anti-Pattern)
Java Forum Stuttgart - Mastering JPA Performance47 6. Juli 2017
Nutze Caching, wenn
Daten häufig gelesen und
Selten verändert werden
Alte Daten akzeptabel sind
– Vor allem in Cloud-/Container-Umgebungen
Ausreichend Speicher verfügbar ist
JPA 2.2 !
Java Forum Stuttgart - Mastering JPA Performance48 6. Juli 2017
Streams für Query-Ergebnisse
CDI (Attribute Converter)
@NamedQuery - @Repeatable
Java 8 Date & Time
Fazit
Java Forum Stuttgart - Mastering JPA Performance49 6. Juli 2017
JPA funktioniert!
SQL Roundtrips für einzelne Objekte sind teuer
Den gesamten Technologiestack betrachten
Die Probleme betreffen meist nur 5% aller Fälle
Das kann aber 95% der gesamten Probleme ausmachen!
Empfehlung: Jeder Anwendungsfall soll sich deterministisch verhalten!
Bewusstsein für die Infrastruktur hilft, Probleme vorab einzuschätzen
Fragen und AntwortenThomas Bröll
Principal Consultant
Tel. +49 162 2733918
@tvdtkb
6. Juli 2017 Java Forum Stuttgart - Mastering JPA Performance51