TCP 优化

Linux系统内核参数优化

[root@www ~]# cat /etc/redhat-release;uname -r
CentOS Linux release 7.5.1804 (Core) 
3.10.0-862.11.6.el7.x86_64
TLS 的下一层是 TCP 协议,所以对 TCP 的优化是可以直接影响到 TLS 的性能效率。
在对 TCP 的优化中,主要涉及以下几个概念:
(1)拥塞控制(congestion control)机制:在一个 TCP 连接开始时,不知道对方的速度有多快。如果有足够大的带宽,服务器端可以用最快的速度传送数据,但是如果对方的网络很慢,服务器发送的数据太多的话,会压跨连接,导致连接中断。所以,每个 TCP 连接都有一个称为拥塞窗口(cwnd = congestion window)的速度极限。这个窗口最初较小,在通信的过程中,如果双方都能接受这个速度,那么会加大这个拥塞窗口的值(初期增长很快,翻倍增长),这种机制被叫做慢启动(slow start)。
拥塞控制机制对于 TLS 连接的影响比较大,TLS 握手消耗了宝贵的初始连接字节(当拥塞窗口较小时);如果拥塞窗口足够大,那么慢启动不会有额外的延迟。但是,如果握手消息长度超过了拥塞窗口大小,发送方将必须把这个长信息拆分成两块,先发送一块,等待确认(1个往返),增加拥塞窗口,然后再发送剩下的部分。这样就增加了由 TLS 握手造成的延时。
(2) 慢启动阈值 ssthresh(避免 cwnd 增长过快,网络无法承担,造成丢包)
如果 cwnd 小于 ssthresh,表示在慢启动阶段,cwnd是翻倍增长的;如果 cwnd 大于 ssthresh,那么表示在拥塞避免阶段,这时候 cwnd 不再像慢启动阶段那样翻倍增长,而是线性增长,尽量避免网络拥塞。
(3) 接收窗口(rwnd),用来表示最多能保存多少数据,实际中接收窗口rwnd的合理值取决于BDP的大小,也就是带宽和延迟的乘积。如果带宽是 80Mbps,延迟是 100ms,那么计算过程如下:
BDP = 80Mbps * 100ms = (80 / 8) * (100 / 1000) = 1MB = 1024KB = 1048576B
TCP 用16位来记录窗口大小,也就是说最大值是64KB,如果超过这个值,就需要 tcp_window_scaling 机制(默认是开启)。配置内核参数中接收缓冲的大小,就可以控制接收窗口的大小:
net.ipv4.tcp_rmem = <MIN> <DEFAULT> <MAX>
Linux本身有一个缓冲大小自动调优的机制,窗口的实际大小会自动在最小值和最大值之间变化,找到性能和资源的平衡点。确认缓冲大小自动调优机制(0:关闭、1:开启):sysctl -a | grep tcp_moderate_rcvbuf。如果缓冲大小自动调优机制设置成关闭状态,那么就把缓冲的 DEFAULT 值设置为 BDP;如果缓冲大小自动调优机制设置成开启状态,那么就把缓冲的 MAX 设置为 BDP。
(4) 存储 TCP 连接本身一些信息的额外开销:net.ipv4.tcp_adv_win_scale 的值可能是 1 或者 2,如果是 1 的话,则表示二分之一的缓冲被用来做额外开销,如果是 2 的话,则表示四分之一的缓冲被用来做额外开销。按照这个逻辑,缓冲最终的合理值的具体计算方法如下:result=BDP / (1 – 1 / 2^tcp_adv_win_scale)。
(5) 空闲连接回到慢启动:慢启动在一段时间内没有任何流量的连接上起作用,达到降低速度的效果,并且速度下降非常快。所谓的“一段时间”可以是非常小的,比如1秒钟,但在实际场景中,每一个长连接(例如使用 HTTP 长连接)的速度都有可能被调到很慢!为了保持速度建议禁用这个功能。
在 Linux 上,可以在连接空闲时禁用慢启动: 0表示否,1表示开启慢启动,默认是1
可以通过一下命令使其临时生效,但重启以后就失效了,查看:sysctl -a | gerp slow_start_after_idle
临时:sysctl -w net.ipv4.tcp_slow_start_after_idle=0
永久生效:将 net.ipv4.tcp_slow_start_after_idle=0 设置添加到 /etc/sysctl.conf 配置文件中。
对拥塞窗口(cwnd)初始值调优:
启动速度限制被称为初始拥塞窗口(initial congestion window, initcwnd )。2013年4月发布的 RFC6928,google 建议默认情况下初始拥塞窗口设置为10个 MSS(约15 KB)。【Centos 7默认是10MSS】早期的建议是使用2或4个MSS(约3—6KB)。MSS 是 TCP 层上的概念,大小是 1460 字节。IP 层上是 MTU,1500字节。
[root@www ~]# sysctl -a |grep ssthresh
net.ipv4.tcp_max_ssthresh = 0   #在虚拟机中
[root@www ~]# sysctl -a |grep tcp_window_scaling
net.ipv4.tcp_window_scaling = 1
[root@www ~]# cat /proc/sys/net/ipv4/tcp_rmem    # rwnd值
4096    87380   6291456
[root@www ~]# sysctl -a |grep tcp_moderate_rcvbuf        
net.ipv4.tcp_moderate_rcvbuf = 1
[root@www ~]# sysctl -a |grep adv_win_scale
net.ipv4.tcp_adv_win_scale = 1
[root@www ~]# sysctl -a |grep start_after_idle
net.ipv4.tcp_slow_start_after_idle = 1

