前言

通过前面的章节,我们的 RPC 框架已经搭建完成,虽仍有许多待优化的点,但整体的效率性能应该还是不错的,下面我们来对 XRPC 的性能进行测试

测试环境

我所用的测试机器硬件配置为:

  • 操作系统:Windows10
  • CPU: AMD R5 3500X 6-Core
  • 内存:16 GB 3200 MHz DDR4

序列化测试

序列化针对序列化的大小和速度进行测试

序列化后的数据大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class SerializerCompareTest {
private static RpcMessage buildMessage() {
RpcResponse<Object> rpcResponse = RpcResponse.builder()
.requestId(UUID.randomUUID().toString())
.message(SUCCESS.getMessage())
.code(SUCCESS.getCode())
.data(new String("我是结果,我是结果,我是结果")).build();

RpcMessage rpcMessage = RpcMessage.builder()
.requestId(1)
.compress(GZIP.getCode())
.messageType(REQUEST_TYPE)
.codec(KRYO.getCode())
.data(rpcResponse).build();
return rpcMessage;

}

public static void kryoSerializeSizeTest() {
RpcMessage data = buildMessage();
KryoSerializer kryoSerializer = new KryoSerializer();
byte[] serialize = kryoSerializer.serialize(data);
System.out.println("kryo's size is " + serialize.length);
RpcMessage out = kryoSerializer.deserialize(RpcMessage.class, serialize);
assertEquals(out, data);
}


public static void hessianSerializeSizeTest() {
RpcMessage data = buildMessage();
HessianSerializer hessianSerializer = new HessianSerializer();
byte[] serialize = hessianSerializer.serialize(data);
System.out.println("hessian's size is " + serialize.length);
RpcMessage out = hessianSerializer.deserialize(RpcMessage.class, serialize);
assertEquals(out, data);
}


public static void protostuffSerializeSizeTest() {
RpcMessage data = buildMessage();
ProtostuffSerializer protostuffSerializer = new ProtostuffSerializer();
byte[] serialize = protostuffSerializer.serialize(data);
System.out.println("protostuff's size is " + serialize.length);
RpcMessage out = protostuffSerializer.deserialize(RpcMessage.class, serialize);
assertEquals(out, data);
}

@Test
public void sizeTest() {
kryoSerializeSizeTest();
hessianSerializeSizeTest();
protostuffSerializeSizeTest();
}

}

主要针对实现的三种序列化算法进行了实现,结果如下

1
2
3
kryo's size is 100
hessian's size is 274
protostuff's size is 138

kryo 的序列化后数据体积最小

序列化性能

这里使用 JMH 进行测试 http://openjdk.java.net/projects/code-tools/jmh/

JMH 即 Java Microbenchmark Harness,这是专门用于进行代码的微基准测试的一套工具 API。
JMH 由 OpenJDK/Oracle 里面那群开发了 Java 编译器的大牛们所开发 。何谓 Micro Benchmark 呢? 简单地说就是在 method 层面上的 benchmark,精度可以精确到 微秒级。

首先再 pom 中导入依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.35</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.35</version>
</dependency>

JMH 中的注解介绍

@Warmup

