0%

gnet 与 net 网络库性能对比

gnet 与 net 网络库性能对比

在使用 go 语音进行网络编程时,由于 net.Conn.Read 提供的是阻塞读,通常需要将每一个客户端连接放在一个协程中处理,这种方式虽然比较简单,但是会带来一定的性能损耗。

测试背景

在使用 go 语言仿写 redis 服务器时,我尝试使用了三种不同的基础架构:

  1. 每一个客户端开启协程负责协议解析、命令执行、消息写回,数据库采用读写锁;
  2. 每一个客户端开启协程负责协议解析,等待事务协程完成命令执行后,再由客户端协程写入消息,协程使用 channel 通信;
  3. 使用 gnet 网络库,采用传统单线程 reactor 模式。

经过 redis-benchmark 的性能测试,在双核四线程的 CPU 上,这三者的并发量以及 redis-server 的并发量分别如下:

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
# redis-server 
Summary:
throughput summary: 76982.29 requests per second
latency summary (msec):
avg min p50 p95 p99 max
0.387 0.152 0.351 0.655 0.799 1.511

# 方式 1,单独设置事务协程
Summary:
throughput summary: 46289.86 requests per second
latency summary (msec):
avg min p50 p95 p99 max
0.611 0.048 0.551 0.919 1.999 3.711

# 方式 2,不单独设置事务协程
Summary:
throughput summary: 42680.32 requests per second
latency summary (msec):
avg min p50 p95 p99 max
0.620 0.048 0.599 0.863 1.279 4.871

# 方式 3,使用 gnet 网络库
Summary:
throughput summary: 67977.26 requests per second
latency summary (msec):
avg min p50 p95 p99 max
0.590 0.120 0.543 1.079 1.439 2.335

很显然,使用 gnet 网络库的并发量要远远高于使用原生的 go 语言网络库,并发量基本上可以达到 redis-server 的 90%左右,考虑到 go 语言中的 GC 机制,这是一个非常不错的并发量。

火焰图分析

使用 pprof 工具分别对这三种方式进行分析。

单独设置事务协程

在单独设置事务协程时,火焰图如下。可以看到事务的执行时间只占总时间片的 5%左右,其余的 CPU 时间片主要是用于网络 IO 以及 runtime 调度协程。

pprof-1

不单独设置事务协程

在不单独设置事务协程时,火焰图如下,大部分的 CPU 时间片也是用于网络 IO 以及 runtime 调度协程。不过注意到,不单独设置事务协程时,runtime 进行协程调度的时间片要略高。

pprof-2

使用 gnet

在使用 gnet 网络库后,根据火焰图可以看到,原本占据约 1/3 CPU 时间片的协程调度部分消失了,这是 gnet 性能比 net 网络库要高的根本原因。

pprof-3

三种模式的分析对比

无论是否单独设置事务协程,基于 net 架构的系统都只能有一个协程来进行数据库的读写,事务的执行效率其实是类似的。

在单独设置事务协程的情况下,由于事务协程通常是可以连续执行,不会主动进入阻塞状态,能够在一定程度上减少 goroutine 的调度次数,这一点也能够反应在火焰图中。不过单独设置事务协程的情况下,需要使用 channel 在协程之间进行消息传递,这也会在一定程度上造成性能的损耗。在不单独设置事务协程的情况下,协程可能会因为等待数据库锁而进行阻塞状态,当协程数量比较大时可能竞争会比较严重,比较依赖于锁的性能。

在少核环境下,根据测试,单独设置事务协程可能会更具有优势。

使用 gnet 网络库更像是在处理传统的 C/C++ 网络编程,因为不需要 goroutine 的调度,性能确实会比 net 的方式要高出一截。但是同样的,使用 gnet 意味着无法使用 go 语言的一些特性。并且,本次测试的程序,其事务流程都非常简单,事务的执行时间非常短,程序的性能更加程度上取决于网络架构。当服务器的事务执行较为复杂,或者是需要等待系统中其他模块的响应时,事务的执行时间会变长。由于 gent 更像是一个原生的 reactor 模式,单次的读事件如果长时间阻塞就会严重影响整个网络部分的性能。这种情况下,使用 gnet 带来的性能提升就没有那么高,并且可能需要对网络部分进行单独的模块封装,这样会增加系统的复杂度。