您的位置:首页 > 理论基础 > 计算机网络

GO的TCP性能测试,优化结果

2014-11-22 13:18 316 查看
之前做过一次测试,没有任何优化的情况下C++(16Gbps)是GO(4Gbps)的4倍性能,参考http://blog.csdn.net/win_lin/article/details/40744175

这次针对TCP部分对go做了优化,测试结果令人满意。GO单进程(7Gbps)不输c++(8Gbps),是c++使用writev(16Gbps)的一半,GO多进程(59Gbps)完胜c++是c++的好几倍。

测试代码参考:https://github.com/winlinvip/go-srs/tree/master/research/tcp

备注:之前的测试是在虚拟机上,这次在物理机上,结果可能会略有不同。

这次使用的GO的版本是1.3.3。

Why TCP

TCP是网络通讯的基础,而web则是基于HTTP框架,HTTP又基于TCP。RTMP也是基于TCP。

TCP的吞吐率能达标,那么就奠定了这个语言能开发高性能服务器的基础。

之前srs1.0时,调研过go,写了一个go版本的srs,但是性能和red5差不多就删除了。

现在srs2.0单进程单线程网络吞吐能达到4Gbps(6千客户端,码率522Kbps),GO如果不能达到这个目标,那么SRS就不可能用go重写。

Platform

此次测试在24CPU的服务器上,CPU不是瓶颈。

测试使用lo网卡,直接内存拷贝,网络也不是瓶颈。

CPU和网络都资源充足时,服务器本身的执行速度就是关键了。

OS选用Centos6 64bits。

客户端选用c++做客户端,使用同一个客户端测试。

Write

下面是C++服务器,单进程单线程作为服务器:

g++ tcp.server.cpp -g -O0 -o tcp.server && ./tcp.server 1990 4096
g++ tcp.client.cpp -g -O0 -o tcp.client && ./tcp.client 127.0.0.1 1990 4096

----total-cpu-usage---- -dsk/total- ---net/lo-- ---paging-- ---system--
usr sys idl wai hiq siq| read  writ| recv  send|  in   out | int   csw
0   6  92   0   0   1|   0    11k|1073M 1073M|   0     0 |2790    81k
0   6  93   0   0   1|   0  7782B|1049M 1049M|   0     0 |2536    76k

PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
32573 winlin    20   0 11744  892  756 R 99.9  0.0  17:56.88 ./tcp.server 1990 4096
2880 winlin    20   0 11740  900  764 S 85.3  0.0   0:32.53 ./tcp.client 127.0.0.1 1990 4096


单进程的c++效率还是非常高的1049MBps,对比目前SRS1的168MBps,SRS2跑到的391MBps,其实SRS没有达到性能极限。

下面是go作为服务器,no delay设置为1,即go默认的tcp选项,这个选项会造成tcp性能降低:

go build ./tcp.server.go && ./tcp.server 1 1 1990 4096
g++ tcp.client.cpp -g -O0 -o tcp.client && ./tcp.client 127.0.0.1 1990 4096

----total-cpu-usage---- -dsk/total- ---net/lo-- ---paging-- ---system--
usr sys idl wai hiq siq| read  writ| recv  send|  in   out | int   csw
0   5  93   0   0   2|   0  7509B| 587M  587M|   0     0 |2544   141k
0   5  93   0   0   2|   0    10k| 524M  524M|   0     0 |2629   123k

PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
5496 winlin    20   0 98248 1968 1360 S 100.5  0.0   4:40.54 ./tcp.server 1 1 1990 4096
5517 winlin    20   0 11740  896  764 S 72.3  0.0   3:24.22 ./tcp.client 127.0.0.1 1990 4096


可见在开启tcp no delay的go只有c++的性能的一半。

下面是go作为服务器,关闭了tcp no delay选项,单进程作为服务器:

go build ./tcp.server.go && ./tcp.server 1 0 1990 4096
g++ tcp.client.cpp -g -O0 -o tcp.client && ./tcp.client 127.0.0.1 1990 4096

----total-cpu-usage---- -dsk/total- ---net/lo-- ---paging-- ---system--
usr sys idl wai hiq siq| read  writ| recv  send|  in   out | int   csw
0   5  93   0   0   1|   0    10k| 868M  868M|   0     0 |2674    79k
1   5  93   0   0   1|   0    16k| 957M  957M|   0     0 |2660    85k

PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
3004 winlin    20   0 98248 1968 1360 R 100.2  0.0   2:27.32 ./tcp.server 1 0 1990 4096
3030 winlin    20   0 11740  900  764 R 81.0  0.0   1:59.42 ./tcp.client 127.0.0.1 1990 4096


其实在关闭了tcp no delay之后,go的性能和c++相差不大了。

Multiple CPU

go最厉害的是考虑多cpu并行计算。其实c/c++也可以用fork多进程,对于业务代码有很大影响;go在不影响业务代码时直接扩展支持多cpu。

go开启10个cpu,8个客户端,开启no delay(默认)时的性能:

go build ./tcp.server.go && ./tcp.server 10 1 1990 4096
g++ tcp.client.cpp -g -O0 -o tcp.client && for((i=0;i<8;i++)); do (./tcp.client 127.0.0.1 1990 4096 &); done

----total-cpu-usage---- -dsk/total- ---net/lo-- ---paging-- ---system--
usr sys idl wai hiq siq| read  writ| recv  send|  in   out | int   csw
4  37  47   0   0  12|   0   105k|3972M 3972M|   0     0 |  14k  995k
4  37  46   0   0  13|   0  8055B|3761M 3761M|   0     0 |  14k  949k

PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
6353 winlin    20   0  517m 6896 1372 R 789.6  0.0  13:24.49 ./tcp.server 10 1 1990 4096
6384 winlin    20   0 11740  900  764 S 68.4  0.0   1:11.57 ./tcp.client 127.0.0.1 1990 4096
6386 winlin    20   0 11740  896  764 R 67.4  0.0   1:09.53 ./tcp.client 127.0.0.1 1990 4096
6390 winlin    20   0 11740  900  764 R 66.7  0.0   1:11.24 ./tcp.client 127.0.0.1 1990 4096
6382 winlin    20   0 11740  896  764 R 64.8  0.0   1:11.30 ./tcp.client 127.0.0.1 1990 4096
6388 winlin    20   0 11740  896  764 R 64.4  0.0   1:11.80 ./tcp.client 127.0.0.1 1990 4096
6380 winlin    20   0 11740  896  764 S 63.4  0.0   1:08.78 ./tcp.client 127.0.0.1 1990 4096
6396 winlin    20   0 11740  896  764 R 62.8  0.0   1:09.47 ./tcp.client 127.0.0.1 1990 4096
6393 winlin    20   0 11740  900  764 R 61.4  0.0   1:11.90 ./tcp.client 127.0.0.1 1990 4096


也比较厉害了,能跑到30Gbps。

开启10个cpu,8个客户端,关闭no delay时看看go的性能:

go build ./tcp.server.go && ./tcp.server 10 0 1990 4096
g++ tcp.client.cpp -g -O0 -o tcp.client && for((i=0;i<8;i++)); do (./tcp.client 127.0.0.1 1990 4096 &); done

----total-cpu-usage---- -dsk/total- ---net/lo-- ---paging-- ---system--
usr sys idl wai hiq siq| read  writ| recv  send|  in   out | int   csw
5  42  41   0   0  12|   0  8602B|7132M 7132M|   0     0 |  15k  602k
5  41  41   0   0  12|   0    13k|7426M 7426M|   0     0 |  15k  651k

PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
4148 winlin    20   0  528m 9.8m 1376 R 795.5  0.1  81:48.12 ./tcp.server 10 0 1990 4096
4167 winlin    20   0 11740  896  764 S 89.8  0.0   8:16.52 ./tcp.client 127.0.0.1 1990 4096
4161 winlin    20   0 11740  900  764 R 87.8  0.0   8:14.63 ./tcp.client 127.0.0.1 1990 4096
4174 winlin    20   0 11740  896  764 S 83.2  0.0   8:09.40 ./tcp.client 127.0.0.1 1990 4096
4163 winlin    20   0 11740  896  764 R 82.6  0.0   8:07.80 ./tcp.client 127.0.0.1 1990 4096
4171 winlin    20   0 11740  900  764 R 82.2  0.0   8:08.75 ./tcp.client 127.0.0.1 1990 4096
4169 winlin    20   0 11740  900  764 S 81.9  0.0   8:15.37 ./tcp.client 127.0.0.1 1990 4096
4165 winlin    20   0 11740  900  764 R 78.9  0.0   8:09.98 ./tcp.client 127.0.0.1 1990 4096
4177 winlin    20   0 11740  900  764 R 74.0  0.0   8:07.63 ./tcp.client 127.0.0.1 1990 4096


