Home 《 Web 性能权威指南》阅读笔记-网络技术概览
Post
Cancel

《 Web 性能权威指南》阅读笔记-网络技术概览

延迟与带宽

WPO ( Web Performance Optimization ,Web性能优化)产业从无到有,快速增长,充分说明用户越来越重视速度方面的用户体验。速度是关键:

  • 网站越快,用户的黏性越高;
  • 网站越快,用户忠诚度更高;
  • 网站越快,用户转化率越高。 不仅限于网站,移动端也是相同的情况。 延迟和带宽是影响速度的两个关键因素。

延迟是 message 或 packet 从起点到终点经历的时间。这个定义简单明了,但却掩盖了很多有用的信息。事实上,任何系统都有很多因素可能影响传送消息的时间。

  • 传播延迟:消息从发送端到接收端需要的时间,是信号传播距离和速度的函数;
  • 传输延迟:把消息中的所有比特转移到链路中需要的时间,是消息长度和链路速率的函数;
  • 处理延迟:处理分组首部、检查位错误及确定分组目标所需的时间;
  • 排队延迟:到来的分组排队等待处理的时间。

我们都不习惯用毫秒来度量身边的事物,但研究表明:在软件交互中,哪怕 100~200 ms 左右的延迟,我们中的大多数人就会感觉到“拖拉”;如果超过了 300ms 的门槛,那就会说“反应迟钝”;而要是延迟达到 1000 ms( 1s ) 这个界限,很多用户就会在等待响应的时候分神,有人会想入非非,有人恨不得忙点别的什么事儿。要想给用户最佳的体验,而且保证他们全神贯注于手边的任务,我们的应用必须在几百毫秒之内响应。这几乎没有给我们——特别是网络,留出多少出错的余地。若要成功,必须认真对待网络延迟,在每个开发阶段都为它设立明确的标准。 CDN( Content Delivery Network ,内容分发网络)服务的用途很多,但最重要的就是通过把内容部署在全球各地,让用户从最近的服务器加载内容,大幅降低传播分组的时间。或许我们不能让数据传输得更快,但我们可以缩短服务器与用户之间的距离。把数据托管到CDN能够显著提高性能。

延迟中相当大的一部分往往花在了最后几公里,而不是在横跨大洋或大陆时产生的,这就是所谓的“最后一公里”问题。为了让你家或你的办公室接入互联网,本地ISP需要在附近安装多个路由收集信号,然后再将信号转发到本地的路由节点。连接类型、路由技术和部署方法五花八门,分组传输中的这前几跳往往要花数十毫秒时间才能到达ISP的主路由器。

由于无法突破物理条件上的限制,也就是说无法让传播速度跑得比光速更快。如果需要针对延迟采取优化措施,就必须从设计和优化协议及应用着手,并且时刻牢记光速的限制。可以减少往返、把数据部署到接近客户端的地方,以及在开发应用时通过各种技术隐藏延迟。

TCP 的构成

因特网有两个核心协议:IP和TCP。

  • IP ,即 Internet Protocol ,负责联网主机之间的路由选择和寻址;
  • TCP ,即 Transmission Control Protocol ,负责在不可靠的传输信道之上提供可靠的抽象层。 TCP/IP 也常被称为“因特网协议套件”( Internet Protocol Suite ),是由 Vint Cerf 和 BobKhan 在他们 1974 的论文“ A Protocol for Packet Network Intercommunication ”(一种分组网络互通的协议)中首次提出来的。 TCP 负责在不可靠的传输信道之上提供可靠的抽象层,向应用层隐藏了大多数网络通信的复杂细节,比如丢包重发、按序发送、拥塞控制及避免、数据完整,等等。采用 TCP 数据流可以确保发送的所有字节能够完整地被接收到,而且到达客户端的顺序也一样。也就是说, TCP 专门为精确传送做了优化,但并未过多顾及时间。 HTTP 标准并未规定 TCP 就是唯一的传输协议。如果你愿意,还可以通过 UDP (用户数据报协议)或者其他可用协议来发送 HTTP 消息。但在现实当中,由于 TCP 提供了很多有用的功能,几乎所有 HTTP 流量都是通过 TCP 传送的。

三次握手

客户端与服务器在交换应用数据之前,必须就起始分组序列号,以及其他一些连接相关的细节达成一致。出于安全考虑,序列号由两端随机生成。

E2266BA9-7F90-467D-B141-355D4371B4BE

  1. SYN客户端选择一个随机序列号x,并发送一个SYN分组,其中可能还包括其他TCP标志和选项;
  2. SYN ACK服务器给x加1,并选择自己的一个随机序列号y,追加自己的标志和选项,然后返回响应;
  3. ACK客户端给x和y加1并发送握手期间的最后一个ACK分组。

TCP 为什么需要三次握手可以看这篇文章为什么 TCP 建立连接需要三次握手 - 面向信仰编程 ,讲得非常详细。

