写点什么

分享实录 | NGINX 网络协议优化(下)

  • 2023-07-05
    北京
  • 本文字数:3577 字

    阅读完需:约 12 分钟

分享实录 | NGINX 网络协议优化(下)

原文作者:陶辉

原文链接:分享实录 | NGINX 网络协议优化(下)

转载来源:NGINX 开源社区


NGINX 唯一中文官方社区 ,尽在 nginx.org.cn


编者按——本文为 NGINX Sprint China 2022 年度线上大会的分享实录,点击此处免费观看大会完整视频回放。由于文章较长,将分为上下两篇发布,点击《分享实录——NGINX网络协议优化(上)》阅读上篇。


本次分享中,我们将讨论如何通过优化 NGINX 协议栈,可以将并发连接提升到千万级、CPS 与 RPS 提升到百万级,从而拥有一个高性能的应用级软负载均衡。


很高兴大家回到这次深潜之旅,让我们继续挖掘 NGINX 的潜力。今天我的分享包括四个部分。首先从整体上来看一下 NGINX 的协议栈如何进行优化。接着我们将按照 OSI 七层网络模型,自上而下依次讨论 HTTP 协议栈、TLS/SSL 协议栈以及 TCP/IP 协议栈。

3. TLS 协议栈优化

接下来我们再来看 OSI 表示层协议 TLS/SSL 的优化。在全栈加密的今天,绝大部分公网流量都是经由 TLS 协议加密的,而优化 TLS 除了在先进算法与兼容性、性能与安全性之间做权衡外,还要考虑系统架构约束的变化。

3.1 建立会话

先来看 TLS 会话握手,这是最消耗 CPU 性能的过程,通常单颗 CPU 核心的每秒新建数不过一千多,但更为关键的是,握手消耗的 RTT 时间更多,参见下图:

上图中右侧是以 TLS1.2 协议为例看会话建立过程的,相对于图左侧在 TCP 握手中消耗 1 个 RTT(蓝色线条)之外,右侧共消耗了 3 个 RTT(蓝色与绿色线条),这就接近 1 秒时延了。怎么解决呢?参见下图的 TLS 1.3 方案:



上图左侧,TLS1.2 通过 Client Hello、Server Hello、Client Key Exchange、Finished(或者可选的 Server Key Exchange)4 条消息在 2 个 RTT 中完成了握手。我们要分析下,为什么交换密钥不能从 2 次 RTT 降为 1 次 RTT 呢?


这其实是能做到的,只要大幅减少加密算法(在 TLS 中被称为安全套件)的数量,就可以把 Client Hello 这个协商算法的消息与 Client Key Exchange 合并为一条消息,这就变成右图中的 TLS 1. 3 握手了。


我们可以通过 ssl_protocols 指令配置 NGINX 支持的协议版本,但目前至少需要同时支持 TLS 1.2 和 TLS 1.3,因为还有很多古老的客户端不兼容 TLS 1.3 协议。

3.2 传输数据

再来看传输加密数据的过程,我们可以基于内核的 kTLS 提升性能。在介绍 kTLS 之前,咱们需要先回顾下 HTTP 缓存,这实际上是 HTTP 协议栈的优化内容,可又是 NGINX 使用 kTLS 的前置知识点,所以我放在这里简要介绍。


上图中,cache 缓存可以存放在浏览器上,这时它的属性是 private,只针对一个用户有效。缓存还可以存放在正向代理(参考科学上网)、反向代理(参考 CDN)上,此时它的属性是 public,可以被多个用户共享。缓存通过将内容放在空间上距离用户更近的位置上,降低用户下载内容的时间。


我们知道,NGINX 可以使用 Linux 等操作系统提供的零拷贝技术(参见 sendfile 指令),将磁盘上的文件不通过 worker 进程就发送到网卡上。


然而,openssl 是运行在 worker 进程上的,一旦下游客户端走的是 TLS 流量,零拷贝就失效了,因为必须把磁盘上的文件读取到 worker 进程的内存空间上,才能使用 openssl 加密文件,然后再经由内核把加密后的字节流发送到网卡。

所以,只要在内核中使用 TLS 协议加密流量,就可以继续使用零拷贝技术,如下图所示:


关于此处精彩内容,您可以点击文章https://www.nginx.org.cn/article/detail/12641进行查看。

4. TCP/IP 协议栈优化


最后来看 TCP/IP 协议栈的优化。摩尔定律的失效,对 TCP/IP 协议栈的优化影响很大,如下图所示,CPU 在向多核心方向发展:



上图我们重点看绿、蓝、黑 3 条曲线。绿色曲线是 CPU 频率,从 2004 年以后基本就不变了。蓝色曲线是 CPU 单核性能,略有提升是 CPU 架构优化和缓存带来的。黑色曲线则是 CPU 核心数,它的不断增加对开发人员的要求很高。具体到 TCP/IP 协议,就是操作系统的共享协议栈设计,带来的锁竞争概率直接上升!


现代 OS 都是分时操作系统,单核心 CPU 一样可以通过微观上的串行任务,实现宏观上的并发,而且这时的并行多任务在使用自旋锁时,几乎没有锁竞争问题


然而,一旦服务器使用了 64 核等 CPU 时,微观上就会有 64 个线程并行执行,对于高负载的 NGINX 来说锁冲突概率会非常高。此时你升级 CPU,是不会带来线性性能提升的,与此同时,CPU 的 SI 软中断百分比会急剧变大。


