网络延迟是网络上的主要性能瓶颈之一。在最坏的状况下,客户端打开一个连接须要DNS查询(1个 RTT),TCP握手(1个 RTT),TLS 握手(2个RTT),以及最后的 HTTP 请求和响应,能够看出客户端收到第一个 HTTP 响应的首字节须要5个 RTT 的时间,而首字节时间对 web 体验很是重要,能够体如今网站的首屏时间,直接影响用户判断网站的快慢,因此首字节时间(TTFB)是网站和服务器响应速度的重要指标,下面咱们来看影响 SSL 握手的几个方面:前端
咱们知道,小包的载荷率很是小,若网络上出现大量的小包,则网络利用率比较低,就像客运汽车,来一我的发一辆车,可想而知这效率将会不好,这就是典型的 TCP 小包问题,为了解决这个问题因此就有了 Nigle 算法,算法思想很简单,就是将多个即将发送的小包,缓存和合并成一个大包,而后一次性发送出去,就像客运汽车满员发车同样,这样效率就提升了不少,因此内核协议栈会默认开启 Nigle 算法优化。Night 算法认为只要当发送方尚未收到前一次发送 TCP 报文段的的 ACK 时,发送方就应该一直缓存数据直到数据达到能够发送的大小(即 MSS 大小),而后再统一合并到一块儿发送出去,若是收到上一次发送的 TCP 报文段的 ACK 则立马将缓存的数据发送出去。虽然效率提升了,但对于急需交付的小包可能就不适合了,好比 SSL 握手期间交互的小包应该当即发送而不该该等到发送的数据达到 MSS 大小才发送,因此,SSL 握手期间应该关闭 Nigle 算法,内核提供了关闭 Nigle 算法的选项: TCP_NODELAY,对应的 tengine/nginx 代码以下: nginx
须要注意的是这块代码是2017年5月份才提交的代码,使用老版本的 tengine/nginx 须要本身打 patch。web
与 Nigle 算法对应的网络优化机制叫 TCP 延迟确认,也就是 TCP Delay Ack,这个是针对接收方来说的机制,因为 ACK 包是有效 payload 比较少的小包,若是频繁的发 ACK 包也会致使网络额外的开销,一样出现前面提到的小包问题,效率低下,所以延迟确认机制会让接收方将多个收到数据包的 ACK 打包成一个 ACK 包返回给发送方,从而提升网络传输效率,跟 Nigle 算法同样,内核也会默认开启 TCP Delay Ack 优化。进一步讲,接收方在收到数据后,并不会当即回复 ACK,而是延迟必定时间,通常ACK 延迟发送的时间为 200ms(每一个操做系统的这个时间可能略有不一样),但这个 200ms 并不是收到数据后须要延迟的时间,系统有一个固定的定时器每隔 200ms 会来检查是否须要发送 ACK 包,这样能够合并多个 ACK 从而提升效率,因此,若是咱们去抓包时会看到有时会有 200ms 左右的延迟。可是,对于 SSL 握手来讲,200ms 的延迟对用户体验影响很大,以下图:
9号包是客户端的 ACK,对 7号服务器端发的证书包进行确认,这两个包相差了将近 200ms,这个就是客户端的 delay ack,这样此次 SSL 握手时间就超过 200ms 了。那怎样优化呢?其实只要咱们尽可能少发送小包就能够避免,好比上面的截图,只要将7号和10号一块儿发送就能够避免 delay ack,这是由于内核协议栈在回复 ACK 时,若是收到的数据大于1个 MSS 时会当即 ACK,内核源码以下:
知道了问题的缘由所在以及如何避免,那就看应用层的发送数据逻辑了,因为是在 SSL 握手期间,因此应该跟 SSL 写内核有关系,查看 openssl 的源码:
默认写 buffer 大小是 4k,当证书比较大时,就容易分屡次写内核,从而触发客户端的 delay ack。
接下来查看 tengine 有没有调整这个 buffer 的地方,还真有(下图第903行):
那不该该有 delay ack 啊……
无奈之下只能上 gdb 大法了,调试以后发现果真没有调用到 BIO_set_write_buffer_size,缘由是 rbio 和 wbio 相等了,那为啥之前没有这种状况如今才有呢?难道是升级 openssl 的缘由?继续查 openssl-1.0.2 代码:
openssl-1.1.1 的 SSL_get_wbio 有了变化:
缘由终于找到了,使用老版本就没有这个问题。就不细去看 bbio 的实现了,修复也比较简单,就用老版本的实现便可,因此就打了个 patch:
从新编译打包后测试,问题获得了修复。使用新版 openssl 遇到一样问题的同窗能够在此地方打 patch。redis
完整的 SSL 握手须要2个 RTT,SSL Session 复用则只须要1个 RTT,大大缩短了握手时间,另外 Session 复用避免了密钥交换的 CPU 运算,大大下降 CPU 的消耗,因此服务器必须开启 Session 复用来提升服务器的性能和减小握手时间,SSL 中有两种 Session 复用方式:算法
这种方式是最先优化 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 和缓存便可。浏览器
Tengine 开启 Session Ticket 也很简单:缓存
ssl_session_tickets on; ssl_session_timeout 48h; ssl_session_ticket_key ticket.key; #须要集群内全部机器的 ticket.key 内容(48字节)一致
(全文完)服务器
原文连接
本文为云栖社区原创内容,未经容许不得转载。网络