我们是如何大幅度降低安卓客户端网络投诉的(上)

admin 2023年2月3日00:51:54评论76 views字数 10986阅读36分37秒阅读模式




一、背景

随着移动互联网的发展,客户端网络优化已经成为移动互联网开发过程中不可或缺的一环。然而,实际开发中,我们面临着严峻的挑战:由于受到网络环境和用户设备等多种因素的影响,客户端网络请求经常会受到严重影响,从而导致用户体验差、加载速度缓慢、网络资源浪费等问题。因此,我们紧急需要寻求一种可行的解决方案,以优化客户端网络请求,从而提升用户体验和节省网络资源。

在这一过程中,我们面临着诸多困难:首先,由于客户端网络环境复杂,我们很难准确分析出其中的瓶颈,从而采取有效措施;其次,受到设备性能、用户网络技术水平等因素的限制,客户端网络请求的优化有其局限性;最后,由于网络协议复杂,我们遇到了许多跟预期不符的异常现象和部分特殊的用户投诉,需要限时尽快解决。

痛定思痛,我们成立了专门的网络小组,开始了客户端网络优化的工作。本文将就客户端网络优化的一些技术进行介绍,以便帮助网络管理者对客户端网络进行有效的优化。


二、网络框架统一

工欲善其事,必先利其器。Android客户端的绝大多数网络请求通过http连接实现,但团队早期由于历史原因引入了多套网络请求框架,其中主要的有:

  • HttpURLConnection :jdk用于http连接请求的原生api;

  • HttpClient:Apache Jakarta Common 下的子项目,用来提供高效的、最新的、功能丰富的支持 http 协议的客户端编程工具包;

  • volley:google基于httpURLConnection 封装的网络请求框架,适合大量小型请求,对于下载请求支持不友好;

  • OkHttp:一套处理 http 网络请求的依赖库,由 Square 公司设计研发并开源,目前可以在 Java 和 Kotlin 中使用。


借着这次对网络框架进行统一的机会,我们不仅仅是想统一网络请求的接口,也希望能够封装一个最适合团队使用和后续业务扩展的网络框架。

我们基于两个最主要的维度来评价一个网络请求框架:

  1. 高性能。网络请求框架的性能直接决定了应用请求的速度和时延,我们当然希望应用能够同时发起尽可能多的并发请求,并且请求的时延尽量小。基于此从速度,CPU,内存,I/O的使用,以及失败率,崩溃率,协议的兼容方面去评估一个网络请求框架的优劣;

  2. 易用性。由于我们是一个小型团队,也希望选择的网络框架能够尽量易于上手使用,不要有过度设计。同时这个框架也需要便于扩展,框架内部的耦合程度越低越好,有利于我们后期针对业务特点,对框架进行定制性的二次开发。


在此基础之上,我们也调研了另外的一些网络请求框架:

 

OkHttp

Cronet

Mars

DNS管理 支持DNS缓存,支持对接HttpDns 支持DNS缓存,支持对接HttpDns 支持DNS缓存,支持对接HttpDns
连接管理

对于http连接,每个域名最多缓存5个连接,默认Keep time是5分钟

对于http/2连接,每个域名公用一个H2连接

对于http连接,每个域名最多缓存6个连接

对于http/2连接,每个域名公用一个H2连接

支持复合连接,没有连接管理
并发模型

使用多线程实现并发,实际执行线程数受队列机制控制,并发最大请求数是64,

单个域名最大并发请求数是5

每个Host对应一条线程,每个线程可以创建

6个非阻塞Socket请求网络

同一时间最多16个线程

每个短连接都是一个线程,没有线程限制
I/O模型 阻塞Socket,使用Okio包装Socket进行流读写 epoll + 非阻塞Socket epoll + 非阻塞Socket
协议支持 http/1.1,http/2.0,https http/1.1,http/2.0,https,QUIC 信令网络,只支持TCP
网络质量监控 默认不支持

Predictor用于收集使用数据并预测网络行为,

NQE提供当前网络质量评估

SDT模块支持网络侦测与诊断
长连接 支持Websocket 默认不支持

默认支持

