unix domain socket的性能测试

百般思念 提交于 2020-01-18 02:42:02

背景说明

最近在做的一个项目,底层是用以前C写的语音交换能力,由于语音的应用需要对流进行处理,这边为了对接网络或者应用方便,想把流桥接出来,在能力层上面封装一层业务,同时又不想影响以前的流程。

所以一直在考虑进程内通信的问题,组件的话本身以前一直用event socket lib开发也没什么问题,但是之前都是用来做信令控制而已,负载并不高。

这次桥接的语音流初步目标是1000路并发,最少的8K采1000路就是每秒8m,就算单路每秒发25次数据,每次320字节,那每秒网卡那边中断次数也是2w+,这个对并发是一个巨大的挑战。

之前event socket lib开发虽然也是对接1000路,但是总归每秒的请求也就平均一个上下,而且数据量小。

这次我想反正这两个组件暂时不会分开,干脆就进程间通信好了。消息队列共享内存什么的我就不考虑了,因为我写东西比较习惯抽象的接口,考虑到后面流更大了后,微服务模块可能会开发一个强大的硬解模块来解决,也希望兼容网络通信的写法,所以选定了unix domain socket,毕竟接口和写net socket没什么区别。

unix domain socket相对网络通信用于 IPC 更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。这是因为,IPC 机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。

unix domain socket性能

接下来我们就测一下它们的性能对比

先写个unix domain socket的
服务端如下:

package main

import (
 "fmt"
 "net"
 "os"
 "sync/atomic"
 "time"
)

var piece uint64  //记录接受的字节总数量	

func main() {
 var socketCount = 1000	//预设1000路并发

 for i := 0; i < socketCount; i++ {
     unixSocketUrl := fmt.Sprintf("/tmp/swcall/skt%03d", i)
     var unixAddr *net.UnixAddr

     os.Remove(unixSocketUrl)
     unixAddr, err := net.ResolveUnixAddr("unix", unixSocketUrl)
     if err != nil {
        fmt.Println("ResolveUnixAddr err", err)
        return
     }

     unixListener, err := net.ListenUnix("unix", unixAddr)
     if err != nil {
        fmt.Println("ListenUnix err", err)
        return
     }

     go func(listener *net.UnixListener) {
        defer listener.Close()

        for {
                unixConn, err := listener.AcceptUnix()
                if err != nil {
                        continue
                }

                fmt.Println("A client connected : " + unixConn.RemoteAddr().String())
                go unixPipe(unixConn)
        }
     }(unixListener)
 }

 //每3秒打印出接受到的字节数总数
 go func() {
     for {
        fmt.Println("piece ", piece)
        time.Sleep(time.Second * 3)
     }
 }()

 time.Sleep(time.Hour)
}

func unixPipe(conn *net.UnixConn) {
 ipStr := conn.RemoteAddr().String()
 defer func() {
     fmt.Println("disconnected :" + ipStr)
     conn.Close()
 }()

 buff := make([]byte, 1024)

 for {
     readCount, err := conn.Read(buff)
     if err != nil {
        fmt.Println("read error")
        return
     }
     //time.Sleep(time.Millisecond * 150)
     fmt.Println("read count", readCount)
     atomic.AddUint64(&piece, uint64(readCount))
 }
}

客户端代码如下

package main

import (
        "fmt"
        "net"
        "sync"
        "sync/atomic"
        "time"
)

var piece uint64 //发送出去到总数量

func main() {
        var socketCount = 1000 //预设并发数量

        var wg sync.WaitGroup
        for i := 0; i < socketCount; i++ {
                var unixAddr *net.UnixAddr
                unixAddr, _ = net.ResolveUnixAddr("unix", fmt.Sprintf("/tmp/swcall/skt%03d", i))

                conn, err := net.DialUnix("unix", nil, unixAddr)
                if err != nil {
                        fmt.Println("dial unix socket err", err)
                        return
                }
                fmt.Println("connected!")

                wg.Add(1)
                go onMessageRecived(conn, &wg)
        }
        wg.Wait()
        fmt.Println("piece ", piece)
        fmt.Println("do exit")
}

var runenable = true

