美图App的移动端DNS优化实践:HTTPS请求耗时减少近半

本文引用了颜向群发表于高可用架构公众号上的文章《聊聊HTTPS环境DNS优化:美图App请求耗时节约近半案例》的部份内容,感谢原做者。php

一、引言

移动互联网时代,APP 厂商之间的竞争很是激烈,而良好的用户体验是必须优先考虑的,美图产品以高颜值著称,对产品的用户体验很是重视。从技术的角度来看,客户端的体验优化当中 DNS 优化是很是关键的一环,怎么下降 DNS 的耗时、怎么减小域名劫持等问题,都是你们须要重点解决的研发问题。html

本文介绍美图APP在移动端DNS优化的实践(主要针对HTTPS协议),文章内容从DNS问题、原理到最终优化效果,讲解的较全面,值得学习和借鉴。程序员

另外:如您想详细了解移动端DNS的各类杂症及主流解决方案,推荐详读《全面了解移动端DNS域名劫持等杂症:原理、根源、HttpDNS解决方案等》。算法

(原文连接:http://www.52im.net/thread-2172-1-1.html编程

二、相关文章

TCP/IP详解 卷1:协议 - 第14章 DNS:域名系统缓存

网络编程懒人入门(七):深刻浅出,全面理解HTTP协议安全

现代移动端网络短链接的优化手段总结:请求速度、弱网适应、安全保障服务器

移动端IM开发者必读(一):通俗易懂,理解移动网络的“弱”和“慢”网络

移动端IM开发者必读(二):史上最全移动弱网络优化方法总结session

全面了解移动端DNS域名劫持等杂症:原理、根源、HttpDNS解决方案等

三、内容概述

 

DNS 服务做用于网络链接以前,将域名解析为 IP 地址供后续流程进行链接(原理详见:《TCP/IP详解 卷1:协议 - 第14章 DNS:域名系统》)。

DNS 查询时,会先在本地缓存中尝试查找,若是不存在或是记录过时,就继续向 DNS 服务器发起递归查询,这里的 DNS 服务器通常就是运营商的 DNS 服务器。

在这过程当中,会产生一些不可控的问题。

美图的移动端产品在实际用户环境下会面临 DNS 劫持、耗时波动等问题(详见:《全面了解移动端DNS域名劫持等杂症:原理、根源、HttpDNS解决方案等》),这些 DNS 环节的不稳定因素,致使后续网络请求被劫持或是直接失败, 对产品的用户体验产生很差的影响。 

为此,咱们对移动端产品的 DNS 解析进行了优化探索,产生了相应的 SDK。在这过程当中,咱们参考借鉴了业内的主流方案,也进行了一些实践上的思考。

下面的内容会主要以 Android 平台来进行说明。

四、LocalDNS VS  HTTP DNS

在长期的实践中,互联网公司发现 LocalDNS 会存在以下几个问题:

1)域名缓存:运营商 DNS 缓存域名解析结果,将用户导向网内缓存服务器;

2)解析转发 & 出口 NAT:运营商 DNS 转发查询请求或是出口 NAT 致使流量调度策略失效。

什么是LocalDNS?通常来讲,LocalDNS就是指本地ISP运营商的DNS:

 

▲ 图中“局部DNS服务器”便是LocalDNS

为了解决 LocalDNS 的这些问题,业内也催生了 HTTP DNS 的概念(注:如您对LocalDNS、HTTP DNS这些概念还不了解,请务必先阅读《全面了解移动端DNS域名劫持等杂症:原理、根源、HttpDNS解决方案等》)。

HTTP DNS的基本原理以下:

本来用户进行 DNS 解析是向运营商的 DNS 服务器发起 UDP 报文进行查询,而在 HTTP DNS 下,咱们修改成用户带上待查询的域名和本机 IP 地址直接向 HTTP WEB 服务器发起 HTTP 请求,这个 HTTP WEB 将返回域名解析后的 IP 地址。

好比 DNSPod 的实现原理以下:

 

相比 LocalDNS,HTTP DNS 会具有以下优点: 

1)根治域名解析异常:绕过运营商的 DNS,向具有 DNS 解析功能的 HTTP WEB 服务器发起查询;