TCP 快速打开, TCP Fast Open , Linux 3.7 及之后的内核已经在客户端和服务器中支持TFO,因此成为了客户端和服务器操作系统选型的有力候选方案。即便如此,TFO并不能解决所有问题。它虽然有助于减少三次握手的往返时间,但却只能在某些情况下有效。比如,随同SYN分组一起发送的数据净荷有最大尺寸限制、只能发送某些类型的HTTP请求,以及由于依赖加密cookie,只能应用于重复的连接。

流量控制

流量控制是一种预防发送端过多向接收端发送数据的机制。否则,接收端可能因为忙碌、负载重或缓冲区容量有限而无法处理。为实现流量控制,TCP连接的每一方都要通告自己的接收窗口( rwnd ),其中包含能够保存数据的缓冲区空间大小信息。

D49E4851-020F-48FE-AE93-316CBD423570

第一次建立连接时,两端都会使用自身系统的默认设置来发送 rwnd 。浏览网页通常主要是从服务器向客户端下载数据,因此客户端窗口更可能成为瓶颈。然而,如果是在上传图片或视频,即客户端向服务器传送大量数据时,服务器的接收窗口又可能成为制约因素。 假如窗口为零,则意味着必须由应用层先清空缓冲区,才能再接收剩余数据。这个过程贯穿于每个TCP连接的整个生命周期:每个ACK分组都会携带相应的最新rwnd值,以便两端动态调整数据流速,使之适应发送端和接收端的容量及处理能力。

慢启动

流量控制确实可以防止发送端向接收端过多发送数据,但却没有机制预防任何一端向潜在网络过多发送数据。换句话说,发送端和接收端在连接建立之初,谁也不知道可用带宽是多少,因此需要一个估算机制,然后还要根据网络中不断变化的条件而动态改变速度。 1988年, Van Jacobson 和 Michael J. Karels 撰文描述了解决这个问题的几种算法:慢启动、拥塞预防、快速重发和快速恢复。这4种算法很快被写进了 TCP 规范。事实上,正是由于这几种算法加入 TCP ,才让因特网在20世纪80年代末到90年代初流量暴增时免于大崩溃。 根据交换数据来估算客户端与服务器之间的可用带宽是唯一的方法,而且这也是慢启动算法的设计思路。首先,服务器通过 TCP 连接初始化一个新的拥塞窗口( cwnd )变量,将其值设置为一个系统设定的保守值(在 Linux 中就是 initcwnd )。 • 拥塞窗口大小( cwnd ): 
发送端对从客户端接收确认( ACK )之前可以发送数据量的限制。 新TCP连接传输的最大数据量取 rwnd 和 cwnd 中的最小值,而服务器实际上可以向客户端发送4个 TCP 段,然后就必须停下来等待确认。此后,每收到一个 ACK ,慢启动算法就会告诉服务器可以将它的 cwnd 窗口增加1个 TCP 段。每次收到 ACK 后,都可以多发送两个新的分组。 TCP 连接的这个阶段通常被称为“指数增长”阶段,因为客户端和服务器都在向两者之间网络路径的有效带宽迅速靠拢。

39D1C532-4E59-4FE4-95D1-4B86CB41A301

包括HTTP在内的很多应用层协议都运行在TCP之上,无论带宽多大,每个TCP连接都必须经过慢启动阶段。换句话说,我们不可能一上来就完全利用连接的最大带宽。相反,我们要从一个相对较小的拥塞窗口开始,每次往返都令其翻倍(指数式增长)。而达到某个目标吞吐量所需的时间,就是客户端与服务器之间的往返时间和初始拥塞窗口大小的函数。

89995F1E-9C72-4F42-9407-98622FBE7EEC

下面我们就来看一个例子,假设:

  • 客户端和服务器的接收窗口为 65 535 字节( 64 KB );
  • 初始的拥塞窗口:4 段( RFC 2581 );
  • 往返时间是 56 ms(伦敦到纽约); 先不管 64 KB 的接收窗口,新 TCP 连接的吞吐量一开始是受拥塞窗口初始值限制的。计算可知,要达到 64 KB 的限制,需要把拥塞窗口大小增加到 45 段,而这需要 224 ms :

0E162113-1481-46B4-8850-6DD2ACDC7B54

慢启动导致客户端与服务器之间经过几百毫秒才能达到接近最大速度的问题,对于大型流式下载服务的影响倒不显著,因为慢启动的时间可以分摊到整个传输周期内消化掉。 可是,对于很多HTTP连接,特别是一些短暂、突发的连接而言,常常会出现还没有达到最大窗口请求就被终止的情况。换句话说,很多Web应用的性能经常受到服务器与客户端之间往返时间的制约。因为慢启动限制了可用的吞吐量,而这对于小文件传输非常不利。 为演示三次握手和慢启动对简单HTTP传输的影响,我们假设纽约的客户端需要通过TCP连接向伦敦的服务器请求一个 20 KB 的文件,下面列出了连接的参数:

  • 往返时间: 56 ms ;
  • 客户端到服务器的带宽: 5 Mbps ;
  • 客户端和服务器接收窗口: 65 535 字节;
  • 初始的拥塞窗口: 4段( 4×1460 字节 ≈ 5.7 KB );
  • 服务器生成响应的处理时间:40 ms ;
  • 没有分组丢失、每个分组都要确认、GET请求只占1段。

C9BFCC70-68F3-411C-A260-91F99B8BC8FA

  • 0 ms:客户端发送 SYN 分组开始 TCP 握手。
  • 28 ms:服务器响应 SYN-ACK 并指定其 rwnd 大小。
  • 56 ms:客户端确认 SYN-ACK ,指定其 rwnd 大小,并立即发送 HTTP GET 请求。
  • 84 ms:服务器收到HTTP请求。
  • 124 ms:服务器生成20 KB的响应,并发送 4 个TCP段(初始 cwnd 大小为4),然后等待 ACK 。
  • 152 ms:客户端收到 4 个段,并分别发送A CK 确认。
  • 180 ms:服务器针对每个 ACK 递增 cwnd ,然后发送 8 个 TCP 段。
  • 208 ms:客户端接收 8 个段,并分别发送 ACK 确认。
  • 236 ms:服务器针对每个 ACK 递增 cwnd ,然后发送剩余的 TCP 段。
  • 264 ms:客户端收到剩余的 TCP 段,并分别发送 ACK 确认。

如果将 cwnd 值设置为 10 个 TCP 段,那么上图所示的过程将减少一次往返,性能可以提升22%。

通过新 TCP 连接在往返时间为 56 ms 的客户端与服务器间传输一个20 KB的文件需要 264 ms ,作为对比,现在假设客户端可以重用同一个 TCP 连接,再发送一次相同的请求。

DC4BAF2E-F57E-4343-B78F-5960FACE663C

  • 0 ms:客户端发送 HTTP 请求。
  • 28 ms:服务器收到 HTTP 请求。
  • 68 ms:服务器生成 20 KB 响应,但cwnd已经大于发送文件所需的 15 段了,因此一次性发送所有数据段。
  • 96 ms:客户端收到所有 15 个段,分别发送 ACK 确认。

同一个连接、同样的请求,但没有三次握手和慢启动,只花了 96 ms ,性能提升幅度达 275% 。以上两种情况下,服务器和客户端之间的 5 Mbps 带宽并不影响 TCP 连接的启动阶段。此时,延迟和拥塞窗口大小才是限制因素。

拥塞预防

认识到 TCP 调节性能主要依赖丢包反馈机制非常重要。换句话说,这不是一个假设命题,而是一个具体何时发生的命题。慢启动以保守的窗口初始化连接,随后的每次往返都会成倍提高传输的数据量,直到超过接收端的流量控制窗口,即系统配置的拥塞阈值( ssthresh )窗口,或者有分组丢失为止,此时拥塞预防算法介入。 拥塞预防算法把丢包作为网络拥塞的标志,即路径中某个连接或路由器已经拥堵了,以至于必须采取删包措施。因此,必须调整窗口大小,以避免造成更多的包丢失,从而保证网络畅通。

带宽延迟积

发送端和接收端理想的窗口大小,一定会因往返时间及目标传输速率而变化。 发送端和接收端之间在途未确认的最大数据量,取决于拥塞窗口( cwnd )和接收窗口( rwnd )的最小值。接收窗口会随每次 ACK 一起发送,而拥塞窗口则由发送端根据拥塞控制和预防算法动态调整。无论发送端发送的数据还是接收端接收的数据超过了未确认的最大数据量,都必须停下来等待另一方ACK确认某些分组才能继续。等待时间取决于往返时间。 BDP ( Bandwidth-delay product ,带宽延迟积)数据链路的容量与其端到端延迟的乘积。这个结果就是任意时刻处于在途未确认状态的最大数据量。 拥塞窗口和接收窗口会限制吞吐量,不管发送端和接收端的实际带宽为多大。好在窗口大小的协商与调节由网络栈自动控制,应该会自动调整。但尽管如此,窗口大小有时候仍然是TCP性能的限制因素。如果你怎么也想不通在高速连接的客户端与服务器之间,实际传输速度只有可用带宽的几分之一,那窗口大小很可能就是罪魁祸首。要么因为某一饱和端通告的接收窗口很小,要么因为网络拥堵和丢包导致拥塞窗口重置,更可能因为流量增长过快导致对连接吞吐量施加了限制。

队首阻塞

