背景说明
最近在做的一个项目,底层是用以前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实现,这个已经测试过,有时间再开专题讨论。
来源:CSDN
作者:萧燃
链接:https://blog.csdn.net/cyy472949732/article/details/103748092