首页 > 其他分享 >优化 Go 语言数据打包:性能基准测试与分析

优化 Go 语言数据打包:性能基准测试与分析

时间:2024-09-23 17:12:04浏览次数:10  
标签:allocs 20 字节 基准 encode Go ns 打包 op

优化 Go 语言数据打包:性能基准测试与分析

场景:在局域网内,需要将多个机器网卡上抓到的数据包同步到一个机器上。
原有方案:tcpdump -w 写入文件,然后定时调用 rsync 进行同步。
改造方案:使用 Go 重写这个抓包逻辑及同步逻辑,直接将抓到的包通过网络发送至服务端,由服务端写入,这样就减少了一次落盘的操作。

构造一个 pcap 文件很简单,需要写入一个 pcap文件头,后面每一条数据增加一个元数据进行描述。
使用 pcapgo 即可实现这个功能,p.buffer[:ci.CaptureLength] 为抓包的数据。

ci := gopacket.CaptureInfo{
	CaptureLength: int(n),
	Length:        int(n),
	Timestamp:     time.Now(),
}
if ci.CaptureLength > len(p.buffer) {
	ci.CaptureLength = len(p.buffer)
}
w.WritePacket(ci, p.buffer[:ci.CaptureLength])

为了通过区分是哪个机器过来的数据包需要增加一个 Id,算上元数据和原始数据包,表达结构如下

// from github.com/google/gopacket
type CaptureInfo struct {
	// Timestamp is the time the packet was captured, if that is known.
	Timestamp time.Time `json:"ts" msgpack:"ts"`
	// CaptureLength is the total number of bytes read off of the wire.
	CaptureLength int `json:"cap_len" msgpack:"cap_len"`
	// Length is the size of the original packet.  Should always be >=
	// CaptureLength.
	Length int `json:"len" msgpack:"len"`
	// InterfaceIndex
	InterfaceIndex int `json:"iface_idx" msgpack:"iface_idx"`
}

type CapturePacket struct {
	CaptureInfo
	Id   uint32 `json:"id" msgpack:"id"`
	Data []byte `json:"data" msgpack:"data"`
}

有一个细节待敲定,抓到的包使用什么结构发送至服务端?json/msgpack/自定义格式?

json/msgpack 都有对应的规范,通用性强,不容易出 BUG,性能会差一点。
自定义格式相比 json/msgpack 而言,可以去掉不必要的字段,连 key 都可以不用在序列化中出现,并且可以通过一些优化减少内存的分配,缓解gc压力。

自定义二进制协议优化思路如下

  1. CaptureInfo/Id 字段直接固定N个字节表示,对于 CaptureLength/Length 可以直接使用 2 个字节来表达,Id 如果数量很少使用 1 个字节来表达都可以
  2. 内存复用
    1. Encode 逻辑内部不分配内存,这样直接写入外部的 buffer,如果外部 buffer 是同步操作的话,整个逻辑 0 内存分配
    2. Decode 内部不分配内存,只解析元数据和复制 Data 切片,如果外部是同步操作,同样整个过程 0 内存分配
    3. 如果是异步操作,那么在调用 Encode/Decode 的地方对 Data 进行复制,这里可以使用 sync.Pool 进行优化,使用四个 sync.Pool 分别分配 128/1024/8192/65536 中数据

sync.Pool 的优化点有两个

  • 异步操作下每个 Packet.Data 都需要有自己的空间,不能进行复用,使用 sync.Pool 来构造属于 Packet 的空间
  • 元数据序列化固定字节长度的 buffer,使用 make 或者数组都会触发 gc
func acquirePacketBuf(n int) ([]byte, func()) {
	var (
		buf   []byte
		putfn func()
	)
	if n <= CapturePacketMetaLen+128 {
		smallBuf := smallBufPool.Get().(*[CapturePacketMetaLen + 128]byte)
		buf = smallBuf[:0]
		putfn = func() { smallBufPool.Put(smallBuf) }
	} else if n <= CapturePacketMetaLen+1024 {
		midBuf := midBufPool.Get().(*[CapturePacketMetaLen + 1024]byte)
		buf = midBuf[:0]
		putfn = func() { midBufPool.Put(midBuf) }
	} else if n <= CapturePacketMetaLen+8192 {
		largeBuf := largeBufPool.Get().(*[CapturePacketMetaLen + 8192]byte)
		buf = largeBuf[:0]
		putfn = func() { largeBufPool.Put(largeBuf) }
	} else {
		xlargeBuf := xlargeBufPool.Get().(*[CapturePacketMetaLen + 65536]byte)
		buf = xlargeBuf[:0]
		putfn = func() { xlargeBufPool.Put(xlargeBuf) }
	}
	return buf, putfn
}

