19

怎么把网站升级到QUIC以及QUIC特性分析

 6 years ago
source link: https://www.yinchengli.com/2018/06/10/quic/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

QUIC是什么?QUIC是谷歌推出的一套基于UDP的传输协议,它实现了TCP + HTTPS + HTTP/2的功能,目的是保证可靠性的同时降低网络延迟。因为UDP是一个简单传输协议,基于UDP可以摆脱TCP传输确认、重传慢启动等因素,建立安全连接只需要一的个往返时间,它还实现了HTTP/2多路复用、头部压缩等功能。它是一个很大的协议,全称是Quick UDP Internet Connection.

为什么要使用UDP呢?因为TCP是系统内核实现的,如果升级TCP协议,就得让用户升级系统,这个的门槛比较高,而QUIC在UPD基础上由客户端自由发挥,只要有服务器能对接就可以、比较遗憾的是现在主流的代理服务Nginx/Apache都没有实现QUIC,一些比较小众的代理服务如Caddy就实现了。

另外,笔者参加了5月份在上海的WebRTConf,很多讲师都认为QUIC并没有达到谷歌宣传的效果,并且国内运营商有些是屏蔽掉UDP传输的。不过使用UDP本身不是大问题,因为很多游戏如农药、吃鸡的传输协议也是基于UDP封装的,运营商应该只是屏蔽掉了UDP的部分端口如500/4500。

至于QUIC的性能怎么样,我们不妨自已实验一下。怎么在自己的网站开启QUIC呢?

一、开启QUIC

大部分人的网站应该是使用Nginx或者Apache做的服务代理,作用是可以让一台服务器开启多个网站,并支持http/https等协议,业务服务如PHP等就不用去关心类似于怎么建立https连接、加密数据、怎么添加gzip压缩之类的问题了。但是它们都不支持QUIC,可能它们认为这个协议现在还不是特别成熟,迭代比较快,整个协议比较复杂,实现起来比较累。。。

所以需要再加一个 Caddy ,它支持QUIC,那是不是说我们得抛弃nginx,改换Caddy。并不用,我们用它来处理QUIC服务就行了,其它的还是走nginx。因为nginx是监听在TCP的443和80端口,我们可以让Caddy只监听在UDP的443端口,两者不冲突。如下图,用netstat命令输出http/https端口占用情况:

YFNnQzN.png!web

问题在于,你启动Caddy之后,如果给它指定了https的域名,它必定要占用tcp:443端口,而tcp:443已经被nginx占用了,所以它就启动不了了:

$ sudo ./caddy -quic -conf ./conf
Activating privacy features... done.
2018/06/09 10:03:25 listen tcp :443: bind: address already in use

所以这个解决方法在网上搜了一下,有些人是使用一个docker容器,把Caddy跑在里面和nginx的环境隔离开,只把udp:443端口给docker使用,这样就没问题了。但是新开一个docker成本有点高,如果之前没玩过的话。

所以我就想到有没有办法去改它源码,不要让它监听tcp:443,然后重新编译一个呢?试了一下,果然可以,这个方法相应比较简单。

首先,需要安装go和github,如果之前没有装过的话,因为它是使用Golang写的,使用CentOS系统可以这样:

yum install go
yum install github

然后下载源码:

go get github.com/mholt/caddy/caddy
go get github.com/caddyserver/builds

编译安装:

cd ~/go/src/github.com/mholt/caddy/caddy
go run build.go

就会在当前目录下生成一个可执行文件caddy,编译速度非常快,半分钟不到就好了。然后改下源码,在:

caddy/caddyhttp/httpserver/server.go

在这个文件的 Listen这个函数里面,把其中一行注释掉,换成:

// ln, err := net.Listen("tcp", s.Server.Addr)
ln, err := net.Listen("tcp", "127.0.0.1:61234")

就是把原本的443地址换成一个没用的地址,然后重新编译,就能在已经开了nginx的情况下正常启动Caddy了,它需要一个配置文件,如下conf文件所示:

https://www.yinchengli.com
tls /etc/letsencrypt/live/www.yinchengli.com/fullchain.pem /etc/letsencrypt/live/www.yinchengli.com/privkey.pem
root /home/blog/
gzip
fastcgi / 127.0.0.1:9000 php {
    root /home/blog/
}
 
rewrite {
r .*
ext /
to /index.php?{query}
}

指定tls的证书,通过fastcgi指令把服务代理到本地的9000端口的php服务,并指定wordpress所在的目录,还可以开启gzip/server push这些东西。然后启动Caddy:

$ sudo ./caddy -quic -conf ./conf 
Activating privacy features... done.
https://www.yinchengli.com

为了能让Caddy变成一个守护进程运行在后台,可以使用nohup命令:

nohup sudo ./caddy -quic -conf ./conf  >/dev/null 2>&1 &

通过netstat可以看到,已经成功在udp开启443服务了:

VRNzMbr.png!web

浏览器怎么知道网站支持QUIC呢?需要我们给nginx添加一个http头部:

