新车
监听程序当前无法识别连接描述符中请求的服务(字节跳动在 Go 网络库上的实践)



字节跳动框架组主要负责公司内 RPC 框架的开发与维护。RPC 框架作为研发体系中重要的一环,承载了几乎所有的服务流量。随着公司内 Go 语言使用越来越广,业务对框架的要求越来越高,而 Go 原生 net 网络库却无法提供足够的性能和控制力,如无法感知连接状态、连接数量多导致利用率低、无法控制协程数量等。为了能够获取对于网络层的完全控制权,同时先于业务做一些探索并最终赋能业务,框架组推出了全新的基于 epoll 的自研网络库 —— netpoll,并基于其之上开发了字节内新一代 Golang 框架 KiteX。

netpoll 核心是 Reactor 事件监听调度器,主要功能为使用 epoll 监听连接的文件描述符(fd),通过回调机制触发连接上的 读、写、关闭 三种事件。

Server - 主从 Reactor 实现

client 端和 server 端共享 SubReactor,netpoll 同样实现了 dialer,提供创建连接的能力。client 端使用上和 net.Conn 相似,netpoll 提供了 write -> wait read callback 的底层支持。

Nocopy Buffer

为什么需要 Nocopy Buffer ?

两种方式各有优缺,netpoll 采用前者策略,水平触发时效性更好,容错率高,主动 I/O 可以集中内存使用和管理,提供 nocopy 操作并减少 GC。事实上一些热门开源网络库也是采用方式一的设计,如 easygo、evio、gnet 等。

另一方面,常见的 bytes、bufio、ringbuffer 等 buffer 库,均存在 growth 需要 copy 原数组数据,以及只能扩容无法缩容,占用大量内存等问题。因此我们希望引入一种新的 Buffer 形式,一举解决上述两方面的问题。

Nocopy Buffer 设计和优势

Nocopy Buffer 相比常见的 bytes、bufio、ringbuffer 等有以下优势:

  1. 读写并行无锁,支持 nocopy 地流式读写
  • 读写分别操作头尾指针,相互不干扰。
  1. 高效扩缩容
  • 扩容阶段,直接在尾指针后添加新的 block 即可,无需 copy 原数组。
  • 缩容阶段,头指针会直接释放使用完毕的 block 节点,完成缩容。每个 block 都有独立的引用计数,当释放的 block 不再有引用时,主动回收 block 节点。
  1. 灵活切片和拼接 buffer (链表特性)
  • 支持任意读取分段(nocopy),上层代码可以 nocopy 地并行处理数据流分段,无需关心生命周期,通过引用计数 GC。
  • 支持任意拼接(nocopy),写 buffer 支持通过 block 拼接到尾指针后的形式,无需 copy,保证数据只写一次。
  1. Nocopy Buffer 池化,减少 GC
  • 将每个 []byte 数组视为 block 节点,构建对象池维护空闲 block,由此复用 block,减少内存占用和 GC。

RPC 调用通常采用短连接或者长连接池的形式,一次调用绑定一个连接,那么当上下游规模很大的情况下,网络中存在的连接数以 MxN 的速度扩张,带来巨大的调度压力和计算开销,给服务治理造成困难。因此,我们希望引入一种 "在单一长连接上并行处理调用" 的形式,来减少网络中的连接数,这种方案即称为 "连接多路复用"。

基于 netpoll 的连接多路复用设计如下图所示,我们将 Nocopy Buffer(及其分片) 抽象为虚拟连接,使得上层代码保持同 net.Conn 相同的调用体验。与此同时,在底层代码上通过协议分包将真实连接上的数据灵活的分配到虚拟连接上;或通过协议编码合并发送虚拟连接数据。

这里所说的 ZeroCopy,指的是 Linux 所提供的 ZeroCopy 的能力。上一章中我们说了业务层的零拷贝,而众所周知,当我们调用 sendmsg 系统调用发包的时候,实际上仍然是会产生一次数据的拷贝的,并且在大包场景下这个拷贝的消耗非常明显。以 100M 为例,perf 可以看到如下结果:

为了解决这个问题,我们选择了使用 Linux 提供的 ZeroCopy API(在 4.14 以后支持 send;5.4 以后支持 receive)。但是这引入了一个额外的工程问题:ZeroCopy send API 和原先调用方式不兼容,无法很好地共存。这里简单介绍一下 ZeroCopy send 的工作方式:业务进程调用 sendmsg 之后,sendmsg 会记录下 iovec 的地址并立即返回,这时候业务进程不能释放这段内存,需要通过 epoll 等待内核回调一个信号表明某段 iovec 已经发送成功之后才能释放。由于我们并不希望更改业务方的使用方法,需要对上层提供同步收发的接口,所以很难基于现有的 API 同时提供 ZeroCopy 和非 ZeroCopy 的抽象;而由于 ZeroCopy 在小包场景下是有性能损耗的,所以也不能将这个作为默认的选项。

在使用了 ZeroCopy send 后,perf 可以看到内核不再有 copy 的占用:

在我们实践过程中,发现我们新写的 netpoll 虽然在 avg 延迟上表现胜于 Go 原生的 net 库,但是在 p99 和 max 延迟上要普遍略高于 Go 原生的 net 库,并且尖刺也会更加明显,如下图(Go 1.13,蓝色为 netpoll + 多路复用,绿色为 netpoll + 长连接,黄色为 net 库 + 长连接):

由于 Go 在 runtime 中对于 net 库有做特殊优化,所以 net 库不会有以上情况;同时 net 库是 goroutine-per-connection 的模型,所以能确保请求能并行执行而不会相互影响。

希望以上的分享能够对社区有所帮助。同时,我们也在加速建设 netpoll 以及基于 netpoll 的新框架 KiteX。欢迎各位感兴趣的同学加入我们,共同建设 Go 语言生态!

参考资料

  1. http://man7.org/linux/man-pages/man7/epoll.7.html
  2. https://golang.org/src/runtime/proc.go
  3. https://github.com/panjf2000/gnet
  4. https://github.com/tidwall/evio

更多分享

ntent="mp" href="https://www.toutiao.com/i6820210359912104461/?group_id=6820210359912104461" rel="noopener noreferrer" target="_blank">字节跳动混沌工程实践总结

字节跳动基础架构团队是支撑字节跳动旗下包括抖音、今日头条、西瓜视频、火山小视频在内的多款亿级规模用户产品平稳运行的重要团队,为字节跳动及旗下业务的快速稳定发展提供了保证和推动力。

文化上,团队积极拥抱开源和创新的软硬件架构。我们长期招聘基础架构方向的同学,具体可参见 job.bytedance.com (文末“阅读原文”),感兴趣可以联系邮箱 guoxinyu.0372@bytedance.com

欢迎加入字节跳动技术团队


顶一下()     踩一下()

热门推荐

发表评论
0评