@levinzhang
2019-12-29T20:59:11.000000Z
字数 6605
阅读 1339
优步的移动应用程序要适应各种各样的网络状况和网络类型,根据性能测试的结果,他们发现很多延迟问题的根源在于TCP协议本身固有的特点,因此他们切换到了QUIC协议,并进行了性能方面的对比研究。
本文最初发表于优步的开发者博客,由InfoQ中文站翻译分享。
优步在全球的600多个城市开展业务,其移动应用完全依赖于4500多家移动运营商的无线连接。为了实现优步用户的实时高性能,移动应用需要低延迟、高可靠的网络通信。但是,令人遗憾的是,HTTP/2栈在动态的、有损耗的无线网络中运行得很糟糕,我们了解到,这种糟糕的性能常常可以直接追溯到隐藏在操作系统(OS)内核中的传输控制协议(Transmission Control Protocol,TCP)实现。
为了解决这些痛点,优步采用了QUIC协议,这是一个流多路复用的现代传输协议,它是构建在UDP之上实现的,借助它,我们能够更好地控制传输协议的性能。QUIC目前正在被互联网工程任务组(Internet Engineering Task Force,IETF)标准化为HTTP/3。
我们对QUIC进行了全面的测试,得出的结论是,与TCP相比,将QUIC集成到我们的应用程序中将减少末端延迟。在乘客和驾驶员应用中,我们看到HTTPS流量的末端延迟减少了10%到30%。除了提高低连接网络中应用程序的性能外,QUIC还让我们能够对用户空间中的数据包流进行端到端控制。
在本文中,我们将会分享通过转移至支持QUIC的网络栈,优化优步应用程序TCP性能的经验。
在今天的互联网上,TCP是在传送HTTPS流量时应用最广泛的传输协议。TCP提供了可靠的字节流,处理了网络拥塞和链路层数据丢包的复杂性。HTTPS流量广泛使用TCP主要归因于它普遍存在(几乎所有的OS都包含TCP),以及广泛存在的基础设施,比如负载均衡器、HTTPS代理和CDN,其开箱即用的功能适用于大多数的平台和网络。
大多数的用户都是在移动中使用优步的服务,而运行在TCP之上的应用的末端延迟远远不能满足我们对HTTPS流量实时性的要求。具体来讲,世界范围内的用户都感觉到了较高的末端延迟。
尽管印度和巴西网络的延迟比美国和英国的延迟更长,但即使在美国和英国,其末端延迟也明显高于平均延迟。
TCP最初是为有线网络设计的,有线网络的链接在很大程度上是可预测的。但是,无线网络有独特的特点和挑战。首先,无线网络很容易因为干扰和信号衰减而造成丢包。例如,WiFi网络容易受到微波、蓝牙或者其他类型的无线电波的干扰。蜂窝网络则会受到信号损失(或路径损失)的影响,一般这是由环境中的物体(如建筑物)的反射/吸收以及邻近基站的干扰造成的。相对于有线网络,这会导致更长(4到10倍)和更不稳定的往返时间(Round-trip time,RTT),以及包丢失。
为了克服带宽和损耗的间歇性波动,蜂窝网络通常会使用较大的缓冲区来处理流量暴增。但是,较大的缓冲区会导致过多的排队,从而引发更长的延迟。TCP通常会将这种排队解释为超时所导致的丢包,因此它会进行重新传输,这会进一步填充缓冲区。这个问题被称为缓冲膨胀(bufferbloat),是如今互联网所面对的主要挑战。
最后,蜂窝网络的性能在不同的运行商、地区和时间上有着显著的差异。
上述的这些特点最终体现出来就是TCP在无线网络中的低效。然而,在研究TCP的替代方案之前,优步评估了以下的几个因素,确保对问题域有一个清晰的理解:
为了更好地理解如何分析TCP的性能,我们首先来看一下TCP是如何将数据从发送者传送至接收者的。首先,发送者要通过执行所谓的三次握手建立TCP连接:发送者传输一个SYN包,等待来自接收者的SYN-ACK包,然后再发送一个ACK。对于TLS连接的话,还需要额外的两到三轮的往返传输。每个数据包都需要由接收者进行ACK确认,从而确保数据投递的可靠性。
如果出现包或ACK丢失的话,发送者在重传输超时(RTO)之后,会重新传输数据包。RTO是根据各种因素动态计算出来的,比如发送者和接收者之间的RTT估计值。
TCP/TLS的数据包交换,包括重传丢失包的机制
为了确定TCP流在优步App中是如何执行的,我们在优步的边缘服务器上使用tcpdump收集了生产环境流量的TCP包跟踪数据,这些数据都来自印度的连接。然后,我们借助tcptrace分析了TCP连接。除此之外,优步的工程师还构建了一个Android应用,该应用会将模拟流量发送至一个测试服务器,这与真实的流量非常相似。该应用会在后台跟踪TCP数据并将日志上传到后端服务器上。运行该应用的Android手机被分发给了几名印度员工,他们收集了几天的日志。
这两个实验的结果是一致的。我们看到了很高的RTT值并且末端值几乎是中位值的六倍,平均方差超过了一秒。另外,大多数连接都是有丢包的,这导致TCP重新传输了3.5%的数据包。在人群拥挤的地方,比如火车站和机场,我们看到高达7%的数据包被网络丢弃。这样的结果颠覆了我们一种常见的理念,那就是蜂窝网络的高级重传模式会显著降低传输层的丢包。在mock应用上运行测试的结果大致如下所示:
网络指标 | 数据值 |
---|---|
按照毫秒计算的RTT[50%, 75%, 95%, 99%] | [350, 425, 725, 2300] |
按照秒计算的方差 | 平均约1.2秒 |
在有损耗的网络中,包的丢失率 | 平均约3.5%(拥挤地区为7%) |
在这些连接中,至少有一半至少丢掉了一个包,大量的连接丢掉了SYN和SYN-ACK。因为与数据包相比,TCP使用高度保守的RTO值来重新传输SYN和SYN-ACK数据包,所以大多数TCP实现对SYN数据包使用一秒的初始RTO,而对于后续的丢失,RTO会成倍地增加。应用程序的初始加载时间可能会受到影响,因为TCP需要更多的时间来建立连接。
对于数据包来说,当无线网络存在瞬时丢包时,高RTO会显著降低网络使用率。我们发现,平均重传时间约为1秒,而末端时间却几乎是30秒。TCP层的这些高延迟会导致HTTPS传输超时和请求重试,从而进一步导致延迟和网络效率低下。
在我们的网络中,测量到的RTT的第75百分位大约是425毫秒,而TCP重传延迟的第75百分位几乎是3秒。这个结果意味着数据丢失导致TCP需要7倍-10倍的往返才能真正成功地传输数据。这可能是由于RTO的计算效率低下,TCP无法对窗口中最后几个数据包的丢失做出快速反应(尾部丢失),以及拥塞控制算法的效率低下,该算法没有区分无线损耗和网络拥塞造成的损耗。下面,我们总结了TCP丢包测试的结果:
TCP的丢包统计 | 数据值 |
---|---|
至少有一个包丢失的连接所占的百分比 | 45% |
在连接建立过程中有数据包丢失的连接的百分比 | 30% |
在数据交换过程中丢失包的连接的百分比 | 76% |
重传延迟的秒数分布[50%,75%,95%,99%] | [1, 2.8, 15, 28] |
给定数据包或TCP段的重传次数的分布 | [1,3,6,7] |
QUIC最初是由谷歌设计的,是一个基于UDP实现的流复用的现代传输协议。目前,QUIC正在标准化,作为IETF工作的一部分。如下图所示,QUIC透明地位于HTTP/3之下(基于QUIC的HTTP/2正在被标准化为HTTP/3)。它替换了协议栈中HTTPS和TCP层的部分,使用UDP来进行数据包分帧。QUIC只支持安全的数据传输,因此TLS层完全嵌入到了QUIC中。
QUIC位于HTTP/3之下,吸收了以前在HTTP/2之下运行的安全TLS层。
下面,我们概述了QUIC的一些特性,这些特性促使我们除了TCP之外,还采用QUIC作为边缘通信的方式:
为了成功地将QUIC集成进来,并在低连接网络中提升应用程序的性能,我们将优步遗留的网络栈(TLS/TCP之上的HTTP/2)替换成了QUIC协议。我们使用了来自Chromium项目的Cronet网络库,该项目实现了谷歌最初设计的QUIC协议版本(gQUIC)。目前,该协议还在持续演化,以便于遵循最新的IETF规范。
我们首先将Cronet集成到了Android应用中,以便于启用对QUIC的支持。在实现集成的时候,我们采用的方式是确保移动工程师零成本迁移。我们并没有完全替换使用OkHttp遗留网络栈的代码,而是将Cronet库集成到了OkHttp API框架的底层。通过这种方式的集成,我们避免对网络调用API层的修改,这个API层是依赖Retrofit的。
除此之外,我们还避免了对核心网络功能的修改,它们要执行容灾、重定向以及压缩这样的任务。我们利用OkHttp拦截器的特性来实现中间件,从而无缝地拦截来自应用的HTTP流量,并使用Java API将流量发送至Cronet。在特定Android设备加载Cronet失败的情况下,我们依然会使用OkHttp库。
与Android的方式类似,我们通过拦截HTTP流量的办法将Cronet集成到了优步的iOS应用中,这借助NSURLProtocol使用了iOS的网络API。该抽象是由iOS Foundation所提供的,它能够加载特定URL的数据并确保将Cronet集成到iOS应用时不需要付出巨大的迁移成本。
在后端,QUIC终端(termination)是由Google Cloud负载均衡基础设施所提供的,它是通过在响应上增加alt-svc头信息来实现的。在本质上来讲,对于每个HTTP响应,负载均衡器添加了一个alt-svc头信息,该信息会校验该域对QUIC的支持情况。当Cronet客户端接收到带有alt-svc头信息的HTTP响应时,对于该域的后续HTTP请求,它将会使用QUIC。负载均衡器终止QUIC之后,我们的基础设施会透明地将请求通过HTTP2/TCP转发至后端数据中心。
(译者注:Google Cloud会负责为HTTP响应添加alt-svc头信息,同时会将来自客户端的QUIC转换为HTTPS,并发送给后端服务器,所以后端服务并不需要任何的变更,所需要做的就是在负载均衡器上启用QUIC,更多关于Google Cloud负载均衡器对QUIC支持的内容,请参见该文档。)
鉴于性能是我们探索更好的传输协议的根本原因,所以我们首先建立了网络仿真实验,以便于在实验室中测试不同网络条件下QUIC的性能。为了验证QUIC在真正网络下的性能,我们随后在新德里使用模拟流量进行了路面上的实验,这些网络条件与乘客应用中高频率的HTTPS网络调用非常接近。
该实验主要由以下组件组成:
我们测试TCP和QUIC性能的路面实验环境,它由运行OkHttp和Cronet栈的Android设备、终止连接的基于云的代理以及模拟服务器所组成。
实验的结果表明,当在设备上下载HTTPS响应时,QUIC在延迟方面的表现一致且显著优于TCP。具体来说,我们看到了整个系统的延迟降低了50%,从第50个百分位到第99个百分位均是如此。
谷歌在Google Cloud负载均衡器中支持QUIC之后,我们将上述的实验环境进行了一项修改:那就是使用Google Cloud负载均衡器来终止来自设备的TCP和QUIC连接,并将HTTPS流量转发至模拟服务器,而不再使用NGINX。这些负载平衡器分布在全球各地,会根据设备的地理位置选择最近的PoP服务器。
在第二个实验环境中,我们希望对比Google Cloud和自定义的基于云的代理之间的延迟。
在第二个实验中,我们有一些有意思的发现:
PoP终止改善了TCP的性能:由于Google Cloud负载均衡器会在更靠近用户的地方终止TCP连接,并且对性能进行了调优,因此会带来较低的RTT,这样会显著地改善TCP性能。尽管QUIC带来的收益有所降低,但它仍然比TCP的末端延迟大约减少了10-30%。
蜂窝网络/无线网络跳跃(hop)的长尾影响:尽管我们内部的QUIC代理相对Google Cloud的负载均衡器距离设备更远一些(大约会多50毫秒的延迟),但是它提供了类似的性能收益(在TCP上的第99个百分位上会带来15%的下降,而Google Cloud的负载均衡器则是20%的下降)。这些结果表明,最后一公里的蜂窝网络跳跃是主要的瓶颈。
两个实验的结果表明,QUIC与TCP相比会带来明显的性能提升
受这些测试结果的鼓舞,我们在Android和iOS应用程序中添加了对QUIC支持。我们在QUIC和TCP之间进行了A/B测试,以量化QUIC对优步运营的所有城市的影响。总的来说,我们看到在各个维度,如地区、运营商和网络类型,末端延迟都有显著减少。
下面的图片展示了末端延迟的在各种区域和各种网络(包括LTE、3G和2G)下的百分比提升(第95和第99个百分位):
在生产环境测试的中,QUIC在各种区域和网络类型的延迟都优于TCP。
基于这样的测试结果,优步计划未来要把QUIC用到更广阔的领域,同时还会对拥塞场景进行进一步的优化,基于流量和用户访问模式改善QUIC的丢包恢复算法,从而进一步改善延迟。