每个 TCP 分组都会带着一个唯一的序列号被发出,而所有分组必须按顺序传送到接收端。如果中途有一个分组没能到达接收端,那么后续分组必须保存在接收端的 TCP 缓冲区,等待丢失的分组重发并到达接收端。这一切都发生在 TCP 层,应用程序对 TCP 重发和缓冲区中排队的分组一无所知,必须等待分组全部到达才能访问数据。在此之前,应用程序只能在通过套接字读数据时感觉到延迟交付。这种效应称为TCP的队首( HOL,Head of Line )阻塞。

7C97E88A-C5EA-4250-A19B-E9B4C277D7

队首阻塞造成的延迟可以让我们的应用程序不用关心分组重排和重组,从而让代码保持简洁。然而,代码简洁也要付出代价,那就是分组到达时间会存在无法预知的延迟变化。这个时间变化通常被称为抖动,也是影响应用程序性能的一个主要因素。 无需按序交付数据或能够处理分组丢失的应用程序,以及对延迟或抖动要求很高的应用程序,最好选择UDP等协议。

针对 TCP 的优化建议

TCP是一个自适应的、对所有网络节点一视同仁的、最大限制利用底层网络的协议。因此,优化TCP的最佳途径就是调整它感知当前网络状况的方式,根据它之上或之下的抽象层的类型和需求来改变它的行为。 优化的核心原理和影响:

  • TCP 三次握手增加了整整一次往返时间;
  • TCP 慢启动将被应用到每个新连接;
  • TCP 流量及拥塞控制会影响所有连接的吞吐量;
  • TCP 的吞吐量由当前拥塞窗口大小控制。

服务器配置调优:

  • 增大 TCP 的初始拥塞窗口,加大起始拥塞窗口可以让 TCP 在第一次往返就传输较多数据,而随后的速度提升也会很明显。对于突发性的短暂连接,这也是特别关键的一个优化。
  • 慢启动重启,在连接空闲时禁用慢启动可以改善瞬时发送数据的长 TCP 连接的性能。
  • 窗口缩放,启用窗口缩放可以增大最大接收窗口大小,可以让高延迟的连接达到更好吞吐量。

应用程序行为调优: • 再快也快不过什么也不用发送,能少发就少发。 • 我们不能让数据传输得更快,但可以让它们传输的距离更短。 • 重用TCP连接是提升性能的关键。

性能检查清单: • 把服务器内核升级到最新版本(Linux:3.2+); • 确保cwnd大小为10; • 禁用空闲后的慢启动; • 确保启动窗口缩放; • 减少传输冗余数据; • 压缩要传输的数据; • 把服务器放到离用户近的地方以减少往返时间; • 尽最大可能重用已经建立的TCP连接。

UDP 的构成

UDP的主要功能和亮点并不在于它引入了什么特性,而在于它忽略的那些特性。UDP经常被称为无(Null)协议,RFC 768描述了其运作机制,全文完全可以写在一张餐巾纸上。 数据报( datagram )和分组( packet )是两个经常被人混用的词,实际上它们还是有区别的。分组可以用来指代任何格式化的数据块,而数据报则通常只用来描述那些通过不可靠的服务传输的分组,既不保证送达,也不发送失败通知。正因为如此,很多场合下人们都把 UDP 中 User (用户)的 U ,改成 Unreliable (不可靠)的 U ,于是 UDP 就成了“不可靠数据报协议”( Unreliable Datagram Protocol )。这也是为什么把 UDP 分组称为数据报更为恰当的原因。 UDP 最广泛的应用就是 DNS ,至于为什么 DNS 使用 UDP ,具体可以看这篇文章:为什么 DNS 使用 UDP 协议 - 面向信仰编程

无服务协议

IP 层的主要任务就是按照地址从源主机向目标主机发送数据报。为此,消息会被封装在一个 IP 分组内,其中载明了源地址和目标地址,以及其他一些路由参数。注意,数据报这个词暗示了一个重要的信息:IP 层不保证消息可靠的交付,也不发送失败通知,实际上是把底层网络的不可靠性直接暴露给了上一层。如果某个路由节点因为网络拥塞、负载过高或其他原因而删除了 IP 分组,那么在必要的情况下,IP的上一层协议要负责检测、恢复和重发数据。

BAA3E68D-FD92-4469-89AF-D8166B413807

UDP 协议会用自己的分组结构封装用户消息,它只增加了 4 个字段:源端口、目标端口、分组长度和校验和。这样,当IP把分组送达目标主机时,该主机能够拆开 UDP 分组,根据目标端口找到目标应用程序,然后再把消息发送过去。仅此而已。

64E7CA8E-1C0A-403B-9D67-6B4D1A080D31

事实上,UDP 数据报中的源端口和校验和字段都是可选的。IP 分组的首部也有校验和,应用程序可以忽略 UDP 校验和。也就是说,所有错误检测和错误纠正工作都可以委托给上层的应用程序。说到底,UDP仅仅是在IP层之上通过嵌入应用程序的源端口和目标端口,提供了一个“应用程序多路复用”机制。 UDP 特性总结如下:

  • 不保证消息交互,不确认,不重传,无超时。
  • 不保证交付顺序,不设置包序号,不重排,不会发生队首阻塞。
  • 不跟踪连接状态,不必建立连接或重启状态机。 • 不需要拥塞控制 ,不内置客户端或网络反馈机制。

