万字长文聊聊高性能网关

设计高性能网关

设计落地一个高性能网关,需要能承接海量的连接和流量,并在网关层做服务路由和治理等能力增强,实现公共的南北向流量入口。

在设计高性能网关时,可以从如下方向思考如何进行优化实现:
1、应用层优化。在应用层考虑的主要时处理的线程模型、内存池设计以及语言相关的优化(类似GC)
2、操作系统层优化。针对文件描述符、TCP/IP 相关参数、多网卡队列和软中断进行优化。
3、协议层优化。TCP本身可以进一步优化成已UDP为基础的协议栈,以及DNS协议优化。
4、极速场景优化。可以采用Kernel Bypass技术,通过用户态协议栈、避免上下文切换、避免中断、避免数据复制的其他技术保障性能发挥至最佳水平。

1 应用层优化

高性能网关最基本要做到高性能的网络代理,而其核心是如何更充分利用CPU的处理能力代理网络请求,同时做到尽可能避免出现的线程/进程阻塞、频繁的垃圾回收、大量数据复制、上下文切换等现象,从而让网关可以发挥出最大的性能。

网关类产品的特性是要做消息的处理于转发,而请求和相应对象都是 “朝生暮死” 的类型,而在高并发场景下,如何避免大量不合理内存的申请导致频繁GC,就是设计网关的核心。对于网关类型具体措施可以如下:
1、采用内存池的机制优化内存的申请与释放,降低GC的负担
2、减少对象拷贝,以及尽量避免请求 Body 的解析(这样可以极大降低内存的申请与释放,同时由于避免了相应的反序列化,性能也会有很大提升)
3、流控机制必不可少。需要对客户端连接数、QPS限制,还需对内存占用等指标进行流控

1.1 实现非阻塞的关键 “线程模型”

1.1.1 Java 体系的 Netty 框架

Netty 抽象出两组线程池:BossGroup 和 WorkerGroup。BossGroup 专门负责接收客户端的连接,WorkerGroup 专门负责网络的读写。
BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup,NioEventLoopGroup 相当于一个 事件循环组,这个组中 含有多个事件循环 ,每一个事件循环是 NioEventLoop
NioEventLoop 表示一个不断循环的执行处理任务的线程, 每个NioEventLoop 都有一个selector , 用于监听绑定在其上的socket的网络通讯。
NioEventLoopGroup 可以有多个线程, 即可以含有多个NioEventLoop
每个Boss Group 中的 NioEventLoop 循环执行的步骤:
1)轮询accept 事件
2)处理accept 事件,与client建立连接 , 生成 NioScocketChannel,并将其注册 Worker Group 上的某个 NIOEventLoop 上的 selector
3)处理任务队列的任务,即 runAllTasks
每个 Worker Group 中的 NIOEventLoop 循环执行的步骤:
1)轮询 read/write 事件
2)处理 I/O 事件, 即 read/write 事件,在对应的 NioScocketChannel 上处理
3)处理任务队列的任务 , 即 runAllTasks
每个Worker NIOEventLoop 处理业务时,会使用 pipeline(管道)。pipline中包含了 channel,即通过pipline可以获取到对应的 channel,并且pipline维护了很多的 handler(处理器)来对我们的数据进行一系列的处理。

请添加图片描述

1.1.2 Envoy 系列

请添加图片描述

  • Main线程:此线程可以启动和关闭服务器。负责所有xDS API处理(包括DNS , 运行状况检查和常规集群管理 ), 运行时 ,统计刷新,管理和一般进程管理(信号, 热启动等)。 在这个线程上发生的一切都是异步的和“非阻塞的”。

  • Worker线程: 每个Worker线程是一个“非阻塞”事件循环,负责监听每个侦听器,接受新连接,为每个连接实例化过滤器栈,以及处理所有连接生命周期内IO事件。

  • 文件刷新线程:Envoy写入的每个文件(主要是访问日志)都有一个独立的刷新线程。 这是因为即使用O_NONBLOCK写入文件系统有时也会阻塞。 当工作线程需要写入文件时,数据实际上被移入内存缓冲区,最终通过文件刷新线程刷新至磁盘。

由于Envoy将主线程职责与Worker线程职责分开,因此需要在主线程上完成复杂的处理,然后以高度并发的方式让每个Worker线程处理。 本节将介绍Envoy线程本地存储(TLS)系统。

请添加图片描述
在主线程上运行的代码可以分配进程范围的TLS槽。 这是一个允许O(1)访问的向量索引。主线程可以将任意数据置入其槽中。 完成此操作后,数据将作为 “事件” 发布到每个Worker中。Worker接收到“事件”后,可以从其TLS槽读取并获得数据。通过类似于读时复制的思路保障了配置更新时,主从进层均处于无锁的状态。

1.1.3 Nginx 系列

请添加图片描述
Nginx 在服务启动后会产生一个主进程(master process)、多个工作进程(worker processes)、缓存加载进程(cache load processes)、缓存管理进程(cache manager processes),工作进程用于接收和处理客户端请求。

