移动互联网时代 , 如何优化你的网络 —— 域名解析篇
阿里云 移动服务
泠茗
域名(Domain Name),是由一串用点分隔的名字组成的互联网上某台计算机或某组计算机的标识,它的目的是为了方便人们更简单便捷地访问互联网上的服务。在实际的系统实现中,域名通过DNS(Domain Name System)系统转化为服务器的IP地址,以方便机器通过IP进行寻址和通信。上述行为,我们称之为域名解析。
作为一次网络通信最前置的环节,域名解析的重要性不言而喻。在传统的基于浏览器的网站访问场景下,域名解析环节由浏览器内核实现,网站开发者无需关心域名解析的细节。But there are always two sides to every coin,一旦域名解析环节发生异常,开发者面对这样的黑盒架构就会显得束手无策,一个很典型的例子即域名劫持问题,关于这一点我们在后文会有更详细的介绍。
进入移动互联网时代,大量的应用基于C/S架构构建。相较于传统的面向浏览器的Web App,C/S架构的应用赋予了我们非常大的软件定制空间,开发者甚至可以渗透到整个应用的底层网络实现当中,域名解析环节的优化因此变为了可能。本篇文章我们就一起来看一看传统域名解析存在的问题,对应的根源,以及可能的优化方案。
关于域名解析,你应该知道的基本概念
在了解传统域名解析的流程之前,有几个专有名词我们需要了解一下:
根域、顶级域、二级域
DNS系统一般采用树状结构进行组织,以ru.wikipedia.org
为例,org
为顶级域名,wikipedia
为二级域名,ru
为三级域名,域名树状组织结构如下图所示。
权威DNS
权威DNS即最终决定域名解析结果的服务器,开发者可以在权威DNS上配置、变更、删除具体域名的对应解析结果信息。阿里云云解析( https://wanwang.aliyun.com/domain/dns )即权威DNS服务提供商。
递归DNS
递归DNS又称为Local DNS,它没有域名解析结果的决定权,但代理了用户向权威DNS获取域名解析结果的过程。递归DNS上有缓存模块,当目标域名存在缓存解析结果并且TTL未过期时(每个域名都有TTL时间,即有效生存时间,若域名解析结果缓存的时间超过TTL,需要重新向权威DNS获取解析结果),递归DNS会返回缓存结果,否则,递归DNS会一级一级地查询各个层级域名的权威DNS直至获取最终完整域名的解析结果。关于域名解析的具体流程下文会举例说明。
公共DNS
公共DNS是递归DNS的一种特例,它是一种全网开放的递归DNS服务,而传统的递归DNS信息一般由运营商分发给用户。一个比较典型的公共DNS即Google的8.8.8.8
,我们可以通过在操作系统配置文件中配置公共DNS来代替Local DNS完成域名解析流程。
在实际的使用过程中,我们通常不需要手工指定自己的Local DNS地址。运营商会通过DHCP协议在系统网络初始化阶段将Local DNS地址分配给我们的计算机。当我们需要使用公共DNS服务时,我们就必须手工指定这些服务的地址。以Linux为例,我们可以通过在'/etc/resolv.conf'中添加Local DNS地址项来改变本机Local DNS的地址。
了解了上述域名解析相关的常见术语,我们再来仔细看一看一次域名解析流程具体是如何发生的。
如上图所示,以访问www.taobao.com
为例,一次完整的域名解析流程包括:
- 终端向Local DNS发起域名解析请求;
- Local DNS在获取到域名解析请求后首先从Root hints获取根域名服务器的地址(Root hints包含了互联网DNS根服务器的地址信息);
- 获取了根域名服务器地址后Local DNS向根域名服务器发起DNS解析请求,根域名服务器返回
com
顶级域名服务器地址; - 随后Local DNS向
com
域名服务器发起解析请求,并得到taobao.com
二级域名服务器的地址; - Local DNS向
taobao.com
二级域名服务器发起解析请求,并最终获得了www.taobao.com
的IP地址信息; - Local DNS将递归查询获得的IP地址信息缓存并返回给客户端;
Local DNS服务器包含缓存模块,在实际域名解析过程中Local DNS服务器会首先查询缓存,缓存命中且解析结果TTL未过期的情况下直接返回,否则才启动递归查询的流程。
传统的域名解析面临的问题
了解了域名解析的基本概念和整体流程,我们再一起来探究一下传统域名解析存在的一系列问题。
域名劫持
域名劫持一直是困扰许多开发者的问题之一,其表现即域名A应该返回的DNS解析结果IP1被恶意替换为了IP2,导致A的访问失败或访问了一个不安全的站点。下面我们一起看看几种常见的域名劫持的场景。
一种可能的域名劫持方式即黑客侵入了宽带路由器并对终端用户的Local DNS进行篡改,指向黑客自己伪造的Local DNS,进而通过控制Local DNS的逻辑返回错误的IP信息进行域名劫持。另一方面,由于DNS解析主要是基于UDP协议,除了上述攻击行为外,攻击者还可以监听终端用户的域名解析请求,并在Local DNS返回正确结果之前将伪造的DNS解析响应传递给终端用户,进而控制终端用户的域名访问行为。
上述攻击行为的影响面相对比较有限,另一种我们最常碰到的域名劫持现象是缓存污染。我们知道在接收到域名解析请求时,Local DNS首先会查找缓存,如果缓存命中就会直接返回缓存结果,不再进行递归DNS查询。这时候如果Local DNS针对部分域名的缓存进行更改,比如将缓存结果指向第三方的广告页,就会导致用户的访问请求被引导到这些广告页地址上。
对比第一种攻击,这类缓存污染往往能带来更明显的群体伤害,比如某个省份某个运营商的用户群可能因为该地区Local DNS的缓存污染而导致访问服务异常。这类缓存污染行为往往是间歇性、局部性发生的,没有明显的规律,导致开发者很难对其进行量化、评估、预防。
有的同学可能会问,“我使用了HTTPS,是否就可以避免域名劫持的问题”,答案是否定的。域名解析环节发生在网络加密请求交互之前,试想一下,如果客户端还没有服务端的确切地址信息,我们又如何知道应该和谁进行加密的握手协商与通信呢?
调度不精准
除了域名劫持问题,基于传统Local DNS的域名解析还会带来域名调度精准性的问题。对于类似CDN域名访问这类需要按地域、运营商进行智能解析调度的场景,精准调度的诉求是十分强烈的。
关于调度不精准的原因,我们主要可以从两个方面来探究一下。第一个常见的问题即解析转发。
部分Local DNS供应商为了降低运营成本,会将请求到自己节点的域名解析请求转发给其他供应商的Local DNS节点,如上图所示。假如用户请求解析一个CDN域名cdn.aliyun.com
,用户分配到的Local DNS A为了节省成本,把该次请求转发给了另一运营商的Local DNS B,权威DNS在进行域名解析时会根据Local DNS的IP信息进行智能调度,即权威DNS会根据Local DNS B的IP78.29.29.1
进行调度,分配与78.29.29.1
相同运营商并且地理位置最近的CDN节点78.29.29.2
,然而这个CDN节点对于终端135.35.35.1
而言并不是最优的CDN节点,他们分属不同的运营商,并且地理位置上可能相隔很远。这类解析转发行为会严重影响域名解析的精准性并对用户业务访问延迟带来影响。
除了解析转发对调度精准性带来的影响外,Local DNS的布署情况同样影响着域名智能解析的精准性。
如上图所示,部分运营商Local DNS的布点受成本因素制约分布并不均匀,比如在东部地区部署比较密集,但在西部地区部署比较稀疏。这时候当一位西藏的用户准备访问CDN节点时,我们预期他应该会被调度到西藏的CDN节点A上以实现就近接入和访问加速。但由于Local DNS的资源有限,西部地区的终端用户被统一调度到青海的Local DNS B上,这时候权威DNS根据Local DNS B的IP进行CDN域名的智能解析,并将青海的CDN节点B返回给西藏用户,导致用户的网络访问延迟上升。另一种我们实际发现的情况是Local DNS的分配甚至并非遵循就近原则,比如有实际案例显示西藏的用户甚至被分配了北京的Local DNS节点C,导致西藏的用户在进行CDN资源访问时被调度到了北京的CDN节点C上,类似的由于调度精度的缺失带来的访问体验的影响是非常严重的。
解析生效滞后
部分业务场景下开发者对域名解析结果变更的生效时间非常敏感(这部分变更操作是开发者在权威DNS上完成的),比如当业务服务器受到攻击时,我们需要最快速地将业务IP切换到另一组集群上,这样的诉求在传统域名解析体系下是无法完成的。
Local DNS的部署是由各个地区的各个运营商独立部署的,因此各个Local DNS的服务质量参差不齐。在对域名解析缓存的处理上,各个独立节点的实现策略也有区别,比如部分节点为了节省开支忽略了域名解析结果的TTL时间限制,导致用户在权威DNS变更的解析结果全网生效的周期非常漫长(我们已知的最长生效时间甚至高达48小时)。这类延迟生效可能直接导致用户业务访问的异常。
延迟大
DNS首次查询或缓存过期后的查询,需要递归遍历多个DNS服务器以获取最终的解析结果,这增加了网络请求的前置延时时间。特别是在移动互联网场景下,移动网络质量参差不齐,弱网环境的RTT时间可能高达数百毫秒,对于一次普通的业务请求而言,上述延时是非常沉重的负担。另一方面,弱网环境下的解析超时、解析失败等现象屡见不鲜,如何合理优化DNS解析对于整体网络访问质量的提升至关重要。
HTTPDNS
通过上文的介绍,聪明的读者应该可以发现,传统域名解析面临的诸多问题与挑战本质根源在于Local DNS的服务质量不可控,如果有一个更安全、稳定、高效的递归DNS服务帮助我们代理了域名解析的过程,上述问题看起来就可以彻底地得到解决。
HTTPDNS在这样的背景下应运而生。我们一起来看看HTTPDNS的基本概念以及它是如何解决传统DNS解析面临的问题的。
防域名劫持
HTTPDNS使用HTTP协议进行域名解析,代替现有基于UDP的DNS协议,域名解析请求直接发送到HTTPDNS服务端,从而绕过运营商的Local DNS,如下图所示。
HTTPDNS代替了传统的LocalDNS完成递归解析的功能,基于HTTP协议的设计可以适用于几乎所有的网络环境,同时保留了鉴权、HTTPS等更高安全性的扩展能力,避免恶意攻击劫持行为。另一方面,商业化的HTTPDNS服务( https://www.aliyun.com/product/httpdns )缓存管理有严格的SLA保障,避免了类似Local DNS的缓存污染的问题。
精准调度
传统域名解析的调度精准性问题,本质根源在于Local DNS的部署和分配机制上。由于碎片化的管理方式,这些环节的服务质量同样很难得到保障。HTTPDNS在递归解析实现上优化了与权威DNS的交互,通过edns-client-subnet协议( https://datatracker.ietf.org/doc/rfc7871 )将终端用户的IP信息直接交付给权威DNS,这样权威DNS就可以忽略Local DNS IP信息,根据终端用户的IP信息进行精准调度,避免Local DNS的坐标干扰(当然上述精准调度方案的前提是权威DNS需要支持edns-client-subnet,可喜的是当前主流的权威DNS服务都已支持该协议)。精准调度的流程示例如下。
实时生效
在域名解析生效周期方面,HTTPDNS也有着传统域名解析体系所无法具备的能力。前文中我们提到由于各个地区的Local DNS是独立维护的,服务质量参差不齐,缓存实现不一,因此导致的解析变更全网生效滞后的问题,在商业化的HTTPDNS服务上就不会存在(HTTPDNS严格遵循DNS TTL限制进行缓存更新)。另一方面,即便我们假设Local DNS严格遵循域名TTL时间进行缓存管理(这里我们假设开发者配置的域名TTL时间为5min),当开发者业务受到攻击并需要快速进行切换时,Local DNS也会遵循域名TTL,在持续5min的时间段内返回旧IP信息,这5min的业务影响对于中大型企业而言是一个不小的损失(对于电商类的大型企业,5min的访问异常可能意味着几百万的交易额下跌)。以阿里云HTTPDNS服务( https://www.aliyun.com/product/httpdns )为例,HTTPDNS在快速生效方面有专有的方案,配合阿里云的权威DNS服务云解析( https://wanwang.aliyun.com/domain/dns ),用户在权威DNS变更的解析结果将快速同步给HTTPDNS,覆盖原有的缓存记录,帮助用户实现秒级的域名解析切换。
在DNS解析延迟方面,由于HTTPDNS基于HTTP协议,而HTTP基于TCP协议,对比传统的UDP传输多了一些冗余的握手环节,因此从原理上而言网络请求方面的开销并没有降低。但在实际使用过程中,我们可以通过端上的策略来实现一个零延迟DNS解析的方案。接下来我们一起来看看HTTPDNS服务在移动端的最佳实践方案。实时生效的流程如下图所示。
域名解析最佳实践
通过HTTPDNS服务,我们可以实现包括防止域名劫持、精准调度、实时解析生效等功能,但在DNS解析开销的优化上,我们需要客户端一起配合。
预解析
绝大多数的APP在应用初始化阶段都有一个启动期,我们可以在这个启动期做一些preflight工作,即在初始化阶段我们可以针对业务的热点域名在后台发起异步的HTTPDNS解析请求。这部分预解析结果在后续的业务请求中可以直接使用,进而消除首次业务请求的DNS解析开销,提升APP首页的加载速度。
在客户端实际使用HTTPDNS的过程中,有一个大家需要关注的点。标准的Web服务器(以Nginx为例)一般会将HTTP请求头中的Host头的值作为请求的域名信息进行处理(取决于服务端的配置,但一般情况都如此)。比如当我们通过标准的网络库访问www.aliyun.com/index.html
这个地址时,发出的网络请求一般是这样的:
> GET /index.html HTTP/1.1
> Host: www.aliyun.com
> User-Agent: curl/7.43.0
> Accept: /
使用HTTPDNS后,我们需要将HTTP请求URL中的Host域(注意这里的Host域指的是URL中的Host字段,而非HTTP请求头中的Host头)替换为HTTPDNS解析获得的IP,这时由于标准的网络库会将URL中的Host域赋值给HTTP请求头中的Host头,发出的网络请求如下:
> GET /index.html HTTP/1.1
> Host: 140.205.63.8
> User-Agent: curl/7.43.0
> Accept: /
上述Host信息将导致服务端的解析异常(服务端配置的是域名信息,而非IP信息,试想一下如果我们的服务端服务了两个域名www.a.com
和www.b.com
,这时候它接收到一个140.205.63.8/index.html
请求,它如何判断应该返回a的首页还是b的首页信息呢?)。为了解决这个问题,我们需要主动设置HTTP请求Host头的值,以Android的官方网络库HttpURLConnection
为例:
String originalUrl = “http://www.aliyun.com/index.html";
URL url = new URL(originalURL);
String originalHost = url.getHost();
// 同步获取IP
String ip = httpdns.getIpByHost(originalHost);
HttpURLConnection conn;
if (ip != null) {
// 通过HTTPDNS获取IP成功,进行URL替换和Host头设置
url = new URL(originalUrl.replaceFirst(originalHost, ip));
conn = (HttpURLConnection) url.openConnection();
// 设置请求Host头
conn.setRequestProperty("Host", originHost);
} else {
conn = (HttpURLConnection) url.openConnection();
}
主动设置Host头后,发出的网络请求就与未替换URL的网络请求一模一样了。
智能缓存
通过预解析获取的IP有一定的TTL有效时间,我们需要合理地缓存下来进行管理。操作系统本身的DNS缓存粒度比较粗,在客户端我们可以应用更细粒度的缓存管理来提升解析效率。比如在不同的网络运营商环境下,对CDN域名的解析结果会发生变化,当我们使用电信WIFI时,DNS解析会返回就近的电信CDN节点IP,当我们使用联通3G时,DNS解析会返回就近的联通CDN节点IP,针对不同运营商的解析结果缓存可以确保我们在网络切换时能够快速地进行网络请求,减免DNS解析带来的额外开销。甚至更激进的,我们可以做本地的持久化缓存,当下一次APP启动时直接读取缓存用于网络访问,以提升首屏加载的速度。
懒加载
懒加载策略的实施可以让我们真正实现DNS的零延迟解析。所谓懒加载策略,核心的实现思路如下:
- 业务层的域名解析请求只和缓存进行交互,不实际发生网络解析请求。如果缓存中存在记录,不论过期与否,直接返回业务层缓存中的记录;
- 如果缓存中的记录已过期,后台发起异步网络请求进行HTTPDNS解析;
有的同学可能会有疑惑,返回一个过期的IP岂不是违背了TTL设计的初衷?的确,上述行为并不符合标准的规范,但是当我们重新审视一下自己的业务特点,上述的变通策略就显得非常有意义了。绝大多数的业务场景下我们的后端IP是固定的若干个节点,因此连续的解析结果在环境不变的情况下有很大概率是保持一致的,这在一定程度上保证了懒加载的可行性。另一方面,即便我们由于返回过期IP导致了访问异常的行为,后台很快会进行新IP的异步解析和缓存更新,业务本身可以进行重试和快速的复原,因此上述行为带来的影响也是非常小的。再进一步,TTL过期的IP的服务在绝大多数场景下还是持续的,可预期的,因此懒加载可能带来的业务风险是完全可控的。通过0.1%场景下的业务瞬时访问风险来换取99.9%场景下的用户体验提升,这笔买卖还是非常划算的(当然懒加载的使用有赖于合适的业务场景,如果你的业务场景下IP变化频繁,并且TTL过期的IP访问不可用,是不建议应用懒加载策略的)。
下图描绘了预解析+懒加载的实现框架:
综上可以看到,当我们需要实现零延迟解析的效果时,在客户端还是有比较多的工作需要做的。商业化的HTTPDNS服务( https://www.aliyun.com/product/httpdns )提供了终端SDK方便开发者进行终端上的集成和使用,推荐大家可以尝试一下。
Reference
https://en.wikipedia.org/wiki/Domain_name
https://www.aliyun.com/product/httpdns