随着数据量和模型规模的爆炸性增长,多个客户端频繁访问相同数据的场景变得愈发普遍。分布式缓存通过聚合多个节点的本地缓存,形成大容量缓存池,从而提升缓存命中率、增强读带宽和 IOPS,降低读延迟,满足高性能的需求。
然而,节点间的数据交换极度依赖网络性能。带宽不足会限制数据传输速度并增加延迟;过高的网络延迟则会影响缓存响应,降低系统效率;同时,网络数据处理所消耗的 CPU 资源也可能成为瓶颈,制约整体性能。
针对这些问题,近期发布的 JuiceFS 企业版 5.2 对缓存节点间的网络传输进行了多项优化,有效解决了部分性能开销较大的环节,并提升了网卡带宽利用率。优化后,客户端 CPU 开销降低一半以上,缓存节点的 CPU 开销降低到优化前的 1/3; 聚合读带宽达到了 1.2 TB/s,接近跑满 TCP/IP 的网络带宽(基于 100 台 GCP 100Gbps 节点组成的分布式缓存集群)。
TCP/IP 协议作为最广泛支持的网络标准,几乎能在所有环境下即开即用,并凭借内建的拥塞控制和重传机制在复杂网络中达到足够高的性能与稳定性,因此成为分布式缓存的首选。目前,JuiceFS 已能在 TCP/IP 下充分跑满 100Gb 网卡带宽。未来,我们还将引入 RDMA,以进一步释放 200Gb、400Gb 网卡的潜力。本文将详细介绍此次针对 TCP/IP 网络优化的具体实现。
01 Go 语言网络性能优化
在大规模环境下,如当数千个客户端需要从超过 100 个分布式缓存节点访问数据时,缓存节点的网络连接数会显著增加,导致 golang 调度开销显著增大,并且网络带宽利用率下降。我们从连接复用与 EPOLL 事件触发两个关键点入手,分别引入了多路复用机制与事件触发阈值优化,有效缓解了 系统压力并提升了数据处理性能。
多路复用
如果一个连接在同一时刻只能处理一个请求,那么系统的整体并发能力将受限于连接数本身。为了提升并发,就必须建立大量连接。于是,我们引入了连接多路复用功能,允许在同一个连接上同时发送多个请求包至对端。通过多路复用机制,可以有效提升单连接的吞吐能力,减少连接数量需求,从而在提升并发性能的同时降低资源消耗和网络负担。
由于 TCP 单连接存在性能瓶颈,我们在多路复用架构的基础上,还进一步引入了根据实时流量动态调整连接数的能力,以实现性能与资源的最佳平衡,从而提升整体吞吐量和网络效率。系统会根据当前的网络流量自动增减连接数量:
- 当用户请求量较大、总流量不断上升,并且现有连接已无法满足带宽需求时,系统会自动增加连接数,以提升带宽利用效率;
- 反之,当请求变得不活跃,总带宽下降时,系统会自动减少连接数,以避免资源浪费和包碎片问题。
多路复用带来的另一优势是支持小包合并。通过高效的连接管理,多个小包的数据流可以在同一物理连接中一起传输,从而减少网络发送次数,并降低系统调用及内核与用户空间之间的切换开销。
具体来说,在发送端,发送线程会从发送 channel 中连续获取多个请求,直到 channel 为空,或累计待发送的数据量达到 4 KiB 时再统一发送。这样可以将多个小数据包合并为一个较大的数据块,减少系统调用和网络发送次数。在接收端,来自网卡的数据会一次性批量读取到 ring buffer 中,随后由上层业务从 ring buffer 中逐段提取并解析出各个完整的应用层数据包。
接收水位线设置(SO_RCVLOWAT)
在 Golang 的网络框架中,默认使用 epoll 的边缘触发(Edge Triggered)模式来处理 socket。当 socket 状态发生变化(例如从无数据变为有数据),就会产生一个 epoll 事件。
为减少因少量数据频繁触发事件带来的额外开销,我们通过设置 SO_RCVLOWAT(socket 接收低水位线),控制内核在 socket 接收缓冲区的数据量达到指定字节数时才触发 epoll 事件,从而减少了频繁触发事件的系统调用次数,降低了网络 I/O 开销。
例如:- conn, _ := net.Dial("tcp", "example.com:80")
- syscall.SetsockoptInt(conn.(net.TCPConn).File().Fd(), syscall.SOL_SOCKET, syscall.SO_RCVLOWAT, 512*1024)
复制代码 经测试,高并发连接的情况下,epoll 事件数减少了至少 10 倍,仅有每秒 4 万次左右,缓存性能更加稳定,CPU 开销维持在 1核/GB 带宽左右。
02 零拷贝优化:减少 CPU 和内存消耗
在 Linux 网络通信中,“零拷贝(Zero-copy)”是通过减少或消除数据在内核空间和用户空间之间的不必要拷贝,来降低 CPU 和内存的消耗,从而提升数据传输效率的一种技术。特别适用于大规模数据传输场景, nginx、kafka、haproxy 等经典产品都使用了零拷贝技术优化性能。
常见的零拷贝技术包括 mmap 系统调用, sendfile、splice、tee、vmsplice、MSG_ZEROCOPY 机制等。
- sendfile 是 Linux 提供的系统调用,用于将文件数据直接从磁盘读取到内核缓冲区,并通过 DMA 将数据传输到套接字缓冲区,无需经过用户空间。
- splice 允许在内核空间直接移动数据,支持任意两个文件描述符之间的传输(至少一端必须是管道)。通过管道作为中介,数据从输入描述符(如文件)传输到输出描述符(如套接字),而内核仅操作页面指针,避免了数据的实际拷贝。
与 sendfile 相比,splice 更加灵活,能够支持非文件场景的传输(如套接字间转发),并且更适用于高并发的网络环境,例如代理服务器等;而 sendfile 可以减少系统调用的次数,但会阻塞当前线程,影响并发性能。我们在不同场景下分别使用了 splice 和 sendfile 优化数据传输流,以达到最优的效果。
以 splice 为例,客户端在请求文件数据的环节,会通过分布式缓存向缓存节点发起请求,我们在发送端使用 Splice 零拷贝技术优化数据流。具体的数据传输路径如下:
在需要将缓存数据发送给客户端时,进程使用 splice 接口将数据从文件直接读取到内核缓冲区(page cache)中,随后将数据通过 splice 传输到预先创建的 pipe 中,再将 pipe 中的数据通过 splice 传输到套接字缓冲区( socket buffer)。由于数据始终在内核空间内移动,整个过程中仅发生两次 DMA 拷贝,无需 CPU 拷贝,实现了零拷贝缓存服务。
接收端虽也存在优化空间,但由于其零拷贝实现对硬件和运行环境要求较高,通用性不强,因此在当前架构中未引入相关方案。
03 优化 CRC 校验过程
在之前的版本中,从分布式缓存中读取一个数据块时需要进行两次 CRC(Cyclic Redundancy Check) 校验:
- 磁盘加载阶段:从磁盘加载数据到内存的过程中,用于检查磁盘上的数据是否发生了 bit rot;
- 网络传输阶段:发送端重新计算 CRC 并将其写入数据包头部,接收端收到后再计算一次 CRC,并与包头中的值进行比对,以确保传输过程中数据未被篡改或损坏。
为减少不必要的 CPU 开销,新版本优化了网络传输阶段的 CRC 校验过程,发送端不再重新计算 CRC,而是利用磁盘上保存的 CRC 值,合并为一个总的 CRC,写入数据包头部;接收端在收到数据后进行唯一一次 CRC 校验。
这里涉及到一个细节,为了支持随机读,缓存的数据块需要分段计算 CRC 值,即每 32KB 计算一个 CRC;而网络传输过程中需要的是所传输的所有数据的 CRC 值。于是,我们利用查表法高效地将分段 CRC 值合并为整体的 CRC 值,这个环节的开销几乎可以忽略不计。
这种方式在保证数据一致性和完整性的前提下,减少了一次 CRC 计算,有效降低了发送端的 CPU 消耗,提升了整体性能。
04 小结
在大规模模型训练与推理等场景中,数据规模正以前所未有的速度增长。分布式缓存作为连接计算与存储的关键组件,能够将热点数据分布在多个节点上,显著提升系统的访问性能与扩展能力,有效减轻后端存储的压力,提升整体系统的稳定性与效率。
但在应对持续增长的数据和客户端访问压力时,构建高性能的大规模分布式缓存仍面临诸多挑战。为此,JuiceFS 基于 Go 实现的分布式缓存系统,在实践中围绕 Go 的 I/O 机制进行了深度优化,并引入零拷贝等关键技术,显著降低了 CPU 开销,提高了网络带宽利用率,使系统在高负载场景下表现更加稳定、高效。
我们希望本文中的一些实践经验,能为正在面临类似问题的开发者提供参考,如果有其他疑问欢迎加入 JuiceFS 社区与大家共同交流。
来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除 |