2)调度精准:HTTP DNS 可以直接获取到用户的 IP 地址,从而实现准确导流;

3)扩展性强:自己基于 HTTP 协议,能够实现更强大的功能扩展。

那么,是否直接所有走 HTTP DNS 呢?

五、美图APP的DNS 优化策略探索

HTTP DNS 相比 LocalDNS 存在一些优点, 然而 HTTP DNS 自己也是存在必定的成本问题。

美图的产品线丰富,涉及的域名也较为普遍,为了适应各产品的实际场景,在实践中咱们设计了较为灵活的策略控制。 

首先,在策略上咱们并未彻底放弃 LocalDNS。

一个 App 涉及的域名众多,在策略上咱们可以配置其核心 API 域名走 HTTP DNS,而对于非核心请求咱们仍但愿它先尝试走 LocalDNS, 在异常状况下才升级走 HTTP DNS。

那么如何判断 LocalDNS 的异常状况呢?

咱们选择了几个指标来衡量一个 DNS 服务器的质量状况: 

1)IP 记录的 TTL 时间:在 DNS 劫持发生的状况下,返回的 TTL 可能会有很是大的值;

2)解析耗时:若是一个 DNS 服务器解析耗时不理想,那么它也不是咱们但愿的;

3)返回的 IP 的可链接性:对返回的 IP 进行质量测试,若是链接情况不佳,那么这个 DNS 服务器有劫持的可疑。

在 Android 平台上,经过系统方法得到的解析结果信息是很是有限的,上面的指标有的将没法获取,所以在实践中咱们会本身去构造 DNS 查询报文,向运营商的多个 DNS 服务器发起查询。

经过上面几个指标的综合评定,当 LocalDNS 表现不佳的时候,策略上咱们将升级走 HTTP DNS,尝试让用户获取更好的 DNS 解析效果。

在 DNS 解析环节,还有一个咱们比较关心的指标,那就是 DNS 解析的耗时:

1)LocalDNS 在过时的状况下,会发起递归查询,这个时间是不可控的,在部分状况下甚至能达到数秒级别;

2)HTTP DNS 相对会好一些,但正常来看,也会有200ms 左右的耗时。

这个时间可否再优化一些呢? 

咱们 SDK 在本地构建了本身的记录缓存池,每次经过 LocalDNS 或是 HTTP DNS 解析获得记录都存在缓冲池中。

固然,这个是广泛的作法,系统底层的 netdb 库也是这样实现。

区别在于咱们作了一个小改动:对于过时的记录咱们采用懒更新的策略,当查到过时的缓存记录时,先返回过时记录给用户,同时再异步从新发起 DNS 查询更新缓存记录。

这个小改动可以保证咱们二次解析时都能命中本地缓存,极大地下降 DNS 解析耗时,不过它也带来了必定的风险性。

所以实践中:咱们也会添加异步按期的 DNS 记录缓存池扫描功能,及时发现缓存中的过时记录并进行更新,也下降 App 命中过时记录的状况。

五、美图APP无侵入的 SDK 接入方式探索

在 DNS 优化的实践中,咱们遇到最大的问题,倒不是策略层面设计问题,而是咱们的 DNS SDK 运用到实际 App 产品业务上的姿式问题。

5.1 IP直连方案及各类坑

业内对 HTTP DNS 在实际业务中的接入方式多采用 IP 直连的形式,即本来直接请求 http://www.meitu.com,如今咱们先调用 SDK 进行域名解析,拿到 IP 地址好比 1.1.1.1,而后替换域名为: http://1.1.1.1/

这样操做以后, 因为 URL 中 HOST 已是 IP 地址,网络请求库将跳过域名解析环节,直接向 1.1.1.1 服务器发起 HTTP 请求。

在实际操做中,对于 IP 直连的方案咱们踩了很多的坑。

首先,对于 HTTP 请求,采用 IP 直连的方案后,咱们仍是须要进行的一个操做是手动配置 Header 中的 HOST :

URL htmlUrl = new URL("http://1.1.1.1/");

HttpURLConnection connection = (HttpURLConnection) htmlUrl.openConnection();

connection.setRequestProperty("Host","www.meitu.com");

HTTP 协议相对比较容易,只须要处理 HOST,那么 HTTPS 呢?

