8. 统一SQL性能测试报告

8.1. 测试目的

本次测试旨在评估 JAVA 调用统一 SQL 解析和改写的性能表现,测试包括对复杂 SQL 和简单 SQL 的转换进行性能测试,通过单线程基准测试评估统一SQL整个转换流程的性能表现。

8.2. 业务术语定义

JMH :(Java Microbenchmark Harness)是一个用于编写、运行和分析Java基准测试的工具。

JMH基准测试结果中常见的几个参数含义如下:

  1. Mode(模式):表示基准测试的模式,通常有平均时间、吞吐量和样本时间等模式。平均时间模式(Mode.AverageTime)表示每次操作的平均耗时;吞吐量模式(Mode.Throughput)表示每秒钟可以执行的操作数;样本时间模式(Mode.SampleTime)表示每个样本执行的时间。

  2. Cnt(计数):表示执行基准测试的次数。

  3. Score(得分):表示基准测试的结果,通常是操作的平均执行时间或每秒钟可以执行的操作数等。

  4. Error(误差):表示基准测试结果的误差范围。通常使用标准差表示。

8.3. 测试环境

8.3.1. 测试用例

基准测试的测试用例是简单SQL(1kb左右)和复杂SQL(44kb左右)。

simple-sql.sql

complex-sql.sql

对比测试的案例见附件

attachment.zip.rename

备注

下载附件后请去掉 .rename 后缀

8.3.2. 测试场景

基准测试

基准测试使用 C 语言调用 TransferSQL 函数来进行性能测试。

对比测试:

../_images/load-test-002.png

8.3.3. 测试工具

8.3.3.1. 基准测试

表 2-1 测试工具

用途

工具

版本

基准测试

GCC

4.8.5

对比测试

JMeter

5.5

表 2-2 软硬件配置

环境

系统/资源

型号/配置/软件名称/软件版本号

硬件环境

linux 3.10.0

Intel(R) Xeon(R) Gold 6250 CPU @ 3.90GHz

软件环境

sql-convert-runtime-go

unisql-24.2.0.0-26867

8.3.3.2. 对比测试

环境

系统/资源

型号/配置/软件名称/软件版本号

硬件环境

CPU

Intel(R) Xeon(R) Gold 6250 CPU @ 3.90GHz (限制4C)

软件环境

sql-convert-runtime

23.1.1.0

sql-convert-runtime-go

unisql-23.1.1.0-11986

8.4. 测试结果

8.4.1. 基准测试

8.4.1.1. 测试结果

unisql-performance-test-c.zip.rename

备注

下载附件后请去掉 .rename 后缀

表3-1 基准测试结果

是否缓存

SQL大小

并发数

总时间 (us)

每个线程运行次数

用户体感耗时 (us)

总吞吐量 (queries/s)

856

1

748537

5000

149.71

6679.69

856

1

81291729

100000000

0.81

1234567.90

856

2

48707594

50000000

0.97

2083333.33

856

4

26240807

25000000

1.05

3846153.84

856

8

15473441

12500000

1.24

6666666.67

856

16

7889151

6250000

1.26

14285714.29

856

32

4882088

3125000

1.56

25000000.00

22426

1

15667669

5000

3133.53

319.13

22426

1

241179383

10000000

24.12

41493.77

22426

2

120316536

5000000

24.06

83333.33

22426

4

61748721

2500000

24.70

163934.43

22426

8

30528287

1250000

24.42

333333.33

22426

16

15970875

625000

25.55

666666.67

22426

32

10281346

310000

33.17

992000.00

8.4.1.2. 测试结论

根据测试结果,在单线程的基准测试场景下,可以得出以下结论:

  1. 不走缓存的场景下,简单SQL的平均转换耗时是约149微秒(us),复杂SQL的平均转换耗时是约3133微秒(us)。

  2. 缓存的场景下,简单SQL的平均转换耗时降低至约1微秒(us),复杂SQL的平均转换耗时降低至约24微秒(us)。

8.4.2. 对比测试

对比测试的目的是在模仿真实调用的场景下,验证加了统一 SQL 之后,与直连查询的性能对比情况。

8.4.2.1. 测试结果

表 3-2 对比测试结果

Benchmark

线 程数

Median

95%

99%

TPS

转 SQL1 无缓存

1

0

1

1

2189.22

转 SQL2 无缓存

1

1

2

2

799.39

转 SQL3 无缓存

1

20

23

30

47.89

转 SQL1 缓存

1

0

1

1

6792.53

转 SQL2 缓存

1

0

1

1

5355.49

转 SQL3 缓存

1

2

3

13

458.84

直 SQL1

1

0

1

1

2682.71

直 SQL2

1

1

1

2

1066.01

直 SQL3

1

18

22

29

52.03

直 SQL1 缓存

1

0

1

1

6730.69

直 SQL2 缓存

1

0

1