内核协议栈的设计,除了锁竞争问题外,还会引入 3 个问题。如下图所示,内核协议栈必须通过 socket 和系统调用与 NGINX 传递消息,因此系统调用导致的上下文切换内核态与用户态间的内存拷贝硬件 NIC 网卡与协议栈协同引入的软中断,都是不可忽视的因素:



当然,上图的设计优点也很多,比如大幅减少了应用开发的难度,增强了操作系统的稳定性等。但当我们关注性能、优化协议栈时,就不得不使用诸如零拷贝、kTLS 等特性,还要不断关注软中断进程 ksoftirq 的 CPU 占用率。这里简单解释下什么是软中断,如下图所示:



上图有 6 个步骤,其中第 1、2、3 步是从网络中接收的报文复制到 sk_buffer 中,并发起硬中断通知操作系统;第 4、5 步则是操作系统收到软中断后,通过协议栈处理报文,此时 ksoftirq 进程是在工作的。

把两个步骤分开是一种异步化设计,毕竟网卡是硬件,处理报文的速度必须足够快,而 ksoftirq 则可以有延迟。第 6 步就是 NGINX 通过 epoll_wait 拿到就绪的 socket,或者经由 read 或者 write 等函数拷贝报文数据。可见,在这个过程中,软中断对我们的消耗是可观的。


那么,当服务器 CPU 核心增多时,如何解决上述问题呢?Intel dpdk 加上用户态协议栈是一条可选的路径。如下图所示,dpdk 允许用户态进程直接从网卡上读取接收到的报文,或者拷贝数据到网卡来发送报文,绕过内核协议栈:



上图是腾讯 f-stack 给出的方案,它改造了 freebsd 操作系统的 TCP/IP 协议栈,对下通过 dpdk 与网卡交互,对上则以 POSIX API 的静态库形式,在 worker 进程内为传输层之上提供服务。


可以看到,由于每个 NGINX worker 进程内的 IP、TCP 协议栈都是独立的,所以当你修改 IP 地址时,不能使用操作系统的 ifconfig 或者 nmcli 命令,而是必须执行 f-stack 封装的 ff_ifconfig 命令,而且必须为每个 worker 进程分别执行脚本(使用 -p 指定进程 ID),因此,管理每个 worker 进程的配置一致性是比较复杂的。


熟悉 NGINX 的同学都知道,所有 worker 子进程之间的地位是相同的。然而到了上图中的方案时,情况就不一样了。


在多进程架构中,dpdk 要求必须分清主次,也就是第 1 个 fork 出的 worker 子进程是主进程,它必须负责管理大页内存(huge page,dpdk 必须使用这种管理模式,当然 dpdk 无锁内存池的设计非常高明!),而其他 worker 子进程则只是使用大页内存。这种设计导致 NGINX reload 模式会出问题,因为主 worker 退出、新建这段时间内,其他 worker 进程是不能提供服务的,这样 NGINX 的“热加载”功能就要大打折扣了。


worker 进程的数量与网卡的数量并不一致,因此 worker 进程间必须通过哈希算法,各自处理网卡上收到的报文。由于每个 worker 进程绑定了一颗 CPU 核心,所以图中的 cpu 队列等价于 worker 进程处理报文的队列(dpdk 是高性能网络框架,它只针对 CPU 设计独立的报文队列)。


上图中,当蓝色报文到达网卡 1 时,会根据 TCP 四元组(在 dpdk 初始化时可通过 f-stack.conf 配置)分发到 CPU1 队列。worker0 和 worker1 都会循环获取 CPU 队列中的报文,但 worker0 只取 CPU0 队列,所以 worker1 进程会获取到 CPU1 队列上的蓝色报文,经由进程内的独立 TCP/IP 协议栈(无须中断、无须加锁)处理完毕后,交由 NGINX 的 epoll、各 HTTP 模块处理。


可见,这种架构去除了软中断、系统调用、内核态用户态切换,大幅减少了内存拷贝次数,即使单 worker 性能也会有不少提升。但它的最大优势是多核心 CPU,尤其是 CPU 核心达到 32、64 甚至更高时,无锁化设计带来的优势非常明显,可以轻松达到百万级 CPS(每秒新建连接数)、上亿并发连接。


当然,这种带来高性能的独立协议栈设计,还引入了一个大麻烦:NGINX 作为负载均衡使用时,一个会话上的客户端、上游服务器报文都必须在同一个 worker 进程内处理,这是普通的哈希算法无法做到的,如下图所示:


关于此处精彩内容,您可以点击文章《分享实录 | NGINX 网络协议优化(下)》进行查看。


最后总结一下。今天我介绍了 HTTP 协议栈、TLS/SSL 协议栈和 TCP/IP 协议栈的优化思路,最终如何应用还要根据实际的应用场景来拍板,但取舍前一定要先了解当前协议栈的性能天花板在哪。好,谢谢大家,祝大家今天下午后面的旅程一切顺利!



更多资源


NGINX 唯一中文官方社区 ,尽在 nginx.org.cn

更多 NGINX 相关的技术干货、互动问答、系列课程、活动资源:

开源社区官网:https://www.nginx.org.cn/

微信公众号:https://mp.weixin.qq.com/s/XVE5yvDbmJtpV2alsIFwJg

用户头像

NGINX唯一中文官方社区 2022-07-04 加入

- 微信公众号:https://mp.weixin.qq.com/s/XVE5yvDbmJtpV2alsIFwJg - 微信群:https://www.nginx.org.cn/static/pc/images/homePage/QR-code.png?v=1621313354 - B站:https://space.bilibili.com/628384319

评论

发布
暂无评论
分享实录 | NGINX 网络协议优化(下)_nginx_NGINX开源社区_InfoQ写作社区