每个工作进程使用了异步非阻塞的方式,可以处理多个客户端的请求。当某个工作进程接收到客户端的请求后,使用事件驱动方式管理socket,将socket设置为非阻塞方式。在涉及IO操作时,可以调用Linux文件系统提供的AIO接口,使用异步IO方式完成调用,针对任务繁重的IO操作可以将等待IO的操作卸载给线程池中其它线程处理,从而避免主线程(worker进程的主循环)阻塞。

  • master 进程。主进程主要负责读取Nginx配置文件并验证其有效性和正确性;建立、绑定、关闭socket连接;按照配置生成、管理进程和结束工作进程;接收外界指令,如重启、退出等
  • worker 进程。工作进程主要负责接收并处理客户端请求;将请求以此送入各个功能模块进行处理;IO调用,获取响应数据等等。
  • cache loader(缓存加载进程)。在开启缓存服务器功能下,在Nginx主进程启动一段时间后(默认1分钟),由主进程生成cache loader,在缓存索引建立完成后将自动退出。
  • cache manager(缓存管理进程)。在开启缓存服务器功能下,在Nginx主进程的整个生命周期内,管理缓存索引,主要对索引是否过期进程判断。

⼯作进程是有主进程⽣成的,主进程使⽤fork()函数,在Nginx服务器启动过程中主进程根据配置⽂件决定启动⼯作进程的数量,然后建⽴⼀张全局的⼯作表⽤于存放当前未退出的所有的⼯作进程,主进程⽣成⼯作进程后会将新⽣成的⼯作进程加⼊到⼯作进程表中,并建⽴⼀个单向的管道并将其传递给⼯作进程,该管道与普通的管道不同,它是由主进程指向⼯作进程的单项通道,包含了主进程向⼯作进程发出的指令、⼯作进程ID、⼯作进程在⼯作进程表中的索引和必要的⽂件描述符等信息。

1.2 内存池

Netty采用了jemalloc的思想,这是FreeBSD实现的一种并发malloc的算法。jemalloc依赖多个Arena来分配内存,运行中的应用都有固定数量的多个Arena,默认的数量与处理器的个数有关。系统中有多个Arena的原因是由于各个线程进行内存分配时竞争不可避免,这可能会极大的影响内存分配的效率,为了缓解高并发时的线程竞争,Netty允许使用者创建多个分配器(Arena)来分离锁,提高内存分配效率。

线程首次分配/回收内存时,首先会为其分配一个固定的Arena。线程选择Arena时使用round-robin的方式,也就是顺序轮流选取。每个线程各种保存Arena和缓存池信息,这样可以减少竞争并提高访问效率。Arena将内存分为很多Chunk进行管理,Chunk内部保存Page,以页为单位申请。

内存池结构主要包括如下基本组件:

  • PoolArena:PoolArena是功能的门面,通过PoolArena提供接口供上层使用,屏蔽底层实现细节。为了减少线程成间的竞争,很自然会提供多个PoolArena。Netty默认会生成2×CPU个PoolArena跟IO线程数一致。
  • PoolChunk:用来组织和管理多个Page的内存分配和释放。实现时采用 memoryMap 和 depthMap 来表示二叉树,其中 memoryMap 存放的是 PoolSubpage 的非配信息,depthMap 存放的是二叉树的深度。分配内存是修改 memoryMap 至已被占用状态,之后以此向上变更更新父节点。
  • PoolSubpage:对于小内存(小于4096)的分配还会将Page细化成更小的单位Subpage。Subpage按大小分有两大类,36种情况:Tiny:小于512的情况,最小空间为16,对齐大小为16,区间为[16,512),所以共有32种情况;Small:大于等于512的情况,总共有四种,512,1024,2048,4096。

2 操作系统优化

对于网关类产品会占用大量文件句柄来承接大规模流量,需要保证具备足够文件描述符。需要观察两个参数

  • 最大文件文件句柄数。查看 cat /proc/sys/fs/file-max ,如需更新在 /etc/sysctl.conf 下 fs.file-max=100000,并执行 sysctl -p
  • 单进程最大句柄数。查看 ulimit -a,如需更新在 /etc/security/limit.conf 下增加 soft nofile 10000 \n hard nofile 100000,重新登录后生效。

TCP/IP 优化(大约50万连接场景)

  • net.ipv4.tcp_rmem:为每个TCP分配读缓冲区内存大小。推荐值:4096 87380 4194304
  • net.ipv4.tcp_wmem:为每个TCP分配写缓冲区内存大小。推荐值:4096 87380 4194304
  • net.ipv4.tcp_mem:为每个TCP连接的内存。
  • net.ipv4.tcp_keepalive_time:最近一次数据包发送与第一次keep alive探测消息发送的间隔。
  • net.ipv4.tcp_keepalive_intvl:在未获得探测消息响应时,发送探测消息的时间间隔。
  • net.ipv4.tcp_keepalive_probes:判断TCP连接失效连续发送的探测消息的个数,达到之后判定连接失败。
  • net.ipv4.tcp_tw_reuse:是否将 TIME_WAIT 重新用于新的TCP连接。
  • net.ipv4.tcp_timestamps:记录数据包的发送时间。
  • net.ipv4.tcp_fin_timeout:关闭时保持在 FIN_WAIT_2 超时时间。

