本文是百度网络优化系列文章的终结篇,该文深入探讨了网络优化中最为复杂且难验证和分析的问题——弱网问题。
前言
网络优化解决的核心问题有三个,第一是安全问题,我们在系列《一》DNS优化进行了详细的讲解。第二是速度问题,我们系列《二》连接优化也做了详细的介绍。第三是弱网问题,它是网络优化中最为复杂且需要反复验证和分析的问题,我们的系列《三》弱网优化就是要深入探讨这个问题。
背景
弱网优化需要解决的核心问题有两点:
移动网络环境如此复杂,我们如何确定当下就是弱网环境。
确定为弱网环境下,我们如何提升弱网下的成功率,降低弱网下的时延,进而提升用户的网络体验。
百度 App 承载着亿级流量,弱网比例 0.95%,可谓不小,这个比例是如何得来的呢?还是要从什么是判断弱网指标说起。
判断弱网的指标
首先我们来探讨下都有哪些指标会影响到网络的质量,包括 httprtt,tcprtt,throughput,signal strength,bandwidth-delay product。
影响网络质量的指标
1.httprtt
httprtt(http Round-Trip Time)又名 TTFB(Time to first byte),指从客户端请求的第一个字节开始发送到接收到 http header 的第一个字节的时间差。httprtt 的时间如果过长,一方面是客户端本身接入网络质量的问题,另一方面是服务的延时比较大。
2.tcprtt
tcprtt(tcp Round-Trip Time)指客户端 tcp 信道第一个字节发送到接收第一个字节的时间差。因为 HTTP 协议底层是基于 TCP 的,所以在复用同一条 tcp 连接的前提下,httprtt 的时间是包含 tcprtt 的时间的。大部分情况下 httprtt 已经可以说明问题的原因。
3.throughput
throughput,中文名字吞吐量,它是用来衡量单位时间内成功传送数据的数量,是可以比较客观的衡量网络质量的指标。吞吐量 =(获 bits 结束大小 - 获 bits 开始大小)/(获 bits 结束时间 - 获 bits 开始时间),这里有个细节需要注意,posix socket 的 read 函数返回值是 bytes,所以要乘以 8 得到 bits。通常在 httprtt 比较小的情况下,网络依然很慢,这个时候就可以使用吞吐量来确定网络的质量。
4.signal strength
signal strength,这里指的是无线信号强度,在 Android 上可以通过 PhoneStateListener 的 onSignalStrengthsChanged 方法获取到信号强弱,但要注意只能在 Android M 以上的版本才生效。iOS 上暂时没有靠谱的实现。
5.bandwidth-delay product
bandwidth-delay product,中文名带宽时延乘积,指的是一个数据链路的能力(throughput)与来回通信延迟(rtt)的乘积。带宽时延乘积的结果是比特不是位,这个比特值反应出当前网络管道的最大容量。TCP 中有一个窗口大小的概念,会限制发送和接收数据的大小,所以 TCP 窗口大小的调节是直接受带宽时延乘积的影响,根据带宽时延乘积的值去设置套接字的 setsockopt 方法,设置的 option 是 SO_RCVBUF(接收缓冲区大小)和 SO_SNDBUF(发送缓冲区大小)。
通过上面的内容,我们对影响网络质量的指标有了一定了解,对于不同的产品,影响网络质量的指标可以理解成一样的,但对于每个指标的阈值肯定是不一样的,因为这包含着业务场景,比如抖音是视频类网络传输,微信是长连接数据传输,百度是文本图片类数据传输。还包括服务端配备,不同产品线的服务集群能力肯定不一样,比如返回客户端的服务端耗时肯定不一样。所以针对不同的产品弱网指标是基本一致的,但是指标的取值肯定是不一样的。
如何建立弱网标准
建立弱网标准是一个循序渐进的过程,在一穷二白的时候我们应该如何建立这个标准呢?答案分为三个阶段。
建立弱网标准的步骤:
第一阶段,线下进行测试。获取一些符合我们预期的阈值,这个时候我们需要借助一些网络测试工具,比如苹果的 Network Link Conditioner,Facebook 的 ATC(Augmented Traffic Control),来获取到线下不同网络情况的阈值,一般我们会测试 App 冷启动的场景,网络切换的场景,DNS 故障场景,弱网场景(一般都是配置上下行的带宽,丢包率,延迟,DNS 延迟参数,或者更为简单的是使用工具默认的一些弱网配置)。
第二阶段,线上进行验证。通过线下充分测试获取到的阈值,在线上可以获取到弱网的比例,在这里百度 App 是针对特定场景的,比如 Feed 刷新,搜索落地页打开等,就算是在移动时代被大家公认的网络体验好的微信,也只是在信令传输(收发消息)上做到极致优化,所以针对场景搜集弱网数据很重要。
第三阶段,线上的反复试验。想做到理想的弱网效果,少不了线上反复的阈值调整,通过调整阈值比较针对场景的网络请求的成功率、耗时、连接复用率等指标,使我们获得趋向于针对场景的合理阈值。
聊了这么多,那么弱网的探测如何实现呢?
网络探测的整体架构和实现
网络探测是弱网检测的基础,是否能即时,正确的检测出网络质量,是我们首先要解决的问题。我们把网络探测划分为两部分,主动网络探测和被动网络采集。
1.主动网络探测
所谓主动探测,就是在触发了某些条件后,主动的进行网络探测,并按照一定的条件检查出是否是弱网状态。百度 App 自研了主动探测组件,如下图所示。
主动网络探测
策略层
探测策略层通过多种策略的组合,使主动探测的即时性和准确性得以大大提高,我们结合上面的策略层图来解释下检测维度的意义。
我们分别在网络请求成功和失败的时候触发了弱网检测的逻辑。主要分为如下三种逻辑。
1)成功时,如何判断进入弱网状态?检查 weakhttprtt 的阈值,这个值取决于业务的设置(一般这个值会针对特殊场景的请求取 95 分位或者更大分位的值),大于这个值就会进入弱网检测,为了防止频繁触发探测加了时间间隔维度,目前定义的是 10 分钟。从线下模拟测试来看,只要大于这个阈值,检测结果必然是弱网状态。
2)成功时,如何判断退出弱网状态?检查 goodhttprtt 的阈值,这个值取决于业务的设置(一般这个值会取整体网络的 95 分位或者更大分位的值),小于这个值证明要切换回正常网络状态,为了防止频繁触发探测加了时间间隔维度,目前定义的是 30 秒。从线下模拟测试来看,只要小于这个阈值,检测结果必然是正常状态。如果大于或者等于这个阈值,也不能证明一定不是正常网络,所以也需要发起网络探测,但是由于这是在成功回调里,频次会很高,所以我们加上时间间隔的限制 30 秒,还加入了次数的限制,连续成功次数 %次数阈值(4 次)等于 0。但这看起来还是频次有点高,所以我们引入了阶梯递增机制,随着次数的增长,成 60 秒几何倍数增长。
3)失败时,如何判断进入弱网状态?首先会判断连续失败次数,连续失败次数/次数阈值(2 次)等于 1 并且连续失败次数 %次数阈值(2 次)等于 0,相比成功,失败的次数检查较为苛刻,主要还是考虑多次触发网络检测损耗性能。
进入弱网状态后,就会触发基础能力层的 ping 和 dns query 的探测。
基础能力层
探测基础能力层,主要提供弱网检测的手段,一是 dns query,一是 ping,百度 App 使用 C++实现了这两个能力。为什么要选用这两种手段呢?我们在系列二中介绍过,一个网络请求,分为 DNS-》TLS-》TCP-》数据传输 四个阶段。想判定网络连通性主要在 DNS 和 TCP 阶段,所以 dns query 和 ping 就是用来检测这两个阶段的连通性手段。dns query 向百度核心域名mbd.baidu.com发起 dns 查询,查询的 DNS 服务器为系统配置的 DNS 服务器(iOS 通过 res_ninit 函数构建一个__res_state 的结构体,Android 通过 systemproperty 获取 net.dns1 和 net.dns2 的值,便可获取系统配置的 DNS 服务器),DNS 查询的超时时间为 3s。ping 的目标地址为百度核心域名mbd.baidu.com,ping 的次数为两次,每次超时时间是默认的 1s。
判断出弱网状态后,会将结果提供给接口层。
接口层
接口层主要提供主动探测出来的网络状态,目前包括 GOOD,BAD,UNKNOWN,OFFLINE。
1)GOOD:dns 查询成功并且 ping 也成功,即标记为 GOOD 状态。
2)BAD:ping 失败一次标记为 BAD 状态。
3)UNKNOWN:初始状态或者识别不出来状态为 UNKNOWN 状态。
4)OFFLINE:dns server 错误(没有获取到要发送的 DNS server 地址),网关错误(读取/proc/net/route 文件内容失败),发送 dns 错误(发送 dns 数据出错),ping 读写错误(ping 的过程中读写错误),接收 dns 错误(接收 dns 数据出错),ping 地址错误(ping 地址是空),dns 未知域名错误(dns 没有查询到域名错误),初始化 icmp 错误(初始化 icmp 失败),dns udp 错误(创建 UDP socket 失败),即标记为 OFFLINE 状态。
2.被动网络采集
所谓被动采集,就是每一次网络请求的所有细节都进行记录,并按照一定的条件将原始信息进行上报,上层再根据条件判断是否是弱网状态。百度 App 基于 cronet 的 NQE(Network Quality Estimator)进行了二次订制开发。
首先我们讲解下需要采集的数据,包括 tcprtt、httprtt、throughput 三个维度,如下图所示。
被动采集数据
1)tcprtt,基于 posix 和 windows 的 socket 编程接口来获取 tcprtt。获取时机在连接完成,读完成和写完成。
2)httprtt,基于 HTTP 协议栈实现,通过计算接收 response 数据开始和开始发送的时间差,来获取 httprtt。获取时机在读首包完成时。
3)throughput,通过上面的计算公式需要获取 bytes 和时间,基于 posix 和 windows 的 socket 编程接口来获取 bytes。获取时机在读完成时记录接收的 bytes,在写完成时记录发送的 bytes。时间的获取在吞吐量管理模块里完成,下面会讲到。获取时机在请求完成和请求销毁时。
如下为被动网络采集的整体架构图。
被动网络采集
能力层
能力层内容上面我们已经讲过,主要采集 tcprtt、httprtt、throughput 三个维度的数据。
策略层
被动采集策略层通过多种策略的组合,降低各种采集数据的上报时机,降低性能的影响。
1)套接字管理模块,首先负责获取 tcprtt 的值,如何获取 tcprtt 呢?通过 getsockopt 函数获取 tcp_info 结构体里的 tcpi_rtt 值。其次由于 tcprtt 的上报频次比较频繁,所以做了 1 秒的时间间隔上报限制。
2)吞吐量管理模块,负责吞吐量的计算,上面介绍了计算公式,从网络活动监控器模块获取 bytes,但吞吐量的计算单位是 bits(位),所以将 bytes 乘以 8。只有 GET 请求会被列入统计计算,并且至少要累计 5 个请求后才能开始统计计算。排除精准度的干扰,比如 localhost,私有子网上的主机,特定用途子网主机,可参考 RFC1918。
3)网络质量管理模块,从套接字管理模块获取 tcprtt,从吞吐量管理模块获取吞吐量,并且在 HTTP 协议栈读首包完成时获取 httprtt。获取到这三个值后,需要经过一些策略限制上报的频次,10 秒间隔的限制;网络类型不能是 UNKNOWN(1.3 的第三部分会详细讲解);网络不能频繁切换;rtt 和吞吐量总大小各 300 个。
接口层
接口层主要提供被动采集出来的网络状态,目前包括 GOOD,BAD,UNKNOWN,OFFLINE。
1)GOOD:3G 和广义的 4G,任一条件满足标记为 GOOD 状态。通过阈值标记 3G 和广义的 4G,httprtt 大于等于 273ms,tcprtt 大于等于 204ms,即标记为 3G 状态。小于这两个值被标记为广义的 4G,所谓广义的 4G 包含 4G、WiFi、以及质量较好的各种接入网络。
2)BAD:慢 2G,2G 和 httprtt 大于 1.31 秒,任一条件满足标记为 BAD 状态。通过阈值标记慢 2G 和 2G,httprtt 大于等于 2.01 秒,tcprtt 大于等于 1.87 秒,标记为慢 2G。httprtt 大于等于 1.42 秒,tcprtt 大于等于 1.28 秒,标记为 2G。httprtt 大于 1.31 秒,为百度 App 的 Feed 刷新业务阈值。
注:上面涉及的时间值为 nqe 内部的机制,具有普适性。
3)UNKNOWN:非法的 httprtt,tcprtt,吞吐量,任一条件满足标记为 UNKNOWN 状态。何为非法?值为-1 为非法,那什么条件被标记为-1 呢?首先初始化时会被标记为-1,其次在从来没有获取到过 httprtt,tcprtt,throughput 的值时,会使用本地默认的值做为判断标准,这是一种容错处理。
4)OFFLINE:依赖平台能力进行判断,Android 平台依赖 ConnectivityManager 获取 NetworkInfo,通过 NetworkInfo 的 isConnected 获取是否连接,如果未连接则判断为 OFFLINE 状态,如果 NetworkInfo 为空则判断为 OFFLINE 状态。
弱网状态下百度 App 如何改善用户体验
百度 App 在弱网下的手段
QUIC 在百度 App 弱网下的最佳实践
QUIC(Quick UDP Internet Connections)是新一代的互联网传输协议,最早源于 Google,它的详尽内容可参考资料【3】,本章我们不做 QUIC 的科普介绍。
百度 App 的普通网络请求在弱网状态下会切换到 QUIC,本章重点讲解下百度 App 针对弱网下开启 QUIC 后遇到的问题,一是开启 QUIC 一旦遇到问题是否可以回滚?二是在弱网下如何能让流量尽可能的走 QUIC?针对这两个问题,我们的解决方案是 QUIC 升降级原理和 QUIC 预连接。
1.1QUIC 升降级原理
QUIC 升降级原理
如上图所示,QUIC 的升降级依赖于 HTTP Alternative Services,HTTP Alternative Services 是与 HTTP 有关的一个协议,它不是为 QUIC 专门设计的,在 HTTP 协议上主要负责新服务的替换,对于 HTTP1.1 协议,它是通过 HTTP 响应头传输回来的,所以只能在第二次请求时生效,如下面格式。
Alt-Svc: quic=“alt.example.com:443”, quic=":443"; ma=2592000
如上信息表明切换到 quic 协议,指定了域名服务和端口,并且指定了生效时间,以秒为单位。
Alt-Svc: clear
如上信息表明将 alter 配置清空
在网络库内部有一个 alter 连接和原连接的竞争机制,如果 alter 信息已经存在,优先发送 alter 连接,而原连接会延迟发送,延迟时间默认 300ms,谁先成功就使用哪个连接,如果 alter 连接在 QUIC 握手时失败,会记录这个 alter 信息的失败次数,并根据失败的次数,计算出一个过期时间,这个过期时间会随失败次数指数增加,最长为 2 天。当过期时间到期后,会清除这个 alter 信息,当这个 alter 连接在 QUIC 握手成功后,会清除这个 alter 信息。
1.2QUIC 预连接
所谓 QUIC 的预连接,就是在进入弱网状态前提前建立 QUIC 连接。大家都知道 QUIC 引以为傲的 0RTT,但第一次建立连接的时候是需要 1RTT 的,客户端首先会向服务器发送一个 client hello 消息,服务器会回复一个 server reject 消息,这个消息中包括了 server config,有了 server config 后客户端就可以直接计算出密钥,完成 0RTT,详尽内容可参考资料【4】。
通过上面的原理,客户端拉取 server config 的成功概率会直接影响 QUIC 在弱网下的流量,所以我们在 App 启动的过程中会做一次 QUIC 预连接,将 server config 拉取下来,这样等进入弱网后 alter 连接会大概率的竞争过原连接,进而走 QUIC 协议。
2.复合连接在百度 App 弱网下的最佳实践
复合连接的具体原理可以查看《百度 App 网络深度优化系列《二》连接优化》里的具体介绍,百度 App 目前在弱网下只是针对图片网络请求开启了复合连接,因为图片请求不管是 HTTPDNS 结果还是 localDNS 的结果都是多个 IP,这是满足复合连接的前提。在弱网下多 IP 的尝试会比单 IP 的结果好些,另外弱网的比例相对较小,复合连接对于服务器的负载也会小些。
百度 App 网络整体架构
百度 App 网络整体架构
百度 App 网络整体架构,以网络门面为中间层,隔离上层的最佳实践和底层的基础网络库。
最佳实践
客户端网络库的一部分工作量是在如何让最佳实践做的更好,在音视频上,不管是 iOS 的 AVPlayer,还是双端的 ijkPlayer,都是使用 HTTPDNS 组件接管 DNS 模块,没有全部接管网络模块。ReactNative 的网络模块 RCTNetworking,图片库 Android 的 Fresco 和 iOS 的 SDWebImage,WebView 组件 Android 的 Chromium 和 iOS 的 WKWebView,以及百度 App 的自有业务,都是通过网络门面的接口层直接接管。而对于第三方业务考虑到与宿主耦合的关系,直接使用 Android 的 HttpURLConnection 和 iOS 的 URLSession 系统标准的接口。
网络门面
网络门面主要包括,拦截器模块(提供给业务订制网络门面的机制)、并发队列模块(提供高中低以及非常高的网络请求优先级)、网络探测组件(弱网主动探测能力)、网络诊断模块(包括 https,ping,dns 的校验)、HTTPDNS 组件(《百度 App 网络深度优化系列《一》DNS 优化》里详细讲解)、网络监控模块(客户端的打点机制,服务端的例行和突发监控)、HttpURLConnection 封装层、URLSession 封装层。
基础网络库
基础网络库包含两部分,一部分是基于 cronet 二次订制的统一网络库,一部分是 WebSocket 基础库(Android 的 JavaWebSocket,iOS 的 SocketRocket)。统一网络库内部包含连接优化的内容(在《百度 App 网络深度优化系列《二》连接优化》里详细讲解),弱网优化的内容(上面提到的被动采集)。通过 AOP 的方式将底层协议栈注入进 HttpURLConnection(利用 URLStreamHandlerFactory)和 URLSession(利用 URLSessionConfiguration 的 protocolClasses 属性),两者都是系统提供的 URL Loading 机制。
收益
弱网优化的收益我们主要从上面讲到的进入弱网状态后的手段来看,包括开启 QUIC,QUIC 预连接,开启复合连接。
1)弱网下开启 QUIC 后,网络连接成功率提升 0.01%,平均耗时降低 23.5%。
2)弱网下开启 QUIC 预连接后,QUIC 协议的 pv 从 37 万涨到 90 万。
3)弱网下开启复合连接后,bad 状态下耗时降低 2.5%,offline 状态下耗时降低 7.7%。
结语
系列一到系列三的内容到今天全部完成,希望能对大家的工作和学习有所帮助,感谢大家的持续关注和鼓励。生命不息,优化不止,做技术我们是认真的。
作者介绍
蔡锐,9 年移动端开发经验,在百度先后主导过订制 ROM 领域、多屏互动领域、Hybrid 跨平台领域等多个技术领域的开发,目前担任百度 App 的客户端资深工程师,参与基础技术的研究,专攻动态化、性能和网络优化方向。
参考资料
https://chromium.googlesource.com/chromium/src/+/HEAD/docs/android_build_instructions.md
https://chromium.googlesource.com/chromium/src/+/HEAD/docs/ios/build_instructions.md
评论