# 设置cwnd
[root@www ~]# ip route
default via 172.16.216.2 dev ens33 
169.254.0.0/16 dev ens33 scope link metric 1002 
172.16.216.0/24 dev ens33 proto kernel scope link src 172.16.216.188 
[root@www ~]# ip route | while read p; do ip route change $p initcwnd 10; done
[root@www ~]# ip route
default via 172.16.216.2 dev ens33 initcwnd 10 
169.254.0.0/16 dev ens33 scope link metric 1002 initcwnd 10 
172.16.216.0/24 dev ens33 proto kernel scope link src 172.16.216.188 initcwnd 10
# initcwnd 10:初始化cwnd
# 单方面提升发送端 cwnd 的大小并不一定有效,因为网络中实际传输的未经确认的数据大小取决于  rwnd 和 cwnd 中的最小值,所以一旦接收方的 rwnd 比较小的话,会抑制 cwnd 的发挥。

# 设置initrwnd(linux kernel 2.6.33 and newer)
[root@www ~]# ip route
default via 172.16.216.2 dev ens33 
169.254.0.0/16 dev ens33 scope link metric 1002 
172.16.216.0/24 dev ens33 proto kernel scope link src 172.16.216.188 
[root@www ~]# ip route | while read p; do ip route change $p initrwnd 10; done 
[root@www ~]# ip route
default via 172.16.216.2 dev ens33 initrwnd 10 
169.254.0.0/16 dev ens33 scope link metric 1002 initrwnd 10 
172.16.216.0/24 dev ens33 proto kernel scope link src 172.16.216.188 initrwnd 10 

# 一些系统的rwnd值:
# Linux 2.6.32		                                3*MSS (usually 4380)
# Linux 3.0.0		                                10*MSS (usually 14600)
# Windows NT 6.1 	(Windows 7 or Server 2008 R2)	8192 ^ 2
  • 优化 tcp time_wait ,减少time_wait 状态的连接。主动关闭的一方会出现time_wait状态。
time_wait 状态的连接要等待2个 MSL 的时间才会 close,会占用资源,尽量避免连接进入 time_wait 状态。linux 里 MSL一般是30秒,2 个MSL 是1分钟,这个数值是硬编码在内核中的,除非重新编译内核,否则没法修改。注:MSL最长报文生命周期:Maximum Segment Lifetime,MSL 。
修改 fin_wait2 的值,减少 fin_wait2 的等待时间,超时以后会回收连接。
开启长连接:绝大多是浏览器在开启长连接的情况下,接收到服务器断开连接的fin以后,会恢复一个 ack;而不会不发送自己这一端的 fin ,这样服务器一端就会等待 fin_timeout 时间后,回收连接。
若不开启长连接,服务器端关闭链接以后,链接的状态会从 fin_wait2 转换到 time_wait 。
还可以考虑促使客户端关闭链接,配置 keepalive_timeout 20s 10s; (nginx 配置),使客户端的超时小于服务器端,浏览器会先关闭链接,这样time_wait 状态就会在客户端,不过通过实验看出只有火狐浏览器支持,狐火浏览器会识别 Keep-Alive: timeout=time 这个参数,而别的浏览器不会。
不要设置回收 recycle=1 和 重用 reuse=1,NAT模式下会造成连接失败( SYN 包不会被响应)
time_wait 状态的连接被重用(reuse)的条件是如下2个之一:
1)初始序列号比time_wait状态的老连接最末的序列号大。
2)如果使用时间戳,那么新到来的连接的时间戳比老连接的时间戳大。
tcp_tw_reuse和tcp_tw_recycle要生效,必须 tcp_timestamps 是开启的,默认也是开启的。
  • 参数优化