网卡多队列和软中断

  • 随着网络带宽不断提升,可以通过多队列网卡驱动实现各个队列通过中断绑定到不同的CPU内核,满足吞吐量的需求。查看方式 lspci -vvv 或者 ethtool -l 网卡 interface 名
  • 如果内核版本支持RPS,可开启实现软中断,实现模拟多队列的功能。

3 网络协议优化

由于TCP本身限制,所以如果进一步想突破,可以从网络协议层面进行拓展。目前可以看到很多基于 UDP 的传输协议的存在,其中Google 主导的 QUIC 就是其中最有名的。

Quic 相比现在广泛应用的 http2+tcp+tls 协议有如下优势:
1、精细流量控制。主要依赖于三种关键幀能达到流量控制,一是 window_update 帧,告诉对端自己“可以且最多可以”接收的字节数绝对偏移量;二是blocked 帧,告诉对端数据发送,由于流量控制被阻塞,暂时无法发送;三是stop_waiting 帧,用于通知对端,它不应该继续等待包小于特定值的包。对于直播而言,在直播发起时,在转码跟不上的时候,告诉服务端停止发送直播流或者做出一些应急处理,推动定制的幀去做,保证服务可用性。
2、无队头阻塞的多路复用。QUIC 的多路复用,在一条 QUIC 连接上可以发送多个请求 (stream),一个连接上的多个请求(stream)之间是没有依赖的。比如说这个packet丢失,不会影响其他的stream。这个特性对于直播来说是,在弱网下的推流可以保证流畅。
3、ORTT连接。QUIC的连接将版本协商、加密、和传输握手交织在一起以减少连接建立延迟。这个特性对于直播来说,使首幀更快,延迟更小。
4、连接迁移。传统NAT遇到的问题,比如小区运营商切换端口,导致设备端判断不了新的连接标识,需要重联。而QUIC使用公共包头和连接ID,可以在网络切换的时候不重连,从室内到室外,在理论上可以做到连接不断网。
5、加密认证的报文。QUIC把TLS(1.3)等效加密,几乎每个UDP包都加密,报文body都经过加密,从头到脚几乎无死角,对证书也有一些压缩优化,每一个加密包独立认证。这个特性对直播来说,在客户端的防盗链、盗播、劫持上是有好处的。
6、向前纠错。QUIC采用向前纠错(FEC)方案,即每个数据包除了本身的数据以外,会带有其他数据包的部分数据,在少量丢包的情况下,可以使用其他数据包的冗余数据完成数据组装而无需重传,从而提高数据的传输速度。对于直播来说可以减少ARG、减少卡顿、提升秒开成功率。
7、改进的拥塞控制。这是QUIC最重要的一个特性,TCP的拥塞控制包含了四个算法:慢启动,拥塞避免,快速重传,快速恢复。QUIC 协议当前默认使用TCP协议的Cubic拥塞控制算法,同时也支持 CubicBytes, Reno, RenoBytes, BBR, PCC 等拥塞控制算法。同时QUIC拥有完善的数据包同步机制,在应用层做了很多网络拥塞控制层面的优化,能有效降低数据丢包率,有助降低复杂网络下的直播卡顿率,提升传输效率,使得推流更流畅。

4 极速场景优化

针对极速场景需要进一步提升整体的性能,那在高并发高流量下的瓶颈主要就体现在内核(当然硬件和网络规划也有很重要影响,但不在本文讨论范围内)。内核成为瓶颈主要体现在:

  • 上下文切换。包括了用户态/内核态的切换,多进程/线程上下文的切换;
  • 中断风暴。网卡驱动收发包部分是通过硬件中断和下半部软中断实现,当高并发场景下会发现CPU被大量消耗在软中断和其处理函数上;
  • 强大且复杂的网络协议栈。内核网络栈为了通用性实现了太多复杂的逻辑,例如Netfilter和相关表和规则、conntrack系统和各种QoS(质量服务)。
  • 数据复制。用户态和内核态中间数据复制也十分消耗CPU资源。

而解决上述解决方案就是绕过内核的 DPDK 或者其他用户态协议栈技术,本文就不展开了(后续如感兴趣可单独展开)。

5 参考文档

1、https://blog.csdn.net/qq_36389060/article/details/124232377
2、https://medium.com/envoyproxy/envoy-threading-model-a8d44b922310
3、https://mp.weixin.qq.com/s?__biz=MzAxOTE5NjQwOA==&mid=2650167897&idx=1&sn=2c792119e057d31e1f2e34387b473e7b#rd
4、https://blog.csdn.net/qq_41652863/article/details/99095769
5、https://blog.csdn.net/u010039418/article/details/102828441
6、https://zhuanlan.zhihu.com/p/374308768

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值