TA的每日心情 | 衰 2021-2-2 11:21 |
---|
签到天数: 36 天 [LV.5]常住居民I
|
对于大多数典型的 spring/hibernate 企业应用而言,其性能表现几乎完全依赖于持久层的性能。此篇文章中将介绍如何确认应用是否受数据库约束,同时介绍七种常用的提高应用性能的速成法。本文系 OneAPM 工程师翻译整理。 : Q1 }& n1 ~, b; A3 z/ X
( ^+ l1 d/ W* d B7 N0 T! s
: ~0 \9 C, j( ~# d如何确认应用是否受限于数据库
; ^/ E3 l9 R, R1 G8 a3 g0 H3 [确认应用是否受限于数据库的第一步,是在开发环境中进行测试,并使用 VisualVM 进行监控。VisualVM 是一款包含在 JDK 中的 java 分析器,在命令行输入 jvisualvm 即可调用。
2 `4 U3 D# [" M* D* \ / I7 a1 N) V* r: v: I
& v+ [6 t' N0 z! c/ n启用 Visual VM 之后,尝试以下步骤:
% w3 p' K, f% d0 c7 s+ F
6 } a1 P# ~! ~+ C5 J ! E4 G6 O! ]% n0 ~( [. B# l
1.双击你正在运行的应用
& K5 S3 E! `+ b5 |2.选择 Sampler
: i* m0 C$ o, Y# s' x3.点击 Settings 复选框 + F' v4 D! j& _
4.选择Profile only packages,然后输入下列包: 9 ]" }% o) e; A/ a
your.application.packages.* L+ g+ _; f0 }- W5 d+ @/ l, D
' R y) A# W9 ^1 d$ }' h 8 S& m' o9 S( W; b' W0 m( h
org.hibernate.* * U0 ~) ~1 q/ j4 n+ M e
% U( {% h1 u! X6 o
F: ?7 J' V g1 Y" q: ]
org.springframework.*
- A) P/ u2 r6 o( v6 z# z* H2 g
3 h: I5 l' m; T6 F1 g
7 y2 U; y/ d) fyour.database.driver.package, 比如 oracle.* $ ]* ~ k! q j) S6 ?
# |, W% Q9 ?, c& e
4 b8 I+ B B4 u6 B0 k点击 Sample CPU
4 a& Y- [$ ?' x/ p6 p
1 U' |/ l3 X( m) ^# ]0 x; L / w, Y2 d. s# k" E) _
如果应用性能受限于数据库,其 CPU 分析结果会非常恐怖
5 v9 |4 a( R/ M C- m# K
3 [3 H+ k9 | J9 {
9 R' U4 b) C- B' |Spring/Hibernate 应用性能优化
9 @9 ~0 R4 b" t/ X; I
/ o2 z9 _% ?$ [7 o# C k* I
! b( I6 b2 G7 [* i1 S我们看到,客户端 Java 进程花在等待数据库从网络中返回结果的时间占56%。 / w) f9 J% I" K
6 Q8 `! i! T5 M2 Z" ~& ~' F( L8 n
# ?$ o4 T' e) e) v; T: y5 [9 |看到数据库查询是导致应用运行缓慢的原因,其实是好兆头。Hibernate 反射调用占比32.7%是正常情况,无法进一步优化。 # a$ } v* n b' Y# ]5 a4 G. _% D
V |) Z$ d5 P2 e9 ~. c
' O7 N) b; f4 k3 J- y性能调优第一步:定义基准运行 6 K7 k- W2 F8 b6 n0 u! r2 }) P
性能调优的第一步是为程序定义基准运行,我们要定义一组能有效执行的输入数据,让程序基准运行与生产环境下的运行差不多。 8 |7 z/ L0 O {' w( a: N+ ^
* Q* G' @8 Z% O5 h
6 y0 }0 }1 v, M, p/ V8 i2 _1 C
主要的区别在于基准运行的耗时要小很多。作为参考,5到10分钟的执行时间比较不错。
5 H7 C' ^7 e5 ]" Z' h * I3 w, H5 u$ y3 a' z8 h
& |! Y" W9 w+ [3 G2 e什么是好的基准? % l1 r& z/ A L& _* W
好的基准应该具备以下特征:
7 j/ X" R7 n) p4 L5 n5 j
! S8 l/ _ R8 M4 o
: @; l: q; Q6 C% s- 功能正确
- 输入数据的种类与生产环境下相似
- 在短时间内执行完毕
- 基准运行的优化方案可以外推至完整运行( Z `$ H. W. W+ p% U i
" {) r- k& T8 G3 Q/ e1 V定义好的基准是成功解决问题的一半。
+ a" c+ i, \( `# D2 i* Q4 n$ V8 O8 \
9 m2 o% I4 T8 e: {
6 Q1 Z) k9 Z+ ~+ D7 |: ?+ F: T. l% S: N什么是不好的基准 ' Q2 X$ a* ]" M0 n
例如,通过批量运行处理通讯系统的电话数据记录,选取10000条记录就是错误的做法。 3 w/ i" E; F4 {
! H7 t' Q" _# F& k% d& N ! C0 t- T* _- y ?( I
原因是:前10000条记录可能多为语音电话,而未知的性能问题可能发生在短信流量的处理过程中。一开始如果基准不够好,就会导致错误的结论。 3 M. |, O- p8 R1 Q1 n+ N) `% ~
5 K- W$ [3 y8 w! E- m; f / m! {; X$ ?6 h1 `5 U
收集 SQL 日志与查询时间
4 c. ]5 L) n2 z- b+ x0 TSQL 查询的执行语句与其执行时间可以通过 log4jdbc等方式收集。详细了解如何使用 log4jdbc 收集 SQL 查询信息,点击文章 使用 log4jdbc 优化 Spring/Hibernate 应用 SQL 日志。
- o. Z# t! u7 k1 P# h 2 [, H2 w' s( n" Z! }# }
0 C& a4 Q* |, c) c/ |
查询的执行时间是从 Java 客户端收集的,该时间包含查询数据库的来回网络调用。SQL 查询的日志如下: , ^7 ]* ? X8 {, j0 U) ~
6 a: F1 D# E& a: S1 Q+ m
! S4 y+ f* @9 G% u/ D6 y& V16 avr. 2014 11:13:48 | SQL_QUERY /* insert your.package.YourEntity */ insert into YOUR_TABLE (...) values (...) {executed in 13 msec}
* _8 N% R6 P9 M" K. R: t3 {! n预处理语句也是很重要的信息来源,它们常常会透露出常用的查询类型。了解更多的日志讯息,可以查看文章:Hibernate 为什么/在何处使用该 SQL 查询? 1 h3 D8 S* Q# K D
$ w# F6 U8 F, G$ b9 Y, x ! Q* ]4 [5 ]8 T* a
通过 SQL 日志可以了解哪些指标? ' @) a7 o6 S+ |
SQL 日志可以回答下列问题: s' x6 j" u3 u( h/ f8 d
! E! }$ `8 u$ G
* m/ _* ]" I9 f7 J
- 哪些是执行过的最慢查询?
- 哪些是最常用的查询?
- 生成主键的耗时是多少?
- 是否有数据适合缓存?7 S7 ^. L; @$ m/ O
/ D; E2 P2 ]+ u) _, ^
如何解析 SQL 日志 * x* U+ O4 z" S
对于大量的日志文件,最可行的解析方式就是使用命令行工具,该方法的好处是非常灵活,只要写一小段脚本或命令,我们可以抽取出几乎大多数指标。只要你喜欢,任何命令行工具都适用。
; e0 ` H1 m3 j6 u! p) m3 Z
6 C# A. c/ n* P4 [' w% z
. V* } A8 [+ ^/ \- m6 W如何你习惯了 Unix 命令行,bash 或是一个好选择。Bash 也可以在 Windows 工作站使用,Cygwin 或 Git 都包含了 bash 命令行。 ! _& |7 J2 W7 P) `
* Z& S5 q; m' Q" W) B7 s4 c
0 Z5 R! d& B+ j- i4 r* N
常用的速成法
3 H! [+ s% [ y I, s6 g下面介绍的速成法能找出 Spring/Hibernate 应用中常见的性能问题,以及对应的解决方案。 $ t. R& K/ g9 @. K' l \
$ ~+ X+ V# v$ v( N
& B/ ]+ K3 Y7 I: z8 ~- r+ J h速成法1——减少生成主键的代价 / o% }2 T7 z: } z4 ]: [ G7 g
在插入操作频繁的进程中,主键的生成策略很重要。生成 id 的一种常见方法是使用数据库序列,通常一张表一个 id,从而避免在不同表间进行插入时的冲突。
! f6 p4 O6 W+ d ) H, Z7 F1 W* C* U
7 Z# _: y2 ]' z5 r
问题在于,如果要插入50条记录,我们希望为了获取这50个 id,可以避免50趟查询数据库的来回网络调用,让 Java 进程不一直等待。
- l* I/ [, K. _1 E2 _
" X. N9 M& z8 X: S' G; P 4 A8 l$ V1 H4 B8 n2 B1 l2 L
Hibernate 通常如何解决此问题?
( U: U2 s4 u$ H4 A4 FHibernate 提供了优化的 ID 生成器以避免此问题。也即,对于序列,会默认使用 HiLo id 生成器。以下是 HiLo 序列生成器的工作方式: / F" r: U1 y1 E! r
N$ H+ E* P8 S3 q) F+ C & p6 v. Q6 v0 Y8 k' G& |
- 调用一次序列,获得 1000 (高值)
- 用以下方式计算50个 id
8 E; ? F5 ]0 X7 [- Q
9 D! L* {9 V$ V% y1000 * 50 + 0 = 50000
, k: e! p, x- m& @% |1 r# |( @ 8 | Q6 o+ X1 n/ V& o+ e8 T" C6 S
6 `$ R7 ]8 g% j$ x. J, _1000 * 50 + 1 = 50001
. D# d* k# G+ I9 S4 M 4 l; |2 m- v: K- L G
7 Q5 X9 l( d2 B. o$ T...
6 j; k1 |+ Y2 Y# ^8 x. s+ U2 s
/ p, C8 ]0 u) o* }0 a
+ ]. R! C \1 l9 H R! ^) s, e1000 * 50 + 49 = 50049, 达到低值 (50)
# t( i' S% V2 r- E% Q5 |
% K7 _& L' @% @, K+ e0 w! i " r% M7 T$ N* G" P0 S/ g
为新的高值1001调用序列,依次类推
6 p' Y( }1 r0 ?: J+ z6 s $ ?+ d: f3 } Y' d
/ V" Y( a4 m! g# U% ^( O/ D$ a因此一次序列调用,可生成50个键,从而减少数次来回网络调用导致的负担。 + z _* W! q& h, `
4 {' N) J) r# h7 n& R5 Q' _ ) Q2 W; I, g5 n7 u; R- r8 m" G
这些优化的键生成器默认在 Hibernate 4中开启。如要禁用,可将 hibernate.id.new_generator_mappings 设置为 false。
# a- K6 N) p, ]# ]; K 1 W6 ?- O% P( O: B3 L; N) n
) C6 {" e- L/ R; C* h; {4 X: A为什么生成主键仍是一个问题? : Y) J, E! f- B ~4 t: D1 V
问题在于,如果你声明键生成策略为 AUTO,且未启用优化的键生成器,那么应用最后会面临大量的序列调用。 / \- T& i) @0 i' v
( [0 T* T4 ^( l* ~2 i L: S9 m
4 \$ T) l/ D) j* S( r C! E
为了确保启用优化的键生成器,请将键生成策略改为 SEQUENCE 而非 AUTO。
- g4 R+ s# a/ B7 b ) u- ^( B- k" A) I6 f3 |
& [- A2 y& h5 _- A) d0 M. C) b
@Id
; s' j1 Z. p! t, o" f1 B H@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "your_key_generator")
8 O0 p( m+ p% w+ S1 Q/ hprivate Long id; % b" d" \# f+ V
改变设定之后,在插入操作频繁的应用中能看到10%到20%的性能提升,而且几乎没有改动代码。 + c- h }$ V0 |( Q' y
+ W) J% e5 `& u2 U* p, ] / m Y% E0 ]" b
速成法2——使用 JDBC 批处理 inserts/updates
+ W2 L; h7 Z7 ]- e对于批处理程序,JDBC 驱动程序提供了旨在减少网络来回传输的优化方法:”JDBC batch inserts/updates“。使用该方法后,插入或更新会先在驱动层排队,然后再传送到数据库。
- n. t4 I& b" d; g8 n 4 U/ N" t5 Y4 n
! z2 F) l& O8 @当达到阈值后,所有排队的语句都会一次性传给数据库。这可以避免驱动程序逐一传送语句,导致网络来回传送的负担。 ( E; l+ ?: p0 k4 }! x$ k
6 B: z3 q4 M3 o4 P; H) E# W2 X ! a% Y7 Z5 G+ y* @6 F% J% W
经过以下配置,就能激活批处理 inserts/updates: ) J" D& q8 Z2 Z; W
! @; |! d* O4 q: V8 H0 u
! j9 J( C$ f( p8 M<prop key="hibernate.jdbc.batch_size">100</prop> 1 K. I5 Q: {2 H t4 W
<prop key="hibernate.order_inserts">true</prop>
|; H: j3 M% {6 ^; a) a. L+ {; \<prop key="hibernate.order_updates">true</prop> ' z0 K( W* I, I, {/ l" ?2 y
仅设置 JDBC 批处理大小并不够。因为 JDBC 驱动程序只会在收到对同一张表 insert/updates 时批处理这些语句。
3 e: M: w, q7 s7 q# C3 C 0 a. K0 p' c$ R# q
$ S& {' s N a( n+ q7 Q如果收到对一张新表的插入语句,JDBC 驱动程序会先清除对前一张表的批处理语句,然后开始分批处理针对新表的 SQL 语句。
0 v1 L; ~6 B9 b8 f 2 u% R% P) `$ ?/ O- Z
8 j1 }# X/ ]: N( w
Spring Batch 内置了相似的功能。该优化能在插入操作频繁的应用中带来30%到40%的性能提升,而不用改动任何代码行。
- \ L% t x% I% o, U$ b8 e- { ) V/ R! v# `8 `; Q. Q+ U
, ~; c z1 c8 @5 \5 @2 C- \
速成法3——定期清理 Hibernate 会话 6 r* `3 B3 E1 s, c
在向数据库添加或修改数据时,Hibernate 会在会话中保留一版已经存在的实体,以防在会话关闭之前这些实体再度被修改。
. i B+ x& E: b' l # R1 |8 C8 k S% y% y
" @. @+ v! a7 W9 P* E/ \, d1 L
但是,多数情况下,一旦对应的插入操作已经在数据库中完成,我们就可以安心地丢弃那些实体。这会释放 Java 客户端进程中的内存,避免过久的 Hibernate 会话导致的性能问题。 " `( o8 X* Y M, m2 Q/ |
! h+ { N! t, T# P: `$ J0 ?
1 p V5 e0 c0 v3 T/ i3 Z这种长久的会话应该尽量避免。但如果出于某种原因不得不使用它们,以下是控制内存消耗的方法: l6 q6 ^. M N+ Y+ Z
+ |! e% s% D: U( w
' D4 v( W m- F7 w1 ]: W# }entityManager.flush(); 3 A; e# Q3 C7 Q0 r6 k: C' _! @
entityManager.clear();
4 N8 s, g. Z1 C& _% {% Zflush 会触使新实体中的插入语句传送至数据库。clear 则会释放会话中的新实体。
% D% t, Y8 Q5 J x7 d/ [9 F, [3 ~
" t4 c+ r$ L3 u# T
; G+ R. [) b+ j8 s4 n/ G5 D速成法4——减少 Hibernate dirty-checking(脏数据检查) 的代价 0 Y) O" b: C& X' ^9 j
Hibernate 内部使用了一种机制用于追踪被修改的实体,名为 dirty-checking。该机制并不基于实体类中的 equals 和 hashcode 方法。 2 @" S7 U" H# @. s
5 G5 D+ E9 L* O7 m8 i & B$ m* J; ~5 s L @' T. h: W
Hibernate 尽可能将 dirty-checking 的性能成本保持在最低值,只在需要时使用 dirty-check。但是该机制也有成本,在列数很多的表中该成本尤其可观。
# K& s1 p! U2 @4 Q" z% e' C$ P; t 3 O' k) ?& ~4 z( }
6 U3 A8 @& {+ z4 a
在进行任何优化之前,最重要的是使用 VisualVM 测量 dirty-checking 的成本。
6 e0 J! H' u) M; Q
R6 e" N4 c n1 d9 K' t7 j 5 v; D0 y7 p1 h+ j
如何避免 dirty-checking ? ) x! d& P5 q# v* k2 h* N
dirty-checking 可以通过以下方式禁用: " [9 h% \3 K: t9 z5 V! p
l) {7 y* m) H _- r5 N
2 u: [0 F9 d( r" |, V. n
@Transactional(readOnly=true) 8 ~8 N/ W6 G: N. m9 h+ Z' q4 M- M6 [3 J8 K
public void someBusinessMethod() {
8 U- y% Z/ l& b, |. V8 ]0 v.... + J0 g2 \' u# T
}
1 r4 b; A" v; i& |5 F7 F禁用 dirty-checking 的另一种方式是使用 Hibernate 无状态会话,预知详情请查看文档。
( b; `% e: `% ^ # ~3 v8 s$ M7 j. d V* n. U
7 }8 o& @; n! r
速成法5——搜索”坏“查询计划 $ M% ~; ~* C" a
$ a8 R" \( h/ w( o' j& N8 Y( O0 q# ?
3 X4 r7 q* Y4 q
检查最慢查询列表,看看有没有好的查询计划。最常见的”坏“查询计划包括: : O% _6 O) x; P: d2 G% H% ^# X
% |* v8 N6 _, k" y8 `- P
6 d9 {, ]% S* G, K- 全表搜索:通常缺少一个索引或表统计过期时进行全表搜索。
- 全笛卡尔连接:意思是计算多张表的全笛卡尔乘积。检查一下缺少的连接条件,或拆分为几个步骤以简化查询。
, K( C2 ^9 O6 A+ C . c* j; ?3 W& [- w. i, }, [& O
2 Y8 s% j4 x- m& O* ]$ d% [0 H
) `4 A# h0 H- s速成法6——检查错误的提交间隔
0 l$ h# c5 @, t5 y如果你使用批处理程序,提交间隔会对性能造成十倍甚至百倍的影响。 ; L# H9 b5 B7 s2 w& G& S. M4 {! I
$ ^) F) K9 W3 \( [3 m 1 M" X- w6 T* a
请确保提交间隔是符合预期的(对于 Spring 批任务,通常是100到1000之间)。经常,该参数的配置不正确。 0 e: ?: w- u7 A7 o5 \
' n+ x) {9 N1 S
' w# l- @% }1 g( Y9 F) B' U( ~/ r速成法7—— 使用二级查询缓存 c9 g2 i6 R6 z. D
如果一些数据可以缓存,则可以查看本文了解如何设置 Hibernate 缓存:Hibernate 二级/查询缓存的陷阱。 % C; y8 l8 D& J( ^' Y
* \3 s, l% `# R$ |6 m
) x0 i- V& y6 |5 [% M! T2 n, c& `结论 9 v+ v# }, n' e$ {: W' d5 {( d7 p* G
解决应用性能问题的关键,在于通过收集一些指标发现当前的瓶颈。 % P8 y4 l) K/ M( s; F
, R7 {/ z+ i1 I7 A; V! i, g. H
4 q# U# v, q1 v8 Z没有一些测量指标,往往无法在短时间内找到真正的问题根源。
8 N1 b* q& ?7 m' X5 `( ~ k+ s : C4 R! `# W7 C: I8 U6 b. ^
- C) R1 h9 L: {2 H此外,很多典型的数据库驱动应用的性能陷阱,如果一开始就使用了 Spring Batch,就能够避免。 $ A7 k. p7 d2 w' L" k& P
/ @' }/ d7 _- D' u [ ( \$ m4 ]$ S; T( }! G
OneAPM for Java 能够深入到所有 Java 应用内部完成应用性能管理和监控,包括代码级别性能问题的可见性、性能瓶颈的快速识别与追溯、真实用户体验监控、服务器监控和端到端的应用性能管理。想阅读更多技术文章,请访问OneAPM官网' a) b8 m! d0 k5 a" ]% L( O0 m& C! d
|
|