func (binaryPack) EncodeTo(p *CapturePacket, w io.Writer) (int, error) {
	buf := metaBufPool.Get().(*[CapturePacketMetaLen]byte)
	defer metaBufPool.Put(buf)

	binary.BigEndian.PutUint64(buf[0:], uint64(p.Timestamp.UnixMicro()))
    ...
	return nm + nd, err
}

数据包构造大小(By 通义千问)

方法 原始数据长度 (字节) 编码后数据长度 (字节) 变化字节数 (字节)
Binary Pack 72 94 +22
Binary Pack 1024 1046 +22
Binary Pack 16384 16406 +22
MsgPack 72 150 +78
MsgPack 1024 1103 +79
MsgPack 16384 16463 +79
Json Pack 72 191 +119
Json Pack 1024 1467 +443
Json Pack 16384 21949 +5565
Json Compress Pack 72 195 +123
Json Compress Pack 1024 1114 +90
Json Compress Pack 16384 15504 -120

分析

  • Binary Pack

    • 对于较小的数据(72字节),编码后增加了22字节。
    • 对于较大的数据(16384字节),编码后增加了22字节。
    • 总体来看,Binary Pack的编码效率较高,增加的字节数相对较少。
  • MsgPack

    • 对于较小的数据(72字节),编码后增加了78字节。
    • 对于较大的数据(16384字节),编码后增加了79字节。
    • MsgPack的编码效率在小数据量时不如Binary Pack,但在大数据量时仍然保持较高的效率。
  • Json Pack

    • 对于较小的数据(72字节),编码后增加了119字节。
    • 对于较大的数据(16384字节),编码后增加了5565字节。
    • Json Pack的编码效率较低,特别是对于大数据量,增加的字节数较多。
  • Json Compress Pack

    • 对于较小的数据(72字节),编码后增加了123字节。
    • 对于较大的数据(16384字节),编码后增加了120字节。
    • Json Compress Pack在小数据量时增加的字节数较多,但在大数据量时增加的字节数较少,表明压缩效果较好。

通过这个表格,你可以更直观地看到不同数据打包方法在不同数据量下的表现。希望这对你有帮助!

benchmark

json

可以看到使用 buffer 进行复用提升比较明显,主要还是减少内存分配带来的提升。

BenchmarkJsonPack/encode#72-20                    17315143             647.1 ns/op             320 B/op          3 allocs/op
BenchmarkJsonPack/encode#1024-20                   4616841              2835 ns/op            1666 B/op          3 allocs/op
BenchmarkJsonPack/encode#16384-20                   365313             34289 ns/op           24754 B/op          3 allocs/op
BenchmarkJsonPack/encode_with_buf#72-20           24820188             447.4 ns/op             128 B/op          2 allocs/op
BenchmarkJsonPack/encode_with_buf#1024-20         13139395             910.6 ns/op             128 B/op          2 allocs/op
BenchmarkJsonPack/encode_with_buf#16384-20         1414260              8472 ns/op             128 B/op          2 allocs/op
BenchmarkJsonPack/decode#72-20                     8699952              1364 ns/op             304 B/op          8 allocs/op
BenchmarkJsonPack/decode#1024-20                   2103712              5605 ns/op            1384 B/op          8 allocs/op
BenchmarkJsonPack/decode#16384-20                   159140             73101 ns/op           18664 B/op          8 allocs/op

msgpack

同样看到使用 buffer 进行复用的提升,和 json 的分水岭大概在 1024 字节左右,超过这个大小 msgpack 速度快很多,并且在解析的时候内存占用不会随数据进行增长。