1

5429.59

直 SQL3 缓存

1

2

3

14

422.01

转 SQL1 无缓存

3

1

1

1

5299.47

转 SQL2 无缓存

3

1

2

2

2173.32

转 SQL3 无缓存

3

21

31

38

130.64

转 SQL1 缓存

3

0

1

1

15327.56

转 SQL2 缓存

3

0

1

1

12167.60

转 SQL3 缓存

3

2

3

13

1329.01

直 SQL1

3

0

1

1

6253.94

直 SQL2

3

1

1

2

3027.08

直 SQL3

3

19

24

30

148.44

直 SQL1 缓存

3

0

1

1

15062.11

直 SQL2 缓存

3

0

1

1

12185.33

转 SQL1 无缓存

20

1

5

11

10514.89

转 SQL2 无缓存

20

3

9

18

4839.59

转 SQL3 无缓存

20

36

63

80

510.15

转 SQL1 缓存

20

1

1

3

32707.27

转 SQL2 缓存

20

1

1

5

25815.99

转 SQL3 缓存

20

4

8

17

4757.39

直 SQL1

20

1

3

6

15194.98

直 SQL2

20

2

4

8

9762.83

直 SQL3

20

31

48

62

617.87

直 SQL1 缓存

20

1

1

3

33059.32

直 SQL2 缓存

20

1

1

5

26639.60

直 SQL3 缓存

20

4

9

17

4713.76

转 SQL1 无缓存

50

4

13

28

10266.72

转 SQL2 无缓存

50

9

29

49

4746.66

转 SQL3 无缓存

50

81

165

222

569.96

转 SQL1 缓存

50

1

2

4

34075.55

转 SQL2 缓存

50

2

3

8

26932.50

转 SQL3 缓存

50

7

23

39

5521.49

直 SQL1

50

2

7

23

16353.33

直 SQL2

50

3

10

36

10785.25

直 SQL3

50

66

127

166

698.90

直 SQL1 缓存

50

1

2

4

33175.86

直 SQL2 缓存

50

2

3

8

26798.36

直 SQL3 缓存

50

7

24

47

5441.85

8.4.2.2. 测试结论

表 3-3 TPS 对比表格

单线程

20线程

50线程

对比项

直连

LTSQL

直连

LTSQL

直连

LTSQL

无缓存SQL1

2683

2189

15195

10515

16353

10267

无缓存SQL2

1066

799

9763

4840

10785

4747

无缓存SQL3

52

48

618

510

699

570

有缓存SQL1

6731

6793

33059

32707

33176

34076

有缓存SQL2

5430

5355

26640

25816

26798

26933

有缓存SQL3

422

459

4714

4757

5442

5521

按照 TPS 计算平均时间(微秒 us),整理表格如下:

表 3-4耗时(微秒)对比表格-1

1线程

3线程

对比项

直连

LTSQL

耗时比

直连

LTSQL

耗时比

无缓存SQL1

373

457

122.54%

480

566

118.01%

无缓存SQL2

938

1251

133.35%

991

1380

139.28%

无缓存SQL3

19221

20880

108.63%

20209

22962

113.62%

有缓存SQL1

149

147

99.09%

199

196

98.27%

有缓存SQL2

184

187

101.38%

246

247

100.15%

有缓存SQL3

2370

2179

91.97%

2266

2257

99.60%

表 3-5耗时(微秒)对比表格-2

20线程

50线程

对比项

直连

LTSQL

耗时比

直连

LTSQL

耗时比

无缓存SQL1

1316

1902

144.51%

3057

4870

159.28%

无缓存SQL2

2049

4133

201.73%

4636

10534

227.22%

无缓存SQL3

32369

39204

121.12%

71541

87726

122.62%

有缓存SQL1

605

611

101.08%

1507

1467

97.36%

有缓存SQL2

751

775

103.19%

1866

1856

99.50%

有缓存SQL3

4243

4204

99.08%

9188

9056

98.56%

通过整理的结果可发现:

  • 如果是走了缓存的,性能与直连几乎无差异(大部分业务场景都可以走入缓存)

  • 如果是完全动态的 SQL,单线程的性能损失最高大约在 30% 左右;如果外部时间较长,耗时会变得不明显

  • 带有 IO 的操作,如果计算需要的 CPU 变高,在并发数超越 CPU 核心数的时候会放大这种差异,甚至可增加一倍耗时

实际应用中很少会有不能缓存的 SQL。不能缓存的 SQL 可能是以下几种情况:

  • MyBatis 中使用 ${} 符号来拼接 SQL,每次都不同

  • MyBatis 中使用大量的 <if> ,具有非常大量的排列组合,且每次的条件分支都不相同

  • 全动态 SQL,每次都不相同

而以下场景是可以缓存的:

  • MyBatis 使用 #{} 绑定了变量的。因为变量不视为 SQL,所以可以用缓存

