上周做了一个《QUIC 协议解析》的部门分享。主要介绍了:
- 什么是 QUIC
- 为什么是 QUIC
- QUIC 应用现状
PPT在这里,需者自取。
随着微服务架构以及的广泛普及,很多公司都会使用或者自行开发自己的API Gateway, 甚至在内部服务也会应用Service Mesh.
不久前,看到了一篇Dropbox公司介绍其内部服务代理Bandaid的文章: Meet Bandaid, the Dropbox service proxy. 不得不说设计细节一贯独角兽风格,非常有收获,对于改进我们自己设计的proxy 也有一定参考意义。这里做一个简单的读后笔记。英语好的同学可以直接阅读原文。(插一句,Dropbox不久前在美股上市了,在中美贸易战中为数不多逆势上涨的股票之一,玩美股的同学可以关注一下。再插一句:股市有风险,投资须谨慎。)
Bandaid 是有公司内部的反向代理服务演变而来,使用Golang实现。反向代理有很多成熟的解决方案,之所以选择自行开发主要有以下几个原因:
上面的大部分因素根我们进行微服务组件开发时候的考量基本一致。这也是我们当初没有使用Go kit 这种工具套装进行架构微服务改造的顾虑,当然,那个时候还没有这些工具链。
丰富的负载均衡策略,以及对HTTP/2和gRPC的支持实例亮点。要知道,nginx 从 1.13.10才开始支持gRPC.
另一方面,从支持的特性看,如果要求不是特别多,直接用来作为Service Mesh也是相当不错的。而且由于是使用Go开发,对于本来就在使用Go作为技术栈的团队来说,无论是使用还是二次开发,门槛和学习成本都是很低的。

接收的请求按照LIFO 后入先出的方式进行处理。这个设计有点反直觉,但却是合理的:


Worker采取固定大小的工作池设计,一方面可以精确的控制并发数量,另一方面,也避免频繁创建worker的开销。不过文中也承认,池的大小在设置的时候需要结合业务考虑清楚,否则可能不能充分利用服务器资源,错误的触发服务降级。
worker在处理队列中的请求时,支持按照优先和权重处理。因此,可以非常容易的实现金丝雀发布和逻辑上的upstream隔离。



这种方式在在大部分时候可以工作得很好,但对于那种快速失败的小服务会失效。因为这种服务有很大概率被选中,但是并不意味着其当前负载较低。缓解该策略的方式是absolute least connection.


从 Bandaid 的设计看,无论是作为 reverse proxy 还是 service mesh 都有不错的潜力。不过 Dropbox 团队当前还没有公开Bandaid性能测试数据,代码也还没有开源。因此,猴急的同学可能还需要等一段时间。
在本站开启支持 QUIC 的方法与配置后,主观感觉从国内访问快了很多。看了一下Chrome的timing, 大部分建立连接都能够做到0-RTT:


既然这样,顺手实现一个基于QUIC的http代理,把平时查资料时使用的网络也顺带加速一下。(对了,前两天看到Google发布了Outline, 看来这项运动从来都不缺少运动员哪……)
http 代理处理http和https请求的方式有所不同。对于http请求:

这里有一个细节需要注意,浏览器向代理服务器发送的http请求URI与直接访问有所不同。
浏览器直接访问 GET http://www.yahoo.com 的http请求格式为:
GET / HTTP/1.1
User-Agent: Quic-Proxy
...
而向代理服务器发送的http请求格式为:
GET http://www.yahoo.com HTTP/1.1
User-Agent: Quic-Proxy
...
也就是浏览器想代理服务器发送的http请求URI中包含了scheme和host,目的是为了让代理服务器知道这个代理请求要访问的目标服务器地址。
对于https请求,一般是通过CONNECT建立隧道:

