本文转载自微信公众号《代码农桃花源》,作者风云就是她。转载本文请联系码农桃花源公众号。前段时间(已经2年前了??)优化了golangudp客户端和服务端的性能。这里我简单描述一下udp服务的优化过程。当然udp的性能已经很高了。即使不优化,也能轻松达到几十万的qps,但是我们想更好的优化goudpserver和client。UDP有粘半包的问题吗?我们知道应用之间的网络传输会存在粘半包的问题。这里就不描述问题的根源了,大家自己搜索吧。使用tcp会有这个问题,而udp则没有这个问题。为什么?tcp是无界的,tcp是基于流传输的,tcp头没有长度可变,udp是有界的,基于消息的,可以解决粘包的问题。udp协议中有16位来描述数据包的大小。16位决定的最大数是65536,除去udp头和ip头的大小,最大的包差不多有65507字节。但是根据我们的测试,udp并没有完美解决应用层半包卡顿的问题。如果你的goudpserver的readbuffer是1024,那么client发送的数据不能超过serverreadbuf定义的1024byte,否则你还是要处理半个包。如果发送的数据小于1024字节,就不会出现粘包问题。//xiaoui.c??cbuf:=make([]byte,1024)for{n,_:=ServerConn.Read(buf[0:])ifstring(buf[0:n])!=s{panic(...)...Linux下用strace查找syscallreadfd时,最多只能得到1024字节。这个1024就是上面配置的读缓冲区大小。//xiaoui.c??c[pid25939]futex(0x56db90,FUTEX_WAKE,1)=1[pid25939]read(3,"Introduction...hidden...overviewofIPython'",1024)=1024[pid25939]epoll_ctl(4,EPOLL_CTL_DEL,3,{0,{u32=0,u64=0}})=0[pid25939]close(3[pid25940]<...restart_syscallresumed>)=0[pid25939]<...closeresumed>)=0[pid25940]clock_gettime(CLOCK_MONOTONIC,{19280781,509925143})=0[pid25939]pselect6(0,NULL,NULL,NULL,{0,1000000},0[pid25940]pselect6(0,NULL,NULL,NULL,{0,20000},0)=0(Timeout)[pid25940]clock_gettime(CLOCK_MONOTONIC,{19280781,510266460})=0[pid25940]futex(0x56db90,FUTEX_WAIT,0,{60,0}下面是socketfd的源码在golang中读取,可以看到你传入了一个多大的字节数组,它会syscall读取多少数据。//xiaorui.ccfuncread(fdint,p[]byte)(nint,errerror){var_p0unsafe.Pointeriflen(p)>0{_p0=unsafe.Pointer(&p[0])}else{_p0=unsafe.Pointer(&_zero)}r0,_,e1:=Syscall(SYS_READ,uintptr(fd),uintptr(_p0),uintptr(len(p)))n=int(r0)ife1!=0{err=errnoErr(e1)}return}http2比http1快,因为http2实现s标头hpack编码协议。为什么thrift比grpc快?thrift和protobuf单独对比协议结构,性能差不多,但是和网络应用层协议相比,thrift更快。因为grpc运行在http2上,grpc服务器不仅要解析http2的header,还要解析http2的body,也就是protobuf的数据。因此,高效的应用层协议也是高性能服务的重要标准。前面我们使用了自定义的TLV编码,t是类型,l是长度,v是数据。一般来说,这是解决网络协议上数据完整性的思路。当然,我也是。如何优化udp应用协议的开销?上面说了udp在大小合理的情况下不需要依赖应用层协议来解析数据包问题。那么我们只需要在客户端控制发送包的大小,服务器端控制接收的大小即可,这样可以节省应用层协议带来的性能效率。??不要小看应用层协议的cpu消耗!解决golangudp的锁竞争问题在udp压力测试中,发现client和server都跑不上cpu。一开始以为是golangudpserver的问题。去掉所有相关的业务逻辑后,我只是做原子计数,但还是不够CPU。通过gotoolpprof的函数调用图和火焰图,看不出问题。尝试使用iperf进行udp压测,golangudpserver压力直接干到满载。可以说是压力源不足。那么udp性能提升的问题就显得很明显了,应该是golangudpclient的问题。我尝试在goudpclient中加入多协程编写,10个goroutines,100个goroutines,500个goroutines,但是都没有明显的提升效果,性能抖动很明显。??进一步排查问题,通过lsof分析client进程的描述符列表,client和udpserver只有一个连接。即500个协程共享一个连接。然后用strace做syscall系统调用统计,发现futex和pselect6系统调用过多,说明锁竞争过多。查看golangnet的源码,发现golang在写socketfd时,存在写锁竞争。TODO图片//xiaorui.cc//Writeimplementsio.Writer.func(fd*FD)Write(p[]byte)(int,error){iferr:=fd.writeLock();err!=nil{return0,err}deferfd.writeUnlock()iferr:=fd.pd.prepareWrite(fd.isFile);err!=nil{return0,err}}如何优化锁竞争?实例化多个udp连接到一个数组池,在客户端代码中随机使用udp连接。这减少了锁竞争。以上就是udp性能调优过程的总结。简单来说就是两点:一是消除应用层协议带来的性能消耗,二是golangsocket写锁带来的竞争。当我们遇到一些性能问题时,往往会使用perf和strace函数,然后配合golangpprof分析火焰图来分析问题。不行的话直接看golang源码。
