弱网诊断探索之路

73 阅读15分钟

背景

随着移动互联网的普及,网络环境的多样性和不确定性对应用体验的影响日益突出。在现实环境中,网络状况可能会因用户地理位置、网络覆盖、移动设备切换信号、网络拥塞等原因变得不稳定,导致弱网环境的出现。这种情况下,用户会遇到应用加载慢、视频卡顿、下载失败等问题,严重影响用户体验。

主题概述

本次分享的主题是“弱网环境下的应用优化和诊断策略”。目标是通过深入剖析常见的弱网问题,探索应用在复杂网络环境中的性能优化方法,并分享一些实战经验和技术工具,帮助大家提升用户体验。

基本概念

一些网络性能指标

带宽(Bandwidth)

  • 带宽指的是网络连接可以承载的数据量上限,通常表示为一秒钟内可传输的最大数据量。它是网络连接的容量,决定了在给定时间内能够传输多少数据。
  • 单位通常为 bps(bits per second),也常见 Mbps(百万位每秒)或 Gbps(千兆位每秒)等。
  • 举例:带宽类似于一条高速公路的车道数,车道越多(带宽越大),就能容纳更多车辆(数据)同时通过。

速率(Speed 或 Transmission Rate)

  • 速率指的是实际传输数据的速度,即在实际应用中每秒传输的数据量。它取决于多个因素,比如网络质量、信号干扰、服务器响应速度等。
  • 实际速率通常会低于带宽,因为带宽只是理论上的最大值,而实际使用中可能存在延迟、数据包丢失等情况。
  • 举例:速率就像高速公路上的行驶速度,车道数(带宽)可能很多,但实际车辆行驶的速度(速率)可能会因为交通状况而变慢。

吞吐量(Throughput)

  • 吞吐量是指单位时间内实际传输的数据量,它反映了网络或系统的实际数据处理能力。吞吐量通常以Mbps(百万位每秒)、Gbps(千兆位每秒)等单位表示,和速率相似,但更侧重于实际效果。
  • 吞吐量会受到各种网络因素的影响,比如网络拥堵、丢包率、信号质量、硬件性能等,是网络在实际使用中能达到的最高数据传输量。
  • 举例:吞吐量就像是高速公路上实际通过的车流量,既取决于车道数(带宽),也受限于车辆(数据)在不同路段的行驶速度(速率)和交通堵塞情况。

时延(Latency)

  • 时延是指数据从发送端传输到接收端所需的时间,即网络传输的延迟。它表示网络反应的速度,单位通常是毫秒(ms)。
  • 时延主要由以下几个部分组成:
  • 传播时延:信号在传输介质中传播所需的时间,受限于光速或电磁波传播速度。
  • 处理时延:网络设备(如路由器、交换机)处理数据包所消耗的时间。
  • 排队时延:数据包在网络节点上排队等待处理或传输的时间。
  • 传输时延:数据通过链路传输的时间,通常由数据量和带宽决定。
  • 时延影响因素:网络带宽、设备性能、网络拓扑结构、信号衰减等都会影响网络的时延。
  • 举例:时延就像是在高速公路上行驶时,车辆从一个点到达另一个点所需的时间,受路程、红绿灯(网络设备)、道路情况(信号质量)等因素影响。

往返时间RTT(Round-Trip Time)

  • RTT是指一个数据包从发送端发出,到达接收端后,接收端返回响应数据包,整个过程所花费的时间。RTT 包含了数据包传输的时延以及接收端的处理时间,是衡量网络通信延迟的一个重要指标,单位通常为毫秒(ms)。
  • RTT 包含的主要时延因素有:
  • 发送时延:数据包从发送端到接收端的传输时间。
  • 处理时延:接收端对数据包进行处理的时间。
  • 返回时延:响应数据从接收端返回到发送端的时间。
  • 举例:RTT 类似于在高速公路上寄一封信,从出发到对方收到再寄回的整个过程所花费的时间。这个过程既受路程长短(传播时延)的影响,也受到信件处理速度(处理时延)和交通情况(网络状况)的影响。

HttpRTT