8.4.3. 启动时间测试

测试方法:

  1. 启动一个 jres 服务,通过统一 sql 连接到一个 PostgreSQL 数据库,执行 select 1,然后关闭应用

  2. 启动一个 java 程序,仅计算统一 SQL 的类加载时间

Jres 应用测试次数为 10 次,结论如下:

表 3-6 Jres 服务启动时间对比

用例

平均耗时(ms)

用 Maven 加载统一 SQL

4274

用 unisql.lib.full-path加载统一SQL

4217

用 unisql.lib.dir 加载统一 SQL

4162

不加载统一 SQL

3980

类加载的案例为在 300s 内反复启动,取纯粹类加载消耗的时间的平均值,结论如下:

表 3-7类加载平均时间对比

用例

平均耗时(ms)

用 Maven 加载统一 SQL

267.61

用 unisql.lib.full-path 加载统一SQL

207.01

用 unisql.lib.dir 加载统一 SQL

210.01

结论:统一 SQL 类加载时间大约在 200ms 左右,主要耗时为 jnr 的动态库加载机制;使用 Maven 的加载方式会慢 60ms ,其中包含了从 jar 包中解压动态库字节流并写入到文件的时间。

对于一般的 Jres 应用,使用了统一 SQL 后,应用启动耗时会增加 200ms - 300ms。

8.5. 并发性能损耗大分析过程

为详细验证性能组件的真实性能与损耗,使用纯 GO 调用、GO 通过 CGO 调用、GO 调用动态库、C 语言调用动态库、Java 调用五种场景,用三种复杂程度的 SQL来验证 SQL 解析功能的性能,测试机器为 32 cpu 服务器,结果较多,没有全部列出。

sql1 是 105 字节的简单 select

sql2 是 2305 字节的 select

sql3 是 12716 字节的 select

单线程执行情况如下:

表 4-1 单线程对比

场景

sql1

sql2

sql3

Pure GO

17.84

168.67

1157.31

GO with CString

18.81

170.39

1182.83

GO with dynamic lib

22.62

175.60

1162.94

C with dynamic lib

21.84

167.58

1118.42

Java

24.18

184.39

1235.37

结论:Java 调用会带来少量损耗,在复杂 sql 场景下大约有 5%。

为对比程序详细性能,对程序进行了分段的测试,与直连 LightDB 做了对比。

测试 PG 直接执行的方法是:使用 c 语言 libpq 库,直连到本地数据库,执行相同的 SQL,库中无数据。

PG 日志 elapsed 是执行了 sql 后,在 lightdb-x 日志中找到 PARSER STATISTICS 下方的 system usage stats ,查看 elapsed 项的时间。

结果如下:

表 4-2 分拆程序步骤测试结果

场景

sql1

sql2

sql3

PG 日志 elapsed

2

27

156

PG 直接执行

39.84

88.01

275.92

GO 仅解析

11.55

94.65

585.43

GO 解析+转换

15.18

126.74

893.42

GO 解析+转换+转换为SQL

17.84

168.67

1157.31

结论:GO 编写的解析功能性能不如 PG 中 C 语言的 实现,从 sql3 看出差异大约在 1倍左右;

转换时间大约是解析时间的 35%;

将结果还原成 SQL 的时间约为解析时间的 30%。

以 sql2 为基准,各种测试方式的 CPU 使用率如下:

表 4-3 多线程 CPU 使用率

场景

1

2

3

4

8

16

32

64

Pure GO

100

200

297

394

760

1380

1870

2069

GO with CString

100

201

300

398

764

1389

1701

1764

GO with dynamic lib

103

204

307

407

781

1458

1937

1787

C with dynamic lib

103

205

307

337

779

1437

2109

1993

Java

106

209

311

411

777

1438

2133

2029

以 sql2 为基准,各种测试方式的 单次转换响应时间为:

表 4-4 多线程单次转换响应时间

场景

1

2

3

4

8

16

32

64

Pure GO

169

190

204

215

277

439

901

1712

GO with CString

170

197

211

234

318

513

1038

1890

GO with dynamic

176

205

228

254

368

587

1069

2215

C with dynamic

168

201

225

293

362

586

978

2053

Java

184

216

239

266

374

583

1010

2065

以上两表格结论:

从 16并发与 32 并发的数据来看,统一 SQL 无法打满 CPU,原因推测是内存分配方面的原因,暂未深入分析。

表 4-5 纯go下多线程转换测试结果(压测持续30秒)

场景

1

8

16

32

256字节的SQL - TPS

28714.29

27106.58

41279.42

38849.79

256字节的SQL – 单次转换平均耗时(us)

34.83

36.89

24.23

25.74

4000字节的SQL - TPS

2837.08

4517.92

5926.89

7788.56

4000字节的SQL – 单次转换平均耗时(us)

352.48

221.34

168.72

128.39