gnet 与 net 网络库性能对比
在使用 go 语音进行网络编程时,由于 net.Conn.Read 提供的是阻塞读,通常需要将每一个客户端连接放在一个协程中处理,这种方式虽然比较简单,但是会带来一定的性能损耗。
测试背景
在使用 go 语言仿写 redis 服务器时,我尝试使用了三种不同的基础架构:
- 每一个客户端开启协程负责协议解析、命令执行、消息写回,数据库采用读写锁;
- 每一个客户端开启协程负责协议解析,等待事务协程完成命令执行后,再由客户端协程写入消息,协程使用 channel 通信;
- 使用 gnet 网络库,采用传统单线程 reactor 模式。
经过 redis-benchmark 的性能测试,在双核四线程的 CPU 上,这三者的并发量以及 redis-server 的并发量分别如下:
1 | redis-server |
很显然,使用 gnet 网络库的并发量要远远高于使用原生的 go 语言网络库,并发量基本上可以达到 redis-server 的 90%左右,考虑到 go 语言中的 GC 机制,这是一个非常不错的并发量。
火焰图分析
使用 pprof 工具分别对这三种方式进行分析。
单独设置事务协程
在单独设置事务协程时,火焰图如下。可以看到事务的执行时间只占总时间片的 5%左右,其余的 CPU 时间片主要是用于网络 IO 以及 runtime 调度协程。
不单独设置事务协程
在不单独设置事务协程时,火焰图如下,大部分的 CPU 时间片也是用于网络 IO 以及 runtime 调度协程。不过注意到,不单独设置事务协程时,runtime 进行协程调度的时间片要略高。
使用 gnet
在使用 gnet 网络库后,根据火焰图可以看到,原本占据约 1/3 CPU 时间片的协程调度部分消失了,这是 gnet 性能比 net 网络库要高的根本原因。
三种模式的分析对比
无论是否单独设置事务协程,基于 net 架构的系统都只能有一个协程来进行数据库的读写,事务的执行效率其实是类似的。
在单独设置事务协程的情况下,由于事务协程通常是可以连续执行,不会主动进入阻塞状态,能够在一定程度上减少 goroutine 的调度次数,这一点也能够反应在火焰图中。不过单独设置事务协程的情况下,需要使用 channel 在协程之间进行消息传递,这也会在一定程度上造成性能的损耗。在不单独设置事务协程的情况下,协程可能会因为等待数据库锁而进行阻塞状态,当协程数量比较大时可能竞争会比较严重,比较依赖于锁的性能。
在少核环境下,根据测试,单独设置事务协程可能会更具有优势。
使用 gnet 网络库更像是在处理传统的 C/C++ 网络编程,因为不需要 goroutine 的调度,性能确实会比 net 的方式要高出一截。但是同样的,使用 gnet 意味着无法使用 go 语言的一些特性。并且,本次测试的程序,其事务流程都非常简单,事务的执行时间非常短,程序的性能更加程度上取决于网络架构。当服务器的事务执行较为复杂,或者是需要等待系统中其他模块的响应时,事务的执行时间会变长。由于 gent 更像是一个原生的 reactor 模式,单次的读事件如果长时间阻塞就会严重影响整个网络部分的性能。这种情况下,使用 gnet 带来的性能提升就没有那么高,并且可能需要对网络部分进行单独的模块封装,这样会增加系统的复杂度。