BenchmarkMsgPack/encode#72-20                     10466427              1199 ns/op             688 B/op          8 allocs/op
BenchmarkMsgPack/encode#1024-20                    6599528              2132 ns/op            1585 B/op          8 allocs/op
BenchmarkMsgPack/encode#16384-20                   1478127              8806 ns/op           18879 B/op          8 allocs/op
BenchmarkMsgPack/encode_with_buf#72-20            26677507             388.2 ns/op             192 B/op          4 allocs/op
BenchmarkMsgPack/encode_with_buf#1024-20          31426809             400.2 ns/op             192 B/op          4 allocs/op
BenchmarkMsgPack/encode_with_buf#16384-20         22588560             494.5 ns/op             192 B/op          4 allocs/op
BenchmarkMsgPack/decode#72-20                     19894509             654.2 ns/op             280 B/op         10 allocs/op
BenchmarkMsgPack/decode#1024-20                   18211321             664.0 ns/op             280 B/op         10 allocs/op
BenchmarkMsgPack/decode#16384-20                  13755824             769.1 ns/op             280 B/op         10 allocs/op

压缩的效果

在内网的情况下,带宽不是问题,这个压测结果直接被 Pass

BenchmarkJsonCompressPack/encode#72-20               19934            709224 ns/op         1208429 B/op         26 allocs/op
BenchmarkJsonCompressPack/encode#1024-20             17577            766349 ns/op         1212782 B/op         26 allocs/op
BenchmarkJsonCompressPack/encode#16384-20            11757            860371 ns/op         1253975 B/op         25 allocs/op
BenchmarkJsonCompressPack/decode#72-20              490164             28972 ns/op           42048 B/op         15 allocs/op
BenchmarkJsonCompressPack/decode#1024-20            187113             71612 ns/op           47640 B/op         23 allocs/op
BenchmarkJsonCompressPack/decode#16384-20            35790            346580 ns/op          173352 B/op         30 allocs/op

自定义二进制协议

对于序列化和反序列化在复用内存后,速度的提升非常明显,在同步的操作下,能做到 0 字节分配。异步场景下,使用 sync.Pool 内存固定字节分配(两个返回值在堆上分配)

BenchmarkBinaryPack/encode#72-20                  72744334             187.1 ns/op             144 B/op          2 allocs/op
BenchmarkBinaryPack/encode#1024-20                17048832             660.6 ns/op            1200 B/op          2 allocs/op
BenchmarkBinaryPack/encode#16384-20                2085050              6280 ns/op           18495 B/op          2 allocs/op
BenchmarkBinaryPack/encode_with_pool#72-20        34700313             109.2 ns/op              64 B/op          2 allocs/op
BenchmarkBinaryPack/encode_with_pool#1024-20      39370662             101.1 ns/op              64 B/op          2 allocs/op
BenchmarkBinaryPack/encode_with_pool#16384-20     18445262             177.2 ns/op              64 B/op          2 allocs/op
BenchmarkBinaryPack/encode_to#72-20              705428736             16.96 ns/op               0 B/op          0 allocs/op
BenchmarkBinaryPack/encode_to#1024-20            575312358             20.78 ns/op               0 B/op          0 allocs/op
BenchmarkBinaryPack/encode_to#16384-20           100000000             113.4 ns/op               0 B/op          0 allocs/op
BenchmarkBinaryPack/decode_meta#72-20           1000000000             2.890 ns/op               0 B/op          0 allocs/op
BenchmarkBinaryPack/decode_meta#1024-20         1000000000             2.886 ns/op               0 B/op          0 allocs/op
BenchmarkBinaryPack/decode_meta#16384-20        1000000000             2.878 ns/op               0 B/op          0 allocs/op
BenchmarkBinaryPack/decode_with_pool#72-20       106808395             31.51 ns/op              16 B/op          1 allocs/op
BenchmarkBinaryPack/decode_with_pool#1024-20     100319094             35.94 ns/op              16 B/op          1 allocs/op
BenchmarkBinaryPack/decode_with_pool#16384-20     26447718             138.6 ns/op              16 B/op          1 allocs/op

总结一下

通义千问的

Binary Pack:
- encode_to:性能最优,几乎没有内存分配,适用于高性能要求的场景。
- encode_with_pool:使用内存池优化,显著减少了时间和内存开销,适用于大多数场景。
- encode:标准方法,时间和内存开销较高。
MsgPack:
- encode_with_buf:使用预分配的缓冲区,显著减少了时间和内存开销,适用于大多数场景。
- encode:标准方法,时间和内存开销较高。
- decode:解码性能一般,内存开销较高。
Json Pack:
- encode_with_buf:使用预分配的缓冲区,显著减少了时间和内存开销,适用于大多数场景。
- encode:标准方法,时间和内存开销较高。
- decode:解码性能较差,内存开销较高。
Json Compress Pack:
- encode:标准方法,时间和内存开销非常高,不推荐用于高性能要求的场景。
- decode:解码性能较差,内存开销较高。