这个更厉害了,能跑到59Gbps,厉害!

Writev

GO中是没有writev的,可以用c/c++的writev和go的多进程比一比。

考虑SRS2目前使用writev提升了一倍性能,若srs使用go则可以使用多进程,这两个对比还是有意义的。

同时,客户端也使用readv来一次读取多次,弥补客户端单进程的瓶颈。

g++ tcp.server.writev.cpp -g -O0 -o tcp.server && ./tcp.server 64 1990 4096
g++ tcp.client.readv.cpp -g -O0 -o tcp.client && ./tcp.client 127.0.0.1 1990 64 4096

----total-cpu-usage---- -dsk/total- ---net/lo-- ---paging-- ---system--
usr sys idl wai hiq siq| read  writ| recv  send|  in   out | int   csw
0   6  93   0   0   1|   0    15k|1742M 1742M|   0     0 |2578    30k
0   6  93   0   0   1|   0    13k|1779M 1779M|   0     0 |2412    30k

PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
9468 winlin    20   0 12008 1192  800 R 99.8  0.0   1:17.63 ./tcp.server 64 1990 4096
9487 winlin    20   0 12008 1192  800 R 80.3  0.0   1:02.49 ./tcp.client 127.0.0.1 1990 64 4096


使用writev确实能提升一倍的性能,减少了系统调用的时间。

对比使用readv客户端的go,禁用tcp no delay的情况:

go build ./tcp.server.go && ./tcp.server 1 0 1990 4096
g++ tcp.client.readv.cpp -g -O0 -o tcp.client && ./tcp.client 127.0.0.1 1990 64 4096

----total-cpu-usage---- -dsk/total- ---net/lo-- ---paging-- ---system--
usr sys idl wai hiq siq| read  writ| recv  send|  in   out | int   csw
0   5  93   0   0   1|   0  5734B| 891M  891M|   0     0 |2601   101k
0   5  93   0   0   2|   0  9830B| 897M  897M|   0     0 |2518   103k

PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
9690 winlin    20   0 98248 3984 1360 R 100.2  0.0   2:46.84 ./tcp.server 1 0 1990 4096
9698 winlin    20   0 12008 1192  800 R 79.3  0.0   2:13.23 ./tcp.client 127.0.0.1 1990 64 4096


这时候go单进程没有提升,因为瓶颈不在客户端,所以客户端使用readv之后也没有改变。

对比go用10个cpu,客户端使用readv,禁用tcp no delay:

go build ./tcp.server.go && ./tcp.server 10 0 1990 4096
g++ tcp.client.readv.cpp -g -O0 -o tcp.client && for((i=0;i<8;i++)); do (./tcp.client 127.0.0.1 1990 64 4096 &); done

----total-cpu-usage---- -dsk/total- ---net/lo-- ---paging-- ---system--
usr sys idl wai hiq siq| read  writ| recv  send|  in   out | int   csw
5  41  42   0   0  12|   0  7236B|6872M 6872M|   0     0 |  15k  780k
4  42  41   0   0  12|   0  9557B|6677M 6677M|   0     0 |  15k  723k

PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
10169 winlin    20   0  655m 7072 1388 R 799.9  0.0  51:39.13 ./tcp.server 10 0 1990 4096
10253 winlin    20   0 12008 1192  800 R 84.5  0.0   5:05.05 ./tcp.client 127.0.0.1 1990 64 4096
10261 winlin    20   0 12008 1192  800 S 80.6  0.0   5:04.77 ./tcp.client 127.0.0.1 1990 64 4096
10255 winlin    20   0 12008 1192  800 R 79.9  0.0   5:05.32 ./tcp.client 127.0.0.1 1990 64 4096
10271 winlin    20   0 12008 1192  800 S 79.3  0.0   5:05.15 ./tcp.client 127.0.0.1 1990 64 4096
10258 winlin    20   0 12008 1192  800 S 78.3  0.0   5:05.45 ./tcp.client 127.0.0.1 1990 64 4096
10268 winlin    20   0 12008 1192  800 R 77.6  0.0   5:06.54 ./tcp.client 127.0.0.1 1990 64 4096
10251 winlin    20   0 12008 1188  800 R 76.6  0.0   5:03.68 ./tcp.client 127.0.0.1 1990 64 4096
10265 winlin    20   0 12008 1192  800 R 74.6  0.0   5:03.35 ./tcp.client 127.0.0.1 1990 64 4096

