QUIC 协议在蚂蚁金服落地
自 2015 年以来,QUIC 协议开始在 IETF 进行标准化并被国内外各大厂商相继落地。鉴于 QUIC 具备“0RTT 建联”、“支持连接迁移”等诸多优势,并将成为下一代互联网协议:HTTP3.0 的底层传输协议,蚂蚁集团支付宝客户端团队与接入网关团队于 2018 年下半年开始在移动支付、海外加速等场景落地 QUIC。
QUIC背景:简单全面的介绍下 QUIC 相关的背景知识
方案选型设计:详细介绍蚂蚁的落地方案如何另辟蹊径、优雅的支撑 QUIC 的诸多特性,包括连接迁移等
落地场景:介绍 QUIC 在蚂蚁的两个落地场景,包括:支付宝客户端链路以及海外加速链路
几项关键技术:介绍落地 QUIC 过程中核心需要解决的问题,以及我们使用的方案,包括:“支持连接迁移”、“提升 0RTT 比例", "支持 UDP 无损升级”以及“客户端智能选路” 等
几项关键的技术专利
QUIC 背景介绍
一、QUIC 是什么?
二、为什么是 QUIC ?
网络设备支持 TCP 时的僵化,表现在:对于一些防火墙或者 NAT 等设备,如果 TCP 引入了新的特性,比如增加了某些 TCP OPTION 等,可能会被认为是攻击而丢包,导致新特性在老的网络设备上无法工作。 网络操作系统升级困难导致的 TCP 僵化,一些 TCP 的特性无法快速的被演进。 除此之外,当应用层协议优化到 TLS1.3、 HTTP2.0 后, 传输层的优化也提上了议程,QUIC 在 TCP 基础上,取其精华去其糟粕具有如下的硬核优势:
三、QUIC 生态圈发展简史
一套落地框架
QUIC LB 组件:基于 NGINX 4层 UDP Stream 模块开发,用来基于 QUIC DCID 中携带的服务端信息进行路由,以支持连接迁移。 NGINX QUIC 服务器:开发了 NGINX_QUIC_MODULE,每个 Worker 监听两种类型的端口: (1)BASE PORT ,每个 Worker 使用的相同的端口号,以 Reuseport 的形式监听,并暴露给 QUIC LB,用以接收客户端过来的第一个 RTT 中的数据包,这类包的特点是 DCID 由客户端生成,没有路由信息。 (2)Working PORT,每个 Worker 使用的不同的端口号,是真正的工作端口,用以接收第一个 RTT 之后的 QUIC 包,这类包的特定是 DCID 由服务端的进程生成携带有服务端的信息。
在不用修改内核的情况下,完全在用户态支持 QUIC 的连接迁移,以及连接迁移时 CID 的 Update 在不用修改内核的情况下,完全在用户态支持 QUIC 的无损升级以及其他运维问题 支持真正意义上的 0RTT ,并可提升 0RTT 的比例
两个落地场景
支持的 QUIC 版本是 gQUIC Q46。 NGINX QUIC MODULE 支持 QUIC 的接入和 PROXY 成 TCP 的能力。 支持包括移动支付、基金、蚂蚁森林在内的所有的 RPC 请求。 当前选择 QUIC 链路的方式有两种 :
Backup 模式,即在 TCP 链路无法使用的情况下,降级到 QUIC 链路。 Smart 模式,即 TCP和 QUIC 竞速,在 TCP 表现力弱于 QUIC 的情况下,下次请求主动使用 QUIC 链路。
在客户端连接发生迁移的时候,可以不断链继续服务 客户端在首次发起连接时,可以节省 TCP 三次握手的时间 对于弱网情况,QUIC 的传输控制可以带来传输性能提升
通过 QUIC 长连接的上的 Stream 承载 TCP 请求,避免每次的跨海建联。 对于跨海的网络,QUIC 的传输控制可以带来传输性能提升。
三篇关键专利
专利一
专利二
专利三
四项关键技术
技术点1.优雅的支持连接迁移能力
先说 连接迁移面临的问题 ,上文有提到,QUIC 有一项比较重要的功能是支持连接迁移。这里的连接迁移是指:如果客户端在长连接保持的情况下切换网络,比如从 4G 切换到 Wifi , 或者因为 NAT Rebinding 导致五元组发生变化,QUIC 依然可以在新的五元组上继续进行连接状态。QUIC 之所以能支持连接迁移,一个原因是 QUIC 底层是基于无连接的 UDP,另一个重要原因是因为 QUIC 使用唯一的 CID 来标识一个连接,而不是五元组。
再说 我们的解决方法,为了解决此问题,我们设计了开篇介绍的落地框架,这里我们将方案做一些简化和抽象,整体思路如下图所示:
在四层负载均衡上,我们设计了 QUIC LoadBalancer 的机制:
我们在 QUIC 的 CID 中扩展了一些字段(ServerInfo)用来关联 QUIC Server 的 IP 和 Working Port 信息。 在发生连接迁移的时候,QUIC LoadBalancer 可以依赖 CID 中的 ServerInfo 进行路由,避免依赖五元组关联 Session 导致的问题。 在 CID 需要 Update 的时候,NewCID 中的 ServerInfo 保留不变,这样就避免在 CID 发生 Update 时,仅依赖 CID Hash 挑选后端导致的寻址不一致问题。
在 QUIC 服务器多进程工作模式上,我们突破了 NGINX 固有的多 Worker 监听在相同端口上的桎梏,设计了多端口监听的机制,每个 Worker 在工作端口上进行隔离,并将端口的信息携带在对 First Initial Packet 的回包的 CID 中,这样代理的好处是:
无论是否连接迁移,QUIC LB 都可以根据 ServerInfo,将报文转发到正确的进程。 而业界普遍的方案是修改内核,将 Reuse port 机制改为 Reuse CID 机制,即内核根据 CID 挑选进程。即便后面可以通过 ebpf 等手段支持,但我们认为这种修改内核的机制对底层过于依赖,不利于方案的大规模部署和运维,尤其在公有云上。 使用独立端口,也有利于多进程模式下,UDP 无损升级问题的解决,这个我们在技术点 3 中介绍。
这里先 介绍 QUIC 0RTT 原理。前文我们介绍过, QUIC 支持传输层握手和安全加密层握手都在一个 0RTT 内完成。TLS1.3 本身就支持加密层握手的 0RTT,所以不足为奇。而 QUIC 如何实现传输层握手支持 0RTT 呢?我们先看下传输层握手的目的,即:服务端校验客户端是真正想握手的客户端,地址不存在欺骗,从而避免伪造源地址攻击。在 TCP 中,服务端依赖三次握手的最后一个 ACK 来校验客户端是真正的客户端,即只有真正的客户端才会收到 Sever 的 syn_ack 并回复。
类似于 Session Ticket 原理,Server 会将客户端的地址和当前的 Timestamp 通过自己的 KEY 加密生成 STK。 Client 下次握手的时候,将 STK 携带过来,由于 STK 无法篡改,所以 Server 通过自己的 KEY 解密,如果解出来的地址和客户端此次握手的地址一致,且时间在有效期内,则表示客户端可信,便可以建立连接。 由于客户端第一次握手的时候,没有这个 STK,所以服务度会回复 REJ 这次握手的信息,并携带 STK。
因为 STK 是服务端加密的,所以如果下次这个客户端路由到别的服务器上了,则这个服务器也需要可以识别出来。
STK 中 encode 的是上一次客户端的地址,如果下一次客户端携带的地址发生了变化,则同样会导致校验失败。此现象在移动端发生的概率非常大,尤其是 IPV6 场景下,客户端的出口地址会经常发生变化。
再介绍下我们的解决方法。第一个问题比较好解,我们只要保证集群内的机器生成 STK 的秘钥一致即可。第二个问题,我们的解题思路是:
我们在 STK 中扩展了一个 Client ID, 这个 Clinet ID 是客户端通过无线保镖黑盒生成并全局唯一不变的,类似于一个设备的 SIMID,客户端通过加密的 Trasnport Parameter 传递给服务端,服务端在 STK 中包含这个 ID。 如果因为 Client IP 发生变化导致校验 STK 校验失败,便会去校验 Client ID,因为 ID 对于一个 Client 是永远不变的,所以可以校验成功,当然前提是,这个客户端是真实的。为了防止 Client ID 的泄露等,我们会选择性对 Client ID 校验能力做限流保护。
技术点3. 支持 QUIC 无损升级
我们知道 UDP 无损升级是业界难题。无损升级是指在 reload 或者更新二进制时,老的进程可以处理完存量连接上的数据后优雅退出。以 NGINX 为例,这里先介绍下 TCP 是如何处理无损升级的,主要是如下的两个步骤:
老进程先关闭 listening socket,待存量连接请求都结束后,再关闭连接套接字 新进程从老进程继承 listening socket , 开始 accept 新的请求
在热升级的时候,old process fork 出 new process 后,new process 会继承 listening socket 并开始 recv msg。 而 old process 此时如果关闭 listenging socket, 则在途的数据包便无法接收,达不到优雅退出的目的。 而如果继续监听,则新老进程都会同时收取新连接上的报文,导致老进程无法退出。
这里介绍下相关的解决方法。针对此问题,业界有一些方法,比如:在数据包中携带进程号,当数据包收发错乱后,在新老进程之间做一次转发。考虑到接入层上的性能等原因,我们不希望数据再做一次跳转。结合我们的落地架构,我们设计了如下的 基于多端口轮转的无损升级方案,简单来说,我们让新老进程监听在不同的端口组并携带在 CID 中,这样 QUIC LB 就可以根据端口转发到新老进程。为了便于运维,我们采用端口轮转的方式,新老进程会在 reload N 次之后,重新开始之前选中的端口。如下图所示:
无损升级期间,老进程的 Baseport 端口关闭,这样不会再接受 first intial packet, 类似于关闭了 tcp 的 listening socket。 老进程的工作端口,继续工作,用来接收当前进程上残余的流量。 新进程的 Baseport 开始工作,用来接收 first initial packet, 开启新的连接,类似于开启了 tcp 的 listening socket。 新进程的 working port = (I + 1) mod N, N 是指同时支持新老进程的状态的次数,例如 N = 4, 表示可以同时 reload 四次,四种 Old, New1, New2, New3 四种状态同时并存,I 是上一个进程工作的端口号,这里 + 1 是因为只有一个 worker, 如果 worker 数有 M 个,则加 M。
建好的连接便被 Load Balancer 转移到新进程的监听端口的 Working Port 上。
在带宽紧张的时候,UDP 会经常被限流。 一些防火墙对于 UDP 包会直接 Drop。 NAT 网关针对 UDP 的 Session 存活时间也较短。
做个总结
未来规划
我们将利用 QUIC 在应用层实现的优势,设计一套统一的具备自适应业务类型和网络类型的 QUIC 传输控制框架,对不同类型的业务和网络类型,做传输上的调优,以优化业务的网络传输体验。 将 gQUIC 切换成 IETF QUIC,推进标准的 HTTP3.0 在蚂蚁的进一步落地。 将蚂蚁的 QUIC LB 技术点向 IETF QUIC LB 进行推进,并最终演变为标准的 QUIC LB。 探索并落地 MPQUIC(多路径 QUIC) 技术,最大化在移动端的收益。 继续 QUIC 的性能优化工作,使用 UDP GSO, eBPF,io_uring 等内核技术。 探索 QUIC 在内网承载东西向流量的机会。