TA的每日心情 | 衰 2021-2-2 11:21 |
---|
签到天数: 36 天 [LV.5]常住居民I
|
对于大多数典型的 spring/hibernate 企业应用而言,其性能表现几乎完全依赖于持久层的性能。此篇文章中将介绍如何确认应用是否受数据库约束,同时介绍七种常用的提高应用性能的速成法。本文系 OneAPM 工程师翻译整理。 2 I8 C2 I. K. a# V6 o+ U. |
6 }! V0 |# R' |) I* v
( R! E; x$ y/ G& x6 Y7 F7 O
如何确认应用是否受限于数据库
6 S' |7 C! S" W7 {" z, J3 F! H确认应用是否受限于数据库的第一步,是在开发环境中进行测试,并使用 VisualVM 进行监控。VisualVM 是一款包含在 JDK 中的 java 分析器,在命令行输入 jvisualvm 即可调用。 0 j$ y! ^3 q; l5 ]* O
* e) D1 N% z3 B 7 A0 V P- y+ g/ E) i& @6 k+ o
启用 Visual VM 之后,尝试以下步骤:
, ~4 q! h) K2 q+ N [& q Q8 J
) Z4 ~! }' _4 x4 c2 t, t $ z" i; c- m0 ], t6 D) a7 C
1.双击你正在运行的应用
6 M7 L! T8 S; B: B9 C% x2.选择 Sampler
1 @7 Y$ t- s2 w/ Y% u& Q4 H3.点击 Settings 复选框
8 u c# h- m$ |* F) Q$ X4.选择Profile only packages,然后输入下列包:
( T3 V( }1 ~# Z& G* O5 S& ` Xyour.application.packages.* # h2 G4 n- P4 j7 ?6 X# n n
2 K0 g8 | [& Y$ o M$ m
. e$ A' `7 `9 aorg.hibernate.* : F# V: Z$ p$ e$ A
. X5 t$ m' Z B3 o* c. I
6 f) L6 b6 F* Uorg.springframework.*
! t1 j" `% b( M7 [4 `5 j 0 b* q3 J* p- W: g% G- h
, m2 t x' e; g2 B2 j3 D4 ]
your.database.driver.package, 比如 oracle.* ! x7 g4 q1 k. k
) q5 Z$ b+ O1 E- M & y0 @$ S% d8 p, p& R
点击 Sample CPU 4 _4 K+ `& K# ^) b' p4 ^
1 i& z d- m& o
$ v/ q6 I' _4 G# Y0 X; X
如果应用性能受限于数据库,其 CPU 分析结果会非常恐怖 / S& L% v9 h( p/ n: C
% l m r$ W2 \ & Y, c1 Y4 X: p m4 R3 a2 v
Spring/Hibernate 应用性能优化 1 e V$ Y. t0 F! c D" j( l3 N
* C3 R9 N+ k U
+ R& R, d+ c# k/ R! g我们看到,客户端 Java 进程花在等待数据库从网络中返回结果的时间占56%。 4 w' r) k3 k; x2 w- K
4 K4 | [% v# S3 `; Z
" W# R6 b% p! ] A V1 k9 D8 h& h' M* x看到数据库查询是导致应用运行缓慢的原因,其实是好兆头。Hibernate 反射调用占比32.7%是正常情况,无法进一步优化。 8 b5 \7 `3 H+ w( K& K
9 Y1 U2 Y0 G( J3 I# Y5 Y+ ~# T 6 ^& `8 I/ q5 a2 Z+ ?/ a
性能调优第一步:定义基准运行 + O x1 W! t y9 l$ `0 w3 C7 w
性能调优的第一步是为程序定义基准运行,我们要定义一组能有效执行的输入数据,让程序基准运行与生产环境下的运行差不多。
/ v2 E/ |) w7 H8 z. P( W3 S
7 W, w2 K3 E( W! Q . p: b9 U4 A1 J( E, H! ^* y0 q
主要的区别在于基准运行的耗时要小很多。作为参考,5到10分钟的执行时间比较不错。
( J( Q' F8 M( ~, g 6 K0 |! H! x: R/ z
# X, ^' y8 }- C8 Y9 J/ j/ f+ x0 k什么是好的基准?
6 @2 X% G2 ?7 O- g+ d好的基准应该具备以下特征:
, W3 X- `2 A, d. x
/ P- l5 _5 u+ k8 y: s& r
5 R* b9 O! ?- i0 G( }6 ]0 l. u! f- 功能正确
- 输入数据的种类与生产环境下相似
- 在短时间内执行完毕
- 基准运行的优化方案可以外推至完整运行( x% U: A$ T7 U# q5 s7 x1 g
8 g- X7 z @) G定义好的基准是成功解决问题的一半。
. @+ `# V0 |0 z3 K- y
+ [) A% E6 e' e$ u( v3 @ / x5 ~% @- B6 d% Y
什么是不好的基准
+ T% N! W5 d- \$ C0 J' o: ?例如,通过批量运行处理通讯系统的电话数据记录,选取10000条记录就是错误的做法。 # ^: |$ x7 w5 G& K7 V& T
0 U8 Y" h" ?# P! Y
3 }1 M3 e7 D" x& V% t! q/ ?0 M原因是:前10000条记录可能多为语音电话,而未知的性能问题可能发生在短信流量的处理过程中。一开始如果基准不够好,就会导致错误的结论。 9 H M1 w `8 |9 L0 y
9 l2 S9 p0 H( { {' a1 |
1 N- x) w( t0 Z9 E$ C& {' p收集 SQL 日志与查询时间
8 N, S6 C( }5 J% f* E" ySQL 查询的执行语句与其执行时间可以通过 log4jdbc等方式收集。详细了解如何使用 log4jdbc 收集 SQL 查询信息,点击文章 使用 log4jdbc 优化 Spring/Hibernate 应用 SQL 日志。
, t& `: M0 ~/ T( b
& Q1 r+ A+ I+ q+ e& o, W
4 i/ F H! V j/ g( [ E查询的执行时间是从 Java 客户端收集的,该时间包含查询数据库的来回网络调用。SQL 查询的日志如下:
. p' U7 l* L" s a: r% z' y% R/ Z
* e$ W! \) m: b7 m# X; z j16 avr. 2014 11:13:48 | SQL_QUERY /* insert your.package.YourEntity */ insert into YOUR_TABLE (...) values (...) {executed in 13 msec}
6 h3 {; {( `; @9 p预处理语句也是很重要的信息来源,它们常常会透露出常用的查询类型。了解更多的日志讯息,可以查看文章:Hibernate 为什么/在何处使用该 SQL 查询?
2 R* @! J% \1 k* E- I . O/ m/ [. U# E6 F8 x+ h6 y0 L
6 W1 }2 ~" M7 X) [$ |. U通过 SQL 日志可以了解哪些指标? / f, Y0 H, G3 h1 `! |- C; I |' E- k
SQL 日志可以回答下列问题: R' f+ }3 ~/ V9 b# b+ c
0 i# f4 b6 \. a+ N6 J* q
% P1 k q2 f- [" a' E5 w; x0 x- 哪些是执行过的最慢查询?
- 哪些是最常用的查询?
- 生成主键的耗时是多少?
- 是否有数据适合缓存?
S+ x% V: T5 b0 l2 f + i7 i) x/ D o% {# I
如何解析 SQL 日志
! a9 x; f+ _: L: X; T0 R" H对于大量的日志文件,最可行的解析方式就是使用命令行工具,该方法的好处是非常灵活,只要写一小段脚本或命令,我们可以抽取出几乎大多数指标。只要你喜欢,任何命令行工具都适用。 G( F- T- j# F
* N$ `$ Z' k" q ) r' X' F0 H' Y" {3 d5 y
如何你习惯了 Unix 命令行,bash 或是一个好选择。Bash 也可以在 Windows 工作站使用,Cygwin 或 Git 都包含了 bash 命令行。
* x7 O- a2 B. N: f( o # C% d; c. _/ h. V7 ?: F* {
0 H$ q t: l, |& q# a+ |; D' w( v7 m常用的速成法
+ a% J0 Q& T; H: p下面介绍的速成法能找出 Spring/Hibernate 应用中常见的性能问题,以及对应的解决方案。
9 Z8 q4 Q; u' ]4 L: ]: \ }/ z" q" o
) ^! l7 T. E9 {' p4 J$ i
$ w6 o! P# U4 k速成法1——减少生成主键的代价 4 h/ b! ]- J# W! K, u' \
在插入操作频繁的进程中,主键的生成策略很重要。生成 id 的一种常见方法是使用数据库序列,通常一张表一个 id,从而避免在不同表间进行插入时的冲突。
( B& b3 M& f/ W7 K1 Y. Q' g: {0 Y6 U + ^& R6 Y4 s4 W) l2 z' C+ m
O6 t/ ]! K: c( _8 z' Y问题在于,如果要插入50条记录,我们希望为了获取这50个 id,可以避免50趟查询数据库的来回网络调用,让 Java 进程不一直等待。 6 Q4 U- K9 `) E& m& \
6 j1 F" [8 c! x3 ?: i
- B6 r5 @8 O" t# g1 `% B
Hibernate 通常如何解决此问题?
7 w/ _1 l- E b* ^3 Q h g+ V* i( j; DHibernate 提供了优化的 ID 生成器以避免此问题。也即,对于序列,会默认使用 HiLo id 生成器。以下是 HiLo 序列生成器的工作方式: 0 G# d0 U' ^" y+ U
& U% v9 {/ N3 P' I# \3 g
, N& w! U' I6 Q! F. d/ H) W$ }
- 调用一次序列,获得 1000 (高值)
- 用以下方式计算50个 id2 N) P# k: W, [$ T Q' v" Q2 m
3 J/ p8 L6 v) i c& Z
1000 * 50 + 0 = 50000 * A4 C/ S: x/ a! B9 \, N8 l% X
. x! S8 W. N5 n( U& G4 H
0 E6 }( U! E6 I. [" A E
1000 * 50 + 1 = 50001 7 X( |! [( u; o/ I$ X. u4 b1 v4 d7 x! L- Z
( Y5 t3 T$ T: C6 b1 t
" Z2 u9 l4 i2 p' Z! R... % Q1 a9 V, s2 `% U* }2 D5 m8 d
) z( X; S0 P) `( A" H; e E
: ?( ]/ e; y9 P: f( C; ~! D1000 * 50 + 49 = 50049, 达到低值 (50) ' Y3 g* Z2 ^, p+ D9 Q
* T8 h1 V* E/ k! {5 u 6 I9 F2 `9 |& S& P& o8 t( E
为新的高值1001调用序列,依次类推
8 r. [( i9 ^' I, d, B; E% ]
+ q/ k9 H/ l6 F5 ^9 d5 @9 @& R 3 l1 e1 o/ u n5 K
因此一次序列调用,可生成50个键,从而减少数次来回网络调用导致的负担。 & N$ E# P5 j+ v6 p9 p: ]: X4 \ R4 h' _
+ L# {# W/ ?5 Z# S
3 L0 H* {4 b8 K" V* I这些优化的键生成器默认在 Hibernate 4中开启。如要禁用,可将 hibernate.id.new_generator_mappings 设置为 false。
! ~: x7 Y, X, f6 J# F0 S& Y8 @
" l; `/ m6 H% ]# L
( c$ _ x$ h6 l$ d为什么生成主键仍是一个问题? % F4 \8 h- u7 d8 g
问题在于,如果你声明键生成策略为 AUTO,且未启用优化的键生成器,那么应用最后会面临大量的序列调用。 8 E- H' ^; L2 D9 k+ _ [/ y7 X
# l8 H5 }; K% m# X2 T! T
; E% H7 N/ W! \. y% v7 E为了确保启用优化的键生成器,请将键生成策略改为 SEQUENCE 而非 AUTO。
' y8 l8 B/ p" Z$ V, \, u' x5 Z; J + h5 x: P/ [5 T4 b: A: |
9 ]$ m3 l7 _/ n2 [5 p- i9 X7 H@Id 4 J! \: D" X) w3 q5 D$ W
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "your_key_generator") 6 H- \ R* t: ?
private Long id; - b: G8 K7 ^' _) f' y
改变设定之后,在插入操作频繁的应用中能看到10%到20%的性能提升,而且几乎没有改动代码。 _) O4 i w! x9 M
0 \2 i9 n8 d( v- X3 y6 n- f
( T& E% s2 B- v, V0 h3 u速成法2——使用 JDBC 批处理 inserts/updates
- R- T) r! e$ S: @对于批处理程序,JDBC 驱动程序提供了旨在减少网络来回传输的优化方法:”JDBC batch inserts/updates“。使用该方法后,插入或更新会先在驱动层排队,然后再传送到数据库。 9 R) _6 i, y0 `: q8 C# e) @0 y
+ Y! c9 _' d; g* J/ G
9 \$ A; P/ l4 x6 l, ?当达到阈值后,所有排队的语句都会一次性传给数据库。这可以避免驱动程序逐一传送语句,导致网络来回传送的负担。
2 F8 ?$ [) ~9 n; K1 e
, S2 Q ~3 ~. L. ?' ~+ w5 q
, I9 B7 [/ D. q& U( R经过以下配置,就能激活批处理 inserts/updates:
( N# y% F! U W! t
8 x6 q# p7 A6 b4 z. ~% \3 b( N
) P. |, ~4 |6 i6 J+ B<prop key="hibernate.jdbc.batch_size">100</prop>
$ [ O$ H0 {9 j* Z: P% X9 ?<prop key="hibernate.order_inserts">true</prop>
, m6 v% J4 a6 K6 u6 l* o<prop key="hibernate.order_updates">true</prop>
( C5 D6 w8 g! F" v仅设置 JDBC 批处理大小并不够。因为 JDBC 驱动程序只会在收到对同一张表 insert/updates 时批处理这些语句。
1 W, d# E. w$ Q
' x! e4 S0 ~' m4 {4 H. @) S8 H2 W
7 @8 v7 f2 p( F7 U6 t* v/ o如果收到对一张新表的插入语句,JDBC 驱动程序会先清除对前一张表的批处理语句,然后开始分批处理针对新表的 SQL 语句。 7 {3 B6 U( k! y' S7 r8 r
8 I/ {; X5 _; ~+ J; k
, `! T( b8 q5 d7 bSpring Batch 内置了相似的功能。该优化能在插入操作频繁的应用中带来30%到40%的性能提升,而不用改动任何代码行。 3 s: H9 F* F; } {" O& F
' G/ G2 @, q8 c2 T, [9 T, S) F H
3 A' [% Q) O T速成法3——定期清理 Hibernate 会话 $ x; U, m; a; x* \7 d$ D
在向数据库添加或修改数据时,Hibernate 会在会话中保留一版已经存在的实体,以防在会话关闭之前这些实体再度被修改。
) K, d8 ^7 e5 g8 P9 R# _+ l # g4 W1 U' s5 ?8 R# e8 t# K4 o
- D9 w8 |3 t) [4 q0 S但是,多数情况下,一旦对应的插入操作已经在数据库中完成,我们就可以安心地丢弃那些实体。这会释放 Java 客户端进程中的内存,避免过久的 Hibernate 会话导致的性能问题。
, j# r+ Q) C$ C/ }( U% R4 B 7 l; w2 k8 q, m$ r
+ ?# m$ r" w* z. Y; V; u4 Y
这种长久的会话应该尽量避免。但如果出于某种原因不得不使用它们,以下是控制内存消耗的方法:
2 h- z0 I1 W) W( b, g9 ?5 Y8 z / R8 @1 L7 J7 }+ z' M
! D0 n' B, z# w+ }3 L5 R. B# i0 Q
entityManager.flush();
; [) z6 k, C: ?. m- Y* eentityManager.clear();
% h5 [8 I( p0 n( u) rflush 会触使新实体中的插入语句传送至数据库。clear 则会释放会话中的新实体。 * C x+ K. u1 A: J! a
- e1 ], X0 y; ~- T4 n7 A
- _1 H9 L! c" ^2 C s1 @$ \速成法4——减少 Hibernate dirty-checking(脏数据检查) 的代价 3 g3 W. q% \8 |
Hibernate 内部使用了一种机制用于追踪被修改的实体,名为 dirty-checking。该机制并不基于实体类中的 equals 和 hashcode 方法。 # r9 w7 c" i8 u7 ^. F# @
8 j: b5 I8 ?, k. C! \ j
1 y8 j, {( n. n/ @) C5 Y
Hibernate 尽可能将 dirty-checking 的性能成本保持在最低值,只在需要时使用 dirty-check。但是该机制也有成本,在列数很多的表中该成本尤其可观。 % g4 F3 O1 ]( M" ?
/ Q* f* q0 Y9 g% P & {7 t: N% Y( f5 e7 H
在进行任何优化之前,最重要的是使用 VisualVM 测量 dirty-checking 的成本。
4 H7 K( K- ]; \, s
, U$ s8 S- Q6 {3 D8 H, S! D% E - z8 b5 y0 {4 N1 S( B9 n
如何避免 dirty-checking ? 8 G) b( e, a4 h3 z5 l! P# {
dirty-checking 可以通过以下方式禁用: , Z1 ^- Y, B) \( |5 V, W: k. a
$ V u6 i+ Q' N
' d: e: w( q* q5 @! j@Transactional(readOnly=true)
1 P% u- J* e' g. Wpublic void someBusinessMethod() {
/ i* v: Q" m6 c$ ^7 e/ ] e% k+ v.... , R# b0 a& d) z1 u- l
} - |4 b6 ?: I1 ~/ Q
禁用 dirty-checking 的另一种方式是使用 Hibernate 无状态会话,预知详情请查看文档。
2 D8 X2 r" c$ c- y \- q) i" q4 _" w1 C6 Y) F
) {2 G! e8 l7 ~& |6 C8 q+ g+ O. B) Y; `. ~. h
速成法5——搜索”坏“查询计划
& P& t- p& f5 W, I$ |# X8 _" O8 r
* Y! n7 F' C9 K3 e0 p 7 S$ Y Q2 v* M X1 B6 o8 z
检查最慢查询列表,看看有没有好的查询计划。最常见的”坏“查询计划包括: " _, q+ A3 Z* Z/ ]* Q8 ~" S
: l% {5 b: t& v3 `4 S6 J & D S1 ^- _* `+ j7 f4 n9 h
- 全表搜索:通常缺少一个索引或表统计过期时进行全表搜索。
- 全笛卡尔连接:意思是计算多张表的全笛卡尔乘积。检查一下缺少的连接条件,或拆分为几个步骤以简化查询。* A! _5 ^$ }; X/ a' ^
& e/ ^( S; |8 b Q' ]" d
$ z- Q7 d" v+ Y) q0 D
! B" P8 I0 f( J% K9 K- g速成法6——检查错误的提交间隔
A5 [) W8 i) ]1 Y' h9 T$ j如果你使用批处理程序,提交间隔会对性能造成十倍甚至百倍的影响。 " _( j$ ~ Z; Q1 q- |
9 x8 i% ^$ U# C& G% u
2 z2 p2 n, A8 X, S7 x
请确保提交间隔是符合预期的(对于 Spring 批任务,通常是100到1000之间)。经常,该参数的配置不正确。 5 Y- Y$ Z7 m7 t7 ]' W0 [
4 I# E' R' l4 T, ^, ]. c6 E
; Q2 _2 Z3 i& u$ ^1 K9 m* F速成法7—— 使用二级查询缓存
- Y( R# x# M( F! L如果一些数据可以缓存,则可以查看本文了解如何设置 Hibernate 缓存:Hibernate 二级/查询缓存的陷阱。 1 O( m; H) J" d0 | u6 t1 U) w
; J5 W9 u* [6 j0 I
( ^! a, S* |) Z/ u7 G" O
结论 + N0 m0 s* M6 N- d
解决应用性能问题的关键,在于通过收集一些指标发现当前的瓶颈。
8 ~! V* b0 n' D1 F" H 3 g0 o' C5 z( M' o- c3 v9 n
6 h U! t/ e* y% O
没有一些测量指标,往往无法在短时间内找到真正的问题根源。 % `5 g. D$ w: f( _: o
3 X6 P' M- k# o/ T( l; c 7 }3 N0 W ]1 F! r8 w. C0 j( d
此外,很多典型的数据库驱动应用的性能陷阱,如果一开始就使用了 Spring Batch,就能够避免。 ( T/ L4 q) v0 e1 i& O
) S4 u1 @# V( \6 y0 b
/ l$ Z' V( p% n4 o3 E* r" y3 a$ Q: n' H
OneAPM for Java 能够深入到所有 Java 应用内部完成应用性能管理和监控,包括代码级别性能问题的可见性、性能瓶颈的快速识别与追溯、真实用户体验监控、服务器监控和端到端的应用性能管理。想阅读更多技术文章,请访问OneAPM官网8 u7 B7 c4 D) L5 _& i4 A- r9 f
|
|