net.ipv4.tcp_max_syn_backlog = 1024 #SYN队列的长度,默认是1024,加大队列到8192或更大,可缓存更多等待的网络连接。
net.ipv4.tcp_max_tw_buckets = 180000 #保存 TIME_WAIT 状态的套接字的最大数量,一旦超过这个数,TIME_WAIT套接字将立刻被清除,并发出警告。
net.ipv4.ip_local_port_range = 1024 65535 # 向外连接的端口范围。缺省值:32768到61000,可以扩大 1024 到 65535。
net.ipv4.tcp_syncookies = 1 #开启SYN Cookies,SYN等待队列溢出时,使用cookies来处理,可防范少量SYN攻击。
net.ipv4.tcp_retries2 = 15 #TCP失败重传的次数 ,默认15 ,可以调小一些,例如5。
还可以配置用于 TCP/IP 链接所使用的内存,配置总内存的话,单位是“页” ,具体的一个页的大小可以通过 getconf PAGE_SIZE 这个命令获取;读写所占用的内存单位是字节。
[root@www ~]# getconf PAGE_SIZE
4096
总内存
net.ipv4.tcp_mem = 93408 124544 186816
写(缓冲)
net.ipv4.tcp_wmem = 4096 16384 3985408
读(缓存)
net.ipv4.tcp_rmem = 4096 87380 3985408
[root@www ~]# cd /proc/sys/net/ipv4
[root@www ipv4]# cat tcp_fin_timeout 
60
[root@www ~]# sysctl -a |grep timestamps
net.ipv4.tcp_timestamps = 1

initcwnd

ip-sysctl

TLS 协议优化

  • 对TLS协议进行安全和速度调优
1.密钥交换
使用TLS最大的成本除了延迟以外(多了2次往返),就是用于安全参数协商的CPU密集型操作,也就是密钥交换(key exchange)。密钥交换的CPU消耗很大程度上取决于服务器选择的私钥算法、密钥长度和密钥交换算法。
破解密钥的难度取决于密钥的长度,密钥越长越安全。但是也要考虑加密与解密所消耗的计算资源。目前有两种私钥算法可以使用:RSA和ECDSA。
现在RSA算法的密钥仍然大量存在,即使使用它进行密钥交换的时候不支持前向加密。但是RSA还是可以用在身份认证上,当前RSA密钥算法推荐最小长度2048位,并且考虑升级到3072位(虽然升级后效率下降较多),随着RSA密钥的增长它开始变得越来越慢。ECDSA会快很多,越来越多的站点支持ECDSA,中等长度256位的ECDSA提供与3072位RSA一样的安全性,却有更好的性能。
推荐优先使用:ECDSA256_ECDHE256 与 RSA2048_ECDHE256 。

2. 证书
  • 证书链
a、TLS握手的时候,服务器端会把证书链发送给客户端进行验证。
b、证书链尽可能短。
c、证书链要完整。
d、尽量使用椭圆曲线证书链。
  • 证书吊销检查与OCSP服务
虽然证书吊销状态在不断变化,并且客户端(浏览器)对如何检查证书吊销差异很大,但作为服务器端,要做到尽可能快的传递吊销信息。
  • 使用带OCSP信息的证书。
OCSP被设计用于提供实时查询,允许客户端访问吊销信息。因此查询简短而快速(1个HTTP请求)。相比之下CRL是一个包含大量被吊销证书的列表。一些客户端只有当OCSP信息不可用的时候才下载CRL,在下载CRL的时候浏览器与服务器端的通信将暂停,直到CRL下载完成,所消耗的时间可能会有几十秒。
  • 选择具有快速且可靠的OCSP响应程序的CA