TCP 是一个面向字节流的协议,能够以多个分组形式发送应用程序消息,且对分组中的消息范围没有任何明确限制。因此,连接的两端存在一个连接状态,每个分组都有序号,丢失还要重发,并且要按顺序交付。相对来说, UDP 数据报有明确的限制:数据报必须封装在 IP 分组中,应用程序必须读取完整的消息。换句话说,数据报不能分片。 UDP 是一个简单、无状态的协议,适合作为其他上层应用协议的辅助。实际上,这个协议的所有决定都需要由上层的应用程序作出。

UDP 与 NAT

IPv4 地址只有 32 位长,因而最多只能提供 42.9 亿个唯一IP地址。1990 年代初,互联网上的主机数量呈指数级增长,但不可能所有主机都分配一个唯一的 IP 地址。1994 年,作为解决 IPv4 地址即将耗尽的一个临时性方案,IP 网络地址转换器( NAT , Network Address Translator ) 规范出台了,这就是 RFC 1631。建议的 IP 重用方案就是在网络边缘加入 NAT 设备,每个 NAT 设备负责维护一个表,表中包含本地 IP 和端口到全球唯一(外网) IP 和端口的映射。这样,NAT设备背后的IP地址空间就可以在各种不同的网络中得到重用,从而解决地址耗尽问题。

AB32E125-33F3-4D39-A1B3-6B0A8C4791E6

为了避免路由错误和其它一些问题, IANA 为私有网络保留了三段 IP 地址,这些 IP 地址只能在 NAT 设备后面的内网中看到:

AA3FF07D-8C67-474F-B3C1-568E653CB302

连接状态超时

NAT 转换的问题(至少对于 UDP 而言)在于必须维护一份精确的路由表才能保证数据转发。 NAT 设备依赖连接状态,而 UDP 没有状态。这种根本上的错配是很多 UDP 数据报传输问题的总根源。 发送出站 UDP 不费事,但路由响应却需要转换表中有一个条目能告诉我们本地目标主机的 IP 和端口。因此,转换器必须保存每个 UDP 流的状态,而 UDP 自身却没有状态。 NAT 设备还被赋予了删除转换记录的责任,但由于 UDP 没有连接终止确认环节,任何一端随时都可以停止传输数据报,而不必发送通告。为解决这个问题, UDP 路由记录会定时过期。定时多长?没有规定,完全取决于转换器的制造商、型号、版本和配置。因此,对于较长时间的 UDP 通信,有一个事实上的最佳做法,即引入一个双向 keep-alive 分组,周期性地重置传输路径上所有 NAT 设备中转换记录的计时器。

对 TCP 的影响: 从技术角度讲, NAT 设备不需要额外的 TCP 超时机制。 TCP 协议就遵循一个设计严密的握手与终止过程,通过这个过程就可以确定何时需要添加或删除转换记录。遗憾的是,实际应用中的 NAT 设备给 TCP 和 UDP 会话应用了类似的超时逻辑。这样就导致 TCP 连接有时候也需要双向 keep-alive 分组。如果你的 TCP 连接突然断开,那很有可能就是中间NAT超时造成的。

NAT 穿透

更为严重的则是很多应用程序根本就不能建立 UDP 连接。尤其是 P2P 应用程序,涉及 VoIP 、游戏和文件共享等,它们客户端与服务器经常需要角色互换,以实现端到端的双向通信。 实现 UDP 传输的充分条件:

  1. 应用程序想与私有网络外部的主机通信,它就必须要知道自己的外网 IP 地址,否则则传输就会失败;
  2. NAT 设备的转换表中也需要有一个条目可以将外网 IP 的分组转换为内部主机的 IP 地址和端口号,否则无法确定将分组发送给哪台内部主机。

81DFEC0D-6BEE-43A7-A9C5-D037244EE722

针对 UDP 的优化建议