本文将从客户端开始发送请求,到收到响应首字节的耗时记为 HttpRTT。它与常见的 TTFB(Time to First Byte)口径接近,但是否包含 DNS、TCP、TLS 建连耗时,取决于埋点位置以及连接是否复用。HttpRTT 同时受到客户端网络、连接建立和服务端处理耗时的影响,不能仅凭这一项直接归因。

TcpRTT

TcpRTT(TCP Round-Trip Time)通常指 TCP 层根据数据包与 ACK 估算出的往返时延,例如 TCP_INFO 中的平滑 RTT。它不是“发送第一个字节到收到第一个字节”的耗时。对于已复用的 TCP 连接,HttpRTT 通常包含一个或多个网络 RTT 与服务端处理时间,但不再包含 TCP 三次握手;因此应结合连接阶段、服务端耗时和 TCP 指标综合判断。

弱网诊断观察的指标

弱网诊断根据HttpRTT和吞吐量来观察用户网络环境。

  • HttpRTT:如果能够通过 Server-Timing、服务端埋点或分阶段计时剔除服务器处理耗时,它可以更接近地反映端到端网络时延;否则只能作为综合异常信号。
  • 吞吐量(throughput):用户的额定速率能被系统提供的API获取到,然而其仅能表示设备能够提供的最大速率(一般很大),却不是真实速率,而测量真实的吞吐量更能体现出用户当前真实网络环境。

如何建立弱网标准

弱网标准建立流程

第一阶段,线下进行测试:

获取一些符合我们预期的阈值,这个时候我们需要借助一些网络测试工具,比如苹果的Network Link Conditioner,Facebook的ATC(Augmented Traffic Control),来获取到线下不同网络情况的阈值,一般我们会测试App冷启动的场景,网络切换的场景,DNS故障场景,弱网场景(一般都是配置上下行的带宽,丢包率,延迟,DNS延迟参数,或者更为简单的是使用工具默认的一些弱网配置)。

使用 OkHttp 统计请求耗时

// 自定义拦截器来获取请求时间
class TimingInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request: Request = chain.request()

        // 请求开始时间
        val startNs = System.nanoTime()
        YLog.logDebug("HttpTime", "start url=${request.url} on ${chain.connection()}")

        // 执行网络请求
        val response: Response = chain.proceed(request)

        // 请求结束时间
        val tookMs = (System.nanoTime() - startNs) / 1e6
        YLog.logDebug("HttpTime", "end url=${request.url} in $tookMs ms")
        return response
    }
}

葱管家请求统计

放了一晚上刚亮屏一会

设备放置一晚后的请求统计

不熄屏放了一个小时

设备亮屏一小时的请求统计

刚启动那会一般耗时都会大点,OkHttp 连接池缓存时间,默认空闲连接保留时间:5 分钟(300 秒)。默认最大空闲连接数量:5。

应用刚启动时的请求耗时

OkHttp 连接复用后的请求耗时

第二阶段,线上进行验证:

通过线下充分测试获取到的阈值,在线上可以获取到弱网的比例,在这里百度App是针对特定场景的,比如Feed刷新,搜索落地页打开等,就算是在移动时代被大家公认的网络体验好的微信,也只是在信令传输(收发消息)上做到极致优化,所以针对场景搜集弱网数据很重要。

第三阶段,线上的反复试验:

想做到理想的弱网效果,少不了线上反复的阈值调整,通过调整阈值比较针对场景的网络请求的成功率、耗时、连接复用率等指标,使我们获得趋向于针对场景的合理阈值。

网络探测的整体架构和实现

网络探测是弱网检测的基础,是否能即时,正确的检测出网络质量,是我们首先要解决的问题。我们把网络探测划分为两部分,主动网络探测和被动网络采集。

主动网络探测

所谓主动探测,就是在触发了某些条件后,主动的进行网络探测,并按照一定的条件检查出是否是弱网状态。百度App自研了主动探测组件,如下图所示。

主动网络探测架构

策略层

探测策略层通过多种策略的组合,使主动探测的即时性和准确性得以大大提高,我们结合上面的策略层图来解释下检测维度的意义。 我们分别在网络请求成功和失败的时候触发了弱网检测的逻辑。