发起HTTPS请求首先须要进行 SSL/TLS 握手,其流程以下: 

1)客户端发送 Client Hello,携带随机数、支持的加密算法等信息;

2)服务端收到请求后,选择合适的加密算法,连同公钥证书、随机数等信息返回给客户端;

3)客户端检验服务端证书的合法性,计算产生随机数并用证书公钥加密发送给服务端;

4)服务端经过私钥获取随机数信息,基于以前的交互信息计算获得协商密钥并通知给客户端;

5)客户端验证服务端发送的数据和密钥,经过后双方握手完成,开始进行加密通讯。

在咱们采用 IP 直连的形式后,上述 HTTPS 的第三步会发生问题,。

客户端检验服务端下发的证书这动做包含两个步骤: 

1)客户端用本地保存的根证书解开证书链,确认服务端的证书是由可信任的机构颁发的;

2)客户端须要检查证书的 Domain 域和扩展域是否包含本次请求的 HOST。

证书的验证须要这两个步骤都检验经过才可以进行后续流程,不然 SSL/TLS 握手将在这里失败结束。

因为在 IP 直连下,咱们给网络请求库的 URL 中 host 部分已经被替换成了 IP 地址,

所以证书验证的第二步中,默认配置下 “本次请求的 HOST” 会是一个 IP 地址,这将致使 domain 检查不匹配,最终 SSL/TLS 握手失败。

那么该如何解决这个问题? 

解决 SSL/TLS 握手中域名校验问题的方法在于咱们从新配置 HostnameVerifier, 让请求库用实际的域名去作域名校验。

代码示例以下: 

finalURL htmlUrl = newURL("https://1.1.1.1/");

HttpsURLConnection connection = (HttpsURLConnection) htmlUrl.openConnection();

connection.setRequestProperty("Host","www.meipai.com");

connection.setHostnameVerifier(newHostnameVerifier() {

      @Override

      publicbooleanverify(String hostname, SSLSession session) {

          returnHttpsURLConnection.getDefaultHostnameVerifier()

                    .verify("www.meipai.com",session);

      }

});

咱们又解决了一个问题,那么 IP 直连下, HTTPS 的问题都搞定了吗?

没有,HTTPS 还有 SNI 的场景要特殊处理。

SNI(Server Name Indication)是为了解决一个服务器使用多个域名和证书的SSL/TLS扩展。

它的基本工做原理以下:

1)服务端配置有多个域名和对应的证书。客户端在与服务器创建SSL连接之时,先发送本身要访问站点的域名;

2)服务器根据这个域名返回一个合适的证书。

跟上面 Domain 校验的状况相似,这里的网络请求库默认发送给服务端的 "要访问站点的域名" 就是咱们替换后的 IP 地址。

服务端在收到这样一个 IP 地址形式的域名后将是一脸懵逼,找不到对应的证书,最后只好下发一个默认的域名证书回来。

接下来发生的是,客户端在检验证书的 Domain 域时,怎么也检查不经过,由于服务端下发的证书原本就不是对应该域名的。

最后 SSL/TLS 握手失败了结。

上述这个 SNI 场景下的问题,咱们是否有办法解决呢? 

能够解决,需用客户端从新定制 SSLSocketFactory , 不过修改的代码相对较多,这里就不列举了。

若是咱们 SDK 要接入到 App 实际业务中,到 HTTPS SNI 场景处理这里,相信不少同窗都崩溃了,接入的工做量其实也不低。

不少状况下可能就作了妥协,只有 Okhttp 场景才使用这个 SDK,由于 Okhttp 自己支持 DNS 替换,没有上面那些问题。

在美图的实践中,咱们不只仅但愿 Okhttp 的请求才进行这个 DNS 优化,咱们但愿在 App H5 页面加载、播放器播放等场景也能应用相应的优化。

在这样的需求下,IP 直连的接入方案带来的接入工做量其实不低,甚至须要改动到部分轮子。

在最初的实践中,咱们也的确尝试了落实 IP 直连 到各个模块,然而即便克服了改造的工做量问题,实际运行上仍是会有很多坑。

5.2 美图最终使用的无侵入式 DNS SDK 集成方案