UDP 是一个简单常用的协议,经常用于引导其他传输协议。事实上, UDP 的特色在于它所省略的那些功能:连接状态、握手、重发、重组、重排、拥塞控制、拥塞预防、流量控制,甚至可选的错误检测,统统没有。这个面向消息的最简单的传输层在提供灵活性的同时,也给实现者带来了麻烦。你的应用程序很可能需要从头实现上述几个或者大部分功能,而且每项功能都必须保证与网络中的其他主机和协议和谐共存。 与内置流量和拥塞控制以及拥塞预防的 TCP 不同, UDP 应用程序必须自己实现这些机制。拥塞处理做得不到位的 UDP 应用程序很容易堵塞网络,造成网络性能下降,严重时还会导致网络拥塞崩溃。如果你想在自己的应用程序中使用 UDP ,务必要认真研究和学习当下的最佳实践和建议。 RFC 5405 就是这么一份文档,它对设计单播 UDP 应用程序给出了很多设计建议,简述如下:

  • 应用程序必须容忍各种因特网路径条件;
  • 应用程序应该控制传输速度;
  • 应用程序应该对所有流量进行拥塞控制;
  • 应用程序应该使用与 TCP 相近的带宽;
  • 应用程序应该准备基于丢包的重发计数器;
  • 应用程序应该不发送大于路径 MTU 的数据报;
  • 应用程序应该处理数据报丢失、重复和重排;
  • 应用程序应该足够稳定以支持 2 分钟以上的交付延迟;
  • 应用程序应该支持 IPv4 UDP 校验和,必须支持 IPv6 校验和;
  • 应用程序可以在需要时使用 keep-alive (最小间隔 15 秒)。

设计新传输协议必须经过周密的考虑、规划和研究,否则就是不负责任。 WebRTC 符合上述要求。

TLS

SSL 协议在直接位于TCP上一层的应用层被实现。SSL 不会影响上层协议(如 HTTP 、电子邮件、即时通讯),但能够保证上层协议的网络通信安全。在正确使用 SSL 的情况下,第三方监听者只能推断出连接的端点、加密类型,以及发送数据的频率和大致数量,不能实际读取或修改任何数据。

1E6D76AF-93C8-4E07-B0FC-5362808B4F7E

IETF 后来在标准化 SSL 协议时,将其改名为 Transport LayerSecurity ( TLS ,传输层安全)。很多人会混用 TLS 和 SSL ,但严格来讲它们并不相同,因为它们指代的协议版本不同。

加密,身份验证与完整性

TLS 协议的目标是为在它之上运行的应用提供三个基本服务:加密、身份验证和数据完整性。从技术角度讲,并不是所有情况下都要同时使用这三个服务。比如,可以接受证书但不验证其真实性,而前提是你非常清楚这样做有什么安全风险且有防范措施。实践中,安全的 Web 应用都会利用这三个服务:

  • 加密,混淆数据的机制;
  • 身份验证,验证身份标识有效性的机制;
  • 完整性,检测消息是否被篡改或伪造的机制;

TLS 握手

DAF247B6-7385-4BA1-A466-CC3F6B703AE7

协商内容包括 TLS 版本、加密套件, 必要时还会验证证书。 然而, 协商过程的每一步都需要一个 分组在客户端和服务器之间往返一次,因而所有 TLS 连接启动时都要经历 一定的延迟。握手过程: • 0 ms:TLS 在可靠的传输层( TCP )之上运行,这意味着首先必须完成 TCP 的“三次握手”,即一次完整的往返; • 56 ms:TCP 连接建立之后,客户端再以纯文本形式发送一些规格说明,比如它所运行的 TLS 协议的版本、它所支持的加密套件列表,以及它支持或希望使用的另外一些 TLS 选项; • 84 ms:然后, 服务器取得 TLS 协议版本以备将来通信使用,从客户端提供的加密套件列表中选择一个,再附上自己的证书,将响应发送回客户端。作为可选项,服务器也可以发送一个请求,要求客户端提供证书以及其他 TLS 扩展参数; • 112 ms:假设两端经过协商确定了共同的版本和加密套件, 客户端把自己的证书提供给了服务器。 然后, 客户端会生成一个新的对称密钥, 用服务器的公钥来加密, 加密后发送给服务器, 告诉服务器可以开始加密通信了。 到目前为止, 除了用服务器公钥加密的新对称密钥之外, 所有数据都以明文形式发送; • 140 ms:最后, 服务器解密出客户端发来的对称密钥, 通过验证消息的 MAC 检测消息完整性,再返回给客户端一个加密的“ Finished ”消息; • 168 ms:客户端用它之前生成的对称密钥解密这条消息, 验证 MAC , 如果一切 顺利,则建立信道并开始发送应用数据; 公钥加密系统只在建立 TLS 信道的会话中使用。在此期间,服务器向客户端提供它的公钥,客户端生成对称密钥并使用服务器的公钥对其加密,然后再将加密的对称密钥返回服务器。服务器继而用自己的私钥解密出客户端发来的对称密钥。接下来,客户端与服务器间的通信就全都使用客户端生成的共享密钥加密,这就是对称密钥加密。之所以这样设计,很大程度上是出于性能考虑,因为公钥加密需要很大的计算量。

ALPN