location / {
     add_header alt-svc 'quic=":443"; ma=2592000; v="39"';
}

浏览器还是先用TCP发起http请求,如果检测到这个头部:

qauq2eI.png!web

就会切换到QUIC,但是它要是发现QUIC连接不可建立,就还是会继续使用TCP连接,所以不用担心QUIC不可用的问题。我们可以装一个 HTTP/2 and SPDY inspector ,如果是网站是使用QUIC协议的话,闪电就会变成绿色(普通的HTTP/2是蓝色):

VVz6nuv.png!web

或者是打开 chrome://net-internals/#quic 也是会显示使用QUIC的网站,如下图所示:

aeINjmV.jpg!web

可以看到,我们已经成功开启了QUIC。并且发现谷歌搜索、谷歌邮箱等谷歌的主要服务都使用了QUIC,国内的主要有腾讯云提供支持,QQ空间就是用的QUIC.

现在比较一下QUIC和TCP方式的加载速度,加载同一个网页,禁掉缓存,连续刷新5次,如下图所示:

MneqMva.png!web

我们发现,似乎在网络比较好的情况下,QUIC似乎并没有太大的提升。那是不是说只有在网络比较差,丢包比较厉害的情况下QUIC才能发挥它的优势?实际上QUIC并不是专门针对抗丢包设计的,它的目的是做为一种通用传输协议。当然,丢包在网络传输也是一个很重要的话题。

二、QUIC协议

Chromium文档 里介绍了使用QUIC的优点,有以下5点:

  1. 通过减少往返次数,以缩短连接建立时间
  2. 使用一种新的ACK确认机制(包含了NACK),达到更好的拥塞控制
  3. 多路复用,并解决HTTP/2队头阻塞问题,即一个流的TCP包丢失导致所有流都暂停组装。在QUIC里面,一个流的包丢失只会影响当前流,不会影响其它流。
  4. 使用FEC(前向纠错)恢复丢失的包,以减少超时重传
  5. 使用一个随机数标志一个连接,取代传统IP + 端口号的方式,使得切换网络环境如从4G到wifi仍然能使用之前的连接。

我们将围绕这几个点研究一下QUIC协议。

1. 建立连接

QUIC只需要一次往返就能建立HTTPS连接,如 下图 所示:

7biy2q6.png!web

左一表示TCP连接的3次握手需要100ms,左二表示建立HTTPS连接,首次建立需要300ms,已经建立过的需要200ms(算上TCP的三次握手),左三使用QUIC第一次建立只需要100ms,如果建立过了不需要再确认直接0ms.

我们再一次实验一下,QUIC建立连接时间是否会比使用TCP的短,首先是使用TCP的时间,如下图所示:

V7FNjqN.png!web

可以看到初始化建立连接的平均时间为60ms左右,建立SSL连接时间为30ms左右,这个SSL时间是包含在了初始化建立时间里面。

再看一下开启QUIC的:

InUzyqA.png!web

初始化建立连接时间和SSL握手时间是一样的,在这一组实验里面平均时间仅为23ms,所以可以看到这个QUIC的连接建立延迟还是有比较大的提升。

那么QUIC连接建立的过程是怎么样的呢?怎么实现一次往返就要包含TCP的三次握手以及建立HTTPS连接的功能?

我们先回忆一下HTTPS建立握手的过程,如下图所示:

ze2qq2u.png!web

这个过程我在《 https连接的前几毫秒发生了什么 》做了详细介绍。这个过程最少需要3个RTT时间,上图省略了一些ACK的确认。QUIC的一个RTT是怎么做到的呢?如下图所示:

iE3Ujmr.png!web

QUIC的报文是使用key/value的键值对表示的,key是32位字符串,例如Client Hello用CHLO表示,这4个字母在ASCII表分别为:0x43、0x48、0x4c、0x4f,所以key值为4348 4c4f,共4个字节32位。如果key值只有3个字母,最后一位就为NULL(0)。

第1步客户端发送一个CHLO报文,这个报文里面包含了想要连接的域名和客户端支持的加密套装,还有一个STK,它是Source Address Token的缩写,这个是服务端下发的,第一次连接客户端是没有这个的。

第2步服务端收到Client Hello之后,会发一个Reject响应,由于缺少必要的信息所以拒绝连接,并且会下发一个第1步提到的STK,这个用来区分不同的IP,主要是做为防止DDOS攻击的一个手段,因为DDOS攻击通常会使用假的IP地址,导致TCP连接第三次握手失败,服务端会一直等待最后一个ACK报文,但是由于源IP是一个假的IP,永远等不到,所以就消耗了连接池的资源,当黑客通过病毒等方式控制了很多台计算机之后,每台计算机在同一个时间段不断地伪造源IP发起握手请求,就会导致服务端处理不过来直接瘫痪了。但是在QUIC里面由于服务端根据不同的IP下发一个token,客户端在发起连接请求的时候需要带上这个token,如果token不对的话,服务端可直接拒绝连接,这样DDOS的不断地变换IP就没效了。