主要分为如下三种逻辑:

进入疑似弱网判断后,就会触发基础能力层的 Ping 和 DNS Query 探测。

基础能力层

探测基础能力层,主要提供弱网检测的手段,一是dns query,一是ping,百度App使用C++实现了这两个能力。 **为什么要选用这两种手段呢?**我们在系列二中介绍过,一个网络请求,分为DNS → TCP → TLS → 数据传输 四个阶段。DNS Query 用于验证域名解析;Ping 基于 ICMP,可用于观察 IP 可达性和往返时延,但它并不等价于 TCP 连通性,部分网络还会直接屏蔽 ICMP。生产环境应结合 TCP/TLS 建连或轻量 HTTP 探测综合判断。dns query向百度核心域名mbd.baidu.com发起dns查询,查询的DNS服务器为系统配置的DNS服务器(iOS通过res_ninit函数构建一个\_\_res_state的结构体,现代 Android 应通过 ConnectivityManager 获取当前 Network 对应的 LinkProperties.dnsServers,不宜再依赖 system property 中的 net.dns1/net.dns2),DNS查询的超时时间为3s。ping的目标地址为百度核心域名mbd.baidu.com,ping的次数为两次,每次超时时间是默认的1s。

判断出弱网状态后,会将结果提供给接口层。

private val executors =
    CoroutineScope(context = Executors.newSingleThreadExecutor().asCoroutineDispatcher())
// 综合判断逻辑
fun checkConnectivity() {
    executors.launch {
        val dnsResult = dnsQuery("mbd.baidu.com", 3000) // 3 秒超时时间
        if (!dnsResult) {
            YLog.logDebug("mytest","DNS 查询失败")
            return@launch
        }

        val pingResult = ping("mbd.baidu.com", 2, 1) // ping 两次,每次超时 1 秒
        if (!pingResult) {
            YLog.logDebug("mytest","Ping 失败")
            return@launch
        }
        YLog.logDebug("mytest","网络连通性检测成功")
    }
}
// DNS 查询
suspend fun dnsQuery(hostname: String, timeout: Long): Boolean = withContext(Dispatchers.IO) {
    try {
        withTimeout(timeout) {
            // 使用 InetAddress 进行 DNS 查询
            val address = InetAddress.getByName(hostname)
            YLog.logDebug("mytest","DNS查询==="+address)
            address != null
        }
    } catch (e: TimeoutCancellationException) {
        false
    } catch (e: UnknownHostException) {
        false
    }
}

// Ping 操作
suspend fun ping(hostname: String, count: Int, timeout: Int): Boolean = withContext(Dispatchers.IO) {
    try {
        // 构建 ping 命令
        val command = "ping -c $count -W $timeout $hostname"
        val process = Runtime.getRuntime().exec(command)

        // 读取命令输出
        val reader = BufferedReader(InputStreamReader(process.inputStream))
        var line: String? = reader.readLine()

        // 用于统计成功 ping 次数
        var receivedCount = 0

        while (line != null) {
            if (line.contains("icmp_seq")) {
                YLog.logDebug("mytest", "ping===" + line)
                receivedCount++  // 统计每次成功的 ping
            }
            line = reader.readLine()
        }

        // 等待命令完成并检查返回码
        val exitCode = process.waitFor()

        // 确认接收到的 ping 次数是否与预期一致
        receivedCount == count && exitCode == 0
    } catch (e: Exception) {
        e.printStackTrace()
        false
    }
}

实现提示: 上述代码用于说明探测流程。InetAddress.getByName() 是阻塞调用,协程的 withTimeout 不能保证立即中断底层 DNS 查询;不同 Android 设备的 ping 命令与参数也可能不同。生产实现应使用可取消的异步 DNS、受控执行器或网络库提供的分阶段指标,并使用自有可控域名作为探测目标。

guanghe-360-2.4G请求

2.4G Wi-Fi 下的请求结果

guanghe-360-5G请求

5G Wi-Fi 下的请求结果

使用Charles 模拟 3G 网络后,可以看到网络请求变慢了,但是ping和dns查询限制不到