跨平台 java/Kotlin实现,不支持跨平台 C++实现,支持跨平台 C++实现,支持跨平台
二次开发难度 简单 实现复杂,扩展困难 实现复杂,扩展困难

 

虽然Chromium的Cronet和腾讯的mars都作为跨平台框架,适应的场景更为复杂,尤其mars作为微信内核的网络库,对于长连接场景有诸多优化,但考虑到OkHttp底层使用java和kotlin开发,和团队使用的技术栈完全匹配,二次开发的难度也比较低。最终选择“性价比”更高的OkHttp框架作为底层网络库,并在其之上封装了一套便于使用的网络请求api。同时在今年应用内部的即时通信模块进行了自研的改造,也使用了OkHttp的Websocket作为长连接的底层支持。

由于好大夫的Android客户端共有患者端和医生端两个app,所以网络库采用组件化的开发形式,依托Android端现有的组件化能力,封装的网络库可以同时供两端app使用,保持了一致性。在经历了漫长的dirty work之后,终于将应用中的其他网络框架清除干净,我们也更有精力去对网络请求进行真正的优化。


三、网络优化的指标评估

1.横向来看,网络优化有三个核心内容:

(1)速度。在用户网络情况正常的情况下,能够更合理地利用带宽,进一步提升网络请求的速度。

(2)弱网络。移动端网络复杂多变,在出现网络连接不稳定的时候,能够最大程度保证网络的连通性。

(3)安全。在考虑性能的情况下安全也不容忽视,需要有效防止网络连接被第三方劫持,窃听甚至篡改。

2.纵向来看,先来细化下http网络请求的整个过程:

我们是如何大幅度降低安卓客户端网络投诉的(上)


整个网络请求可以被划分为几个阶段,整个请求的耗时可以细分到每一个阶段里。

(1)dns解析。通过dns服务器将域名解析为ip地址的过程。在这个阶段,我们主要关注dns解析耗时情况,运营商LocalDns的劫持,dns调度优化这些问题。

(2)创建连接。和目标服务器建立连接,主要包括tcp三次握手,tls密钥协商等工作。在这个阶段,我们主要关注多个IP/端口应该如何选择,是否要使用https,能否减少创建连接的时间。

(3)发送/接受数据。成功建立连接之后,接下来和服务端进行交互,组装数据、发送数据、接收数据、解析数据。在这一阶段,我们主要关注如何根据网络状况最大化地利用带宽,并通过技术手段监测网络情况变化,动态调整包大小。

(4)关闭连接。传统的http连接是一个个短链接,http2之后连接可以进行复用。这个阶段我们主要关注连接的管理和释放时机,防止由于连接没有被正常关闭造成的错误复用,资源泄漏以及流量消耗问题。

通过围绕横向的三个核心:速度,弱网络,安全,在纵向的整个网络请求过程中,对每个阶段进行优化,减少每个阶段的耗时,最终完成整个网络请求链路的优化。

3.对网络进行优化之后,可以通过这些指标来量化整个网络优化的结果:


  1. 吞吐量:网络接口接收和传输的每秒字节数;

  2. 延迟:系统调用,发送/接收延迟、连接延迟,首包延迟,网络往返时间等;

  3. 连接数:每秒的连接数;

  4. 错误:丢包计数,超时等。


他山之石,可以攻玉,在web领域衡量性能的一些通用指标同样可以反映衡量移动端网络请求的优劣情况,如TTFB。

Time to First Byte (第一字节时间),是一个衡量对资源的请求和响应的第一个字节开始和到达之间时间的指标。这个指标比上面列出的指标更为宏观一些,可以作为整个网络优化结果的一个总体观察评价。

我们是如何大幅度降低安卓客户端网络投诉的(上)


四、网络优化实践

策无略无以为恃,计无策无以为施。如果没有战略的话,那么在任何问题面前或者为某种目标努力,其实都是一种漫无目的的行为。前述的一些网络相关的优化理论,为整个网络优化指明了方向。在此基础之上,我们要做的就是针对每个具体的技术点制定优化战术,各个击破,最终完成整体网络优化的目标。下面是Android客户端在网络优化方面的一些实践:

4.1 HttpDns

  1. 好大夫在线患者端app不会含有广告等诱导消费的内容,但是却有患者反馈打开的页面中有广告内容;好大夫医生版app开屏中会有一些活动引导的banner,有医生反馈banner图片变成了广告。经过我们的排查,这些请求被劫持到了其他页面或被注入了广告内容;

  2. 在一段时间内有用户集中反馈页面无法打开或打开缓慢的问题,经过我们的专项排查,发现由于劫持或者LocalDns调度不优的问题,用户的网络请求存在失败的情况。通过网络监控统计数据发现,UnknownHostException在网络失败类型(TOP5)里占第一位(≈90%)。

域名解析处于网络请求的初始阶段,解析的结果直接影响到后面连接的建立与交互,解析结果错误或不优会对整个网络请求产生阻塞式的影响,需要通过技术手段解决这一问题。

4.1.1 HttpDns的原理

使用LocalDns容易出现域名劫持,并且由于其使用了无状态的UDP协议,难以定位解决。HttpDns 基于 http 协议向 DNS 服务器发送域名解析请求,替代了基于 DNS 协议向运营商 LocalDns 发起解析请求的传统方式,可以避免 LocalDns 造成的域名劫持和跨网访问的问题,解决移动互联网服务中域名解析异常带来的困扰,同时更有效地保障 App避免移动互联网中的劫持、跨网域名解析错误等问题。

LocalDns造成的用户访问异常的原因可以分为以下几类:

  1. LocalDns会缓存DNS域名解析的结果,请求数据中被插入第三方广告数据也是由于LocalDns会把域名解析结果指到广告联盟的内容缓存上;

  2. 运营商的LocalDns会将解析转发,将域名请求转发到其他运营商LocalDns进行递归,造成域名解析错误,用户跨网访问。

使用HttpDns不光可以解决劫持相关的问题,同时可以改善LocalDns调度不准确的问题,比如使用LocalDns经常会出现的跨地区,跨运营商调度的问题,导致增加用户的访问延迟。除此之外,LocalDns的解析耗时也无法定制,在Android客户端现网统计数据中,有一些用户的LocalDns解析耗时甚至能达到10s以上,在如今端到端网络请求延迟达到秒级用户可能都无法接受,使用HttpDns后可以统一DNS的TTL,提高解析结果同步的及时性,并实现超时逻辑,减少用户等待。

采用HttpDns主要有以下优势:

  1. 解决了根域名解析异常:由于绕过了运营商的LocalDns,用户解析域名的请求通过http协议直接透传到了HttpDns服务器IP上,用户在客户端的域名解析请求将不会遭受到域名解析异常的困扰;

  2. 实现成本低:接入HttpDns的业务仅需要对客户端接入层做少量改造,并且采用的http协议请求可以兼容基本上全部的Android手机;

  3. 调度精准,扩展性强:在接入HttpDns服务的服务端及运维层面,我们没有选择自己搭建,而是选用了腾讯云作为HttpDns的服务提供商。为了保证高可用及提升用户体验,HttpDns通过接入腾讯公网交换平台的BGP Anycast网络,与全国多个主流运营商建立了BGP互联,保证了这些运营商的用户能够快速地访问到HttpDns服务;另外HttpDns在多个数据中心进行了部署,任意一个节点发生故障时均能无缝切换到备份节点,保证用户解析正常。


我们是如何大幅度降低安卓客户端网络投诉的(上)


简单来说就是,Android客户端直接访问HttpDns接口,获取业务在域名配置管理系统上配置的访问延迟最优的IP。

在接入过程中,进行了一些策略上的优化:

(1)容灾策略:当HttpDns服务不可用并且缓存也不可用的情况下,会触发降级策略,采用LocalDns的域名解析结果。为防止HttpDns解析时间过长,也将请求的超时时间设置为1s,超时也会触发此降级策略。

(2)安全策略:从LocalDns到HttpDns,协议由UDP变成了http,但安全性还是没有保障,在此之上采用https协议传输请求,提高了安全性。

(3)缓存策略:为防止频繁调用HttpDns服务,造成解析失败率提高的风险,在客户端对解析结果进行缓存。为防止缓存时间过长,结果过期造成解析结果错误或缓存命中率不高的问题,也针对缓存更新机制做了一些优化。如当网络状态发生切换时更新缓存,当当前时间到达ttl的75%时,就提前预请求解析接口等。

下面是android端HttpDns的域名解析流程:

我们是如何大幅度降低安卓客户端网络投诉的(上)


4.1.2 HttpDns的实践

(1)OkHttp接入HttpDns

OkHttp 提供了 DNS 接口,用于向 OkHttp 注入 DNS 实现。得益于 OkHttp 的良好设计,使用 OkHttp 进行网络访问时,实现 DNS 接口即可接入 HttpDns 进行域名解析,在较复杂场景(http/https/WebSocket + SNI)下也不需要做额外处理,侵入性极小。下面是实现HttpDns的伪代码:

mOkHttpClient =    new OkHttpClient.Builder()        .dns(new Dns() {            @NonNull            @Override            public List<InetAddress> lookup(String hostname) {               if (HttpDns.isMatch(hostname)) {                    //满足HttpDns解析域名                    return lookupHttpDns(hostname);                }  else {                    //否则查找LocalDns                    return lookupLocal(hostname);                }            }        })        .build();


(2)glide接入HttpDns

类似网络库统一,团队内部也进行过一次图片加载库统一的技术改造,现在图片库已经统一为单一框架:glide。图片请求在我们应用里静态请求的占比是最多的,尤其在一些关键业务场景,如医患交流中的病历图片查看,如果图片加载失败会给用户带来非常恶劣的体验,因此图片加载也需要接入HttpDns。

由于OkHttp已经接入HttpDns,实际上也就是将glide内部的网络请求托管到OkHttp上。

首先,需要依赖两个库,glide官方提供一个将glide和OkHttp进行桥接的组件,由于glide V4版本之后的组件发现改为注解处理器解析方式,因此需要依赖glide的注解处理器。

apply plugin: 'kotlin-kapt'  dependencies {    kapt 'com.github.bumptech.glide:compiler:4.7.1'    implementation "com.github.bumptech.glide:okhttp3-integration:4.11.0"}


然后,通过注解发现组件的方式进行glide和OkHttp的集成:

@GlideModulepublic final class OkHttpLibraryGlideModule extends LibraryGlideModule {  @Override  public void registerComponents(Context context, Glide glide, Registry registry) {    registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());  }}


4.1.3 HttpDns的效果评估


我们是如何大幅度降低安卓客户端网络投诉的(上)

我们是如何大幅度降低安卓客户端网络投诉的(上)

我们是如何大幅度降低安卓客户端网络投诉的(上)

 

上线前后对比,UnknownHostException占比明显变少,域名劫持情况得到明显改善;同时请求失败率也下降许多,表明之前域名解析调度不优的情况得到很大缓解。

 

4.2 网络请求过程优化

在完成dns解析之后,网络请求接下来会进入连接建立以及请求/响应阶段,在这些阶段我们团队也做了很多安全性能上的优化。

4.2.1 https&&http2

除了性能以外,数据安全也是网络优化中非常重要的一部分。一方面,http协议使用明文通信,虽然我们已经对HttpDns做了优化,解决了域名劫持的问题,但是通信的数据仍然有可能被运营商修改并插入广告。此外通信的数据也完全暴露给外界,安全无法保障;另一方面,http2的使用需要基于https,并且从Android 9系统开始谷歌强制要求应用使用https。因此将Android客户端的网络请求迁移到https成为了我们下一阶段的工作。

https协议的安全性由ssl/tls协议实现,tls作为ssl协议的继承升级,是目前使用的主流协议。ssl/tls层负责客户端和服务器之间的加解密算法协商,密钥交换及连接通信的建立,这主要通过子协议:握手协议来实现。

tips:https的握手过程需要2-RTT(Round-Trip Time,即往返时间),增加了网络请求的成本,提高了弱网环境下的请求时延。为了减少握手次数,TLS 1.3可以实现0-RTT协商,但是前提是网站服务器域名需要支持。

我们是如何大幅度降低安卓客户端网络投诉的(上)


由于此阶段我们的网络框架已经统一成为OkHttp,包括应用中的普通post请求和图片请求,而OkHttp对于https有着非常好的底层支持,因此Android端接入https的代价和风险都较小。在接入过程中有一些细节需要注意:

(1)证书相关

如果我们使用的服务端提供的证书是可信ca颁发的数字证书,直接使用OkHttp默认的加密相关配置就可以完成https连接的建立。但如果有使用私有ca签发证书的需求,就需要重新实现TrustManager中校验服务端证书的方法,否则会遇到:javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found 的错误。

(2)调试相关

完成https的改造之后,客户端的请求都变成https,会存在调试困难的问题,比如测试在使用代理软件时,必须安装代理证书才可以看到请求的内容,非常不方便。针对这个问题,我们在debug环境下单独配置了网络相关的控制权限:

buildTypes {     debug {        manifestPlaceholders = [                NETWORK_SECURITY_CONFIG: "@xml/network_security_config_debug",        ]        ...    } }


而配置文件的内容中,通过将debug包的cleartextTrafficPermitted参数设置为true,将请求降级到http。

<?xml version="1.0" encoding="utf-8"?><network-security-config>    <base-config cleartextTrafficPermitted="true">        <trust-anchors>            <certificates src="system" overridePins="true" />            <certificates src="user" overridePins="true" />        </trust-anchors>    </base-config></network-security-config>


(3)为了进一步提升安全性,在https协议之上我们又做了如下安全方面的优化:

  1. 特定敏感数据又做了额外的加密,如用户密码等隐私信息,客户端日志等;

  2. 对请求url及参数按照一定规则(如随机数加盐)生成摘要参数,防止接口被恶意抓取重放。

 

为了解决https协议升级造成连接时延变长,网络请求状况变差的问题,我们又进行了http2协议的改造升级。关于http2协议的参考文章可以看这里https://web.dev/performance-http2/

DNS的query需要一个RTT,TCP的握手需要1-1.5个RTT,TLS的握手和密钥交换需要两个RTT,通过HttpDns的改造可以利用缓存优化掉DNS的这段时间,在连接阶段我们也是一样的思路,通过连接复用,避免每次请求都重新建连。

tips:在使用http/1.1协议时,我们通过开启keep-alive来维持连接的保持,OkHttp的连接池(ConnectionPool)会维持复用未关闭的连接,在http/2协议中连接也是在此连接池中管理。

http/2中多路复用的优化可以进一步提升连接复用率,简单来说就是http/2复用的连接支持处理多个请求,所有的请求都可以并发在这条连接上进行。

我们是如何大幅度降低安卓客户端网络投诉的(上)


除了连接层面的优化外,在传输阶段减少数据量也是一个优化的思路。

(1)http/2协议采用基于HPACK的头部压缩技术一方面,头信息使用HPACK算法压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号。

(2)对于请求的body体来说,可以用Protocol Buffers这种二进制格式代替Json进行数据传输,在数据压缩率,序列化和反序列化速度上都有很大的提升,但是可读性和可调试性会变差。

(3)使用google的brotli或facebook的Z-standard这些压缩率更高的算法,替换gzip压缩请求数据。对于特定数据我们还可以采用别的压缩方法,如针对Android客户端图片下载启用了webP格式后,图片大小总体平均减少 62%

 

Android客户端接入http2,需要注意的是除了需要服务器开启http/2协议支持之外,OkHttp的版本需要高于3.4.2(之前的版本存在http/2协议重用已关闭连接,导致请求无限循环的问题)。

OkHttp的http/2开启需要依赖于https,由于我们的框架已经先支持了https的升级,所以这里不存在障碍。http/2的开启通过ALPN选择协议协商进行,在tls握手阶段,客户端在clientHello中会携带支持的http协议版本,如果服务器也支持http/2协议,就会在ServerHello中指定 ALPN 的结果为h2,从而开启http/2协议。

我们是如何大幅度降低安卓客户端网络投诉的(上)


在代码实现中,RealConnection类connectTls建立tls连接过程中,顺带通过ALPN完成http/2协议的协商,并进行了http/2协议的启动。

private void connectTls(ConnectionSpecSelector connectionSpecSelector)                    throws IOException {    // Success! Save the handshake and the ALPN protocol.    String maybeProtocol = connectionSpec.supportsTlsExtensions()    ? Platform.get().getSelectedProtocol(sslSocket)    : null;     protocol = maybeProtocol != null    ? Protocol.get(maybeProtocol)    : Protocol.http_1_1;} private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,    int pingIntervalMillis, Call call, EventListener eventListener)    throws IOException {    connectTls(connectionSpecSelector);     if (protocol == Protocol.http_2) {        starthttp2(pingIntervalMillis);    }}



http/2不是在所有的网络优化场景中都有正向的加成,甚至在一些特定的网络场景中会产生反向的效果。对于http/2的连接,同一个域名只会保留一条长连接,在一些大文件上传/下载或弱网络场景中,使用http/2会遇到队首阻塞问题,链路上的所有请求都会受到影响,在丢包率比较高的场景http/1.1的性能甚至会好于http/2。我们也收集到一些和http2相关的崩溃(OkHttp3.internal.http2.StreamResetException: stream was reset: PROTOCOL_ERROR/ CANCEL),参见OkHttp的bug issue:https://github.com/square/OkHttp/issues/3955

对于这种问题,可以采取如下措施:

  1. 在特定场景回退到http/1.1协议实现;

  2. 待时机成熟时使用quic协议,避免http/2队头阻塞的问题。


4.2.2 其他优化

除此之外,我们还对网络框架做了如下改进以期改善网络质量:

  1. 关键场景预取数据,如在冷启动时提前请求首页数据,先于页面加载获取到展示数据,进一步提高冷启动及首页打开速度;

  2. 视频大文件上传使用多文件分片上传,并行上传缩短上传时间,提高用户体验;

  3. 配合运维建立多cdn平台调度,提高用户进行动态/静态请求时获取cdn节点的质量;

  4. 对OkHttp的初始化参数进行调优:

    1. 通过A/B Test对OkHttp超时参数进行实验,调小OkHttp建连超时时间(connectTimeout)和写超时时间(writeTimeout),缩短用户进行网络请求等待时间;

    2. 通过A/B Test和线上监控对OkHttp单域名最大并发数量进行调整,适当扩大并发数,避免网络请求等待队列过长,阻塞后续请求的问题。

  5. 自建websocket长连接通道,满足医患交流业务的im场景需求。


五、总结

在本篇中,我们首先对网络框架的使用进行了重构,通过“化零为整”的方式对网络框架的使用进行了统一,将网络请求、图片加载、上传和下载功能的底层网络服务收拢到OkHttp。在此基础之上,对http网络请求的各个阶段进行了加固优化,通过httpdns,https以及http2协议优化整改,网络框架的整体性能和安全性有了明显的提高,在解决一些诸如网络劫持等疑难杂症时有显著效果,用户投诉量有了一定程度的下降。

虽然从宏观上看客户端网络请求的稳定性有了很大提升,但是我们仍然会收到一些户反馈的个例网络问题,十分难受下篇我们会带来对网络框架进行进一步细致、个性化定制的策略讲解,以及我们是如何通过更详细的志监控、上报等手段,以排查效率、更彻底地改善用户使用体验的相关分享,敬请期待


------- END -------

【作者简介】
  • 赵瑞萱:好大夫在线Android高级开发工程师,负责Android客户端网络优化,底层日志系统开发等相关工作。

  • 黄斌:好大夫在线Android架构师。专注于研究、设计、开发和维护Android应用程序框架,负责app整体性能优化。


我们是如何大幅度降低安卓客户端网络投诉的(上)

好大夫在线创立于2006年,是中国领先的互联网医疗平台之一。目前已收录国内1万+正规医院的90万+医生信息,其中25万名医生在平台上实名注册,直接向患者提供线上医疗服务。好大夫在线以“提升医疗效率,守护健康生活”为使命 ,始终追求“成为值得信赖的医疗服务平台”


也许你还想看:

原文始发于微信公众号(HaoDF技术团队):我们是如何大幅度降低安卓客户端网络投诉的(上)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年2月3日00:51:54
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   我们是如何大幅度降低安卓客户端网络投诉的(上)https://cn-sec.com/archives/1534385.html

发表评论

匿名网友 填写信息