TA的每日心情 衰 2021-2-2 11:21
签到天数: 36 天
[LV.5]常住居民I
对于大多数典型的 spring /hibernate 企业应用而言,其性能表现几乎完全依赖于持久层的性能。此篇文章中将介绍如何确认应用是否受 数据库 约束,同时介绍七种常用的提高 应用性能 的速成法。本文系 OneAPM 工程师翻译整理。
, h p3 T) }/ j6 w# z/ [. ~
@4 f: q) e1 o2 ?6 v % { t6 t1 D; ~& L0 n3 Y
如何确认应用是否受限于数据库
, g& G( p* f8 |: ?: m$ H( P 确认应用是否受限于数据库的第一步,是在开发环境中进行测试,并使用 VisualVM 进行监控。VisualVM 是一款包含在 JDK 中的 java 分析器,在命令行输入 jvisualvm 即可调用。 ( D. ^. \! C" A; R$ K7 E3 F! d* A$ A
8 C2 a, `4 H. ^# O3 q! z1 W& u * T1 |5 ^% d2 G+ X3 k, m. [
启用 Visual VM 之后,尝试以下步骤: * _# K& }: x! A0 {$ u. c, J
, \* h8 Q7 P+ |6 k7 s
2 @9 t/ _0 _! s( i 1.双击你正在运行的应用
9 d. ~0 g7 d% Q. X8 f0 ~ 2.选择 Sampler
8 h8 R* R! l+ ~7 h 3.点击 Settings 复选框 * n: W- \1 S7 s2 J
4.选择Profile only packages,然后输入下列包: ( o# F! y/ r$ O
your.application.packages.*
& ?- T" I- p1 Q( D
\& Z" q9 I* @4 L , q% r$ p7 y% f7 `& \' \
org.hibernate.*
- c. l6 b6 }/ j5 O6 K5 i/ O 6 H) t, `6 g% b6 k) w$ U' a
; m( [1 O( c! ?; O
org.springframework.* # A, u1 L1 R. V; P% r9 Y9 p% T1 H
/ T5 D4 m* O. E5 L8 i+ V3 Q6 _) Q
$ G# r9 U7 E$ `) `: I your.database.driver.package, 比如 oracle.* ; A- O( _) N' y% a, N& [- X
( H* c, j3 K3 W, e
Y: z/ g) ?$ ^9 X+ V$ M3 r/ T, h9 ^ 点击 Sample CPU / n. q$ D* i# L3 J1 P
6 n8 q. g1 i; }# t% L7 s * W! T& {: t' ]9 \% v
如果应用性能受限于数据库,其 CPU 分析结果会非常恐怖 8 Z8 a/ }- y8 f" k) b
?* t2 |0 ^7 m2 t
9 g* A" |5 o+ C: {" T1 z Spring/Hibernate 应用性能优化 7 Q- O; i; n# K( b
4 j# G" w; e9 d, `8 c 5 I8 u2 j" J7 a* Y$ u+ R' d
我们看到,客户端 Java 进程花在等待数据库从网络 中返回结果的时间占56%。 , K1 m' {; k# f
- l& B" N" i" Q" d
# j# z6 T4 Y: L, ?+ {3 j I
看到数据库查询是导致应用运行缓慢的原因,其实是好兆头。Hibernate 反射调用占比32.7%是正常情况,无法进一步优化。
) b+ g" l) v# A6 E* t! v - d( h9 g0 U9 e0 V% }
6 d, e. S; t+ I 性能调优第一步:定义基准运行 1 j3 d0 }+ x# m+ Q
性能调优的第一步是为程序定义基准运行,我们要定义一组能有效执行的输入数据,让程序基准运行与生产环境下的运行差不多。 ( G% W. J+ b* d0 h: I
8 V% y/ X9 C# {% J9 A
! G1 p+ H# n! [ 主要的区别在于基准运行的耗时要小很多。作为参考,5到10分钟的执行时间比较不错。
2 R/ L) v% ]/ m/ p8 { j$ T
; e2 ^8 W% s6 |! {5 W
$ N+ T+ U7 C# h( a 什么是好的基准? . ?. b) d7 ~7 h; F7 S( C' C, u
好的基准应该具备以下特征: # Z7 v# o/ h% Y8 P* i
# ?4 K; e! g, c9 ]7 x
% @! G2 x5 z, a* B: e 功能正确 输入数据的种类与生产环境下相似 在短时间内执行完毕 基准运行的优化方案可以外推至完整运行 + D" v1 e1 i' }8 ]+ O2 m
7 o/ D" n& A9 j1 q4 `4 ^+ ]
定义好的基准是成功解决 问题 的一半。 * x' A3 `8 `0 ?7 w2 b1 @
' w8 N1 m/ P( h& E 2 R8 j, @/ y) q7 D8 u+ W
什么是不好的基准 & G) i8 ^* K! m" p& \8 x: e
例如,通过批量运行处理通讯 系统 的电话数据记录,选取10000条记录就是 错误 的做法。 9 k' f; |" ~# L
) {2 F# h9 i0 ~6 }
) D- S/ v4 l, W# `& J( Q 原因是:前10000条记录可能多为语音电话,而未知的性能问题可能发生在短信流量的处理过程中。一开始如果基准不够好,就会导致错误的结论。 . ^8 b- ]$ p" \; w: N4 Y
0 g% {# `* B( f* T: h) B# d/ L" l1 Q
% i8 G3 @ W: E+ e0 v2 G/ ]; l o 收集 SQL 日志与查询时间
0 z9 J) G; F; Q5 t' m. z SQL 查询的执行语句与其执行时间可以通过 log4jdbc等方式收集。详细了解如何使用 log4jdbc 收集 SQL 查询 信息 ,点击文章 使用 log4jdbc 优化 Spring/Hibernate 应用 SQL 日志。
; ~/ b7 Y7 |5 P) o - R) c4 l) }; W! ~2 m L5 s
# F- U* m2 x) }; b3 J
查询的执行时间是从 Java 客户端收集的,该时间包含查询数据库的来回网络调用。SQL 查询的日志如下:
4 \- k' o6 Y7 P# K8 j0 j" ]; _# V
' L- F+ f- ^8 N. G ( u! [. [+ H5 e# V' `* x
16 avr. 2014 11:13:48 | SQL_QUERY /* insert your.package.YourEntity */ insert into YOUR_TABLE (...) values (...) {executed in 13 msec}
; u" a3 \4 t# q" h 预处理语句也是很重要的信息来源,它们常常会透露出常用的查询类型。了解更多的日志讯息,可以查看文章:Hibernate 为什么/在何处使用该 SQL 查询? 6 @9 g) h$ H# R: M
& I) v; ^$ w4 O. `
6 z( s9 x* J6 F2 L4 W% B2 B
通过 SQL 日志可以了解哪些指标? 1 G( w& k2 a7 p! N" f/ @
SQL 日志可以回答下列问题:
! h& ^5 h4 `5 n8 b- {
+ p9 J y8 d* O1 m: T
7 D' K$ s; ^- e+ s5 Y 哪些是执行过的最慢查询? 哪些是最常用的查询? 生成主键的耗时是多少? 是否有数据适合缓存? ) ~8 A& I7 G2 }! k, i
& C$ p1 j7 i/ L& _
如何解析 SQL 日志 ' I( V+ e3 r0 K% D
对于大量的日志文件,最可行的解析方式就是使用命令行工具,该方法的好处是非常灵活,只要写一小段脚本或命令,我们可以抽取出几乎大多数指标。只要你喜欢,任何命令行工具都适用。
2 ]" K) N. D# h! \4 o0 Z m! ]* n# z4 `
7 b$ o* x1 s0 n+ w& ` # ~8 o8 G9 }6 x4 b; W' u( s. h; @4 U
如何你习惯了 Unix 命令行,bash 或是一个好选择。Bash 也可以在 Windows 工作站使用,Cygwin 或 Git 都包含了 bash 命令行。
/ n3 ^( \5 V; f
6 g% J5 B2 O( L! z( ^/ J$ r5 A
, k6 J( W% I: {" z7 p! w6 h& Y: O 常用的速成法 * K$ q, y) D- l3 \' W
下面介绍的速成法能找出 Spring/Hibernate 应用中常见的性能问题,以及对应的解决方案。 0 K/ U, T. w0 r J9 C2 [ w
2 \, C O# v# }, T* q& f
. l& |( A: {0 a# i) \ O, w: f/ } 速成法1——减少生成主键的代价 . g7 o( n( l2 O7 D2 W- H0 X
在插入 操作 频繁的进程中,主键的生成策略很重要。生成 id 的一种常见方法是使用数据库序列,通常一张表一个 id,从而避免在不同表间进行插入时的冲突。 % i2 G; W R0 E$ P
5 b7 k7 o& ]6 Z( a; W. A
7 p/ c \1 O' J0 D 问题在于,如果要插入50条记录,我们希望为了获取这50个 id,可以避免50趟查询数据库的来回网络调用,让 Java 进程不一直等待。
8 P8 H8 T7 t0 b# j% e
- E( |# d* a) D, R ' ^/ B7 p |, U
Hibernate 通常如何解决此问题? , {4 N5 A6 g" }% f/ f1 ^* e
Hibernate 提供了优化的 ID 生成器以避免此问题。也即,对于序列,会默认使用 HiLo id 生成器。以下是 HiLo 序列生成器的工作方式:
1 V; U6 [9 P; d6 S/ P: O9 v5 O5 U / i, v R V" k$ v
" _ S1 G" u# b a0 ?6 ]7 r7 H, @) A
调用一次序列,获得 1000 (高值) 用以下方式计算50个 id " n; j) N" h$ a* A: O7 R
* d" |8 ]5 P0 i2 k, T& {% q# C
1000 * 50 + 0 = 50000 - l. W# R9 W- C, S9 b( p
3 a7 N- w- |% k/ G: l( W0 Q
3 q9 G9 w1 h4 X 1000 * 50 + 1 = 50001
. w% {- l" _+ x) k
4 [3 O9 _' g3 u( `0 Y! T( c J ; P* {' u. u y3 O3 r
...
4 X: o/ O8 M, o: U/ f- e; Z( F
9 U; v; E) G" G 3 s) ^4 c; g/ s& m3 u5 W
1000 * 50 + 49 = 50049, 达到低值 (50) : t' L$ a' v- q8 ^& r t$ S' c
, }9 T8 D( ?" K7 c # @4 h% Q$ i! C. |6 X) D1 I
为新的高值1001调用序列,依次类推
7 O1 t( S6 S- {" H; {6 s# d$ [
+ W: u- r" L/ x% T6 x; A( N : P5 _5 X; X) v7 p5 p- C
因此一次序列调用,可生成50个键,从而减少数次来回网络调用导致的负担。
% B0 ?! t) L7 ?9 ?( E7 p) J% \
3 m. P+ t9 j( I& A( y3 c3 Y1 q5 i ; C# x2 t) _8 N1 ]# T
这些优化的键生成器默认在 Hibernate 4中开启。如要禁用,可将 hibernate.id.new_generator_mappings 设置为 false。 + |% q& K) P5 [6 u4 O. y/ n
% H% d; i8 q; y , f6 K4 `" A2 {, A! V1 Y: f
为什么生成主键仍是一个问题? 7 y* b- H! p, Z0 e- z: q' G
问题在于,如果你声明键生成策略为 AUTO,且未启用优化的键生成器,那么应用最后会面临大量的序列调用。 ' g( n; _% i1 I
7 A& j( k% F. x 9 R2 `3 ]$ d7 n6 {% @
为了确保启用优化的键生成器,请将键生成策略改为 SEQUENCE 而非 AUTO。 1 {- D9 a' X3 T1 U# [
5 h& e4 U) @3 | 6 B6 E- I' ]6 W; @/ I
@Id # Z2 ?2 f7 e6 H' O! ^6 l: F; N; m4 _
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "your_key_generator")
: N, R3 X# s# ~: W/ l, U& V5 Z0 n private Long id;
! f* @4 f& g1 u% u, b 改变设定之后,在插入操作频繁的应用中能看到10%到20%的性能提升,而且几乎没有改动代码。
, m" g* z J5 q 3 t6 S2 b! x, v6 G! t; Y
& ~- l9 ]$ b1 f
速成法2——使用 JDBC 批处理 inserts/updates
' O: S @1 ^/ k# B' [# `9 |+ e 对于批处理程序,JDBC 驱动程序提供了旨在减少网络来回传输的优化方法:”JDBC batch inserts/updates“。使用该方法后,插入或更新会先在驱动层排队,然后再传送到数据库。 # E% q# p7 F- s e& h* B
7 P. P) L c' w: g) X + E; r, B* X6 U7 [
当达到阈值后,所有排队的语句都会一次性传给数据库。这可以避免驱动程序逐一传送语句,导致网络来回传送的负担。
- ^5 t3 M. K8 ] ( L& q% ?6 h( _0 T
4 t/ |$ S& c8 T 经过以下 配置 ,就能激活批处理 inserts/updates: 6 ~+ O( g1 q: u u
, Q0 h' E$ ?6 `9 l1 n) ] 3 ^0 G- o7 T) f% i8 z) Y1 O7 I& p& `
<prop key="hibernate.jdbc.batch_size">100</prop> 4 b" X7 o* U, Y% w5 o: t
<prop key="hibernate.order_inserts">true</prop> # [# f8 l6 L: X# F- L9 k; B; @: H
<prop key="hibernate.order_updates">true</prop>
( G' l6 @- ]: J& u) F4 ^. z 仅设置 JDBC 批处理大小并不够。因为 JDBC 驱动程序只会在收到对同一张表 insert/updates 时批处理这些语句。 9 H% S/ \( z" e4 ]2 k; B
, ]; c. p# r O' l( Y
M4 D. r/ [. \8 }; E$ f9 C 如果收到对一张新表的插入语句,JDBC 驱动程序会先清除对前一张表的批处理语句,然后开始分批处理针对新表的 SQL 语句。
2 s7 l( n) `6 S
" m& q& R( j6 ]' W
! k' R+ q# R8 ], L* m Spring Batch 内置了相似的功能。该优化能在插入操作频繁的应用中带来30%到40%的性能提升,而不用改动任何代码行。
3 P5 T1 A4 v3 L6 V) o
9 c1 k! k7 E; F; x
4 O. e6 M5 T# H- c4 l 速成法3——定期清理 Hibernate 会话
& j6 b1 {$ j6 b- B; g- ` N9 ~ 在向数据库添加或修改数据时,Hibernate 会在会话中保留一版已经存在的实体,以防在会话关闭之前这些实体再度被修改。
! H* ^0 M9 i& e1 D 5 G- M0 i; M8 g$ [1 k
* `" ~& E# c8 F q1 P 但是,多数情况下,一旦对应的插入操作已经在数据库中完成,我们就可以安心地丢弃那些实体。这会释放 Java 客户端进程中的内存,避免过久的 Hibernate 会话导致的性能问题。 ( \: J9 g* G' P' Z, h- }* p, o; C
% Q1 f) W* K3 z, v0 G3 }$ g
8 |" h0 }0 ^ c 这种长久的会话应该尽量避免。但如果出于某种原因不得不使用它们,以下是控制内存消耗的方法: F: z; `; {, ~& b+ }
' u! k* b! o( f' j
3 `3 S3 b& R+ _0 Q$ C) r( \ entityManager.flush(); . k J* j4 ~2 d' k& t/ f
entityManager.clear(); 1 Y! H$ T! [: w" r) g: {9 p
flush 会触使新实体中的插入语句传送至数据库。clear 则会释放会话中的新实体。 $ w" K' j! ]( }: m1 g5 [9 c( x, S
9 C y% U) Z; |3 ~( b9 m7 Z) u
. a" W3 ^+ z4 ]& J0 d 速成法4——减少 Hibernate dirty-checking(脏数据检查) 的代价 ! |- V8 p, {" n, C( D! u+ W
Hibernate 内部使用了一种机制用于追踪被修改的实体,名为 dirty-checking。该机制并不基于实体类中的 equals 和 hashcode 方法。 7 r8 B1 {. F: f8 u) \
8 R/ \9 C# D$ p+ i) q
) Y& c' j6 a" l/ L, ?
Hibernate 尽可能将 dirty-checking 的性能成本保持在最低值,只在需要时使用 dirty-check。但是该机制也有成本,在列数很多的表中该成本尤其可观。
% |; f* {% ~& F
% _+ F" W1 Q, y$ @" Y 8 ~4 j' C" M7 t. w" O9 F- F
在进行任何优化之前,最重要的是使用 VisualVM 测量 dirty-checking 的成本。 * a% X+ V0 w* A/ n
# x. u$ I1 y* ?. e; s
0 b, A5 }4 e' l9 W1 m 如何避免 dirty-checking ?
) v0 v4 U/ k+ p dirty-checking 可以通过以下方式禁用:
& r( j! g3 d& `! s5 d9 F
# K; p0 m9 g7 ~* S7 k 8 G; a9 c6 b3 M6 Z/ Q0 s& T( E
@Transactional(readOnly=true) 1 P0 J* K. s; s2 l. U
public void someBusinessMethod() {
7 y# | Y% ]+ X5 n8 X/ {6 A! U .... 5 a8 H9 V. N. |0 |
}
9 v7 A7 x9 \$ X& o4 m" P 禁用 dirty-checking 的另一种方式是使用 Hibernate 无状态会话,预知详情请查看文档。
# I$ {4 S* @+ I/ H6 j
3 w& q8 w. C, C, k) d8 `' |
; \. J# g6 h: u) h 速成法5——搜索”坏“查询计划
0 o9 L3 t& i; S$ @
, ? Z8 z8 t4 @0 w5 S ! D* v( D2 L- z ^
检查最慢查询列表,看看有没有好的查询计划。最常见的”坏“查询计划包括: ; G m0 \) J6 b3 }5 Q2 Z) {
! F7 Z2 \: L& }$ M: f
! y! n0 L1 K$ x+ | L7 { 全表搜索:通常缺少一个索引或表统计过期时进行全表搜索。 全笛卡尔连接:意思是计算多张表的全笛卡尔乘积。检查一下缺少的连接条件,或拆分为几个步骤以简化查询。
& I: t# z" u# }' W6 p3 y8 \: }
, J/ ~6 m9 N% L* u# X1 {+ F
. ^8 S9 Z1 U" g9 B/ ^) |. O E
! C+ w0 ?" ~2 C* ^ 速成法6——检查错误的提交间隔 3 [ T! ~ Q/ ?) u( c" [
如果你使用批处理程序,提交间隔会对性能造成十倍甚至百倍的影响。
# K% n9 Q$ Z7 z% X. Y4 q , ?. y# f( k7 k# X" \ V" p
% [; k( a8 B3 b& _
请确保提交间隔是符合预期的(对于 Spring 批任务,通常是100到1000之间)。经常,该参数的配置不正确。 . L- M W) U3 ~% w: T7 L
& H- w6 \+ r3 H/ y+ o
- }( ?* [- L+ U U 速成法7—— 使用二级查询缓存
$ B7 r/ V2 e# E3 J9 \* J( m# ^' V( _ 如果一些数据可以缓存,则可以查看本文了解如何设置 Hibernate 缓存:Hibernate 二级/查询缓存的陷阱。
/ S! E& C h# o/ ]/ D% k 4 Y' m* J& v: M5 l, F$ w* ~; y
* {2 |% \. u4 M8 \; b! K M
结论 & n- E4 O% @$ |/ Y8 k2 A
解决应用性能问题的关键,在于通过收集一些指标发现当前的瓶颈。 * k$ ?3 F3 ^1 _0 ^/ M: }9 M
# H1 l. j% Y& U/ A3 x) l# w6 g+ G * ~! c F, y4 @% P( t' V! y" n* y
没有一些测量指标,往往无法在短时间内找到真正的问题根源。
* \* @4 O B% u$ C. g
3 D9 n+ N2 h R4 E7 {, i - `. x2 a2 I, v3 e& l7 v
此外,很多典型的数据库驱动应用的性能陷阱,如果一开始就使用了 Spring Batch,就能够避免。
" X7 p Y; F* q3 v7 k 4 v0 O: R9 r/ |4 _" I, _
% l& D3 f6 m' i7 }" E5 r+ @" r! x OneAPM for Java 能够深入到所有 Java 应用内部完成应用性能管理和监控,包括代码级别性能问题的可见性、性能瓶颈的快速识别与追溯、真实用户体验监控、 服务器 监控和端到端的应用性能管理。想阅读更多技术文章,请访问OneAPM官网 2 P# U! E2 B6 w9 |' `5 o
科帮网 1、本主题所有言论和图片纯属会员个人意见,与本社区立场无关2、本站所有主题由该帖子作者发表,该帖子作者与科帮网 享有帖子相关版权3、其他单位或个人使用、转载或引用本文时必须同时征得该帖子作者和科帮网 的同意4、帖子作者须承担一切因本文发表而直接或间接导致的民事或刑事法律责任5、本帖部分内容转载自其它媒体,但并不代表本站赞同其观点和对其真实性负责6、如本帖侵犯到任何版权问题,请立即告知本站,本站将及时予与删除并致以最深的歉意7、科帮网 管理员和版主有权不事先通知发贴者而删除本文
JAVA爱好者①群:
JAVA爱好者②群:
JAVA爱好者③ :