除了下发STK,还有一个Server Config,这个Server Config里面包含了服务端支持的密钥交换算法列表和每个算法对应的公钥,客户端决定它想要用的密钥交换算法后取出相应的公钥,再加上服务端的随机数Server Nonce和客户端自己生成的随机数,使用这个3个数生成一把加密数据的主密钥。所以在这个RTT之后,客户端就可以开始加密和发送数据了,就达到了一个RTT就能发数据的目的。在以后建立的连接,只要有这个Server Config就能直接发数据,达到再次连接0RTT的目的。这个Server Config有一个效期,超过有效期需要重新获取。但是这样会有一个问题,就是每次交换的公钥是一样的,就会有 重放攻击Replay Attack )的危险,所以这个虽然可以发数据了,但是需要有第3步确保后续的操作更加安全。Chrome限制了在这个阶段发送的数据类型只能是GET类型的。

第3步客户端发送一个完整的CHLO,除了第一步有的key/value之外,还新加了客户端的随机数nonce、密钥交换算法和公钥。

第4步服务端收到之后再返回一个实时生成的公钥给客户端,客户端收到之后重新生成主密钥,用新的主密钥加密数据。QUIC里面的公钥是可以动态变换的,例如1分钟就换一次。

这一整个就是一个比较完整的QUIC的连接建立过程了,更详细的说明可以见 谷歌文档

在实际的传输中,观察到0s的连接建立:

NvIbIvb.png!web

这个应该就是QUIC的0RTT了。

2. 新的ACK确认机制

其中包括NACK的确认机制,NACK是Negative ACK的意思,意思是接收方告知发送方哪些包丢了。在普通的TCP里面,如果发送方收到三个重复的ACK就会触发快速重传,如果太久没收到ACK就会触发超时重传,而使用NACK可以直接告知发送方哪些包丢了,不用等到超时重传。TCP有一个SACK的选项,也具备NACK的功能,QUIC的NACK有一个区别它每次重传的报文序号都是新的。

QUIC的ACK帧包含了NACK区,如下图所示:

bAziieA.png!web

比较详细的QUIC ACK确认机制本篇不再深入讨论。

3. 解决多路复用队头阻塞问题

HTTP/2里的多路复用就是指多个HTTP请求共用一个TCP连接,以减少TCP连接数,达到复用高速信道的作用。但是由于它是基于TCP,会导致如果有一个包丢了,那么TCP的接收窗口就不会往右移动了,直到那个包重传补上了才能往右移动。由于QUIC是基于UDP自已实现的,所以想怎么搞就怎么搞,就可以手动解决这个问题,所有流都连续接收,丢的包的那个流不能够组装,但是其它流不会影响到。

4. FEC前向纠正拥塞控制

FEC是Forward Error Correction前向错误纠正的意思,就是通过多发一些冗余的包,当有些包丢失时,可以通过冗余的包恢复出来,而不用重传。这个算法在多媒体网关拥塞控制有重要的地位。QUIC的FEC是使用的XOR的方式,即发N + 1个包,多发一个冗余的包,在正常数据的N个包里面任意一个包丢了,可以通过这个冗余的包恢复出来,使用异或可以做到:

let
E = XOR(A, B, C, D)
then 
-A = XOR(B, C, D, E)
-B = XOR(A, C, D, E)
-C = XOR(A, B, D, E)
-D = XOR(A, B, C, E)

E是一个冗余的包,如果A、B、C、D任意一个包丢失都可以借助E计算恢复出来,这里面可能会涉及到一些矩阵运算,具体过程不深入探讨。这种XOR FEC的优点是计算相对简单,缺点是只能只有一个包丢的情况才能恢复,丢了两个就恢复不了了。

5. 切换网络操持连接

经常会有从4G切换到wifi网络或者是从wifi切换到4G网络的场景,由于网络的IP变了,导致需要重新建立连接,而QUIC使用一个ID来标志连接,即使切换网络也可以使用之前的建立连接的数据如交换的密钥,而不用再重新HTTPS握手,不过切换的过程可能会导致有些包丢了,需要利用FEC恢复或者重传。

三、小结

本文介绍了使用Caddy结合nginx提供QUIC服务,需要改一下Caddy的源码,让Caddy只监听在UDP的443端口不要的正常的nginx占用的tcp:443产生冲突。由Caddy支持QUIC服务,而Nginx还是提供普通的HTTP/2服务。

为什么要升级到QUIC呢,因为QUIC是改良版的TCP + HTTPS + HTTP/2,我们可以在实践中使用一下收集一些数据,做一些比较。QUIC是一个很大的协议,本文没有面面俱到,主要是介绍了QUIC握手的过程,重点分析了1个RTT是怎么实现的,还介绍了QUIC使用的NACK和FEC,以达到更好的拥塞控制。

更多关于QUIC的内容可以看Chromium的文档 : QUIC, a multiplexed stream transport over UDP .

Post Views: 13


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK