TA的每日心情 | 衰 2021-2-2 11:21 |
---|
签到天数: 36 天 [LV.5]常住居民I
|
对于大多数典型的 spring/hibernate 企业应用而言,其性能表现几乎完全依赖于持久层的性能。此篇文章中将介绍如何确认应用是否受数据库约束,同时介绍七种常用的提高应用性能的速成法。本文系 OneAPM 工程师翻译整理。 , I- q) l' ~8 @
' e# f! ]3 F- p 8 ^" u! \! z% c" Q7 i4 z# y
如何确认应用是否受限于数据库 ; I* _) Y. O1 }' n* f
确认应用是否受限于数据库的第一步,是在开发环境中进行测试,并使用 VisualVM 进行监控。VisualVM 是一款包含在 JDK 中的 java 分析器,在命令行输入 jvisualvm 即可调用。
8 N1 b# e* p0 `$ G# F: \( D 0 ]/ Q5 j5 D( {# y- m1 b* g
6 N2 c6 R/ X% g8 ?
启用 Visual VM 之后,尝试以下步骤: / D$ Q# ]2 g9 n5 B
0 T* o) K/ }& M- t6 M
( I y- e* u+ C% ]4 h* {1 ?
1.双击你正在运行的应用
3 O- `; G$ G0 |, @2.选择 Sampler
* r5 U( ~& L7 e, H3.点击 Settings 复选框 5 W7 P( l& x$ Q+ A- m' k2 z
4.选择Profile only packages,然后输入下列包:
- d4 u/ l' Z xyour.application.packages.* 8 a9 z0 Q* {2 l8 L/ ?6 W5 x( l
+ z Z* z$ S) F/ z* x 8 S, a. u$ t0 s: `2 z. w% k* B: c
org.hibernate.*
' C! b N9 c( j: C3 Q + @- m- B3 A6 v9 Z) w
/ P) x2 c% k! c, V8 c, N3 W% e1 U
org.springframework.*
7 B$ C( u# X6 u7 L v
7 w. `) }( y+ r2 f . q9 M2 t5 [. [
your.database.driver.package, 比如 oracle.*
+ W J6 r3 u, p4 c2 F+ x) R 3 B& F+ d# `7 q( k, Q% }5 t/ I
( L! j9 v q( R( |5 o( @ m7 t+ N8 _点击 Sample CPU 4 a! g. S$ h+ m' Y5 U
$ K, }& c5 {1 m" ?: S6 G- J
2 w# N( |! Y" j: M如果应用性能受限于数据库,其 CPU 分析结果会非常恐怖 ) X8 `" o0 ^7 t% ^2 n5 X7 \) M
6 u o1 V L# n# M& B' r, N
* A! C. F* L" {% L S- [
Spring/Hibernate 应用性能优化 # l; C8 w; _3 R Q( W
" q) f- J% u9 N% e
7 z, b9 i( }1 F, g4 }, Z
我们看到,客户端 Java 进程花在等待数据库从网络中返回结果的时间占56%。 , n3 P6 V# D5 G2 `- U
3 s! r. |- _, k+ w- o
, u' k: V5 `7 {8 g! n; i% p* O看到数据库查询是导致应用运行缓慢的原因,其实是好兆头。Hibernate 反射调用占比32.7%是正常情况,无法进一步优化。
* j; z& Z& A+ E V( ?, P2 X' o% [# O7 \$ \3 R8 \
8 O. P: k S: {* M0 V$ D% R性能调优第一步:定义基准运行
6 C3 I4 ?( E- ^. y性能调优的第一步是为程序定义基准运行,我们要定义一组能有效执行的输入数据,让程序基准运行与生产环境下的运行差不多。
7 q8 ~% M* y$ j* g. @ U- s1 A% w& B 7 s1 m3 J! O2 [7 Y" K3 ]% V; V
- V' {3 f9 S$ k' K
主要的区别在于基准运行的耗时要小很多。作为参考,5到10分钟的执行时间比较不错。 % h; I& v. C9 ^
& N' Z/ |2 J& b1 ^8 q
1 K- n! f1 p8 r+ w3 p
什么是好的基准?
' c+ x" m; q" o0 \* x好的基准应该具备以下特征:
' d9 U$ f; z% r$ Z2 w: i
3 C! B0 \% h3 r7 }8 E" F' I - s" j5 K7 \$ P5 @# h( K
- 功能正确
- 输入数据的种类与生产环境下相似
- 在短时间内执行完毕
- 基准运行的优化方案可以外推至完整运行* c' U. g. Z$ }* Y' C
2 L" B6 w4 e, A7 V* s
定义好的基准是成功解决问题的一半。 ( J4 N. a) S R
* L1 _5 j: B. ^8 ]0 f4 |! K
+ n! C* \3 B, w- H什么是不好的基准
6 @ k* n( M) K+ { b例如,通过批量运行处理通讯系统的电话数据记录,选取10000条记录就是错误的做法。 $ o, o" J* w- w, z0 y6 Y2 w5 W
. h2 C/ f! l5 S
7 O2 ~- T5 B, a( u2 }% `' n原因是:前10000条记录可能多为语音电话,而未知的性能问题可能发生在短信流量的处理过程中。一开始如果基准不够好,就会导致错误的结论。
# U$ l2 D* B1 l+ X8 M
; G4 q8 P4 W! D2 M+ l
. j* B; ?) L _( u2 f/ e4 D收集 SQL 日志与查询时间 , D. _ a: b1 U
SQL 查询的执行语句与其执行时间可以通过 log4jdbc等方式收集。详细了解如何使用 log4jdbc 收集 SQL 查询信息,点击文章 使用 log4jdbc 优化 Spring/Hibernate 应用 SQL 日志。
- F$ A! v' {, f
- R! b0 \) S2 k2 J
+ k; e% y0 q2 N7 ]: `* g Z; d. P; U查询的执行时间是从 Java 客户端收集的,该时间包含查询数据库的来回网络调用。SQL 查询的日志如下: ) G: F8 X+ o( P
$ s0 [8 p/ L1 M" y8 q& b( C
7 U3 Z) i' H( L+ ]* {16 avr. 2014 11:13:48 | SQL_QUERY /* insert your.package.YourEntity */ insert into YOUR_TABLE (...) values (...) {executed in 13 msec} 9 d# ]+ n$ b4 u K$ @( ^ F: j
预处理语句也是很重要的信息来源,它们常常会透露出常用的查询类型。了解更多的日志讯息,可以查看文章:Hibernate 为什么/在何处使用该 SQL 查询?
8 y7 ^; X3 n$ N8 l2 y/ K
4 R- s" }7 Q; m0 Q' P: c : @' W4 L) f6 V& U
通过 SQL 日志可以了解哪些指标? ( `4 o/ B- P+ i
SQL 日志可以回答下列问题: % I# S) G' k7 L1 A/ k) n \; \2 B
9 O* R& _3 T* U8 f/ N: `8 M, V5 { - \4 @' O' q8 Y# F J
- 哪些是执行过的最慢查询?
- 哪些是最常用的查询?
- 生成主键的耗时是多少?
- 是否有数据适合缓存?; a$ y5 @2 D: R6 U& `1 p& k3 c: p
: g+ Z; I! c# r% n
如何解析 SQL 日志 / h" T2 w1 {4 ?5 H
对于大量的日志文件,最可行的解析方式就是使用命令行工具,该方法的好处是非常灵活,只要写一小段脚本或命令,我们可以抽取出几乎大多数指标。只要你喜欢,任何命令行工具都适用。 " w) f9 `& n/ d# X, v
7 C: d# E. ~# W2 B* R. G0 Q
) \8 o3 g x& `9 B
如何你习惯了 Unix 命令行,bash 或是一个好选择。Bash 也可以在 Windows 工作站使用,Cygwin 或 Git 都包含了 bash 命令行。
* E. Y% e2 D5 s- F2 h 1 i) L* F' K6 b
6 y! ?2 m! k/ y/ G7 B3 N
常用的速成法 4 x/ P$ y/ {8 y' Q7 H% J
下面介绍的速成法能找出 Spring/Hibernate 应用中常见的性能问题,以及对应的解决方案。
% O* F5 U4 {/ F( R4 C
8 X% `& E$ O% a: [0 P2 r" ] ) F2 A2 e" x. f! f* z7 L4 D: V
速成法1——减少生成主键的代价
, M! C r* |+ D2 ]3 _# v在插入操作频繁的进程中,主键的生成策略很重要。生成 id 的一种常见方法是使用数据库序列,通常一张表一个 id,从而避免在不同表间进行插入时的冲突。
" |$ ^: E0 T4 T8 {% D2 t: n! X 7 o$ ~5 R# j4 O$ D. l
3 ~$ {* S; R& k% V& x
问题在于,如果要插入50条记录,我们希望为了获取这50个 id,可以避免50趟查询数据库的来回网络调用,让 Java 进程不一直等待。 0 D) o! K1 q9 i" U9 P B
3 A' {+ z* o' G1 o" X) l
* K, b' X3 D& K, uHibernate 通常如何解决此问题?
% A/ i) F- Q' a6 hHibernate 提供了优化的 ID 生成器以避免此问题。也即,对于序列,会默认使用 HiLo id 生成器。以下是 HiLo 序列生成器的工作方式:
# g4 D: O) T8 D. F- c$ l
) \1 x) w6 K6 r% z3 R1 S( J5 J * c( ]8 k! ~* j, c& l: j j! X9 P9 t
- 调用一次序列,获得 1000 (高值)
- 用以下方式计算50个 id6 T& P$ J7 A& d4 ^! ^4 R
* z3 V$ @* P; w1000 * 50 + 0 = 50000
0 C+ G2 `( Z5 M1 H5 W0 @& I
}7 r3 B& i# ? 0 m2 q+ R# @0 r1 J4 t
1000 * 50 + 1 = 50001 + s: }9 d" K1 J5 _* _' X
2 ]! o5 Z9 j* Q' O- ?; k- J 8 L) `/ w& V4 _2 Q0 O1 K. _( \
...
3 ~, j v5 q5 d- ~( l
" Y4 Y" c$ @9 p0 h6 ]# X
. r4 e% k% \9 T6 u5 A2 o1000 * 50 + 49 = 50049, 达到低值 (50) $ F. @, R+ R% G0 E+ J7 H
. V* m7 H2 I- C3 P
" N# x ~7 s: A ^# F& i0 A( m6 o为新的高值1001调用序列,依次类推
0 |( p2 O7 A. `; N7 R- D/ i / K: ~4 \6 X# r% ], a( Z
1 \5 M( q, q R( s1 `因此一次序列调用,可生成50个键,从而减少数次来回网络调用导致的负担。
; N& w' U; I8 E8 d! Z; o - g8 i0 I; S/ D2 Q5 f' g
! D5 \6 s N7 v& l& R1 L& j
这些优化的键生成器默认在 Hibernate 4中开启。如要禁用,可将 hibernate.id.new_generator_mappings 设置为 false。
3 ?1 W6 M% F5 T# h$ o
1 O( ~+ p7 H: s# Y1 U4 |
$ g5 p. Y1 p, {2 E0 c为什么生成主键仍是一个问题?
5 ?; G) E- z" E( E9 ]' |" b, [问题在于,如果你声明键生成策略为 AUTO,且未启用优化的键生成器,那么应用最后会面临大量的序列调用。
: @3 ]7 M/ ~4 u! v+ v9 k1 w( ]
8 W5 j4 }% N0 I. Z$ }
: X2 a4 x0 D9 \( q% q为了确保启用优化的键生成器,请将键生成策略改为 SEQUENCE 而非 AUTO。
4 D0 F. M* G# M7 w: S 7 M5 U+ H0 o& d- g+ l
( W7 n* H# L- T@Id
; ^* V8 E* Q# \7 M9 M1 l/ O6 S@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "your_key_generator")
8 V) Q$ W0 [& a8 K# C* |% b0 Hprivate Long id; $ ?3 t1 c8 a/ _ m/ \9 @$ p2 N) r
改变设定之后,在插入操作频繁的应用中能看到10%到20%的性能提升,而且几乎没有改动代码。
7 {1 _ L3 Y; N ! B/ e: n/ T& `. d
3 w/ @2 l; |% ]: C& F6 \3 _
速成法2——使用 JDBC 批处理 inserts/updates
+ b% l. L1 F& f* P对于批处理程序,JDBC 驱动程序提供了旨在减少网络来回传输的优化方法:”JDBC batch inserts/updates“。使用该方法后,插入或更新会先在驱动层排队,然后再传送到数据库。 + }' d9 J$ K: x+ r; ~2 s
. M D, {/ c( W) O& G
% `) e6 w5 D5 I6 P( j9 d W) N当达到阈值后,所有排队的语句都会一次性传给数据库。这可以避免驱动程序逐一传送语句,导致网络来回传送的负担。 / {5 t, h' G8 }( J% K
4 \' ]# ^0 |4 W$ T
6 x9 m' Q1 s, u& y" _5 {$ \
经过以下配置,就能激活批处理 inserts/updates:
3 ~* ~& t' \! t9 n" s2 E
5 ]8 | S* b8 D- x' Q/ G8 F/ r7 i
1 O8 v6 r. B0 [$ w. h- ~ a0 H' L<prop key="hibernate.jdbc.batch_size">100</prop> 4 X8 m9 a* Z) b9 A; o) P* V
<prop key="hibernate.order_inserts">true</prop>
* B3 e! O4 }+ b# U+ D<prop key="hibernate.order_updates">true</prop>
2 @1 n2 n) ^% ^! m5 _4 ?仅设置 JDBC 批处理大小并不够。因为 JDBC 驱动程序只会在收到对同一张表 insert/updates 时批处理这些语句。
: |, s3 b& J! W3 r7 [! l
1 p; L/ w( w7 {9 u- t- ^" ~& h% S; X
+ k+ u# Z x: {- m9 y1 _如果收到对一张新表的插入语句,JDBC 驱动程序会先清除对前一张表的批处理语句,然后开始分批处理针对新表的 SQL 语句。
9 f" w* W3 e0 n, D; B 9 ^1 h2 `2 ]' X6 _0 b" t+ O7 y
/ g: P, p1 Z/ _0 E; `Spring Batch 内置了相似的功能。该优化能在插入操作频繁的应用中带来30%到40%的性能提升,而不用改动任何代码行。
/ C; U& E9 Y) L4 G7 c% l T
& [. m( I: m8 Q& Q0 k4 E5 J' H ?) `7 x: }4 T- Z
速成法3——定期清理 Hibernate 会话 4 V' b; B! x6 D. K
在向数据库添加或修改数据时,Hibernate 会在会话中保留一版已经存在的实体,以防在会话关闭之前这些实体再度被修改。 ( C& h/ O6 F7 K2 K9 O7 o9 J3 X/ F
! G8 D+ u4 G% y; _, n
8 f1 |5 m5 |) b# L: r
但是,多数情况下,一旦对应的插入操作已经在数据库中完成,我们就可以安心地丢弃那些实体。这会释放 Java 客户端进程中的内存,避免过久的 Hibernate 会话导致的性能问题。 , {5 s( O3 t* e: a) r- h2 j# ?: h& M
0 B/ O, A7 i. }% x C0 i
" ?- q- C% a8 Y
这种长久的会话应该尽量避免。但如果出于某种原因不得不使用它们,以下是控制内存消耗的方法: + K5 j. z S8 S6 K+ o6 {& n
) n1 H" i$ v6 v8 h5 o% ^; P0 W
% o1 }6 |& i: ientityManager.flush(); + J4 C6 q# H7 y$ S$ f
entityManager.clear(); 6 V; P5 {4 `% z6 R
flush 会触使新实体中的插入语句传送至数据库。clear 则会释放会话中的新实体。
$ Y. r- E6 P& r" S! g7 o- Z0 v
) \- v2 ~3 I) o1 u - t/ n6 R7 p* i; b
速成法4——减少 Hibernate dirty-checking(脏数据检查) 的代价 + y7 t# d) S' l$ q/ ] x
Hibernate 内部使用了一种机制用于追踪被修改的实体,名为 dirty-checking。该机制并不基于实体类中的 equals 和 hashcode 方法。
6 W3 I! M7 ?6 N. Z( f* a$ U k / [( B, ]* A; Q7 {
- y; L# ~/ [ f5 g ?" P, F4 n
Hibernate 尽可能将 dirty-checking 的性能成本保持在最低值,只在需要时使用 dirty-check。但是该机制也有成本,在列数很多的表中该成本尤其可观。
D+ w8 N; |$ M. J1 e4 P: V. l
' H* O( Q$ Z l0 Z/ W V# u8 S
. |" O. |; I7 h+ h" j! `3 l/ ~在进行任何优化之前,最重要的是使用 VisualVM 测量 dirty-checking 的成本。
- d+ Q( P% e; b" L2 ]; F
* B& D+ n' S" K- D0 o
& u3 m# }5 t' J* b" r1 i; l% S如何避免 dirty-checking ? 5 @$ b( j+ K9 e- a
dirty-checking 可以通过以下方式禁用:
0 C, v- g% x4 T/ x. b! d4 X* W 6 v- V8 H% J/ O
& ~$ R4 p8 B( @* M6 R
@Transactional(readOnly=true) , O: _0 ]/ w9 D% M+ n% `* V
public void someBusinessMethod() {
3 s, v" v" H) S, a/ t' e+ |....
' O6 j/ d E! U! E& v* M}
$ ] [0 j9 P1 K7 D: K禁用 dirty-checking 的另一种方式是使用 Hibernate 无状态会话,预知详情请查看文档。 . T* n3 m [. `/ D* i' s( ?
6 i* s2 K! G3 p
1 Z; ?5 E, H o% t; ?速成法5——搜索”坏“查询计划 : q1 s3 h1 G8 |0 F0 n
; e; [3 i+ V _3 z2 x + m, A# D9 }; Q
检查最慢查询列表,看看有没有好的查询计划。最常见的”坏“查询计划包括:
' z* {7 ?, [( X" @( q$ F8 L1 Y9 V9 R
$ Z# W4 X" k: D6 W$ m
5 p G" r" p R8 u) L4 R* ?7 E( }- 全表搜索:通常缺少一个索引或表统计过期时进行全表搜索。
- 全笛卡尔连接:意思是计算多张表的全笛卡尔乘积。检查一下缺少的连接条件,或拆分为几个步骤以简化查询。
9 ?. T. f) u5 Q0 T2 f2 u! l
, x9 ?; [2 D% h
3 z! m1 Z) y& v8 U* y
% T6 V L0 n. G. ^. Y9 m速成法6——检查错误的提交间隔
y- i. M/ A; U! ?/ t; f7 C如果你使用批处理程序,提交间隔会对性能造成十倍甚至百倍的影响。
( O! V. Z x: F9 ]# @& A7 m
^0 S# g/ l3 c, s% ?7 I# Z
% Q2 e$ g! L9 \# Q" h' K请确保提交间隔是符合预期的(对于 Spring 批任务,通常是100到1000之间)。经常,该参数的配置不正确。
/ D5 `/ D6 ]) M4 t' y/ r- p! @! G% C# K
/ f/ d7 O! R1 H) @
+ W" o. k7 f1 C速成法7—— 使用二级查询缓存
1 U: D& C% N, L0 @" S: ^如果一些数据可以缓存,则可以查看本文了解如何设置 Hibernate 缓存:Hibernate 二级/查询缓存的陷阱。
- F/ r, P; t f* I. E. o7 P 7 e- j! M9 |! }, b& X, v) y3 B
) }0 ^: K& K- U+ P9 @* P9 D结论 3 @4 U. }+ h6 A0 ~6 k, ^+ t0 l
解决应用性能问题的关键,在于通过收集一些指标发现当前的瓶颈。
- f# o; ` E% Q$ v$ P q: b
, Z; N' j$ h5 j( n 6 M1 A- R# r F! o
没有一些测量指标,往往无法在短时间内找到真正的问题根源。
- s* h- Y9 s# h# A : B9 Z$ ?$ Y; i, q
& B) @! c% N$ A; ~+ D; `$ D此外,很多典型的数据库驱动应用的性能陷阱,如果一开始就使用了 Spring Batch,就能够避免。
6 i/ g) V" h0 C! J% ?$ _4 x! [/ u & G& D0 N0 A# z* s6 s% i8 N2 S
* H0 q3 \2 c# O. T2 dOneAPM for Java 能够深入到所有 Java 应用内部完成应用性能管理和监控,包括代码级别性能问题的可见性、性能瓶颈的快速识别与追溯、真实用户体验监控、服务器监控和端到端的应用性能管理。想阅读更多技术文章,请访问OneAPM官网
- E. L, P3 v, R" ^: Y7 ]! p: w" p |
|