CONNECT请求。CONNECT请求中包含的host信息,向目标服务器建立TCP连接,然后向浏览器返回200连接成功的响应。
QUIC Proxy的部署结构与上面http代理原理稍微有所不同。主要区别是增加了qpclient。主要原因是应用程序与代理服务器支架的请求是明文传输(http请求代理是全明文,https请求代理时的CONNECT头会泄露目标服务器信息)。我们是要隐私的人(虽然小扎可能并不care),因此,在应用程序与qpserver之间加了一个qpclient,之间使用QUIC作为传输层。
QUIC Proxy使用Go实现,猴急的同学可以直接到github看源码:Quic Proxy, a http/https proxy using QUIC as transport layer.
代码比较简单,基于标准库的http.Server根据http代理的原理进行了一点http请求的修改。然后,因为qpclient和qpserver之间使用QUIC作为transport,而QUIC上的每一个connection都是可以多路复用(multiplexing)的,因此,对于qpserver需要自己实现一个传入http.Server的listener:
type QuicListener struct {
quic.Listener
chAcceptConn chan *AcceptConn
}
type AcceptConn struct {
conn net.Conn
err error
}
func NewQuicListener(l quic.Listener) *QuicListener {
ql := &QuicListener{
Listener: l,
chAcceptConn: make(chan *AcceptConn, 1),
}
go ql.doAccept()
return ql
}
func (ql *QuicListener) doAccept() {
for {
sess, err := ql.Listener.Accept()
if err != nil {
log.Error("accept session failed:%v", err)
continue
}
log.Info("accept a session")
go func(sess quic.Session) {
for {
stream, err := sess.AcceptStream()
if err != nil {
log.Error("accept stream failed:%v", err)
sess.Close(err)
return
}
log.Info("accept stream %v", stream.StreamID())
ql.chAcceptConn <- &AcceptConn{
conn: &QuicStream{sess: sess, Stream: stream},
err: nil,
}
}
}(sess)
}
}
func (ql *QuicListener) Accept() (net.Conn, error) {
ac := <-ql.chAcceptConn
return ac.conn, ac.err
}
同样的,qpclient向qpserver建立连接也需要考虑到多路复用的问题,实现实现一个基于QUIC的dialer:
type QuicStream struct {
sess quic.Session
quic.Stream
}
func (qs *QuicStream) LocalAddr() net.Addr {
return qs.sess.LocalAddr()
}
func (qs *QuicStream) RemoteAddr() net.Addr {
return qs.sess.RemoteAddr()
}
type QuicDialer struct {
skipCertVerify bool
sess quic.Session
sync.Mutex
}
func NewQuicDialer(skipCertVerify bool) *QuicDialer {
return &QuicDialer{
skipCertVerify: skipCertVerify,
}
}
func (qd *QuicDialer) Dial(network, addr string) (net.Conn, error) {
qd.Lock()
defer qd.Unlock()
if qd.sess == nil {
sess, err := quic.DialAddr(addr, &tls.Config{InsecureSkipVerify: qd.skipCertVerify}, nil)
if err != nil {
log.Error("dial session failed:%v", err)
return nil, err
}
qd.sess = sess
}
stream, err := qd.sess.OpenStreamSync()
if err != nil {
log.Info("[1/2] open stream from session no success:%v, try to open new session", err)
qd.sess.Close(err)
sess, err := quic.DialAddr(addr, &tls.Config{InsecureSkipVerify: true}, nil)
if err != nil {
log.Error("[2/2] dial new session failed:%v", err)
return nil, err
}
qd.sess = sess
stream, err = qd.sess.OpenStreamSync()
if err != nil {
log.Error("[2/2] open stream from new session failed:%v", err)
return nil, err
}
log.Info("[2/2] open stream from new session OK")
}
log.Info("addr:%s, stream_id:%v", addr, stream.StreamID())
return &QuicStream{sess: qd.sess, Stream: stream}, nil
}
好吧,我承认实现代码似乎在200行左右……但是,我们实现了一个client和一个server, 平均下来基本控制在100行左右,对吧……(?逃……)
注:需要golang版本 >= 1.9
qpservergo get -u github.com/liudanking/quic-proxy/qpserver
qpserver:qpserver -v -l :3443 -cert YOUR_CERT_FILA_PATH -key YOUR_KEY_FILE_PATH
qpclientgo get -u github.com/liudanking/quic-proxy/qpclient
qpclient:qpclient -v -k -proxy http://YOUR_REMOTE_SERVER:3443 -l 127.0.0.1:18080
以 Chrome with SwitchyOmega 为例:

Enjoy!