那么,有没有更合适的一种技术方案,可以下降 咱们 DNS SDK 的接入工做量,也能兼顾各类使用场景,好比 HTTPS、RTMP 协议等?

基于这样的目标,咱们在实践中尝试探索了一种对业务集成友好的无侵入式 DNS SDK 集成方案。下面咱们以 Android 平台进行说明。

咱们知道在 Java 层面上进行 DNS 解析的基本方式是调用以下方法:

InetAddress.getAllByName("www.meipai.com");

Android 平台上经常使用的 Okhttp、HttpUrlConnection 等网络请求库都会依赖这个形式的 DNS 解析。

咱们深刻分析 InetAddress 的运行流程,其大体以下: 

 

在上述流程中咱们能够知道,InetAddress 会有到 AddressCache 尝试获取已缓存记录的动做,而这里 AddessCache 是一个 static 的 map 结构变量。

所以,在这里咱们来对它作点小手脚 :

1)模仿系统的 AddressCache 构造一个咱们本身的 AddressCahce 结构,不过它的 get 方法被替换为从咱们 SDK 获取解析记录;

2)经过反射的形式用咱们修改后的 AddressCache 替换掉系统的 AddressCache 变量。

这个偷天换日的操做以后,HttpsUrlConnection 等 Java 层网络请求在进行 DNS 解析时就会是这样一个流程:

 

经过这个形式,咱们可以完美解决 Java 层的 DNS SDK 接入问题,对于业务方来讲,他们并不须要作任何 URL 替换操做,对应的 HTTPS 场景下的问题也不复存在。

Java 层的接入解决了, 那么 Native 层呢? 

咱们知道在 Android 平台上,像 WebView、播放器等模块他们进行网络链接的操做都是在 native 层进行的,并不会调用到 Java 层的 InetAddress 方法。

首先在 C/C++ 层,咱们知道进行 DNS 解析会使用 getaddrinfo 或是 gethostbyname2 这两个函数。

另外咱们还知道,在 Android 等 Linux 系统下,对于 .so 这类可共享对象文件会是 ELF 的文件格式。

所以从这些已知信息,咱们能够获得下列一些状况:咱们的 App 中 a.so 中直接使用到了系统 libc.so 中的 getaddrinfo 函数,那么根据 ELF 文件规范,在 a.so 的 .rel.plt 表中会有以下关系定义: getaddrinfo ==> 0xFFFFFF 。

.rel.plt 表中的映射关系为 a.so 的运行指出了 getaddrinfo 这个外部符号在当前内存空间中的绝对地址。

正常状况下,a.so 中执行到 getaddrinfo 的函数流程是这样的:

 

那么在这里,咱们是否能够手动修改这个映射表内容,把 getaddrinfo 的内存地址替换成咱们的 my_getaddrinfo 地址呢?

这样,a.so 在实际运行时会被拐到咱们的 my_getaddrinfo 中? 

实际上,确实是可行的。 咱们尝试在 SDK 启动后,对 a.so 的 .rel.plt 表进行修改,达到接管 a.so DNS 的目的。

修改后的 a.so 运行流程以下:

 

经过上面的方式,咱们可以比较完美地接管 App 在 Java 层 和 Native 层 DNS 过程,实现业务方无任何额外改动的状况下运用咱们的 DNS SDK 优化效果。

六、SDK 上线后的效果表现

在实际运用中,咱们取得了比较好的效果。得益于 DNS SDK 在命中本地缓存率上的策略优化,咱们的移动端产品在网络请求中 DNS 解析环节耗时获得下降。

从实际监控数据来看,完整网络请求的耗时也可以下降 100ms 左右:

 

经过 HTTP DNS 的引入和 LocalDNS 优化升级策略,咱们的网络请求成功率有提高,在未知主机等具体错误率表现出降低的趋势。

因为 SDK 层面自己作好了灵活的策略配置,咱们经过线上监控和配置也让各产品在效益和成本之间取得一个最佳的平衡点。

附录:更多网络通讯方面的精华文章

TCP/IP详解 - 第11章·UDP:用户数据报协议

TCP/IP详解 - 第17章·TCP:传输控制协议

TCP/IP详解 - 第18章·TCP链接的创建与终止

TCP/IP详解 - 第21章·TCP的超时与重传