Charles 模拟 3G 网络后的请求结果

弱网环境下的探测结果

路由器按 2G 网络标准,限制上下行速度guanghe-TP-LINK_WDR5620-5G

路由器限速后的请求结果

2G 网络标准下的请求统计

接口层

接口层主要提供主动探测出来的网络状态,目前包括GOOD,BAD,UNKNOWN,OFFLINE。

判断边界: 单次 Ping 失败不能直接证明网络不可用。ICMP 可能被网关或服务端禁用,状态机应结合 DNS、TCP/TLS/HTTP 探测以及近期业务请求的成功率,避免把可用网络误判为 BAD 或 OFFLINE。

被动检测

参考百度APP移动端网络深度优化实践分享(三):移动端弱网优化篇,复杂度较高,可以暂不考虑

弱网优化方案

提升连接成功率

复合连接会按固定间隔阶梯式发起多个候选连接,其中一条连通后立即关闭其他连接。它结合了串行和并发的优势,可以提高弱网下的连接成功率;同时需要通过并发上限、启动间隔、超时和取消机制控制额外的客户端与服务器资源开销,见下图。

复合连接的阶梯式并发机制

离线指令处理

部分指令操作的时候,网络可能出现了问题,等网络恢复以后,要自动同步给服务器。

举一个例子,大家可以试试手机设置为飞行模式,然后在微信里删除一个联系人,看看能不能删除。然后重新打开网络,看看这个数据会不会同步到服务器。

类似的逻辑也适用于已读同步等场景,离线状态看过的信息,要正确的跟服务器同步。

减少不必要请求

一些通用内容,如导航、频道,通常由运营人员主动更新。

区别用户网络,适应不同的策略

  • 弱网下,自动降低视频清晰度
  • 对于海报banner等,后端接口提供两种质量的 Url,WiFi 高质,弱网低质

调整http超时时间

弱网下可以按场景适当延长连接或读取超时,但应同时设置请求总预算、取消机制和重试上限,避免无限等待放大用户等待时间和资源占用。

预连接

通过预连接,让后续的网络请求尽量减少tcp重新握手耗时

长连接

长连接对于提升应用网络交互的及时性大有裨益,一方面用户使用时,节省了三次握手的时间等待,响应快捷;另一方面服务器具备了实时推送能力,不但可以及时提示用户重要信息,而且能通过推拉结合的异步方案,更好的提升用户体验

思考

带宽 vs 速率 vs 吞吐量三者间的关系

  • 带宽:理论上最大可以传输的数据量,定义了网络的最大潜力。
  • 速率:在某一时间点上,实际传输的速度。
  • 吞吐量:网络或系统在一段时间内实际传输的数据量,是带宽和速率的综合结果。吞吐量通常低于带宽,因为它反映的是系统的实际性能,而不是理论上可能达到的最大值。

吞吐量如何计算

  • throughput(bit/s , bps) = 单位时间内通过的数据量(bit) / 单位时间(s)

QUIC 协议是什么

预连接如何建立

  • 在网络库初始化的时候,会根据使用者的配置延迟5s进行预连接的建立,主要是考虑网络库在冷启动下对于启动性能的影响,为了保证网络库的整体性能,预连接的总个数限制在20个。

预连接是如何保持的?

在网络库初始化的时候,除了进行预连接的建立,还会创建一个预连接的定时器,可以每隔5min(okhttp默认连接存活时间5分钟)获取当前是否有存活连接,有新连接上来则重置时间,根据使用者的配置重新建立连接。

展望

用户端的弱网反馈和提示:未来的应用可以提供更直观的弱网反馈给用户,提示用户当前网络状况,同时根据具体情况建议解决方案,如切换 Wi-Fi 或重启路由器。

开发者的网络问题可视化:为开发者提供更详细的网络分析工具,通过 API 和系统级网络分析,可以实时追踪网络延迟、丢包率、带宽变化等指标,让开发者更快地诊断和解决弱网问题。

渐进式增强应用性能:对于弱网用户,应用程序可以动态调整加载策略,如渐进式加载页面、降低图像和视频的分辨率,或减少数据传输量,保证用户体验。

参考资料