TA的每日心情 | 衰 2021-2-2 11:21 |
---|
签到天数: 36 天 [LV.5]常住居民I
|
对于大多数典型的 spring/hibernate 企业应用而言,其性能表现几乎完全依赖于持久层的性能。此篇文章中将介绍如何确认应用是否受数据库约束,同时介绍七种常用的提高应用性能的速成法。本文系 OneAPM 工程师翻译整理。 0 K3 W# G' m$ X" f
2 v, m; \# Z) o9 _8 S0 C+ x
" E2 u R2 c8 e# c, d如何确认应用是否受限于数据库 ' p5 [' t2 n) m/ Y
确认应用是否受限于数据库的第一步,是在开发环境中进行测试,并使用 VisualVM 进行监控。VisualVM 是一款包含在 JDK 中的 java 分析器,在命令行输入 jvisualvm 即可调用。
4 @' I; A; ^# q& b+ g ( [/ K9 \- U: m4 ~( z
: M8 ]6 @8 a: ?+ v4 [
启用 Visual VM 之后,尝试以下步骤:
/ @# Y( u" t* X7 c0 ]6 x. v
* n* a' U: J; Z0 ~) D; m : z6 b9 t" `% y" D0 Y4 d
1.双击你正在运行的应用
: |$ J+ k+ ^1 N8 x2 _) @2.选择 Sampler + O, P- d% j& t
3.点击 Settings 复选框 # C! m- m( O7 @+ K
4.选择Profile only packages,然后输入下列包:
3 F! a/ H& X7 |5 A% P( oyour.application.packages.*
4 v; g$ X* F1 S- `- O/ ^1 h
$ U+ k7 l- Z8 P+ R( ?9 S9 i; z n1 q. M. |1 Y1 @2 t
org.hibernate.* # Y/ }; K0 \3 t; k3 h6 {1 _
/ m0 b P0 U# z, W9 x; \
' ^2 ^4 T* C0 g7 s. {7 `3 sorg.springframework.*
) O2 L- F& d/ b- t; j( _2 \
2 U, y9 r+ `! X( |) ? 9 N9 `3 m- {' D5 T6 U( @
your.database.driver.package, 比如 oracle.*
6 {' G$ q$ t! @! }+ p: r3 Y
- u1 S2 U* e8 w, e+ q
k- \, |- O3 R% A! y0 b点击 Sample CPU 9 [, B' Y0 ^8 j- t
* V m. J8 s: M3 {$ N$ r
' v& ~% _8 D' \0 f% j8 A如果应用性能受限于数据库,其 CPU 分析结果会非常恐怖
& L% l3 s$ [) n
# o$ m, O2 C! t, n0 F2 j
% p3 O9 f' D+ ]9 C' N3 WSpring/Hibernate 应用性能优化 + h6 B* g) r0 d0 c: O2 }
$ v0 I5 \4 v" e , |- U# Z8 D) x/ S
我们看到,客户端 Java 进程花在等待数据库从网络中返回结果的时间占56%。 4 p5 y/ o# R5 R
7 d/ {8 r! g b: x
$ { p" q" Q( n0 L8 ?6 f7 ?% _
看到数据库查询是导致应用运行缓慢的原因,其实是好兆头。Hibernate 反射调用占比32.7%是正常情况,无法进一步优化。 * U# P1 q. v. n- w0 r
, e5 T& }9 F1 ~# d k4 l( w1 u. D$ P
0 q W0 z/ ~% g$ f8 X: @性能调优第一步:定义基准运行 ) }7 L3 n+ F- G0 ~, B7 M
性能调优的第一步是为程序定义基准运行,我们要定义一组能有效执行的输入数据,让程序基准运行与生产环境下的运行差不多。
# X0 O; X- ^5 D4 s { " q) ~! Q8 d- Q/ M8 n+ w
) I. y6 ~; l `* V
主要的区别在于基准运行的耗时要小很多。作为参考,5到10分钟的执行时间比较不错。
! ^; n# ]3 A& h/ ?5 i) |/ r9 y
2 N; O& }2 z v: l' x3 k+ M }7 N
; d+ y2 G. X( w; W. ]/ s3 b1 I什么是好的基准?
# w1 M: K: Y$ F5 O' G* [; a好的基准应该具备以下特征:
/ w8 z4 C I9 T! `/ i$ e
! B; e( G0 K5 o/ Z 1 K- T2 ~! M# T0 {3 f7 x, f: F
- 功能正确
- 输入数据的种类与生产环境下相似
- 在短时间内执行完毕
- 基准运行的优化方案可以外推至完整运行
2 i" K& R* _* c) W7 B' L / x7 i Z9 h& D3 ]: d3 R) t
定义好的基准是成功解决问题的一半。 6 w! W0 W" Z0 {* x( a
# g/ h7 x) A- {
5 E2 V9 M/ t8 Z+ w: L5 X, ?什么是不好的基准 8 W$ V- H& P/ {8 x
例如,通过批量运行处理通讯系统的电话数据记录,选取10000条记录就是错误的做法。 * C" R" S) ]2 d( x5 l3 T, F2 n) u: I
& B- _3 I, [8 N# _3 g8 `
) q9 N" x c; C* ~
原因是:前10000条记录可能多为语音电话,而未知的性能问题可能发生在短信流量的处理过程中。一开始如果基准不够好,就会导致错误的结论。 * L) ?7 x: b' F7 W V
5 g' t7 ~' ~ c& a0 J& A+ ` 4 T" x$ k% w$ J; m6 I
收集 SQL 日志与查询时间 8 m2 G4 q. f! S
SQL 查询的执行语句与其执行时间可以通过 log4jdbc等方式收集。详细了解如何使用 log4jdbc 收集 SQL 查询信息,点击文章 使用 log4jdbc 优化 Spring/Hibernate 应用 SQL 日志。 . ^2 R- j* [% u7 c& g
7 z u5 u; s8 q# w+ E) G
% Y! h6 _9 L% x
查询的执行时间是从 Java 客户端收集的,该时间包含查询数据库的来回网络调用。SQL 查询的日志如下: - F. U5 s: u8 }7 x1 ^. ?7 W
5 Q& B4 a: a& [: f. k, K# A" a
/ z, I" q0 u9 s6 m! l4 [2 ?, U! d
16 avr. 2014 11:13:48 | SQL_QUERY /* insert your.package.YourEntity */ insert into YOUR_TABLE (...) values (...) {executed in 13 msec}
* i4 O S, H5 B" t0 k+ W预处理语句也是很重要的信息来源,它们常常会透露出常用的查询类型。了解更多的日志讯息,可以查看文章:Hibernate 为什么/在何处使用该 SQL 查询? / y- C. C& D% O. n7 G% X5 a
( U; Q6 u5 X6 e , ^: s1 M9 I# v
通过 SQL 日志可以了解哪些指标?
& o& Z* T+ ?$ Z% j8 {4 l2 I) vSQL 日志可以回答下列问题: ) U! Q1 Q+ a# y* R# M: {6 w
" _) B# y9 W! I6 Y4 m 5 j. g5 ]; l7 T; z
- 哪些是执行过的最慢查询?
- 哪些是最常用的查询?
- 生成主键的耗时是多少?
- 是否有数据适合缓存?
5 c( l. |5 }9 f$ Y/ N6 w5 I
3 c6 X- V. u! l% Y如何解析 SQL 日志 7 I& _; O6 d; t# E/ E P5 a+ [
对于大量的日志文件,最可行的解析方式就是使用命令行工具,该方法的好处是非常灵活,只要写一小段脚本或命令,我们可以抽取出几乎大多数指标。只要你喜欢,任何命令行工具都适用。 * Y* i. a, {0 _& J1 [# @
. |1 k! F3 |3 ^
+ e) Q) |6 R8 d) o. R9 M0 p) x如何你习惯了 Unix 命令行,bash 或是一个好选择。Bash 也可以在 Windows 工作站使用,Cygwin 或 Git 都包含了 bash 命令行。 * j: c0 d8 T6 J/ f3 u; i# i$ E3 P
; c9 F6 V" G1 L1 }
# X1 G; a/ r9 O; b/ a) \常用的速成法 5 g1 {6 |! Y# l5 N* a6 ~
下面介绍的速成法能找出 Spring/Hibernate 应用中常见的性能问题,以及对应的解决方案。
) |3 s9 q, I1 i# f* C% c# S2 @ # v; g5 x' n ]3 J3 {
$ D/ C. x0 h3 S速成法1——减少生成主键的代价 $ H4 f# X& A8 v0 M
在插入操作频繁的进程中,主键的生成策略很重要。生成 id 的一种常见方法是使用数据库序列,通常一张表一个 id,从而避免在不同表间进行插入时的冲突。
( k; @. V+ y; \' p( y8 v . r& _/ b5 Y6 H' M" `
6 P K' ]! U, i4 _* A/ E问题在于,如果要插入50条记录,我们希望为了获取这50个 id,可以避免50趟查询数据库的来回网络调用,让 Java 进程不一直等待。
& J d" ~% E* S# x$ Q 0 ~/ N+ v% |% i/ K# I$ r9 W
+ M C/ o1 u* P. b# V( k4 g5 A# sHibernate 通常如何解决此问题?
7 f6 @& Q* M% E9 j& |5 XHibernate 提供了优化的 ID 生成器以避免此问题。也即,对于序列,会默认使用 HiLo id 生成器。以下是 HiLo 序列生成器的工作方式:
1 i5 B* v8 O* z% q5 H& ~" L 6 w2 E, q; C6 h
/ Y* f) Q9 K3 |! h5 h [
- 调用一次序列,获得 1000 (高值)
- 用以下方式计算50个 id
7 M+ O% ]6 O8 h, O + O- {' K: G: E+ J. x5 v: I5 L
1000 * 50 + 0 = 50000
( @: y! P& ^6 o% M7 M6 C ( s% t. m- X' e% z; [8 C8 J. Z
* v* Z2 {0 v8 ~/ T% F# Z6 I
1000 * 50 + 1 = 50001
3 o0 {- N- Z9 U/ ]4 J
$ V) a9 K I: ~! A7 g/ `: Y
0 X6 h2 R4 E0 p; V...
& j3 H |9 Y" |. t
! {2 R! J$ E; h, n , Y7 Z$ ~( H; G" M* J2 O
1000 * 50 + 49 = 50049, 达到低值 (50) 5 ?0 I- l+ ~. v& j M6 ]
" b! [& A) }0 N1 q% i( [" X0 t' p
$ S8 j& `5 n( N1 y& ?$ }
为新的高值1001调用序列,依次类推
- |3 H* }2 y- a 6 A5 \& y& e# `; u; y
9 D8 a3 @$ ]! j/ ]9 U2 }4 q因此一次序列调用,可生成50个键,从而减少数次来回网络调用导致的负担。
' J1 y3 U. w7 R" N! g 3 `, H+ Y( X4 m7 x/ M8 I8 g p
5 u1 `, h- }4 C& D/ i! A$ B. C
这些优化的键生成器默认在 Hibernate 4中开启。如要禁用,可将 hibernate.id.new_generator_mappings 设置为 false。
$ C: v: b2 h1 o7 ]8 A" q * I4 X F4 ?6 y1 }3 T! S
8 ~/ R& [+ C0 Y4 O
为什么生成主键仍是一个问题? ( h! {' d# G* N$ a9 ?
问题在于,如果你声明键生成策略为 AUTO,且未启用优化的键生成器,那么应用最后会面临大量的序列调用。 % z; x( g1 ^7 w; Z+ o& i
O' |! w/ s7 a' @
% Z+ Z% ?" B- O; Y+ U为了确保启用优化的键生成器,请将键生成策略改为 SEQUENCE 而非 AUTO。
/ a! g/ M! T% g: i5 A
4 b0 ^6 E1 F6 O8 f) y: b 0 V1 k+ M6 H; m- x
@Id 2 J/ V" c7 ]6 z# W( ?* ?# ?9 e' q4 R
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "your_key_generator")
' W f. d( j, o, Q5 M/ f+ |2 K |private Long id;
$ x0 }; S8 A, A: K改变设定之后,在插入操作频繁的应用中能看到10%到20%的性能提升,而且几乎没有改动代码。
0 }# f% ^+ R7 O* t2 v# B' g
; k6 J" Z) k* I4 d+ p
$ i" X$ C0 X7 Z) @6 B速成法2——使用 JDBC 批处理 inserts/updates
* Z# j) o# l1 c对于批处理程序,JDBC 驱动程序提供了旨在减少网络来回传输的优化方法:”JDBC batch inserts/updates“。使用该方法后,插入或更新会先在驱动层排队,然后再传送到数据库。 + N( K4 A8 Y; Z( S. s) o' f
7 d+ n Q6 n( Z
$ X0 V0 _( B" A" M当达到阈值后,所有排队的语句都会一次性传给数据库。这可以避免驱动程序逐一传送语句,导致网络来回传送的负担。
' |- ]; _$ ^& B" {8 O; S
+ n+ i+ U; P# I4 N t- }' [. u 3 E7 o @3 F8 C; v+ I+ G: G
经过以下配置,就能激活批处理 inserts/updates: . P. g' |7 }/ I# h4 ?
3 ?: ?: Z p, W' s
4 D, w* n! j" A: O0 V0 |<prop key="hibernate.jdbc.batch_size">100</prop> 8 R5 z. A, i8 j+ R
<prop key="hibernate.order_inserts">true</prop>
3 X2 o6 T$ j: }( `<prop key="hibernate.order_updates">true</prop> / Z. N% z' c$ o+ W: k& ]/ ]
仅设置 JDBC 批处理大小并不够。因为 JDBC 驱动程序只会在收到对同一张表 insert/updates 时批处理这些语句。 l- X6 d$ x0 r! U# ~
, B' ? ~) K3 p4 n: e
8 c- E1 X2 ]" i- L* d如果收到对一张新表的插入语句,JDBC 驱动程序会先清除对前一张表的批处理语句,然后开始分批处理针对新表的 SQL 语句。 / p& i1 N) {/ r) x$ _+ v
3 U ^# s2 _4 w8 O P$ h) S- F5 C/ w
( q; u% N+ ^ X4 ]Spring Batch 内置了相似的功能。该优化能在插入操作频繁的应用中带来30%到40%的性能提升,而不用改动任何代码行。 ! j. S, Z( A' d2 s2 m+ \
1 H* P: \; s+ [; H7 d
: `; r& M' R; q% s) K3 n速成法3——定期清理 Hibernate 会话
) A7 W8 T$ ?. }; E% _, \) [在向数据库添加或修改数据时,Hibernate 会在会话中保留一版已经存在的实体,以防在会话关闭之前这些实体再度被修改。 0 |: K# m4 a! v6 L5 ^
5 D0 o P) H# I4 p' D
5 c V9 D6 L2 j" W7 g但是,多数情况下,一旦对应的插入操作已经在数据库中完成,我们就可以安心地丢弃那些实体。这会释放 Java 客户端进程中的内存,避免过久的 Hibernate 会话导致的性能问题。 ) `) T7 D: J7 _4 ?. W( {7 r$ [
! ]1 _6 E( V# C4 [ * |' ]" a4 x. V* F2 X+ L5 ?$ Z
这种长久的会话应该尽量避免。但如果出于某种原因不得不使用它们,以下是控制内存消耗的方法: 8 d0 |/ g" Z9 ?+ Q
$ F* o# z- v4 e2 m8 @1 Z- G4 W
9 M, m7 i/ A6 ]. f
entityManager.flush();
; v8 p/ l2 [2 kentityManager.clear(); 4 s& L* k0 b/ o7 {5 { F' o
flush 会触使新实体中的插入语句传送至数据库。clear 则会释放会话中的新实体。 : V6 z. u# [; y) Q
9 V- E# G6 L4 {8 ~
1 r. F3 b& v3 F速成法4——减少 Hibernate dirty-checking(脏数据检查) 的代价
+ c' R1 G8 |' pHibernate 内部使用了一种机制用于追踪被修改的实体,名为 dirty-checking。该机制并不基于实体类中的 equals 和 hashcode 方法。 2 M$ @6 ~* _+ R/ E- v
1 } m5 J$ k' A- r
# a; z2 w! b; t; g& c# E MHibernate 尽可能将 dirty-checking 的性能成本保持在最低值,只在需要时使用 dirty-check。但是该机制也有成本,在列数很多的表中该成本尤其可观。 1 O6 F3 [: E4 @+ m
6 E# [- k0 n6 [& b& a+ _ 3 z9 u( c+ v5 J0 @3 z- P1 f
在进行任何优化之前,最重要的是使用 VisualVM 测量 dirty-checking 的成本。 2 M' q( @' H, T0 {& V+ K6 m
5 B, r* l5 I, K9 o ( o/ b g7 X% U. \/ }
如何避免 dirty-checking ?
2 [* ?: c# O Adirty-checking 可以通过以下方式禁用: 4 o. r6 ^) O& Y9 @- ~
! ~9 H! J I2 P2 Y5 h
8 k/ [2 \! F; u C( Z@Transactional(readOnly=true) 5 }' v4 D: Z. t8 @2 U; @: k7 M
public void someBusinessMethod() { ( X* I, w1 E- Y$ R4 D) Q K
....
$ F$ i+ T8 N8 M2 M; p}
3 r6 L! r) m* m9 u& k; V禁用 dirty-checking 的另一种方式是使用 Hibernate 无状态会话,预知详情请查看文档。
* ?( @7 _% T( Q8 }0 R$ P 0 D3 F0 O2 z& ?. a0 u
/ |: q3 l) B" C速成法5——搜索”坏“查询计划
+ e2 ?' A7 b7 u& X4 D! P# p! g
4 w2 z; T2 f5 F9 J
W9 G B3 ~' C9 y# \6 a检查最慢查询列表,看看有没有好的查询计划。最常见的”坏“查询计划包括: 2 r6 L$ w% x8 `
, U4 J" @& h: U+ W2 D Q; f( e* ^ \" X4 V( a
- 全表搜索:通常缺少一个索引或表统计过期时进行全表搜索。
- 全笛卡尔连接:意思是计算多张表的全笛卡尔乘积。检查一下缺少的连接条件,或拆分为几个步骤以简化查询。
! N$ c# `5 i" Q; b' V G/ a 8 D9 r& E& j2 _' O |9 \
- v& U- r( j: N
* I3 D' s9 K) @8 P速成法6——检查错误的提交间隔 ! L9 `) V( u# b3 Z H1 a/ u
如果你使用批处理程序,提交间隔会对性能造成十倍甚至百倍的影响。
: J- J* q. a/ t! ]6 {& C 0 ~. e" F- u" `* t5 B
' V8 M8 c% a& c; c& p6 ^6 G; Y1 m7 W请确保提交间隔是符合预期的(对于 Spring 批任务,通常是100到1000之间)。经常,该参数的配置不正确。
6 g8 V' l% k/ M# ^: N
$ G$ @% q3 |9 ?/ T N 3 e2 F3 N8 D) T8 D6 v
速成法7—— 使用二级查询缓存
* X- G, V; h% D8 g, T如果一些数据可以缓存,则可以查看本文了解如何设置 Hibernate 缓存:Hibernate 二级/查询缓存的陷阱。 3 W p) ?, `0 P5 `/ e
6 b/ C' |0 P' R5 C( m
; E+ z- d( ^- |) y$ n( b结论 # k! V0 a& J0 y& J @) |( I
解决应用性能问题的关键,在于通过收集一些指标发现当前的瓶颈。
6 M3 q& P6 I/ \ / j7 H7 I1 s% r1 f
' S9 i4 }1 L5 o8 O$ ^0 B- J/ |
没有一些测量指标,往往无法在短时间内找到真正的问题根源。 % w% J2 D$ I4 L7 S6 N. {
6 Q- _$ v4 L6 E' {6 |$ p$ U3 _% U
2 _' S+ Q1 h& U+ }" y6 n此外,很多典型的数据库驱动应用的性能陷阱,如果一开始就使用了 Spring Batch,就能够避免。 & E$ G& H1 E2 O! L* K
% t( q; U. L1 P, e6 h . h' |4 D! H) {, Z
OneAPM for Java 能够深入到所有 Java 应用内部完成应用性能管理和监控,包括代码级别性能问题的可见性、性能瓶颈的快速识别与追溯、真实用户体验监控、服务器监控和端到端的应用性能管理。想阅读更多技术文章,请访问OneAPM官网: G; F, Y) r- g* E
|
|