• home > theory > CST > network >

    web传输优化途径分析:TCP/IP/TLS、GZIP/Brotli等手段

    Author:zhoulujun Date:

    网络延迟是网络上的主要性能瓶颈之一TCP IP协议层面 http优化在最坏的情况下,客户端打开一个链接需要DNS查询(1个 RTT),TCP握手(1个

    网络延迟是网络上的主要性能瓶颈之一

    TCP/IP协议层面 http优化

    在最坏的情况下,客户端打开一个链接需要DNS查询(1个 RTT),TCP握手(1个 RTT),TLS 握手(2个RTT),以及最后的 HTTP 请求和响应,可以看出客户端收到第一个 HTTP 响应的首字节需要5个 RTT 的时间

    首字节时间对 web 体验非常重要,可以体现在网站的首屏时间,直接影响用户判断网站的快慢,所以首字节时间(TTFB)是网站和服务器响应速度的重要指标,下面我们来看影响 SSL 握手的几个方面:

    TCP_NODELAY

    小包的载荷率非常小,若网络上出现大量的小包,则网络利用率比较低,就像客运汽车,来一个人发一辆车,可想而知这效率将会很差,这就是典型的 TCP 小包问题。

    为了解决这个问题所以就有了 Nigle 算法,算法思想很简单,就是将多个即将发送的小包,缓存和合并成一个大包,然后一次性发送出去,就像客运汽车满员发车一样,这样效率就提高了很多,所以内核协议栈会默认开启 Nigle 算法优化。

    Night 算法认为只要当发送方还没有收到前一次发送 TCP 报文段的的 ACK 时,发送方就应该一直缓存数据直到数据达到可以发送的大小(即 MSS 大小),然后再统一合并到一起发送出去,如果收到上一次发送的 TCP 报文段的 ACK 则立马将缓存的数据发送出去。虽然效率提高了,但对于急需交付的小包可能就不适合了,比如 SSL 握手期间交互的小包应该立即发送而不应该等到发送的数据达到 MSS 大小才发送,所以,SSL 握手期间应该关闭 Nigle 算法,内核提供了关闭 Nigle 算法的选项: TCP_NODELAY

    TCP Delay Ack

    与 Nigle 算法对应的网络优化机制叫 TCP 延迟确认,也就是 TCP Delay Ack,这个是针对接收方来讲的机制,由于 ACK 包是有效 payload 比较少的小包,如果频繁的发 ACK 包也会导致网络额外的开销,同样出现前面提到的小包问题,效率低下,因此延迟确认机制会让接收方将多个收到数据包的 ACK 打包成一个 ACK 包返回给发送方,从而提高网络传输效率,跟 Nigle 算法一样,内核也会默认开启 TCP Delay Ack 优化。进一步讲,接收方在收到数据后,并不会立即回复 ACK,而是延迟一定时间,一般ACK 延迟发送的时间为 200ms(每个操作系统的这个时间可能略有不同),但这个 200ms 并非收到数据后需要延迟的时间,系统有一个固定的定时器每隔 200ms 会来检查是否需要发送 ACK 包,这样可以合并多个 ACK 从而提高效率,

    具体参看《再深谈TCP/IP三步握手&四步挥手原理及衍生问题—长文解剖IP 》

    TCP的延迟确认机制

    TCP在何时发送ACK的时候有如下规定:

    1. 当有响应数据发送的时候,ACK会随着数据一块发送

    2. 如果没有响应数据,ACK就会有一个延迟,以等待是否有响应数据一块发送,但是这个延迟一般在40ms~500ms之间,一般情况下在40ms左右,如果在40ms内有数据发送,那么ACK会随着数据一块发送,对于这个延迟的需要注意一下,这个延迟并不是指的是收到数据到发送ACK的时间延迟,而是内核会启动一个定时器,每隔200ms就会检查一次,比如定时器在0ms启动,200ms到期,180ms的时候data来到,那么200ms的时候没有响应数据,ACK仍然会被发送,这个时候延迟了20ms.

      这样做有两个目的。

      1. 这样做的目的是ACK是可以合并的,也就是指如果连续收到两个TCP包,并不一定需要ACK两次,只要回复最终的ACK就可以了,可以降低网络流量。

      2. 如果接收方有数据要发送,那么就会在发送数据的TCP数据包里,带上ACK信息。这样做,可以避免大量的ACK以一个单独的TCP包发送,减少了网络流量。

    3. 如果在等待发送ACK期间,第二个数据又到了,这时候就要立即发送ACK!

    按照TCP协议,确认机制是累积的。也就是确认号X的确认指示的是所有X之前但不包括X的数据已经收到了。确认号(ACK)本身就是不含数据的分段,因此大量的确认号消耗了大量的带宽虽然大多数情况下,ACK还是可以和数据一起捎带传输的,但是如果没有捎带传输,那么就只能单独回来一个ACK,如果这样的分段太多,网络的利用率就会下降。为缓解这个问题,RFC建议了一种延迟的ACK,也就是说,ACK在收到数据后并不马上回复,而是延迟一段可以接受的时间。延迟一段时间的目的是看能不能和接收方要发给发送方的数据一起回去,因为TCP协议头中总是包含确认号的,如果能的话,就将数据一起捎带回去,这样网络利用率就提高了。延迟ACK就算没有数据捎带,那么如果收到了按序的两个包,那么只要对第二包做确认即可,这样也能省去一个ACK消耗。由于TCP协议不对ACK进行ACK的,RFC建议最多等待2个包的积累确认,这样能够及时通知对端Peer,我这边的接收情况。Linux实现中,有延迟ACK(Delay Ack)和快速ACK,并根据当前的包的收发情况来在这两种ACK中切换:在收到数据包的时候需要发送ACK,进行快速ACK;否则进行延迟ACK(在无法使用快速确认的条件下也是)

    对于 SSL 握手来说,200ms 的延迟对用户体验影响很大

    开启 TCP 快启(TCP Fast Open)

    2014 年提出的 TCP 快启(TCP Fast Open,TFO)却可以在某些场景下通过一次通信建立 TCP 连接

    TCP 快启策略使用存储在客户端的 TFO Cookie 与服务端快速建立连接。

    TCP 快启(TCP Fast Open,TFO)

    TCP 连接的客户端向服务端发送 SYN 消息时会携带快启选项,服务端会生成一个 Cookie 并将其发送至客户端,客户端会缓存该 Cookie,当其与服务端重新建立连接时,它会使用存储的 Cookie 直接建立 TCP 连接,服务端验证 Cookie 后会向客户端发送 SYN 和 ACK 并开始传输数据,这也就能减少通信的次数。

    1. 客户端发送SYN包,包尾加一个FOC请求,只有4个字节。

    2. 服务端受到FOC请求,验证后根据来源ip地址声称cookie(8个字节),将这个COOKIE加载SYN+ACK包的末尾发送回去。

    3. 客户端缓存住获取到的Cookie 可以给下一次使用。

    4. 下一次请求开始,客户端发送SYN包,这时候后面带上缓存的COOKIE,然后就是正式发送的数据。

    5. 服务器端验证COOKIE正确,将数据交给上层应用处理得到相应结果,然后在发送SYN+ACK时,不再等待客户端的ACK确认,即开始发送相应数据。

    对于已经访问过的用户,TCP快起还是很有优势的。

    https协议优化

    https相比http必然带来更多的性能,只能说尽力优化。光是 TLS 握手就需要消耗两个 RTT(Round-Trip Time,往返时间),这就是造成 HTTPS 更慢的主要原因——使用了3个RTT的时间。三次TCP握手,四次TLS握手,其中一次同时进行(TCP第三次握手的时候,客户端发送支持的TLS协议版本,支持的加密套件列表,以及一些其它的参数。也开始了TLS握手)。当然,HTTPS 要求数据加密传输,加解密相比 HTTP 也会带来额外的开销,不过对称加密本来就很快,加上硬件性能越来越好,所以这部分开销还好。

    TLS False Start

    TLS False Start 是指客户端在发送 Change Cipher Spec Finished 同时发送应用数据(如 HTTP 请求),服务端在 TLS 握手完成时直接返回应用数据(如 HTTP 响应)。这样,应用数据的发送实际上并未等到握手全部完成,故谓之抢跑。这个过程如下图所示


    tls-handshake.pngTLS False Start

    可以看到,启用 False Start 之后,TLS 阶段只需要一次 RTT 就可以开始传输应用数据。False Start 相当于客户端提前发送加密后的应用数据,不需要修改 TLS 协议,目前大部分浏览器默认都会启用,但也有一些前提条件:

    • 服务端必须在 Server Hello 握手中通过 NPN(Next Protocol Negotiation,下一代协议协商,Google 在 SPDY 协议中开发的 TLS 扩展,用于握手阶段协商应用协议)或 ALPN(Application Layer Protocol Negotiation,应用层协议协商,NPN 的官方修订版)表明自己支持的 HTTP 协议,例如:http/1.1、http/2;

    • 使用支持前向安全性(Forward Secrecy)的加密算法。False Start 在尚未完成握手时就发送了应用数据,Forward Secrecy 可以提高安全性;

    Certificate Authorities 证书优化

    TLS 的身份认证是通过证书信任链完成的,浏览器从站点证书开始递归校验父证书,直至出现信任的根证书(根证书列表一般内置于操作系统,Firefox 自己维护)。站点证书是在 TLS 握手阶段,由服务端发送的。

    配置服务端证书链时,有两点需要注意:

    1. 证书是在握手期间发送的,由于 TCP 初始拥塞窗口的存在,如果证书太长可能会产生额外的往返开销;

    2. 如果证书没包含中间证书,大部分浏览器可以正常工作,但会暂停验证并根据子证书指定的父证书 URL 自己获取中间证书。这个过程会产生额外的 DNS 解析、建立 TCP 连接等开销,非常影响性能

    配置证书链的最佳实践是只包含站点证书和中间证书,不要包含根证书,也不要漏掉中间证书。大部分证书链都是「站点证书 - 中间证书 - 根证书」这样三级,这时服务端只需要发送前两个证书即可。但也有的证书链有四级,那就需要发送站点证书外加两个中间证书了。

    ECC Certificate

    如果需要进一步减小证书大小,可以选择 ECC(Elliptic Curve Cryptography,椭圆曲线密码学)证书。256 位的 ECC Key 等同于 3072 位的 RSA Key,在确保安全性的同时,体积大幅减小。下面是一个对比:

    ECC 证书这么好,为什么没有普及呢?最主要的原因是它的支持情况并不好。例如 Windows XP 不支持,导致使用 ECC 证书的网站在 Windows XP 上只有 Firefox 能访问(Firefox 证书那一套完全自己实现,不依赖操作系统)。另外,Android 平台也只有 Android 4+ 才支持 ECC 证书。所以,确定使用 ECC 证书前需要明确用户系统分布情况。

    Session Resumption(回话恢复机制)

    完整的 SSL 握手需要2个 RTT,会话复用的原理很简单,将第一次握手辛辛苦苦算出来的对称密钥存起来,后续请求中直接使用。这样可以节省证书传送等开销,也可以将 TLS 握手所需 RTT 减少到一个,如下图所示:

    TLS False Start

    可以看到会话复用机制生效时,双方几乎不怎么交换数据就协商好密钥了,这是怎么做到的呢?

    SSL Session 复用则只需要1个 RTT,大大缩短了握手时间,另外 Session 复用避免了密钥交换的 CPU 运算,大大降低 CPU 的消耗,所以服务器必须开启 Session 复用来提高服务器的性能和减少握手时间

    回话恢复机制(RFC 5246)在SSL2.0中被第一次引进。它允许服务端生成一个32字节的回话标示(session identifier)做为“ServerHello”消息的一部分。服务端和客户端分别缓存这个标示。当客户端再次创建SSL连接的时候,在进行TLS握手的时候把回话的id直接带过去,从而减少一次RT。

    SSL 中有两种 Session 复用方式:

    Session Identifier(服务端Cache )

    Session Identifier(会话标识符),是 TLS 握手中生成的 Session ID。服务端可以将 Session ID 协商后的信息存起来,浏览器也可以保存 Session ID,并在后续的 ClientHello 握手中带上它,如果服务端能找到与之匹配的信息,就可以完成一次快速握手。

    大概原理跟网页 SESSION 类似,服务端将上次完整握手的会话信息缓存在服务器上,然后将 session id 告知客户端,下次客户端会话复用时带上这个 session id,即可恢复出 SSL 握手需要的会话信息,然后客户端和服务器采用相同的算法即可生成会话密钥,完成握手。

    这种方式是最早优化 SSL 握手的手段,在早期都是单机模式下并没有什么问题,但是现在都是分布式集群模式,这种方式的弊端就暴露出来了,拿 CDN 来说,一个节点内有几十台机器,前端采用 LVS 来负载均衡,那客户端的 SSL 握手请求到达哪台机器并不是固定的,这就导致 Session 复用率比较低。所以后来出现了 Session Ticket 的优化方案,之后再细讲。那服务端 Session Cache 这种复用方式如何在分布式集群中优化呢,无非有两种手段:一是 LVS 根据 Session ID 做一致性 hash,二是 Session Cache 分布式缓存;第一种方式比较简单,修改一下 LVS 就可以实现,但这样可能导致 Real Server 负载不均,我们用了第二种方式,在节点内部署一个 redis,然后 Tengine 握手时从 redis 中查找是否存在 Session,存在则复用,不存在则将 Session 缓存到 redis 并做完整握手,当然每次与 redis 交互也有时间消耗,需要做多级缓存,这里就不展开了。核心的实现主要用到 ssl_session_fetch_by_lua_file 和 ssl_session_store_by_lua_file,在 lua 里面做一些操作 redis 和缓存即可。

    Session Ticket(客户端Cache)

    原理跟网页的 Cookie 类似,客户端缓存会话信息(当然是加密的,简称 session ticket),下次握手时将该 session ticket 通过 client hello 的扩展字段发送给服务器,服务器用配置好的解密 key 解密该 ticket,解密成功后得到会话信息,可以直接复用,不必再做完整握手和密钥交换,大大提高了效率和性能,(那客户端是怎么得到这个 session ticket 的呢,当然是服务器在完整握手后生成和用加密 key 后给它的)

    这种方式不需要服务器缓存会话信息,天然支持分布式集群的会话复用

    Session Identifier 机制有一些弊端,例如:

    • 负载均衡中,多机之间往往没有同步 Session 信息,如果客户端两次请求没有落在同一台机器上就无法找到匹配的信息;

    • 服务端存储 Session ID 对应的信息不好控制失效时间,太短起不到作用,太长又占用服务端大量资源。

    而 Session Ticket(会话记录单)可以解决这些问题,Session Ticket 是用只有服务端知道的安全密钥加密过的会话信息,最终保存在浏览器端。浏览器如果在 ClientHello 时带上了 Session Ticket,只要服务器能成功解密就可以完成快速握手。

    OCSP Stapling

    出于某些原因,证书颁发者有时候需要作废某些证书。那么证书使用者(例如浏览器)如何知道一个证书是否已被作废呢?通常有两种方式

    • 证书撤销名单:CRL(Certificate Revocation List)

      CRL 是由证书颁发机构定期更新的一个列表,包含了所有已被作废的证书,浏览器可以定期下载这个列表用于验证证书合法性。不难想象,CRL 会随着时间推移变得越来越大,而且实时性很难得到保证。

    • 在线证书状态协议:OCSP(Online Certificate Status Protocol)

      OCSP 是一个在线查询接口,浏览器可以实时查询单个证书的合法性。在每个证书的详细信息中,都可以找到对应颁发机构的 CRL 和 OCSP 地址。

    OCSP 的问题在于,某些客户端会在 TLS 握手阶段进一步协商时,实时查询 OCSP 接口,并在获得结果前阻塞后续流程,这对性能影响很大——OCSP查询本质是一次完整的HTTP请求-响应,这中间的DNS查询、建立TCP、服务端处理等环节都可能耗费很长时间,导致最终建立TLS连接时间变得更长。

    而 OCSP Stapling(OCSP 封套),是指服务端在证书链中包含颁发机构对证书的 OCSP 查询结果,从而让浏览器跳过自己去验证的过程。服务端有更快的网络,获取 OCSP 响应更容易,也可以将 OCSP 响应缓存起来——服务端主动获取OCSP查询结果并随证书一起发送给客户单,从而让客户端跳过自己验证的过程,提高TLS握手效率。

    OCSP 响应本身经过了数字签名,无法伪造,所以 OCSP Stapling 技术既提高了握手效率,也不会影响安全性。

    TLS Record Protocol

    TLS Record 是TLS发送的最小单元。可以通过Type字段,区别发送的TLS数据内容的类型,数据类型包括,握手数据,警告数据,和真正的数据。结构如下:

    典型的发送数据流程如下:

    1. 协议处理模块接收应用层的数据

    2. 把接收到的数据切分成多个数据块:每个记录最大2的14次方字节,或者最大16KB。

    3. 应用层数据可以选择压缩。

    4. 添加MAC(Message authentication code)或者 HMAC

    5. 使用协商的秘钥对数据加密

    上述步骤完成之后,加密的数据会向下传递给TCP层进行传输。在数据的接收端,流程和发送端类似,但是流程相反:使用协商的秘钥解密数据,验证MAC,把数据分发给上面的应用层。

    上传的这些工作都是由TLS层自己处理的,不需要应用层关注,对其是透明的。不管怎么样还是要注意一下这些细节:

    • 每个TLS记录最大16KB

    • 每个记录包含5字节的头信息, 一个 MAC信息(SSL3 20字节,TLS1和TLS1.1 32字节)和秘钥偏移。

    • 为了可以解密和验证记录,整个记录必须是完整的。

    选择一个合适的TLS记录大小,也将是一个非常重要的优化。记录太小,TLS记录的结构信息会成为非常大的负载,如果太大可能会导致等待完整的数据增加了延迟。应用程序开发者一般没有机会调整这个大小。如果自己实现TLS的话,请注意这个优化。

    TLS记录的大小调整

    一个TLS的记录最大16kb,在采用不同的加密算法的情况下,每个记录会包含20-40自己的头信息,M AC和其他可选的占用空间。如果一个TLS记录和一个TCP数据包匹配的话,还需要考虑IP包的20字节和TCP最少20字节的头信息。结果是有潜在的60-100字节的协议负载,如果是最大传输数单元差不多1500字节,至少6%的结构负载。


    记录太小,增加结构负载,如果记录太大,增加数据发送的延迟。


    Google选择的方式是:发送第1M的数据,TLS记录大小和TCP segment匹配,直到调整到16KB。并且TLS记录的大小会根据网络环境时时调整。并且会调整OpenSSL的缓存大小。




    升级http协议

    http每次协议的升级,从.9到现在3,几乎是重构了。每次版本更新都是有性能的提升

    升级HTTP2

    HTTP2主要有以下特性:

    1. 二进制分帧,数据使用二进制传输,相比于文本传输,更利于解析和优化。

    2. 多路复用,同一域名下的请求,共用同一条链路进行传输,有效节省消耗。

    3. 头部优化,将头部字段缓存为索引,客户端与服务端维护索引表,通信过程中尽可能采用索引进行通信,收到索引后查询索引表,才能解析出真正的头部信息

    更多可以参看《再谈HTTP2性能提升之背后原理—HTTP2历史解剖》,这里不再赘述

    HTTP2在TLS层的协议协商使用的是NPN(Next Protocol Negotation)协议或者ALPN(Application Layer Protocol Negotation)协议。ALPN和NPN的主要区别在于:谁来决定通信协议。在ALPN的描述中,是让客户端先发送一个协议优先级列表给服务器,由服务器最终选择一个合适的。而NPN则正好相反,客户端有着最终的决定权。客户端和服务端都支持NPN或ALPN协商,是用上HTTP/2的大前提

    升级http3

    http3 的优势无与伦比,参看《浅谈QUIC协议原理与性能分析及部署方案 》,也没有https 的ssl 证书问题的烦扰了


    内容压缩

    第一个就是本身的传输内容压缩,比如html/css/js 压缩,图片 视频压缩。

    gzip压缩,谷歌 推出的 brotli 压缩,cloudflare 最先跟进,国内的CDN没有多少行动。



    参看文章:

    优化 Tengine HTTPS 握手时间 https://zhuanlan.zhihu.com/p/77685619

    TLS 握手优化详解 https://imququ.com/post/optimize-tls-handshake.html

    HTTPS握手过程详解与优化方法 https://tanhuanpei.github.io/2019/01/29/HTTPS握手过程详解与优化方法/



    转载本站文章《web传输优化途径分析:TCP/IP/TLS、GZIP/Brotli等手段》,
    请注明出处:https://www.zhoulujun.cn/html/theory/ComputerScienceTechnology/network/2020_0614_8462.html