@Warmup( iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
warmup 这个注解,可以用在类或者方法上,进行预热配置。可以看到,它有几个配置参数。

  • timeUnit :时间的单位,默认的单位是秒。
  • iterations :预热阶段的迭代数。
  • time :每次预热的时间。
  • batchSize :批处理大小,指定了每次操作调用几次方法。

上面的注解,意思是对代码预热总计 5 秒(迭代 5 次,每次一秒) 。预热过程的测试数据,是不记录测量结果的。为啥要预热呢?
因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。

@Measurement

度量,其实就是一些基本的测试参数。

  • iterations - 进行测试的轮次
  • time - 每轮进行的时长
  • timeUnit - 时长单位

@Benchmark

方法级注解,表示该方法是需要进行 benchmark 的对象,用法和 JUnit 的 @Test 类似。

@BenchmarkMode

基准测试类型。这里选择的是 Throughput 也就是吞吐量。根据源码点进去,每种类型后面都有对应的解释,比较好理解,吞吐量会得到单位时间内可以进行的操作数。

  • Throughput: 整体吞吐量,例如”1 秒内可以执行多少次调用”。
  • AverageTime: 调用的平均时间,例如”每次调用平均耗时 xxx 毫秒”。
  • SampleTime: 随机取样,最后输出取样结果的分布,例如”99%的调用在 xxx 毫秒以内,99.99%的调用在 xxx 毫秒以内”
  • SingleShotTime: 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为 0,用于测试冷启动时的性能。
  • All(“all”, “All benchmark modes”);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@Fork(1)
@Warmup(iterations = 3, time = 5)
//测量次数,每次测量的持续时间
@Measurement(iterations = 3, time = 10)
@BenchmarkMode(Mode.All)
public class SerializerCompareTest {
private static RpcMessage buildMessage() {
RpcResponse<Object> rpcResponse = RpcResponse.builder()
.requestId(UUID.randomUUID().toString())
.message(SUCCESS.getMessage())
.code(SUCCESS.getCode())
.data(new String("我是结果,我是结果,我是结果")).build();

RpcMessage rpcMessage = RpcMessage.builder()
.requestId(1)
.compress(GZIP.getCode())
.messageType(REQUEST_TYPE)
.codec(KRYO.getCode())
.data(rpcResponse).build();
return rpcMessage;

}

@Benchmark
public static void kryoSerializeSizeTest() {
RpcMessage data = buildMessage();
KryoSerializer kryoSerializer = new KryoSerializer();
byte[] serialize = kryoSerializer.serialize(data);
//System.out.println("kryo's size is " + serialize.length);
RpcMessage out = kryoSerializer.deserialize(RpcMessage.class, serialize);
assertEquals(out, data);
}

@Benchmark
public static void hessianSerializeSizeTest() {
RpcMessage data = buildMessage();
HessianSerializer hessianSerializer = new HessianSerializer();
byte[] serialize = hessianSerializer.serialize(data);
//System.out.println("hessian's size is " + serialize.length);
RpcMessage out = hessianSerializer.deserialize(RpcMessage.class, serialize);
assertEquals(out, data);
}

@Benchmark
public static void protostuffSerializeSizeTest() {
RpcMessage data = buildMessage();
ProtostuffSerializer protostuffSerializer = new ProtostuffSerializer();
byte[] serialize = protostuffSerializer.serialize(data);
//System.out.println("protostuff's size is " + serialize.length);
RpcMessage out = protostuffSerializer.deserialize(RpcMessage.class, serialize);
assertEquals(out, data);
}

@Test
public void speedTest() throws RunnerException {
Options options = new OptionsBuilder().build();
new Runner(options).run();

}
}

性能结果

1
2
3
4
Benchmark                                           Mode  Cnt       Score      Error  Units
SerializerCompareTest.hessianSerializeSizeTest thrpt 5 40262.595 ± 2023.531 ops/s
SerializerCompareTest.kryoSerializeSizeTest thrpt 5 18974.527 ± 449.052 ops/s
SerializerCompareTest.protostuffSerializeSizeTest thrpt 5 307698.363 ± 2169.165 ops/s

对比来看,性能上 protostuff>hessian2>kryo, 综合测试的数据来看,似乎 protostuff 作为默认的序列化技术较佳,不过 kryo 的序列化体积确实是最小的。

框架性能测试

这里我们使用线程池估算接口的性能

  • 大量顺序请求下,服务的线程数,JVM 内存布局、系统是否正常。
  • 大量并发请求下,服务的线程数,JVM 内存布局、系统是否正常。
  • JVM 参数配置:-Xmx512m -XX:MaxHeapSize=512m -Xmn256m -XX:MaxNewSize=256m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

@Slf4j
@SpringBootTest(classes = RpcClientSpringBootApplication.class)
public class InvokeCompareTest {
@Autowired
HelloController helloController;

private static ExecutorService executor = new ThreadPoolExecutor(1, 1, 5, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
t.setName("xrpc-test-netty-work");
return t;
}
});

@Test
public void test() throws InterruptedException {
int epoch = 20;
int size = 10000;
float allTime = 0;
// 运行20次
for (int i = 1; i <= epoch; ++i) {
final CountDownLatch latch = new CountDownLatch(size);
final Semaphore semaphore = new Semaphore(300, false);
long startTime = System.currentTimeMillis();
// 每次调用size次
for (int j = 1; j <= size; j++) {
semaphore.acquire();
executor.submit(() -> {
try {
helloController.testSyncBenchMark();
} catch (InterruptedException e) {
} finally {
semaphore.release();
latch.countDown();
}
});
}
log.info("第" + i + "次运行-->提交任务完成");
// 阻塞等待调用size次任务完成
latch.await();
float epochTime = System.currentTimeMillis() - startTime;
allTime += epochTime;
log.info("第" + i + "次运行-->耗时:[{}] ms", epochTime);
if (i == 10) {
Thread.sleep(10000);
} else {
Thread.sleep(100);
}
}
float num = (float) epoch * size;
log.info("平均每次调用-->耗时:[{}] ms", allTime / num);
new CountDownLatch(1).await();
}
}

这里我们使用线程池来完成压力测试

  • 构建一个线程池,线程数模拟并发用户调用
  • size 代表连续调用 size 次
  • i 为重复次数,以平均时间得到合理压测结果
  • 同时使用 CountDownLatch 来阻塞主线程以便计算耗时
  • 使用 Semaphore 来简单限流,限制线程运行数量

大量顺序请求测试

如上述代码,我们将线程池中线程设置为 1,连续调用 1w 次,重复 20 次

  • serializer: protostuff
  • compress: dummy