两端在传输数据时需要提前确定使用什么协议,指定端口号 ( HTTP 是 80 , TLS 是 443 )。 HTTP 规范为了协商协议规定了一个 Upgrade 首部,但是如果使用 Upgrade 需要一次额外的往返时间。于是可以在 TLS 握手阶段同时协商确定协议,应用层协议协商( ALPN, Application LayerProtocol Negotiation )作为 TLS 扩展,让我们能在 TLS 握手的同时协商应用协议,从而省掉了 HTTP 的 Upgrade 机制所需的额外往返时间。具体来说,整个过程分如下几步:客户端在 ClientHello 消息中追加一个新的 ProtocolNameList 字段,包含自己支持的应用协议;服务器检查 ProtocolNameList 字段,并在 ServerHello 消息中以 ProtocolName 字段返回选中的协议。服务器可以从中选择一个协议名,否则如果不支持其中的任何协议,则断开连接。只要 TLS 握手完成、建立了加密信道并就应用协议达成一致,客户端与服务器就可以立即通信。

服务器名称指示 ( SNI )

如果服务器想在一个 IP 地址为多个站点提供服务,而每个站点都拥有自己的 TLS 证书。为了解决这个问题, SNI ( Server Name Indication ,服务器名称指示)扩展被引入 TLS 协议,该扩展允许客户端在握手之初就指明要连接的主机名。 Web 服务器可以检查 SNI 主机名,选择适当的证书,继续完成握手。

TLS 会话恢复

完整 TLS 握手会带来额外的延迟和计算量,从而给所有依赖安全通信的应用造成严重的性能损失。为了挽回某些损失, TLS 提供了恢复功能,即在多个连接间共享协商后的安全密钥。

会话标识符

最早的“会话标识符”( Session Identifier , RFC 5246 )机制是在 SSL 2.0 中引入的,支持服务器创建 32 字节的会话标识符,并在完整的TLS协商期间作为其“ ServerHello ”消息的一部分发送。在内部,服务器会为每个客户端保存一个会话 ID 和协商后的会话参数。相应地,客户端也可以保存会话 ID 信息,并将该 ID 包含在后续会话的“ ClientHello ”消息中,从而告诉服务器自己还记着上次握手协商后的加密套件和密钥呢,这些都可以重用。假设客户端和服务器都可以在自己的缓存中找到共享的会话 ID 参数,那么就可以进行简短握手。否则,就要重新启动一次全新的会话协商,生成新的会话 ID 。

9332B027-EAAB-403F-ABDB-B7E554952E54

借助会话标识符可以节省一次往返,还可以省掉用于协商共享加密密钥的公钥加密计算。由于重用了之前协商过的会话数据,就可以迅速建立一个加密连接,而且同样安全。由于每个打开的TLS连接都要占用内存,因此需要一套会话 ID 缓存和清除策略,对于拥有很多服务器而且为获得最佳性能必须使用共享TLS会话缓存的热门站点而言,部署这些策略绝非易事。

会话记录单

为了解决上述服务器端部署TLS会话缓存的问题,“会话记录单”( Session Ticket,RFC 5077 )机制出台了,该机制不用服务器保存每个客户端的会话状态。相反,如果客户端表明其支持会话记录单,则服务器可以在完整 TLS 握手的最后一次交换中添加一条“新会话记录单”( New Session Ticket )记录,包含只有服务器知道的安全密钥加密过的所有会话数据。然后,客户端将这个会话记录单保存起来,在后续会话的 ClientHello 消息中,可以将其包含在 SessionTicket 扩展中。这样,所有会话数据只保存在客户端,而由于数据被加密过,且密钥只有服务器知道,因此仍然是安全的。无状态恢复机制的优点主要是消除了服务器端的缓存负担,通过要求客户端在与服务器建立新连接时提供会话记录单简化了部署(除非记录单过期)。

信任链与证书颁发机构

身份验证是建立每个TLS连接必不可少的部分。毕竟,加密信道两端可以是任何机器,包括攻击者的机器。为此,必须确保我们与之交谈的计算机是可信任的,否则之前的工作都是徒劳。 Web以及浏览器中的身份验证与上述过程相同,这就意味着此时此刻你应该问自己:我的浏览器信任谁?我在使用浏览器的时候信任谁?这个问题至少有三个答案。

  • 手工指定证书,所有浏览器和操作系统都提供了一种手工导入信任证书的机制。至于如何获得证书和验证完整性则完全由你自己来定。
  • 证书颁发机构 CA( Certificate Authority ,证书颁发机构)是被证书接受者(拥有者)和依赖证书的一方共同信任的第三方。
  • 浏览器和操作系统,每个操作系统和大多数浏览器都会内置一个知名证书颁发机构的名单。因此,你也会信任操作系统及浏览器提供商提供和维护的可信任机构。

C96A1E10-8803-4F58-A280-3837367586E6

证书撤销