我总结的

在内网的环境进行传输,一般网络带宽不会成为瓶颈,所以可以不用考虑数据压缩,上面结果也看到压缩非常占用资源;
如果对数据内容不关心且数据量非常多的情况下(比如传输 pcap 包),那么使用自定义协议可能更合适一些,固定长度的元数据解析起来优化空间巨大,二进制解析比 json/msgpack 快内存分配也非常少。

引用

标签:allocs,20,字节,基准,encode,Go,ns,打包,op
From: https://www.cnblogs.com/shuqin/p/18427020

相关文章

  • Go 语言编程极简教程 2
    Go语言编程极简教程2我将为您提供一个Go语言编程的极简教程。我会尽量详细地解释每个步骤,并探讨多种方法来介绍这个主题。让我们开始吧!文章目录Go语言编程极简教程2介绍Go语言安装Go语言环境创建第一个Go程序解释Go程序结构Go语言的基本数据类型变量声......
  • 3. 无重复字符的最长子串 Golang实现
    题目描述给定一个字符串s,请你找出其中不含有重复字符的最长子串的长度。注意区分子串和子序列。示例3:输入:s="pwwkew"输出:3解释:因为无重复字符的最长子串是"wke",所以其长度为3。请注意,你的答案必须是子串的长度,"pwke"是一个子序列,不是子串。思路分析:1.......
  • CentOS 9 安装 google Chrome Web 浏览器的一种简单方法(2024.9)
    我自己选择了一个简单的方法。1.高级上网,到google的Chrome下载网页,下载Linux的安装包,google-chrome-stable_current_x86_64.rpm,109M如网址:https://www.google.com/intl/en_uk/chrome/next-steps.html?platform=linux&statcb=0&installdataindex=empty&defaultbrowser=0附百度......
  • go cobra实例讲解
    gocobra实例讲解概述cobra 库是golang的一个开源第三方库,能够快速便捷的建立命令行应用程序。优势:cobra 可以快速建立CLI程序,使我们更专注于命令需要处理的具体的业务逻辑。举两个例子:复制代码hugoserver--port=1313gitcloneURL--bare都是命令行程序。基本概......
  • 基于django+vue基于O2O模式的外卖订餐系统【开题报告+程序+论文】-计算机毕设
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景随着互联网技术的飞速发展,电子商务已渗透到人们生活的方方面面,其中外卖订餐系统作为“互联网+餐饮”的典型代表,近年来在全球范围内迅速崛......
  • 基于django+vue基于MVVM架构家政服务平台【开题报告+程序+论文】-计算机毕设
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景随着社会经济的快速发展与生活节奏的加快,现代家庭对于家政服务的需求日益增长,从日常清洁、照料老人儿童到专业护理、家居维修等,家政服务已......
  • 基于django+vue基于MVC框架的新闻发布系统【开题报告+程序+论文】-计算机毕设
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景随着互联网技术的飞速发展,信息传播的速度与广度达到了前所未有的高度。新闻作为社会信息的重要载体,其发布与传播的效率和准确性直接关系到......
  • 基于django+vue基于MVC的社区党建信息系统的设计与实现【开题报告+程序+论文】-计算机
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景随着信息技术的飞速发展和党建工作的不断深入,传统的社区党建管理模式已难以满足新时代下党员管理、组织生活及信息传递的高效性需求。社区......
  • electron nsis打包windows应用程序
    使用electron开发应用程序之后,经常会单独对windows做32位或者是64位程序打包操作,有时候默认的程序做不了相应的需求,往往需要添加单独的页面或者是修改默认的操作,比如一下添加一个默认的选择页面:自定义脚本代码如下所示:!defineMUI_LANGUAGE"Chinese"Unicodetrue!includensDial......
  • mongodb 中rs.stauts()命令参数解析
    rs.status()命令用于获取MongoDB副本集的状态信息。它提供了关于副本集中各个节点的详细信息,包括节点的健康状况、角色、选举状态等。以下是查看一个mongo集群状态返回的参数:rs0:PRIMARY>rs.status(){"set":"rs0","date":ISODate("2024-09-14T06:4......