TA的每日心情 | 衰 2021-2-2 11:21 |
---|
签到天数: 36 天 [LV.5]常住居民I
|
对于大多数典型的 spring/hibernate 企业应用而言,其性能表现几乎完全依赖于持久层的性能。此篇文章中将介绍如何确认应用是否受数据库约束,同时介绍七种常用的提高应用性能的速成法。本文系 OneAPM 工程师翻译整理。
3 U- e2 R& L% V0 D( r" F- ` 6 Y" m0 M: t4 E1 M8 a4 R9 _" p
( c4 B) v* H. ?9 L; F w& }% ]+ G如何确认应用是否受限于数据库 , z" a8 K3 b5 v6 Q; \/ T6 U
确认应用是否受限于数据库的第一步,是在开发环境中进行测试,并使用 VisualVM 进行监控。VisualVM 是一款包含在 JDK 中的 java 分析器,在命令行输入 jvisualvm 即可调用。
: Y- B% c* k0 y 5 t1 r4 V/ f" ]5 h( ~3 U
+ o+ m6 I! {# f- o: x
启用 Visual VM 之后,尝试以下步骤: 6 N# P4 [; g( s/ e# c4 ^+ _0 D
) a& x8 Z( E; z, I/ r8 m: p1 z% X
9 p8 A! _0 k% d; A1.双击你正在运行的应用 5 o- n* B+ N% |+ C5 a, F
2.选择 Sampler
: Q8 A3 o- \' {! ^0 `2 i, K! _- X3.点击 Settings 复选框
2 \* L6 `( @2 t4.选择Profile only packages,然后输入下列包:
( M- l- }' C4 O" W& C* j( l1 Y0 wyour.application.packages.*
8 G/ _0 O! q9 C: S a5 a' M7 f
3 k$ a$ J* V |1 G, R% r$ s
# Z1 ~" f* s' D1 B/ f% J4 ?org.hibernate.* + `6 ^8 B7 Z. M& ~
" n* b% {5 x% {$ v0 o; Q6 @3 K
( a- N* S7 B' V+ c
org.springframework.* ( Y4 {( W: G8 e3 @: j+ J
) o# D% e7 P$ | , |7 r5 Z+ X! s& x
your.database.driver.package, 比如 oracle.* % X5 l+ Q4 }. { ?! I
% H& P1 c; Z$ q( z! g8 w- ^
) P5 M9 M: D% X, ]点击 Sample CPU
% ~4 n3 A/ o( x2 K" Z* l R2 C7 e' H$ o- ^+ c1 `7 A/ ^
9 R$ R6 \: l; w2 @( u3 _
如果应用性能受限于数据库,其 CPU 分析结果会非常恐怖 7 v& f( r" W" Y: B4 j
1 r! a# |) ~- i- ^+ u ! I4 n5 ]* d" z8 m) e C
Spring/Hibernate 应用性能优化 8 B) N, {( i2 ]# h( \& q$ n W
+ D- T+ L' ^ A4 i J
9 N) B- `" f* l$ g0 o- D我们看到,客户端 Java 进程花在等待数据库从网络中返回结果的时间占56%。
1 K& z* q( N) }! F
& j1 Q6 K- c- i. M6 m9 L! Z# s 8 F8 d6 `7 i0 S+ R
看到数据库查询是导致应用运行缓慢的原因,其实是好兆头。Hibernate 反射调用占比32.7%是正常情况,无法进一步优化。 ) D% g3 v7 i" [
9 v, V3 Y1 d) D! C 4 h$ p" J) o' y m
性能调优第一步:定义基准运行 * g; a7 \) |1 L- {$ y& |. S
性能调优的第一步是为程序定义基准运行,我们要定义一组能有效执行的输入数据,让程序基准运行与生产环境下的运行差不多。 2 B# r9 p3 j* V- p8 k
5 Z8 X: f4 y! W6 k/ V% `
3 K- F- T! k ^; D% |. g7 u, ^* b主要的区别在于基准运行的耗时要小很多。作为参考,5到10分钟的执行时间比较不错。
- k; F7 M2 d1 \! p0 }# } 8 p8 X, M4 J5 S$ n2 `
' b# A2 r1 ?( q! Q- t7 l9 O什么是好的基准?
% x1 M% `1 C& S* h& l好的基准应该具备以下特征: * A+ X; Z5 E: T2 y( ~+ N- h
1 ]/ B- F) x- i4 V L' F) {! H
/ @* r9 ^2 b" @( G- 功能正确
- 输入数据的种类与生产环境下相似
- 在短时间内执行完毕
- 基准运行的优化方案可以外推至完整运行
, t L8 p+ a& O+ W9 R % x& a8 D e T2 Z: P
定义好的基准是成功解决问题的一半。 , Y3 M4 A1 O& {$ r) S2 i4 |
; E3 w2 h" C. \9 ` ) p' p/ r6 p$ { d2 K6 j
什么是不好的基准
: k8 u E& y6 J0 w0 f6 W/ D例如,通过批量运行处理通讯系统的电话数据记录,选取10000条记录就是错误的做法。 . p f `- x$ D/ J
' f: T/ O% i; D% Y
+ t( V9 D0 Z2 m2 o P) @5 W
原因是:前10000条记录可能多为语音电话,而未知的性能问题可能发生在短信流量的处理过程中。一开始如果基准不够好,就会导致错误的结论。 3 l: ^9 C& y) Y
/ G0 S/ } r7 {$ A3 v
9 R( k& b b" v, o0 h收集 SQL 日志与查询时间 ' ^; h" \! ?5 j) A% @+ D: [
SQL 查询的执行语句与其执行时间可以通过 log4jdbc等方式收集。详细了解如何使用 log4jdbc 收集 SQL 查询信息,点击文章 使用 log4jdbc 优化 Spring/Hibernate 应用 SQL 日志。 & R8 P: R8 C: w& U8 a* `1 l5 x9 y
9 {+ b: i2 g! B* P
8 a, x# r6 W) Q8 W) F( t查询的执行时间是从 Java 客户端收集的,该时间包含查询数据库的来回网络调用。SQL 查询的日志如下: # e8 b6 W: Q c2 _, R6 ~& Z
4 B# t, i/ R1 {# K6 a$ J ( B9 |( g3 W' k% \# a U/ j: d8 s
16 avr. 2014 11:13:48 | SQL_QUERY /* insert your.package.YourEntity */ insert into YOUR_TABLE (...) values (...) {executed in 13 msec}
$ [$ Z# x6 O% v; S3 L预处理语句也是很重要的信息来源,它们常常会透露出常用的查询类型。了解更多的日志讯息,可以查看文章:Hibernate 为什么/在何处使用该 SQL 查询?
9 c, f; E0 h6 N9 ^5 i * T" ~3 d- O8 N1 h
9 m/ V5 F! |7 j通过 SQL 日志可以了解哪些指标? ) t) I0 d9 G9 f5 W: S' L2 c: ]
SQL 日志可以回答下列问题:
Y: }# a* j! T6 R0 c2 d ( W& o( V$ @% k7 _
. B4 [: c Z% C* p- {7 K; k- 哪些是执行过的最慢查询?
- 哪些是最常用的查询?
- 生成主键的耗时是多少?
- 是否有数据适合缓存?% o+ o6 u5 Y3 a# x# |
& h6 K1 }5 q: R6 G
如何解析 SQL 日志 0 Q! x/ E+ W* l$ s2 T
对于大量的日志文件,最可行的解析方式就是使用命令行工具,该方法的好处是非常灵活,只要写一小段脚本或命令,我们可以抽取出几乎大多数指标。只要你喜欢,任何命令行工具都适用。 $ m' N# K0 v5 s5 h$ k4 S
/ C- {- n1 H1 O0 p) ~1 z
1 L3 ~. {: \1 h, A如何你习惯了 Unix 命令行,bash 或是一个好选择。Bash 也可以在 Windows 工作站使用,Cygwin 或 Git 都包含了 bash 命令行。 # Q+ U8 D ?$ |- s# S
: V& R2 |' {" k; \, M+ h
0 E1 b) J& A# m6 a常用的速成法
6 L3 Q" ]1 m: s* { Y% l下面介绍的速成法能找出 Spring/Hibernate 应用中常见的性能问题,以及对应的解决方案。
7 x; [" F, s( i8 E0 k) F : k2 n- c) Q6 Q
. q" j/ P3 M. I5 [" G% w: l; X9 z% }速成法1——减少生成主键的代价
! F% r: T7 t# F7 T; N) |在插入操作频繁的进程中,主键的生成策略很重要。生成 id 的一种常见方法是使用数据库序列,通常一张表一个 id,从而避免在不同表间进行插入时的冲突。 / j6 `* O- }3 G+ m
2 E$ z, t1 j* q4 {: y) W6 g0 Q) _
4 |5 U( E8 v) m* ^5 q7 |* H. ~ C2 W问题在于,如果要插入50条记录,我们希望为了获取这50个 id,可以避免50趟查询数据库的来回网络调用,让 Java 进程不一直等待。
) N P* ]( P) K% N7 j. J0 R4 Z
4 X' `: W- T& L6 \; D& r " f2 _0 E& e1 b7 W& u4 V
Hibernate 通常如何解决此问题? - c2 c V) C4 D" w e$ w- p
Hibernate 提供了优化的 ID 生成器以避免此问题。也即,对于序列,会默认使用 HiLo id 生成器。以下是 HiLo 序列生成器的工作方式: * c; V$ W4 q3 X2 q4 I' E
# }8 e8 J& T0 w A5 `& i8 V
5 C7 Q& b# `) L2 r r! j! k/ I
- 调用一次序列,获得 1000 (高值)
- 用以下方式计算50个 id
/ {7 f3 X% i3 P, Z2 l 0 @: v/ Q) ?, Y- y: q3 M
1000 * 50 + 0 = 50000 3 o5 x3 b) I1 l* Y# Z. X- G' U; g& j
- ^" Q; g* u9 b% ~: ~" Q5 ]% j3 N9 @
/ @' t- C& Y j' T) s9 J
1000 * 50 + 1 = 50001 ' ^0 V$ A9 c1 Y2 _9 U0 D
9 `- r+ m6 p9 @* A" Z8 R # r# Q* h' i5 B4 [, l
...
% o6 [: d# E) f. f
4 [1 k! J4 t5 A W2 m# z3 l! t
, f: D- @- Z: y5 r1000 * 50 + 49 = 50049, 达到低值 (50)
/ {+ J1 h; y. e) {/ e' e 7 x; J3 x8 ~0 L+ {
. O i9 J: E- D! B为新的高值1001调用序列,依次类推
5 m" F g6 U. D" {2 T5 ~) O
) f5 D( ]+ z* z+ O 6 |6 m# P2 i, q6 f/ k, k
因此一次序列调用,可生成50个键,从而减少数次来回网络调用导致的负担。 . i! U" {$ ?5 ]9 f9 I9 J* X1 W0 o; o n3 S2 [
* l5 m7 B" A7 n4 ?
; K3 e' o' c O, F' \1 P3 c6 _
这些优化的键生成器默认在 Hibernate 4中开启。如要禁用,可将 hibernate.id.new_generator_mappings 设置为 false。
' z ` @# C; W+ t& k) z
$ |. @" t. S0 k
; [1 Q7 I8 g8 ]* g- b为什么生成主键仍是一个问题? 3 |( C4 ^3 M% x1 p! u2 l& n
问题在于,如果你声明键生成策略为 AUTO,且未启用优化的键生成器,那么应用最后会面临大量的序列调用。
; @7 ]4 x; j) q, e$ n+ q9 J
n9 T# J) ]& Q4 s) c) |
" c* c% q- L, Y为了确保启用优化的键生成器,请将键生成策略改为 SEQUENCE 而非 AUTO。 4 h [2 z( W1 z# A7 C9 N& [# k- N
7 S3 I# K1 @9 K+ f, Q; }1 P
B J u) @8 l* u: B v: k. v/ w9 \@Id 9 q0 H3 E7 f* ]3 o, X) K
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "your_key_generator")
! `) @; W+ G% w+ ?6 Pprivate Long id;
s, d9 ~' |1 x+ B6 d改变设定之后,在插入操作频繁的应用中能看到10%到20%的性能提升,而且几乎没有改动代码。 6 e! f2 A- [! p2 b' j9 i
5 e: ]/ W, B3 w7 { 7 |9 @: u7 E8 S, j% u3 w" E
速成法2——使用 JDBC 批处理 inserts/updates
6 ^0 Q2 T T" {" U$ B% i! t对于批处理程序,JDBC 驱动程序提供了旨在减少网络来回传输的优化方法:”JDBC batch inserts/updates“。使用该方法后,插入或更新会先在驱动层排队,然后再传送到数据库。 - x: V B6 \$ A7 B
: {! E+ W. o* ]9 L8 \( \ 1 b( m% _$ o3 y& K
当达到阈值后,所有排队的语句都会一次性传给数据库。这可以避免驱动程序逐一传送语句,导致网络来回传送的负担。
0 P$ r7 f3 J" M/ ? ? / @; {: s7 ]) o
3 z) K& @; U' _0 i H2 ?经过以下配置,就能激活批处理 inserts/updates: 5 d5 {" `; o" k& D' l+ H' ]$ ] S
2 I4 q& M( i* J3 ]1 g; N4 \* Y. u
- Z' V3 c& t; N9 |
<prop key="hibernate.jdbc.batch_size">100</prop>
4 y/ F7 Z6 X0 ^0 X' z( C9 r<prop key="hibernate.order_inserts">true</prop> # v1 [$ k' J8 j/ k' _9 y
<prop key="hibernate.order_updates">true</prop>
, X2 G" ~1 d! Q9 o+ s仅设置 JDBC 批处理大小并不够。因为 JDBC 驱动程序只会在收到对同一张表 insert/updates 时批处理这些语句。
2 y* j$ Q: D. i* B1 u' |
. K/ h; Y5 i! D1 J2 n7 v5 a 4 I, _0 H" b/ P7 H- U7 E- \( Y
如果收到对一张新表的插入语句,JDBC 驱动程序会先清除对前一张表的批处理语句,然后开始分批处理针对新表的 SQL 语句。
* B- ?# h6 |- V- c
e& e# V2 S2 W+ n: J" i / ?( Z& M# Q. \
Spring Batch 内置了相似的功能。该优化能在插入操作频繁的应用中带来30%到40%的性能提升,而不用改动任何代码行。 / T( Q$ Y0 E) A
2 W% n1 I0 o. p2 y N
! o% }$ Z- l, }; K; S4 Z7 k
速成法3——定期清理 Hibernate 会话
|0 Y6 P8 k+ ~, k5 `/ \在向数据库添加或修改数据时,Hibernate 会在会话中保留一版已经存在的实体,以防在会话关闭之前这些实体再度被修改。 : H# c1 s4 G* e. H
( t; F3 `, ?% _" D! H% @; q
+ c% y; m9 i; [; s" ]但是,多数情况下,一旦对应的插入操作已经在数据库中完成,我们就可以安心地丢弃那些实体。这会释放 Java 客户端进程中的内存,避免过久的 Hibernate 会话导致的性能问题。
, ?/ K- d/ B! Y9 w# M/ f
8 G' |' ?8 e% Z7 L8 r) t e6 b" e
! k) L3 D; c# f2 F$ j! ]这种长久的会话应该尽量避免。但如果出于某种原因不得不使用它们,以下是控制内存消耗的方法:
- W; j3 u( y& O( b) X3 B. {6 W
6 m m" W% K3 @0 E- z8 A 1 D' r; x( X" ]7 B& M$ [
entityManager.flush();
" G: V1 S3 U6 R+ u' C+ Q& R, wentityManager.clear(); : L# d5 P! F( ^- S6 z2 M
flush 会触使新实体中的插入语句传送至数据库。clear 则会释放会话中的新实体。
8 G, G! |# X8 r" M1 w7 ` # N+ v# P7 E8 I) t2 c
) l+ b7 M0 D) w" ?! S! v% o速成法4——减少 Hibernate dirty-checking(脏数据检查) 的代价 & A/ B/ v! k! C% H; w: i
Hibernate 内部使用了一种机制用于追踪被修改的实体,名为 dirty-checking。该机制并不基于实体类中的 equals 和 hashcode 方法。 0 {- \6 U0 ~' u1 W. B. X
. x$ L) c$ `5 \0 j; j * v9 r8 ~+ S' s! M
Hibernate 尽可能将 dirty-checking 的性能成本保持在最低值,只在需要时使用 dirty-check。但是该机制也有成本,在列数很多的表中该成本尤其可观。 / S8 U' F/ D% C+ i. g
" A1 x$ i: E% P) E" d) Z$ {
$ N- @" h3 \1 G. Q) Z5 ]在进行任何优化之前,最重要的是使用 VisualVM 测量 dirty-checking 的成本。 + h& v7 K$ r; [* ]( |2 G; x
/ c! H2 J% f' e2 p; q
5 `3 t) u/ v! I2 d; @5 r% B' `
如何避免 dirty-checking ?
q. }+ L# l$ qdirty-checking 可以通过以下方式禁用:
: V% h8 i9 u% B4 e) [1 h& ? _
) s) B$ ~: q( O) F" Y
7 q4 y% C% H3 Y! l6 d@Transactional(readOnly=true) + E1 v4 N L6 z: C* l+ v
public void someBusinessMethod() {
6 n3 l# F. F" \% B- @; S7 q.... 5 d) F% o7 I0 i5 T7 T8 Z
}
, R& Z. s" Z/ G禁用 dirty-checking 的另一种方式是使用 Hibernate 无状态会话,预知详情请查看文档。 4 E/ R$ Y2 l( T# ]
: k) V5 x. F4 z( d6 e" Q1 B 8 g0 h, }, n H t# y i
速成法5——搜索”坏“查询计划 Y: N; y2 ?3 W* P& O
) a/ x$ h; ^: n3 J9 K ; g/ n; h2 P( i8 `
检查最慢查询列表,看看有没有好的查询计划。最常见的”坏“查询计划包括: , n( t+ a. L; V- j
& o. d2 Q; {; c( Z
( e$ _5 i# v& }* W% `8 g6 [8 v+ A- 全表搜索:通常缺少一个索引或表统计过期时进行全表搜索。
- 全笛卡尔连接:意思是计算多张表的全笛卡尔乘积。检查一下缺少的连接条件,或拆分为几个步骤以简化查询。0 M6 z8 x" g( P: z |
3 w& |9 v# K: L0 M* o! F* G7 s H# A
6 j) Z# m8 G8 T" H+ i 4 ]) Y$ f) ?+ c' z
速成法6——检查错误的提交间隔 ) }/ g( x4 p7 a/ I( P: ?
如果你使用批处理程序,提交间隔会对性能造成十倍甚至百倍的影响。 * E$ T5 d. }, j
" V0 x/ o0 i8 V8 A! J u7 {2 o1 M: C! A! g$ `0 z/ s- {$ {
请确保提交间隔是符合预期的(对于 Spring 批任务,通常是100到1000之间)。经常,该参数的配置不正确。
; M' R2 e" Z% Z9 j
k' ?, T; V G6 |% x. i ; D, G6 N$ k2 m- [& n* r5 [
速成法7—— 使用二级查询缓存 1 G/ y( N$ ~' L( r I
如果一些数据可以缓存,则可以查看本文了解如何设置 Hibernate 缓存:Hibernate 二级/查询缓存的陷阱。 $ l' I0 y4 x& t, f" P! D$ r
5 ~! k \' o9 {1 H# p / {0 G) `/ M3 N# P
结论 ' x$ @- G9 @% Z! W# ]& b
解决应用性能问题的关键,在于通过收集一些指标发现当前的瓶颈。 7 o+ v' K1 w. k$ u7 o
5 }* o- g$ t1 Z6 f) t1 f6 E 9 D2 R4 J1 P4 Y
没有一些测量指标,往往无法在短时间内找到真正的问题根源。 " J1 j6 p! |8 _" T
! h7 A' N0 d& W( v# R1 ^5 j8 t Z# c8 _2 M4 q
此外,很多典型的数据库驱动应用的性能陷阱,如果一开始就使用了 Spring Batch,就能够避免。
" c. D6 h5 v A/ Z * r) {' J, K; y5 s
% v/ C# p0 { D; D6 q, B
OneAPM for Java 能够深入到所有 Java 应用内部完成应用性能管理和监控,包括代码级别性能问题的可见性、性能瓶颈的快速识别与追溯、真实用户体验监控、服务器监控和端到端的应用性能管理。想阅读更多技术文章,请访问OneAPM官网
! q1 l# V, F2 M( r/ Y0 ^9 K |
|