摘要
之前楼主是流量相关产品的,其中一项任务主要负责各种协议的数据采集。伴随着单机项目性能指标的不断提升以及分布式情况下采集器与管理中心的数据转发的数量级急剧膨胀。原有的网络数据接收以及转发逐渐无法满足现状。
由此楼主开启了从应用程序到硬件调优的征程,兄弟们坐稳兄弟我要开始吹了!
概览
单机优化
- tcp采集-大量连接场景优化
- udp采集-拒绝丢数据
- 应用层丢数据
- 内核层丢数据
- 硬件层丢数据
分布式集群优化
- tcp、udp采集数据转发速度慢
- 可靠的udp传输
发展方向的展望
- DPDK
- 基于网卡、CPU硬件与内核,进行深度定制的自动化智能网卡
- eBPF
开始吹
单机优化
tcp采集-大量连接场景优化
背景:当时在对接探针的项目中,tcp出现大量丢失数据,查看报文出现大量RST标记。
RST标志只有端口关闭的状态才会出现,就离谱!
开始排查代码:离谱的事情开始了,tcp的服务端采用的Cindy框架。netty上古时代的对手之一。 而问题的原因正是在Cindy的默认采用SimpleDispatcher,他采用的是单Reactor模型,问题就在单Reactor模型最大的问题就是无法面对海量的连接。因只有一个Reactor线程即负责创建socket连接又负责将已有socket连接的数据传递到worker线程。大量的并发压力导致负责Reactor的线程出现挂起问题。导致全部的socket全部中断。
当然,不能这样算了,为了应对更加复杂的采集环境。当然选择重构!果断选择netty,不是为了跟风。而是我想用Epoll的特性,Cindy早就没人维护了,更没有JNI的Epoll包。
优化方案落地
private TCPServer(){
if (Epoll.isAvailable())
EpollSystem();
else if (KQueue.isAvailable())
KqueueSystem();
else
DefaultSystem();
b.group(bossGroup,workGroup)
.childHandler(new TCPServerChannelInitializer())
.option(ChannelOption.SO_BACKLOG,4096) //全队列长度
.option(ChannelOption.SO_REUSEADDR,true)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childOption(ChannelOption.SO_KEEPALIVE,true)
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
}
private void EpollSystem(){
bossGroup = new EpollEventLoopGroup();
workGroup = new EpollEventLoopGroup();
b.channel(EpollServerSocketChannel.class)
.option(EpollChannelOption.SO_REUSEPORT,true);
}
解读
- SO_BACKLOG:TCP全队列长度,默认128。是存放完成三次握手后的socket连接的地方
- SO_REUSEADDR:端口复用,解决指定服务端端口关闭后无法立刻重用问题,即端口占用的问题。
- SO_REUSEPORT:端口重用,Epoll特性。可看作SO_REUSEADDR的plus版本,可实现服务端同一端口绑定多个监听线程。提高创建socket连接性能,在海量短连接场景下效果显著。绝对朴实无华的负载均衡
- 修改文件:/etc/security/limits.conf,soft nofile 32768 #限制单个进程最大文件句柄数,hard nofile 65536 #限制单个进程最大文件句柄数(到达此限制时系统报错),无柄可用,神都救不了你,这内存不能省兄弟们!
- 内核调参:net.core.somaxconn=10240,TCP的全队列长度取决于应用系统与内核参数配置的Min值,所以仅仅修改应用程序是无法解决全队列溢出的问题
int listen(int fd, int backlog) { ... if ((unsigned) backlog > sysctl_somaxconn) backlog = sysctl_somaxconn; ... }
延伸知识
说完全队列,那必然就有人问半连接队列,这里不展开讲解。为了预防SYN Flood攻击,半连接队列溢出导致的正常握手失败。这里需要我们在内核中配置开启tcp_syncookies参数即可。
bool tcp_syn_flood_action(...) { bool want_cookie = false; if(sysctl_tcp_syncookies){ want_cookie = true; } return want_cookie; }
最终效果 因为本人手里资源有限,仅测试到单机长短混连接15w eps处理,非常稳定
udp采集-拒绝丢数据
肯定有小伙伴问,UDP本来就是不可靠传输为什么还要去优化。其实很简单,其一是因为特定业务场景下,有硬性的数据不丢失指标。其二是因为从HTTP 3.0底层采用UDP以及其他采用UDP协议并且需要保证数据完整性的场景越来越多。
应用层
原本设计UDP的接收处理采用JDK原生的API创建socket连接,但是问题也很明显。接收性能严重不足,一个端口一个线程进行接收,即便是数据处理是异步的设计,接收数据包的性能仍然非常有限。且在CPU压力较高时,性能成下凹曲线。非常容易受其他长期持有线程资源任务的影响。
优化方案落地
这里分为了两个方面,其一是老生常谈的socket参数调优,另外一方面就是对JVM的调整
首先考虑到要使用的Epoll特性内容,这里同样采用netty作为底层。
private UDPServer(){
if (Epoll.isAvailable())
EpollSystem();
else if (KQueue.isAvailable())
KqueueSystem();
else
DefaultSystem();
b.group(workGroup)
.option(ChannelOption.RCVBUF_ALLOCATOR,
new AdaptiveRecvByteBufAllocator(16384,32768,65535))
.handler(new UDPServerChannelInitializer())
.option(ChannelOption.SO_REUSEADDR,true)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
}
...
private void EpollSystem(){
workGroup = new EpollEventLoopGroup();
b.channel(EpollDatagramChannel.class)
.option(EpollChannelOption.SO_REUSEPORT,true);
}
SO_REUSEPORT:端口重用,Epoll特性.该特性在UDP协议上的表现要更优有TCP,UDP中没有连接的概念,所以原本一个端口一个服务端socket,现在可以多个服务端socket,相当于在接收这里实现了N倍的接收性能提升。 至于零拷贝技术,地球人都知道,只要用的时候不要强行配制成纯堆内内存池即可。
其次是对JVM的优化
在对UDP做了调整后,仍然出现socket接收缓冲溢出的问题,当我对socket缓冲区进行监控发现了问题,随着应用程序负载的增高,缓冲区的接收出现有规律的堆积,即便增加更多接收线程可以瞬间清空缓冲区内容,但是还是会出现间歇性的堆积。
#!/bin/bash
while [ true ]; do
sleep 1
netstat -an | grep 端口
done
找不到原本记录数据,依稀记得光YGC可以达到每秒10次左右,就更不要提FGC了。
通过配置gc.log发现gc卡顿的时长与缓冲区堆积的曲线基本重合。
-XX:+PrintGCDateStamps:打印 gc 发生的时间戳。-XX:+PrintTenuringDistribution:打印 gc 发生时的分代信息-XX:+PrintGCApplicationStoppedTime:打印 gc 停顿时长-XX:+PrintGCApplicationConcurrentTime:打印 gc 间隔的服务运行时长
而为什么说GC会影响到数据的接收性能,楼主所在部门还在用jdk8。众所周知jdk8默认采用的辣鸡回收器是parallel+cms,而parapllel回收过程是全程stw,cms则是部分stw。stop the word,世界都停止了。还接收个锤子数据!
如果有权限修改JDK版本的小伙伴,建议早早升级到JDK11,哪怕用不上Z GC,起码也要G1.parallel还是早点去掉的好。(楼主提了没通过,所以也没办法告诉大家到底能提升多少)
内核层
承接应用层后,既然无法改变回收机制,那么我就只能寄托于数据达到内核后尽量不丢失。
优化方案落地
net.core.rmem_max = 536870912
net.core.netdev_budget = 600
net.core.netdev_max_backlog = 2400
解读
net.core.rmem_max是配置的socket接收缓冲区的大小,这里的缓存有两个作用,其一是应对数据传输过程中流量凸峰问题,其二是刚刚接入数据的缓冲。当刚刚介入数据时,应用程序会有一个预热的过程,例如线程资源的创建、直接内存的开辟。会导致最开始的接收数据响应速度慢。
net.core.netdev_budget是调整内核软中断一次处理的数据包数量,默认配置为300.从而提升从网卡捞取数据道内核的性能。至于为何修改,就要牵扯到了硬件层优化内容。
net.core.netdev_max_backlog是内核处理从网卡接收数据到协议栈层临时处理的队列,即将skb包从网卡的RingBuffer到内核socket缓冲区的过渡地带。当我们提升了网卡取数据的速度时,也需要扩容netdev_max_backlog,否则容易出现软中断丢包现象。
cat /proc/net/softnet_stat //查看CPU软中断数据
硬件层
随着数据级增加到7w eps左右,网卡已经陆陆续续的丢失数据了。且由于使用的网卡为鲲鹏920配套的板载网卡,这里我就不科普板载网卡与扩展网卡的区别了。
现象
ifconfig出现了大量RX errors ,dropped,overruns数字,dropped的异常说明内核取网卡数据速度不足,导致ring Buffer溢出,overruns的异常说明数据在未到达ring Buffer前,在网卡的栈针部分,向ring Buffer放置数据出现了问题,在CPU负载过高无暇顾及中断导致。
然后我去查看了网卡配置,简直灾难。
[lighthouse@VM-8-9-centos ~]$ ethtool -g eth0
Ring parameters for eth0:
Pre-set maximums:
RX: 1024
RX Mini: 0
RX Jumbo: 0
TX: 1024
[lighthouse@VM-8-9-centos ~]$ ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
RX: 0
TX: 0
Other: 0
Combined: 1
比这个配置还惨!从配置中看的出网卡仅仅是一个单队列网卡,所有数据均只由一个CPU进行中断响应。在海量数据场景下会出现严重的负载问题,当时只有一个CPU进行软中断已经达到了50%,从网卡的流量统计中已经出现严重的抖动。其次就是网卡的ring buffer太小,非常容易被瞬时的数据打爆导致硬件物理层丢失数据。
优化方案落地
没有什么是用钱解决不了的,如果有就多加一点。没错这个硬件太烂了。“产品!我要换扩展网卡!谢谢”/手动狗头。加了一块千兆的扩展网卡。并且对参数进行调优
ethtool -G eth0 rx 4096 tx 4096
ethtool -L eth0 combined 512
ethtool -K eth0 gro on
解读
简单粗暴的优化,加大ring buffer到4096,使用多队列网卡,从以前的所有网卡数据仅有一个CPU中断,变为每个应用层线程的CPU的CPU亲和的多个CPU。将软中断负载均匀的分布在多个CPU上。同时开启GRO将每个数据依次进入协议栈的处理函数变为多个数据包调用一次
最终效果
单机接收UDP数据可达20w eps,再往上测试出现了数据解析的瓶颈以及ES的数据落盘的问题,到达了整体系统的瓶颈。就没有了再向上测试的必要了。
分布式集群优化
tcp、udp采集数据转发速度慢
这件事情的起源就不再详细说明,真就是小孩儿没娘,说来话长。只需要知道,单机优化结束后,分布式情况下,采集器节点转发数据的速度比接收的速度慢,导致节点缓存的数据不断堆积。甚至最后需要做一个顺序写文件的持久化mmap缓存。
由于业务的变更,采集器从原本的纯粹将原始syslog转发变为了支持更多协议数据转发的采集器,兼顾一定预处理能力
优化方案落地
- 这里我采用Disruptor高性能队列当作缓存,分成了两个块做二级缓存。一个放原始数据,一个放预处理后的数据。持久化
- 参考Net Flow、TLV协议的设计,在预处理中做了数据压缩。并且将多条数据合并到一个数据包中。以二进制数据的形式进行传递。减少频繁的转发请求。经典的时间置换空间,而因为批量设计的存在,几乎相当于没有增加时间达到了压缩空间的效果。
扩展知识
1.tlv协议介绍
是BER编码的一种,全称是Tag、Length、value.该协议简单高效,适用于各种通信场景,并且具备良好的可扩展性。
基本格式如下:
| Tag(属性) | Length(长度) | Value(值) |
|---|---|---|
| 2字节 | 4字节 | 指定长度 |
tlv日志格式设计
在原tlv协议的原理上进行魔改,并且弥补存在的一些缺陷。以UDP方式接收二进制数据。
单个属性 header
| Code(校验码) | Tag | Length | Value |
|---|---|---|---|
| 4字节 | 4字节 | 4字节 | Length的值减去12个字节 |
Length的值等于header整个的长度
整条日志结构
| header0 | header1 | header2 | header... |
|---|---|---|---|
| Tag为日志类型,length为整条日志长度 |
进行批量传输方式
通过Code保证接收到的日志完整性的校验,再通过header0中的Length获得整条日志的长度,进行byte数组的切割。
2.NetFlow协议介绍
Netflow是一种网络数据包交换技术,用来对数据交换进行加速,同时统计经过网络设备的IP数据流。
与TLV协议不同的是netflow的二进制数据包的结构式,一个header和多个bady体。header中会包含所有的bady字段信息,以及bady的数量,数据的协议版本内容等。这里就不展开讲了。总之都是压缩数据转批量传递的设计方案。
最终效果
采集器数据几乎无堆积,且读取流量:转发流量,数据比 5:1,极高的压缩比。
可靠的udp传输
- 方案一
当然是QUIC,用了都说好。为了解决队头堵塞、0RTT等性能问题,谷歌使用了UDP进行QUIC协议的底层。QUIC 引入了一个 stream offset 的概念。
一个 stream 可以传输多个 stream offset,每个 stream offset 其实就是一个 PN 标识的数据,即使某个 PN 标识的数据丢失,PN + 1 后,它重传的仍旧是 PN 所标识的数据,等到所有 PN 标识的数据发送到服务器,就会进行重组,以此来保证数据可靠性。
到达服务器的 stream offset 会按照顺序进行组装,这同时也保证了数据的顺序性。
(这个就当知识扩展,毕竟成本略高。不好落地,主要是领导不让!)
- 方案二
参考某某加速器的底层设计。采用人海战术。
FEC:通过多发一些冗余的包, 当有些包丢失时,可以通过冗余的包恢复出来,而不用重传。其实就是模仿了QUIC的部分特性。
进一步设计可再次细分冗余数据,例如每个数据包中都附带一部分其他数据的,并且携带数据的唯一标记,通过数据包自增的id以及数据位置的偏移量组成。
- 方案三
这是鄙人想的野路子,考虑到UDP的不可靠的根本原因在于,数据达到IP层会将原本被分片的数据进行重组。TCP正是带重传的机制,才保证了数据的完整性。所以我就想如果我想办法不让UDP的数据进行分片,或者推迟分片到应用层。由此一套完整且简陋的可靠UDP传输从脑中浮现出来。
(1)先是对数据进行压缩,转化为二进制数据。参考tlv、netflow协议,压缩到整个数据包到1472字节以下,保证带报头整个数据包不大于MTU。预防IP层丢数据。
(2)冗余包,你懂的
(3)这里需要发送端准备一个buffer,每个数据包都有序号,每当序号累加到15次,对接收数据的序号队列进行连续性查询。缺少时向发送端请求缺少的数据包。
(4)对于大包,同样进行切割,单个数据包不超过MTU。并且携带数据包序号以及数据偏移量。由应用层完成分片组装。
发展方向的展望
DPDK
这是相对成熟的技术方向,主旨要义便是将物理层、协议层需要做的事情延后到应用层。通过自定义协议方式将底层操作上升到业务层。
DPDK做了以下几点优化:
- mmap 实现零拷贝(zero copy)
- PMD 减少中断和CPU上下文切换
- HugePages 减少TLB miss
- ...
而mmap零拷贝技术相比传统的libpcap(需要从内核直存拷贝到内核socket内存区域再到用户态socket内存)、netty(从内核直存拷贝到用户态内存)不同,他采用的是完全旁路内核的策略。通过DMA将网卡数据转移到内核直存;随后使用mmap()直接将内核直存的数据映射到用户用户空间。
延伸知识
ES的索引缓存有三种方式,第一种也是最常见的操作系统全部内存,第二种便是mmap文件内存映射的方式,他会稍微比全部内存慢一点点,但是是虚拟内存。可以让系统有更高的上限,但是出现问题也很棘手,不仅会堆外内存溢出,同时溢出的内存大小甚至超过分配给ES的堆大小。第三种就是纯文件,性能最差。
简单的讲PMD就是不再采用硬中断,全面拥抱软中断,且讲中断处理的内容放置到用户态去做。采用类似注册轮训的方式,不断查看rx和tx描述符是否出现了中断标志位(经典空轮训导致CPU资源占用过高问题,推出了Interrupt DPDK模式。数据少时会进入睡眠中断)
HugePages简单的讲就是类似系统的虚拟大页,通过加大单页的容量减少内存页数,大大降低内存页寻址的性能损耗。
存在问题
支持DPDK的网卡硬件大多为intel系列产品,面对当下的社会形势及政策,成本高且有较强的依赖性。
自动化智能网卡
这个是一个相当商业化的产物,由操作系统、网卡等领域的专家深度的定制化开发。
自动化智能网卡做了以下几点优化:
- 数据包处理优化算法,多NUMA节点架构CPU下的动态选择处理管道及策略,提高单个NUMA节点的处理能力。
- 自适应接收端缩放ARSS技术,通过动态规划算法在分片之间迁移RSS间接桶来解决数据包调度不均衡的问题。
- 面向会话的超快检索算法,多维度的索引,例如时间、布隆过滤器、压缩位图RBM、KV等索引。并且根据数据的冷热程度,分布到多级存储单元。原则即确保越慢的硬件越少的访问。
- ...
存在问题
成熟的商业化产品,国际上较多使用的是FPGA智能网卡。东西好是好,也是真贵。一张Napatech NT200A02都要2w刀。
eBPF
众所周知,Netflix出品必出精品。所以你们懂得,我吹爆eBPF好吧。
介绍
BPF是Berkeley Packet Filter的缩写,这项技术诞生于1992年,其作用是提升网络包过滤工具的性能。2014年正式并入Linux主线,在这之后BPF变成了一个更通用的执行引擎。
简单来说BPF提供了一种在各种内核事件和应用发生时运行一段小程序的机制,BPF是一项灵活而高效的技术,由指令集,存储对象和辅助函数等几部分组成。
目前BPF三个主要的应用领域分别是网络,可观测性和安全。扩展后的BPF通常缩写为eBPF,但官方依然简称为BPF不带'e',事实上内核中只有一个执行引擎,同时支持扩展后的eBPF和传统的BPF程序。
优点(网络方向)
高速数据路径技术(XDP): 一个可以使用eBPF编程的快速处理通道,与现有内核软件栈直接集成,不需要绕过内核。可以使用网卡驱动程序中内置的BPF钩子直接访问原始网络帧数据。可以直接告诉网卡是否需要丢弃,也可以回退到正常的网络栈处理。适合数据的转发、快速DDos缓解以及软件定义路由(SDR)等场景。
发送和接收缩放技术:
打散CPU中断瓶颈,将网卡中断和数据包处理均匀的分散给所有CPU。(没错,是不是很熟悉智能网卡那一块。懂的都懂 /手动狗头保命)还有更多的技术,例如NAPI、RSS、RPS、RFS、XPS。
动态插桩与静态插桩
可以在内核中插桩监控内核关于网络数据传递栈的耗时统计,包括并不限于skb_buffer的统计、tx环形队列的延迟计时、skb的drop事件甚至是skb的生命周期等等。
存在问题
问题就是太灵活好用了,没有问题。当然这是扯淡,这个对Linux内核版本要求稍高最好5.x的版本起步。还有就是目前较多的API还是为内核的辅助性API。但是也需要主要是否有坑,被辅助函数导致的主函数性能大量下降的情况又不是没见过。还是要谨慎一些。
结语
因为公司要求研发定期做一些技术分享,轮到我了。我就想不如顺手梳理一下自己在某个方向积累的内容。结果说干就干,但是也顶不住了,每天晚上回家写头发卡卡掉不说。内容是越想越多,写的手好累。所以写到小一半的时候就缩减了内容。如果有大家感兴趣的,可以踢一下楼主,楼主争取找时间单独出几篇更加细分的技术内容。
所以楼主的网络优化之路就暂时写到这里,这只是个起点。优化之路漫漫长,我还能再学八百年!