有时候,出于种种原因,证书颁发者需要撤销或作废证书,比如证书的私钥不再安全、证书颁发机构本身被冒名顶替,或者其他各种正常的原因,像以旧换新或所属关系更替等。 为确保信任链不被破坏,通信的任何一端都可以根据嵌入的指令和签名检查链条中每个证书的状态。 CRL ( Certificate Revocation List ,证书撤销名单)是 RFC 5280 规定的一种检查所有证书状态的简单机制:每个证书颁发机构维护并定期发布已撤销证书的序列号名单。这样,任何想验证证书的人都可以下载撤销名单,检查相应证书是否榜上有名。如果有,说明证书已经被撤销了。 CRL 文件本身可以定期发布、每次更新时发布,或通过 HTTP 或其他文件传输协议来提供访问。这个名单同样由证书颁发机构签名,通常允许被缓存一定时间。实践中,这种机制效果很好,但也存在一些问题: • CRL 名单会随着要撤销的证书增多而变长,每个客户端都必须取得包含所有序列号的完整名单; • 没有办法立即更新刚刚被撤销的证书序列号,比如客户端先缓存了 CRL ,之后某证书被撤销,那到缓存过期之前,该证书将一直被视为有效。 为解决 CRL 机制的上述问题, RFC 2560 定义了 OCSP ( Online Certificate Status Protocol ,在线证书状态协议),提供了一种实时检查证书状态的机制。与 CRL 包含被撤销证书的序列号不同, OCSP 支持验证端直接查询证书数据库中的序列号,从而验证证书链是否有效。总之, OCSP 占用带宽更少,支持实时验证。 • 证书颁发机构必须处理实时查询; • 证书颁发机构必须确保随时随地可以访问; • 客户端在进一步协商之前阻塞 OCSP 请求; • 由于证书颁发机构知道客户端要访问哪个站点,因此实时 OCSP 请求可能会泄露客户端的隐私。

TLS 记录协议

交付应用数据的典型流程如下。 • 记录协议接收应用数据。 • 接收到的数据被切分为块:最大为每条记录214 字节,即 16 KB。 • 压缩应用数据(可选)。 • 添加 MAC( Message Authentication Code )或 HMAC 。 • 使用商定的加密套件加密数据。 以上几步完成后,加密数据就会被交给 TCP 层传输。接收端的流程相同,顺序相反:使用商定的加密套件解密数据、验证 MAC 、提取并把数据转交给上层的应用。

针对 TLS 的优化建议

  • 公钥加密与私钥加密,公钥加密与对称加密相比,需要更大的计算工作量。因此,在 Web 发展早期,通常都需要专门的硬件来进行“ SSL 卸载”。好在现在不这样了。现代硬件突飞猛进的发展为减小这种损失提供了强力支持,原先需要专门硬件来做的工作,今天直接通过CPU就能完成。 如果说重用 TCP 连接对于非加密通信是一个重要的优化手段,那么这个手段对运行在 TLS 上的应用同样至关重要。换句话说,只要能省掉握手,就应该省掉。如果必须握手,那么还有一个可能的技巧:尽早完成。
  • CDN 加速,做到尽早完成的最简单方式,就是在世界各地的服务器上缓存或重复部署数据和服务,而不要让所有用户都通过跨海或跨大陆光缆连接到一个中心原始服务器。当然,这正是 CDN ( Content Delivery Networks ,内容分发网络)服务的内容:通过使用本地代理服务器分流负载等手段降低延迟。虽然 CDN 最常用于在全球优化分发静态资源,但其优点并不止于此。距离客户端更近的服务器还可以缩短 TLS 会话,因为 TCP 和 TLS 握手的对象都是近处的服务器,所以建立连接的总延迟就会显著减少。相应地,本地代理服务器则可以与原始服务器建立一批长期的安全连接,全权代理请求与响应。
  • 会话缓存和无状态恢复,由于会话记录单还是相对新的 TLS 扩展,并非所有客户端都支持它。实践中,为了取得最优结果,应该做好两手准备:在支持的客户端中使用会话记录单,而在不支持的客户端中使用会话标识符。这两种手段不会相互干扰,而是会很好地协同工作。
  • TLS 记录大小调整,小记录会造成浪费,大纪录会导致延迟。
  • TLS 压缩。
  • 证书链的长度。

性能检查清单

  • 要最大限制提升 TCP 性能,请参考2.5节“针对TCP的优化建议”;
  • 把 TLS 库升级到最新版本,在此基础上构建(或重新构建)服务器;
  • 启用并配置会话缓存和无状态恢复;监控会话缓存的使用情况并作出相应调整;
  • 在接近用户的地方完成 TLS 会话,尽量减少往返延迟;
  • 配置TLS记录大小,使其恰好能封装在一个TCP段内;
  • 确保证书链不会超过拥塞窗口的大小;从信任链中去掉不必要的证书,减少链条层次;
  • 禁用服务器的 TLS 压缩功能;
  • 启用服务器对 SNI 的支持;
  • 启用服务器的 OCSP 封套功能;追加 HTTP 严格传输安全首部。
This post is licensed under CC BY 4.0 by the author.

MemorySafety

《 Web 性能权威指南》阅读笔记-HTTP

Comments powered by Disqus.