func onMessageRecived(conn *net.UnixConn, wg *sync.WaitGroup) {
      defer wg.Done()
       //40秒后
       time.AfterFunc(time.Second*40, func() {
               runenable = false
       })

       for {
               if !runenable {
                       conn.Close()
                       return
               }
               var sendbuff = make([]byte, 320)
               _, err := conn.Write(sendbuff)
               if err != nil {
                       fmt.Println("Write error")
                       conn.Close()
                       return
               }

               //模拟流 每40ms发送320字节出去
               atomic.AddUint64(&piece, 320)
               time.Sleep(time.Millisecond * 40)
       }
}

先启动服务端,注意/tmp/swcall文件夹要先手动建一下
我们来观察cpu和内存的浮动范围
运行过程中可以看到server端快速刷收到的单次数据个数
在这里插入图片描述
最终收到的总数如图
在这里插入图片描述
我们看看client端发送的总数如下
在这里插入图片描述
数量完全对的上,还是靠谱的,一共315173440字节,折合300M数据,40秒内

过程中的性能如图
在这里插入图片描述
在这里插入图片描述
cpu空闲占用在5%左右,开启后升至35%,峰值则如下面htop的图片,内存占用忽略不计,两个进程一起不超25M,由于不走网络,所以网络无波动。

server端占用的cpu在client端的两倍左右

该设备cpu是intel 6400四核3Ghz的,这样看1000路带上去也是完全没压力。

bufio性能说明

因为conn可以缓存化成IO操作,以前研究bufio测试过对于小于4K的大小频繁读写应该有提升,所以这边顺便测试一下

代码修改如下

server端

func unixPipe(conn *net.UnixConn) {
        ipStr := conn.RemoteAddr().String()
        defer func() {
                fmt.Println("disconnected :" + ipStr)
                conn.Close()
        }()
	
	
		//bufio默认的缓存大小是4096
        reader := bufio.NewReader(conn)
        buff := make([]byte, 320)

        for {
                //canread := reader.Buffered()
                //fmt.Println(canread)
                
				//这里读取并不是瞬间返回,继承的是conn的读取,超时等设置也是
                readCount, err := reader.Read(buff)
                if err != nil {
                        fmt.Println("read error")
                        return
                }

                fmt.Println("read count", readCount)
                atomic.AddUint64(&piece, uint64(readCount))
        }
}

客户端代码修改:

func onMessageRecived(conn *net.UnixConn, wg *sync.WaitGroup) {
        defer wg.Done()
        defer conn.Close()

        time.AfterFunc(time.Second*40, func() {
                runenable = false
        })
        writer := bufio.NewWriter(conn)
        for {
                if !runenable {
                        return
                }
                var sendbuff = make([]byte, 320)
                sendbuff[0] = 0x7e
                sendbuff[319] = 0xdd
                writer.Write(sendbuff)
                time.Sleep(time.Millisecond * 20)
                atomic.AddUint64(&piece, 320)
        }
}

但是结果和我预期的差距却很大,可以看到cpu的波动很大,而且峰值也是比conn纯读写高出许多。

在这里插入图片描述

这是因为unix domain socket连接建立完成之后在内存开辟一块空间,而server与client在这块内存空间中进行数据传输,本身走的就是内存,并不需要bufio刻意参与来多余的负优化。
用ls指令看下就知道了,可以看到,其实这些都只是文件描述符,文件并不占空间。
在这里插入图片描述

tcp socket性能

因为是测性能,强制开启1000个端口本机传输,看下性能差距能有多少
首先接收数据和发送数据数量一致,可靠性没问题,另外总数据量也是差不多,说明性能不到瓶颈。

在这里插入图片描述

接下来就是性能差距,以下两张图显示,性能差距有,但是较小
大概就是35%对比40%的5%的性能差距,就是用在拆包解包重组校验上了。
在这里插入图片描述

在这里插入图片描述

总结

unix domain socket性能对比tcp略有优势,但是不明显,两种方式如果是快速交换数据处理,对内存占用都不会有压力,测试过程中,内存占用就没超过25M

UDS传输数据是顺序性的可靠的,即使你用Datagram方式传输,还有一个优点在于,进程间通信不需要占用端口,按格式化路径即可满足需求,这点比强开大量端口占用可能导致的端口冲突要安全,同时继承的抽象接口是一样的,后期如果改造成tcp的,也很方便,所以在进程内通信,还是首选unix domain socket,不在于性能,而在于方便和可靠。

另外,本机的http通信也可以基于unix domain socket,而grpc是基于http2.0的,grpc也可以在本机内基于UDS实现,这个已经测试过,有时间再开专题讨论。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!