qgy18

通过 TCP 优化 TLS 来减少延迟

原文链接: blog.cloudflare.com

通过 TCP 优化 TLS 来减少延迟

互联网的分层特性(HTTP 运行于某些可靠传输层之上,如 TCP;TCP 层运行于某些数据报层之上,如 IP;IP 层运行于某些链路层之上,如 Ethernet)在其发展过程中显得尤为重要。随着时间的推移,不同的链路层来来去去(还有在用 802.5 的读者吗?[译者注:诞生于上世纪八十年代])。互联网分层的灵活性意味着:从你浏览器发起的连接,可能会经过家里的 WiFi 网络,然后去往 DSL 线路,穿越光纤,最终经由以太网到达 Web 服务器。每一层都完全不需要关注下一层的实现。

但这个模型也有一些缺陷。在 TLS(用于在互联网传输加密数据最常见的标准,也就是你的浏览器用来访问 https:// 网站的协议)中,运行在 TCP 之上的 TLS 层可能会导致网页交付出现延迟。

这是因为 TLS 会把要传输的数据划分为固定(最大)大小的记录(Record),然后交给 TCP 来传输。TCP 立即将这些 TLS 记录划分为段(Segment),然后发送。最终,这些 TCP 段以 IP 包(Packet)的形式在互联网上传输。

为了防止互联网出现拥塞,确保交付可靠,TCP 在等待接收者确认之前发送的段被收到之前,只会发送有限数量的段。此外,TCP 还会保证将所有段按顺序交付给应用程序。因此,如果在发送者和接收者之间发生丢包,所有 TCP 段都会暂时缓存起来,在丢失的包被成功重传之前,应用程序无法收到缓存的数据。

TLS 和 TCP

对于 TLS,这意味着大型记录会被划分为多个 TCP 段,从而遇到意外延迟。TLS 只能处理完整的记录,因此丢失单个 TCP 段足以延迟整个 TLS 记录。

在 TCP 连接刚开始时,会产生 TCP 慢启动(Slow Start),这使得被划分为多个 TCP 段的 TLS 记录只能缓慢交付。在整个 TCP 连接中,组成 TLS 记录的任何一个 TCP 段的丢失,都会导致整个记录被延迟,直到丢失的段被重传。

因此,更好的做法是不要使用固定的 TLS 记录大小,而是根据 TCP 连接速度的提升(或者收到拥塞控制而下降)动态调整记录大小。在每个连接刚开始时,使用较小的记录大小,使之能与 TCP 能够发送的大小相匹配。一旦连接运行起来,就可以增加记录大小。

CloudFlare 使用 NGINX 处理 Web 请求,但 NGINX 不支持动态设置 TLS 记录大小。NGINX 默认使用 16KB 的固定值做为 TLS 记录大小,允许通过 ssl_buffer_size 配置进行修改。

NGINX 中的动态 TLS 记录

我们对 NGINX 进行了修改,增加了动态 TLS 记录大小的功能,补丁文件已经开源。大家可以在这里找到它。这个补丁给 NGINX 的 ssl 模块增加了一些参数:

ssl_dyn_rec_size_lo:初始 TLS 记录大小。默认为 1369 字节(设计为完全适配单个 TCP 段:1369 = 1500 - 40(IPv6) - 20(TCP) - 10(Time) - 61(TLS 最大开销))。

ssl_dyn_rec_size_hi:TLS 记录大小最大值。默认为 4229 字节(设计为适配 3 个 TCP 段)。

ssl_dyn_rec_threshold:改变记录大小前需要发送多少个记录。

每个连接最开始会使用 ssl_dyn_rec_size_lo 指定的记录大小;在发送完 ssl_dyn_rec_threshold 个记录后,记录大小会增加到 ssl_dyn_rec_size_hi;再以 ssl_dyn_rec_size_hi 发送 ssl_dyn_rec_threshold 个记录后,记录大小会增加到 ssl_buffer_size

ssl_dyn_rec_timeout:指定连接空闲多长时间(单位:秒)后,记录大小会减少到 ssl_dyn_rec_size_lo,再重复上面的逻辑。将这个值设置为 0,代表禁用动态 TLS 记录大小功能,连接会改为使用固定的 ssl_buffer_size 做为记录大小。

结论

希望大家觉得我们这个 NGINX 补丁有用,也很乐意收到来自使用者/改进者的反馈。