技术往事:改变世界的TCP/IP协议(珍贵多图、手机慎点)

通俗易懂-深刻理解TCP协议(上):理论基础

通俗易懂-深刻理解TCP协议(下):RTT、滑动窗口、拥塞处理

理论经典:TCP协议的3次握手与4次挥手过程详解

理论联系实际:Wireshark抓包分析TCP 3次握手、4次挥手过程

计算机网络通信协议关系图(中文珍藏版)

UDP中一个包的大小最大能多大?

P2P技术详解(一):NAT详解——详细原理、P2P简介

P2P技术详解(二):P2P中的NAT穿越(打洞)方案详解

P2P技术详解(三):P2P技术之STUN、TURN、ICE详解

通俗易懂:快速理解P2P技术中的NAT穿透原理

高性能网络编程(一):单台服务器并发TCP链接数到底能够有多少

高性能网络编程(二):上一个10年,著名的C10K并发链接问题

高性能网络编程(三):下一个10年,是时候考虑C10M并发问题了

高性能网络编程(四):从C10K到C10M高性能网络应用的理论探索

高性能网络编程(五):一文读懂高性能网络编程中的I/O模型

高性能网络编程(六):一文读懂高性能网络编程中的线程模型

鲜为人知的网络编程(一):浅析TCP协议中的疑难杂症(上篇)

鲜为人知的网络编程(二):浅析TCP协议中的疑难杂症(下篇)

鲜为人知的网络编程(三):关闭TCP链接时为何会TIME_WAIT、CLOSE_WAIT

鲜为人知的网络编程(四):深刻研究分析TCP的异常关闭

鲜为人知的网络编程(五):UDP的链接性和负载均衡

鲜为人知的网络编程(六):深刻地理解UDP协议并用好它

鲜为人知的网络编程(七):如何让不可靠的UDP变的可靠?

网络编程懒人入门(一):快速理解网络通讯协议(上篇)

网络编程懒人入门(二):快速理解网络通讯协议(下篇)

网络编程懒人入门(三):快速理解TCP协议一篇就够

网络编程懒人入门(四):快速理解TCP和UDP的差别

网络编程懒人入门(五):快速理解为何说UDP有时比TCP更有优点

网络编程懒人入门(六):史上最通俗的集线器、交换机、路由器功能原理入门

网络编程懒人入门(七):深刻浅出,全面理解HTTP协议

网络编程懒人入门(八):手把手教你写基于TCP的Socket长链接

网络编程懒人入门(九):通俗讲解,有了IP地址,为什么还要用MAC地址?

技术扫盲:新一代基于UDP的低延时网络传输层协议——QUIC详解

让互联网更快:新一代QUIC协议在腾讯的技术实践分享

现代移动端网络短链接的优化手段总结:请求速度、弱网适应、安全保障

聊聊iOS中网络编程长链接的那些事

移动端IM开发者必读(一):通俗易懂,理解移动网络的“弱”和“慢”

移动端IM开发者必读(二):史上最全移动弱网络优化方法总结

IPv6技术详解:基本概念、应用现状、技术实践(上篇)

IPv6技术详解:基本概念、应用现状、技术实践(下篇)

从HTTP/0.9到HTTP/2:一文读懂HTTP协议的历史演变和设计思路

脑残式网络编程入门(一):跟着动画来学TCP三次握手和四次挥手

脑残式网络编程入门(二):咱们在读写Socket时,究竟在读写什么?

脑残式网络编程入门(三):HTTP协议必知必会的一些知识

脑残式网络编程入门(四):快速理解HTTP/2的服务器推送(Server Push)

脑残式网络编程入门(五):天天都在用的Ping命令,它究竟是什么?

脑残式网络编程入门(六):什么是公网IP和内网IP?NAT转换又是什么鬼?

以网游服务端的网络接入层设计为例,理解实时通讯的技术挑战

迈向高阶:优秀Android程序员必知必会的网络基础

全面了解移动端DNS域名劫持等杂症:技术原理、问题根源、解决方案等

美图App的移动端DNS优化实践:HTTPS请求耗时减少近半

>> 更多同类文章 ……

(原文连接:http://www.52im.net/thread-2172-1-1.html

相关文章
相关标签/搜索