测试结果和之前差不多。

GO Write Analysis

调试go的TcpConn.Write方法,调用堆栈是:

at /home/winlin/go/src/github.com/winlinvip/srs.go/research/tcp/tcp.server.go:203
203	        n, err := conn.Write(b)


调用的代码是:

func handleConnection(conn *net.TCPConn, no_delay int, packet_bytes int) {
for {
n, err := conn.Write(b)
if err != nil {
fmt.Println("write data error, n is", n, "and err is", err)
break
}
}


s调试进去,调用的是:

net.(*conn).Write (c=0xc20805bf18, b=..., ~r1=0, ~r2=...) at /usr/local/go/src/pkg/net/net.go:130
130		return c.fd.Write(b)


这部分的代码是:

func (c *conn) Write(b []byte) (int, error) {
if !c.ok() {
return 0, syscall.EINVAL
}
return c.fd.Write(b)
}


接下来是:

Breakpoint 2, net.(*netFD).Write (fd=0x0, p=..., nn=0, err=...) at /usr/local/go/src/pkg/net/fd_unix.go:327
327			n, err = syscall.Write(int(fd.sysfd), p[nn:])


这部分代码是:

func (fd *netFD) Write(p []byte) (nn int, err error) {
if err := fd.writeLock(); err != nil {
return 0, err
}
defer fd.writeUnlock()
if err := fd.pd.PrepareWrite(); err != nil {
return 0, &OpError{"write", fd.net, fd.raddr, err}
}
for {
var n int
n, err = syscall.Write(int(fd.sysfd), p[nn:])
if n > 0 {
nn += n
}
if nn == len(p) {
break
}
if err == syscall.EAGAIN {
if err = fd.pd.WaitWrite(); err == nil {
continue
}
}
if err != nil {
n = 0
break
}
if n == 0 {
err = io.ErrUnexpectedEOF
break
}
}
if err != nil {
err = &OpError{"write", fd.net, fd.raddr, err}
}
return nn, err
}


系统调用层是:

syscall.Write (fd=0, p=..., n=0, err=...) at /usr/local/go/src/pkg/syscall/syscall_unix.go:152
152		n, err = write(fd, p)


代码是:

func Write(fd int, p []byte) (n int, err error) {
if raceenabled {
raceReleaseMerge(unsafe.Pointer(&ioSync))
}
n, err = write(fd, p)
if raceenabled && n > 0 {
raceReadRange(unsafe.Pointer(&p[0]), n)
}
return
}


最后是:

#0  syscall.write (fd=12, p=..., n=4354699, err=...) at /usr/local/go/src/pkg/syscall/zsyscall_linux_amd64.go:1228
1228		r0, _, e1 := Syscall(SYS_WRITE, uintptr(fd), uintptr(_p0), uintptr(len(p)))


代码是:

func write(fd int, p []byte) (n int, err error) {
var _p0 unsafe.Pointer
if len(p) > 0 {
_p0 = unsafe.Pointer(&p[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
r0, _, e1 := Syscall(SYS_WRITE, uintptr(fd), uintptr(_p0), uintptr(len(p)))
n = int(r0)
if e1 != 0 {
err = e1
}
return
}


调用的是SYS_WRITE,即write。查找发现是有writev的:

[winlin@dev6 src]$ find . -name "*.go"|xargs grep -in "SYS_WRITEV"
./pkg/syscall/zsysnum_linux_amd64.go:27:	SYS_WRITEV                 = 20

可惜没有发现使用这个的代码,所以go肯定是没有支持writev的了。

最后所有的Syscall,是用asm汇编写的:

syscall.Syscall () at /usr/local/go/src/pkg/syscall/asm_linux_amd64.s:20
20		CALL	runtime·entersyscall(SB)


代码是:

TEXT	·Syscall(SB),NOSPLIT,$0-56
CALL	runtime·entersyscall(SB)
MOVQ	16(SP), DI
MOVQ	24(SP), SI
MOVQ	32(SP), DX
MOVQ	$0, R10
MOVQ	$0, R8
MOVQ	$0, R9
MOVQ	8(SP), AX	// syscall entry
SYSCALL
CMPQ	AX, $0xfffffffffffff001
JLS	ok
MOVQ	$-1, 40(SP)	// r1
MOVQ	$0, 48(SP)	// r2
NEGQ	AX
MOVQ	AX, 56(SP)  // errno
CALL	runtime·exitsyscall(SB)
RET
ok:
MOVQ	AX, 40(SP)	// r1
MOVQ	DX, 48(SP)	// r2
MOVQ	$0, 56(SP)	// errno
CALL	runtime·exitsyscall(SB)
RET

这个过程就结束了。

GO PPROF

使用性能测试工具测试性能,确保性能瓶颈不在应用层。

服务器使用加了两个参数,打开pprof和退出的条件,当连接的客户端超过一定数目就退出,这样就可以收集性能数据了。

go build ./tcp.server.go && ./tcp.server 1 0 1990 4096 1 1
g++ tcp.client.cpp -g -O0 -o tcp.client && ./tcp.client 127.0.0.1 1990 4096

----total-cpu-usage---- -dsk/total- ---net/lo-- ---paging-- ---system--
usr sys idl wai hiq siq| read  writ| recv  send|  in   out | int   csw
0   6  92   0   0   1|   0  8329B| 974M  974M|   0     0 |2734    72k
0   6  92   0   0   1|   0  7782B| 930M  930M|   0     0 |2737    69k

PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
17389 winlin    20   0  247m 2700 1540 S 100.1  0.0   3:23.69 ./tcp.server 1 0 1990 4096 1
17422 winlin    20   0 11740  896  764 R 85.2  0.0   2:52.34 ./tcp.client 127.0.0.1 1990 4096

[winlin@dell-demo tcp]$    go tool pprof tcp.server tcp.prof
Welcome to pprof!  For help, type 'help'.
(pprof) top
Total: 21446 samples
19715  91.9%  91.9%    20124  93.8% syscall.Syscall
165   0.8%  92.7%    21246  99.1% net.(*netFD).Write
123   0.6%  93.3%      203   0.9% runtime.deferreturn
106   0.5%  93.8%    21435  99.9% main.handleConnection
106   0.5%  94.3%      106   0.5% sync/atomic.CompareAndSwapUint64
103   0.5%  94.7%      162   0.8% net.(*fdMutex).RWLock
100   0.5%  95.2%      211   1.0% runtime.exitsyscall
95   0.4%  95.6%      178   0.8% net.(*fdMutex).RWUnlock
83   0.4%  96.0%    21329  99.5% net.(*conn).Write
82   0.4%  96.4%      155   0.7% runtime.reentersyscall


以及开启多cpu的版本:

go build ./tcp.server.go && ./tcp.server 10 0 1990 4096 1 8
g++ tcp.client.readv.cpp -g -O0 -o tcp.client && for((i=0;i<8;i++)); do (./tcp.client 127.0.0.1 1990 64 4096 &); done

----total-cpu-usage---- -dsk/total- ---net/lo-- ---paging-- ---system--
usr sys idl wai hiq siq| read  writ| recv  send|  in   out | int   csw
5  42  41   0   0  12|   0   117k|6867M 6867M|   0     0 |  15k  738k
5  41  42   0   0  12|   0  5598B|6627M 6627M|   0     0 |  15k  750k

[winlin@dell-demo tcp]$    go tool pprof tcp.server tcp.prof
Welcome to pprof!  For help, type 'help'.
(pprof) top
Total: 72052 samples
63097  87.6%  87.6%    65102  90.4% syscall.Syscall
816   1.1%  88.7%     1244   1.7% runtime.deferreturn
801   1.1%  89.8%      801   1.1% sync/atomic.CompareAndSwapUint64
774   1.1%  90.9%    71371  99.1% net.(*netFD).Write
584   0.8%  91.7%      988   1.4% runtime.reentersyscall
412   0.6%  92.3%      790   1.1% net.(*pollDesc).Prepare
403   0.6%  92.8%     1256   1.7% net.(*fdMutex).RWUnlock
389   0.5%  93.4%      816   1.1% runtime.exitsyscall
371   0.5%  93.9%    71742  99.6% net.(*conn).Write
361   0.5%  94.4%    65701  91.2% syscall.Write


可见性能没有问题,都在系统调用那里。

总结

GO单进程关闭tcp no delay和c/c++的write性能差不多,但是只有c/c++的writev的一半。

GO多进程能线性提升性能,在不改变业务代码的前提下,性能是c/c++单进程的数倍。

Winlin 2014.11.22
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