Dynamic TLS Record Size
背景知识
1:TCP 分段
网络报文对的格式一般都是 mac header + ip header + tcp header + tcp payload
由于端对端的网络之间存在不同的链路环境,一个报文传输过程中,经过不同的网络设备,不同的网络设备的MTU不通,即
允许传输 ip层数据(算上ip头)不超过MTU大小的报文,如果ip层数据大于MTU,则进行IP分片,将IP层的负载分在不同的ip头的传输。
TCP为了尽可能避免下层出现这种分片,需要通过TCP握手协商出一个MSS来限制自己的payload大小。
上图中,由于MSS限制,一个5000字节的数据调用send 接口发送后,被分成了4个TCP包。
2:考虑TCP负载为SSL的情况
上图中,一个完整的SSL封装,可能被放置在多个TCP负载中传输。
3:SSL发送:
流程图如上图所示不再赘述。
4:SSL接收:
SSL首先调用read 5字节的 SSL头,从头中读取len字段,该字段指示了SSL负载大小,于是SSL根据这个这个len 去read这个后续的数据,read完len指定的数据之后执行后续数据处理操作,例如解密等。
5:Record size
TLS协议总共包括2层协议,最底层为record procotol,其负载有 handshake protocol、alert procotol、change cipher protocol、application protocol。
上图中,SSL头+密文数据 这种,就是record protocol + application protocol这种模式。而动态record size的优化也是针对这种模式。
不考虑明文数据被加密后,长度的变化,即假设明文数据被加密后与密文数据一致,record size可以简单理解为 送入 SSL_write接口的明文数据的长度。而动态record size就是对于large的明文数据,进行分割,分批次送入SSL_write(和TCP协议栈根据MSS分段数据有异曲同工之妙)。
不同record size对性能的影响
1:小 record size
(1):问题显而易见,SSL头、SSL负载比肯定变大;其次依据小的record size分割大的数据,肯定增加调用SSL write的次数,重复执行部分代码;最后,使用过硬件加速的同学肯定知道,加解密卡中加解密的性能指标更多的是跟 次/秒 有关,即固定每秒 6000次/秒 运算,如果每次传入1B,那么吞吐就是6KB/s,如果每次传入16KB,那么吞吐就是93MB/s,前者显然没有充分利用硬件资源。
(2):好处就是,小record size,其产生的完整的SSL报文,可以存放在单个TCP负载中,即接收端收到一个TCP负载就可以进行解密(因为包含了完整的ssl payload)。在处理HTTP响应中,很多情况可以根据HTTP header中的信息就可以处理下一步了,而不需要等到实体到来。如果header 和 body 在一个SSL报文中,很可能因为body太大,导致SSL报文分在多个TCP负载中,接收端为了能够处理header,必须读完整个超大的SSL报文,才能进行解密。
2:大 record size
(1):解决了小 record size 中(1)中的问题。
(2):制造了小 record size 中(2)中的问题。
3:TCP的行为对record size 的要求
之前已经说过,如果一个SSL报文过大,将被放在多个TCP负载中传输,如果中间某个TCP包丢包,导致重传等情况产生,会严重影响接收端接受一个完整SSL报文的时间,进而影响响应速度。
其次TCP在连接初始阶段处于慢启动阶段,发包受限于cwnd(拥塞控制窗口),连接初始时cwnd为3,从一个round trip来看,其值增长的逻辑是“每收到n个ack就能发送2n个包”,从某一时刻来看,其增长的逻辑是 cwnd += ackd_packet_num,不管怎么样,连接初始阶段,TCP发包会被cwnd。这就导致如果一个SSL报文过大,假设他被放在了10个TCP负载中,那么发送端第一次只会发送3个。。。
4:对record size的取值
综上所述,record size 大小不是一成不变的,需要动态调整,cloudflare 的方案如下
https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency/
https://github.com/cloudflare/sslconfig/blob/master/patches/nginx__dynamic_tls_records.patch
4个变量控制
ssl_dyn_rec_size_lo
ssl_dyn_rec_size_hi
ssl_dyn_rec_threshold
ssl_dyn_rec_timeout
初始阶段,record size设置为ssl_dyn_rec_size_lo,发送完ssl_dyn_rec_threshold个包之后,record size设置为ssl_dyn_rec_size_hi;再次发送完ssl_dyn_rec_threshold之后,将record size设置为 ssl_buffer_size(nginx默认的size)。
如果 应用层过了ssl_dyn_rec_timeout个时间内,没法送过数据,将record size设置为为ssl_dyn_rec_size_lo,重复上述步骤。
ssl_dyn_rec_timeout是有必要的,TCP协议也有类似的操作,socket如果idle了一定时间,其cwnd也会被设置为初始值,cloudflare这么设置也契合了TCP的这个特性。
Cloudflare patch的问题
我觉得不是他们提供的方案有问题,而是可能有些东西不能对外宣布。
首先,ssl_dyn_rec_size_lo和ssl_dyn_rec_size_hi大小看到应该是渐变的而不是简单的翻一番。
其次,结果SSL握手之后,由于证书可能就好几kb,拥塞窗口早就扩大了,所以lo值肯定不是默认值13xx那么简单。
这些变量的值应该需要根据特定的网络、业务环节特定的进行设置才能起到所谓的优化。
如果HTTPS作为前端卸载用,即类似Nginx的SSL卸载,Nginx作为代理服务器,Nginx upstream收到的后端的包大小其实也是1k左右,这个功能基本没什么用,因为即使record size扩的再大,ngx_ssl_write_chain的入参数据也就1k。
但是如果机器开启了GRO,TRO等,收包时会将小包合并成大包,然后一并处理,这种情况下的反向代理,是有效果的。