不同CA之间的OCSP响应速度也不同。缓慢和错误的OCSP响应程序会潜在地导致性能下降。在决定使用OCSP响应之后,要考察CA对OCSP响应的性能与正确性。另一个选择CA的标准是看更新OCSP响应的速度,最好自己的证书一经颁发就加入到OCSP响应程序中,一旦出了安全隐患被吊销,OCSP响应也能迅速的更新。
[root@www ~]# openssl s_client -connect www.openssl.org:443 -status |grep -i ocsp
depth=1 C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
verify error:num=20:unable to get local issuer certificate
OCSP response: 
OCSP Response Data:
    OCSP Response Status: successful (0x0)
    Response Type: Basic OCSP Response

3. 会话恢复
TLS理解两种类型的握手:完整握手和简短握手。理论上完整握手只会在客户端与服务器建立TLS会话(TLS session)的时候进行一次。后续的连接,双方使用简短握手恢复之前协商的会话。简短握手因为不需要密钥交换与密钥生成等操作,所以会更快,并且少一次往返时间。
4.TLS的纪录协议造成的在网络传输中的额外开销
TLS协议的最小传输单位是一个TLS记录,它最多可以包含2^14=16384字节(16K)的数据。一条记录在不加密的情况下只有很小的开销;每个记录以5字节的元数据开头,即内容类型(1字节)、协议版本(2字节)和数据长度(2字节)。流加密、分组加密和已验证密码套件加密后的TLS记录的额外开销。

尽量避免发送小包数据。尽量缓冲应用层数据避免额外的网络开销。
5.对称加密对CPU资源的消耗
加密操作有明显的CPU成本,成本由加密算法、加密模式和完整性校验算法三者决定。
6. TLS 记录的缓存延迟
TLS记录是TLS发送和接收数据的最小单位。TLS记录的大小与下一层TCP包的大小并不匹配,一个全尺寸的TLS记录16 KB需要被拆分成许多小的TCP包(大约12个),通常每个小于1.5 KB(1.3KB)。整个TLS记录被分成小的TCP包后,各个小包会陆续到达,但在全部到齐之前是无法进行解密处理的。这是因为TLS记录同样是数据解密和完整性检验的最小单位。缓存延迟有时可能会比较大。
虽然通过TCP协议可以把丢失和延迟的数据包恢复,但仍然需要消耗一次往返。每一次额外的往返对于整个TLS记录都意味着延迟。 初始拥塞窗口另一个触发额外往返的延迟是在连接初期发送大量数据导致初始拥塞窗口溢出。一旦拥塞窗口满了,发送端必须等待响应(1次往返),等到拥塞窗口增加再发送更多数据。
如果Web服务器支持TLS记录调整,就应该考虑将默认值(16 KB这么大的数值)改成更为合理的值,调整这个值由部署的密码套件和相应的传输开销决定,一般情况下设置成4 KB。如果将TLS记录大小设置为与TCP/IP包准确匹配,那就设置成1400字节左右,然后通过观察数据包逐步调整。IP报文理论上最大是65535个字节,是很大的,但是由于IP分片效果很不好,所以TCP在三次握手中互相得知对方的MSS(MTU减IP头部),不给IP层很大块的数据,避免IP数据报分片
例如,数据链路层最大传输单元(maximum transfer unit,MTU)是1500字节,那么可以预见:
1,500 bytes MTU 去除额外开销,所传数据 1379 —1419 bytes 。
-20 bytes IPv4 herder | - 40 bytes IPv6 header
- 32 bytes TCP header TCP 头部 最小是20字节可拓展的是40字节,最大为60字节
- 29 bytes TLS record | - 49 bytes TLS record

MSS 是1460 bytes :1460 - 32 - 29|49 = 1379 — 1399 bytes
首先MTU的值是变化的。虽然多数客户端继承以太网1500字节的限制,但也有一些协议支持更大的数据。比如,巨型帧(jumbo frame)允许多达9000字节。还有就是使用IPv4和IPv6(IPv4头是20字节,IPv6头是40字节)计算会略有不同,所密码套件的变化也会影响这个数值。
另一个问题是减小TLS记录的大小会增加传输开销,也就是吞吐量会下降。如果将TLS记录长度调大(最大16K),那么由于是加密的数据,得要所有的数据(所有的IP包)都到齐了,才会顺利的解密出明文,等待的时间会较长,吞吐率是上去了,响应的实时性就下降了。nginx上也有配置这个值的选项,只是不能动态调整。