调用结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0次运行-->耗时:[9805.0] ms
1次运行-->耗时:[6021.0] ms
2次运行-->耗时:[6032.0] ms
3次运行-->耗时:[5646.0] ms
4次运行-->耗时:[5637.0] ms
5次运行-->耗时:[6673.0] ms
6次运行-->耗时:[5930.0] ms
7次运行-->耗时:[6144.0] ms
8次运行-->耗时:[5814.0] ms
9次运行-->耗时:[5272.0] ms
10次运行-->耗时:[5292.0] ms
11次运行-->耗时:[5401.0] ms
12次运行-->耗时:[5330.0] ms
13次运行-->耗时:[5402.0] ms
14次运行-->耗时:[5350.0] ms
15次运行-->耗时:[6219.0] ms
16次运行-->耗时:[5339.0] ms
17次运行-->耗时:[5427.0] ms
18次运行-->耗时:[5689.0] ms
19次运行-->耗时:[6094.0] ms
平均每次调用-->耗时:[0.592585] ms

每轮重复调用过程中,单次调用时间稳定在 0.6ms 左右
测试过程中使用采 JvisualVM 查看 JVM 内存和线程情况,线程数量正常,线程数量稳定,垃圾回收整场

image.png)image.png

大量并发请求测试

如上述代码,我们将线程池中线程设置为 32,连续调用 1w 次,重复 20 次

  • serializer: protostuff
  • compress: dummy

调用结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0次运行-->耗时:[8942.0] ms
1次运行-->耗时:[2022.0] ms
2次运行-->耗时:[2182.0] ms
3次运行-->耗时:[2214.0] ms
4次运行-->耗时:[1870.0] ms
5次运行-->耗时:[2050.0] ms
6次运行-->耗时:[2554.0] ms
7次运行-->耗时:[1949.0] ms
8次运行-->耗时:[1866.0] ms
9次运行-->耗时:[1916.0] ms
10次运行-->耗时:[1767.0] ms
11次运行-->耗时:[2205.0] ms
12次运行-->耗时:[1829.0] ms
13次运行-->耗时:[1887.0] ms
14次运行-->耗时:[2062.0] ms
15次运行-->耗时:[2552.0] ms
16次运行-->耗时:[2005.0] ms
17次运行-->耗时:[2034.0] ms
18次运行-->耗时:[2216.0] ms
19次运行-->耗时:[1930.0] ms
平均每次调用-->耗时:[0.24026] ms

在没有业务逻辑,服务端收到即返回的情况下,吞吐量 (32/0.23 )*1000 =13.9w 左右

如上述代码,我们将线程池中线程设置为 128,连续调用 1w 次,重复 20 次

  • serializer: protostuff
  • compress: dummy

调用结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0次运行-->耗时:[8796.0] ms
1次运行-->耗时:[1614.0] ms
2次运行-->耗时:[1730.0] ms
3次运行-->耗时:[1752.0] ms
4次运行-->耗时:[2035.0] ms
5次运行-->耗时:[3431.0] ms
6次运行-->耗时:[1993.0] ms
7次运行-->耗时:[1936.0] ms
8次运行-->耗时:[2045.0] ms
9次运行-->耗时:[1888.0] ms
10次运行-->耗时:[2067.0] ms
11次运行-->耗时:[2282.0] ms
12次运行-->耗时:[2699.0] ms
13次运行-->耗时:[2703.0] ms
14次运行-->耗时:[2289.0] ms
15次运行-->耗时:[2572.0] ms
16次运行-->耗时:[2519.0] ms
17次运行-->耗时:[2438.0] ms
18次运行-->耗时:[2681.0] ms
19次运行-->耗时:[2515.0] ms
平均每次调用-->耗时:[0.259925] ms

在没有业务逻辑,服务端收到即返回的情况下,吞吐量 (128/0.26 )*1000 =49.2w 左右

如上述代码,我们将线程池中线程设置为 256,连续调用 1w 次,重复 20 次

  • serializer: protostuff
  • compress: dummy

调用结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0次运行-->耗时:[8915.0] ms
1次运行-->耗时:[2074.0] ms
2次运行-->耗时:[2036.0] ms
3次运行-->耗时:[1861.0] ms
4次运行-->耗时:[1930.0] ms
5次运行-->耗时:[1966.0] ms
6次运行-->耗时:[1939.0] ms
7次运行-->耗时:[2337.0] ms
8次运行-->耗时:[3372.0] ms
9次运行-->耗时:[2432.0] ms
10次运行-->耗时:[2291.0] ms
11次运行-->耗时:[2494.0] ms
12次运行-->耗时:[2455.0] ms
13次运行-->耗时:[3207.0] ms
14次运行-->耗时:[2450.0] ms
15次运行-->耗时:[2345.0] ms
16次运行-->耗时:[2119.0] ms
17次运行-->耗时:[2542.0] ms
18次运行-->耗时:[2788.0] ms
19次运行-->耗时:[2539.0] ms
平均每次调用-->耗时:[0.27046] ms

当并发量达到 256 时,响应时间仍没有大幅增加,只有小幅增加,吞吐量仍在进一步提升。
当然,由于客服端和服务端都在本地,通信成本相对较低,有条件可以模拟真实的网络环境进行再测试。
image.png