标签归档:HttpDNS

移动互联网时代,如何优化你的网络 —— 域名解析篇HTTPDNS

域名(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为三级域名,域名树状组织结构如下图所示。

52e57c5c96885ac93fe63ee949fc6068a996949a

权威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的地址。

了解了上述域名解析相关的常见术语,我们再来仔细看一看一次域名解析流程具体是如何发生的。

a33d57c0a93ebf9eacf090e6f81a98bcda023334

如上图所示,以访问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解析响应传递给终端用户,进而控制终端用户的域名访问行为。

a23608ec76095d6bed4b7243db5468edc9681ae1

上述攻击行为的影响面相对比较有限,另一种我们最常碰到的域名劫持现象是缓存污染。我们知道在接收到域名解析请求时,Local
DNS首先会查找缓存,如果缓存命中就会直接返回缓存结果,不再进行递归DNS查询。这时候如果Local
DNS针对部分域名的缓存进行更改,比如将缓存结果指向第三方的广告页,就会导致用户的访问请求被引导到这些广告页地址上。

0e34b8f0d2470234749829ed3db428ec1b432cbd

对比第一种攻击,这类缓存污染往往能带来更明显的群体伤害,比如某个省份某个运营商的用户群可能因为该地区Local DNS的缓存污染而导致访问服务异常。这类缓存污染行为往往是间歇性、局部性发生的,没有明显的规律,导致开发者很难对其进行量化、评估、预防。

有的同学可能会问,“我使用了HTTPS,是否就可以避免域名劫持的问题”,答案是否定的。域名解析环节发生在网络加密请求交互之前,试想一下,如果客户端还没有服务端的确切地址信息,我们又如何知道应该和谁进行加密的握手协商与通信呢?

调度不精准

除了域名劫持问题,基于传统Local DNS的域名解析还会带来域名调度精准性的问题。对于类似CDN域名访问这类需要按地域、运营商进行智能解析调度的场景,精准调度的诉求是十分强烈的。

关于调度不精准的原因,我们主要可以从两个方面来探究一下。第一个常见的问题即解析转发。

caea530e4b496790e5d86fd8476cb14ca7fe4767

部分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的布署情况同样影响着域名智能解析的精准性。

8c0b59d3eddfd9452a77c6bb7e394953f6c605f8

如上图所示,部分运营商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切换到另一组集群上,这样的诉求在传统域名解析体系下是无法完成的。

6cf97722b3205f644ca6b713c80821e9b394afc8

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,如下图所示。

6c4402618b6f9580255ba88ea28fa518947629ae

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服务都已支持该协议)。精准调度的流程示例如下。

ab293d37d4e472dd0ee2facf85e2929ce11a177a

实时生效

在域名解析生效周期方面,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服务在移动端的最佳实践方案。实时生效的流程如下图所示。

ca22e0de56ff0cdb0340bf3c0744b0fd80c1924a


域名解析最佳实践

通过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.comwww.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访问不可用,是不建议应用懒加载策略的)。

下图描绘了预解析+懒加载的实现框架:

2cd2f32b7dfd7f1b277cf06146a158a9d40d98c4

综上可以看到,当我们需要实现零延迟解析的效果时,在客户端还是有比较多的工作需要做的。商业化的HTTPDNS服务( https://www.aliyun.com/product/httpdns )提供了终端SDK方便开发者进行终端上的集成和使用,推荐大家可以尝试一下。

如何利用HTTPDNS降低DNS解析开销

1. 背景说明

移动场景下DNS的解析开销是整个网络请求延迟中不可忽视的一部分。一方面基于UDP的localDNS解析在高丢包率的移动网络环境下更容易出现解析超时的问题,另一方面在弱网环境下DNS解析所引入的动辄数百毫秒的网络延迟也大幅加重了整个业务请求的负担,直接影响用户的终极体验。

2. 解决方案

HTTPDNS在解决了传统域名劫持以及调度精确性的问题的同时,也提供了开发者更灵活的DNS管理方式。通过在客户端合理地应用HTTPDNS管理策略,我们甚至能够做到DNS解析0延迟,大幅提升弱网环境下的网络通讯效率。

DNS解析0延迟的主要思路包括:

  • 构建客户端DNS缓存;

通过合理的DNS缓存,我们确保每次网络交互的DNS解析都是从内存中获取IP信息,从而大幅降低DNS解析开销。根据业务的不同,我们可以制订更丰富的缓存策略,如根据运营商缓存,可以在网络切换的场景下复用已缓存的不同运营商线路的域名IP信息,避免网络切换后进行链路重选择引入的DNS网络解析开销。另外,我们还可以引入IP本地化离线存储,在客户端重启时快速从本地读取域名IP信息,大幅提升首页载入效率。

  • 热点域名预解析;

在客户端启动过程中,我们可以通过热点域名的预解析完成热点域名的缓存载入。当真正的业务请求发生时,直接由内存中读取目标域名的IP信息,避免传统DNS的网络开销。

  • 懒更新策略;

绝大多数场景下业务域名的IP信息变更并不频繁,特别是在单次APP的使用周期内,域名解析获取的IP往往是相同的(特殊业务场景除外)。因此我们可以利用DNS懒更新策略来实现TTL过期后的DNS快速解析。所谓DNS懒更新策略即客户端不主动探测域名对应IP的TTL时间,当业务请求需要访问某个业务域名时,查询内存缓存并返回该业务域名对应的IP解析结果。如果IP解析结果的TTL已过期,则在后台进行异步DNS网络解析与缓存结果更新。通过上述策略,用户的所有DNS解析都在与内存交互,避免了网络交互引入的延迟。

2.1 Demo示例

我们在HTTPDNS Demo github中提供了Android/iOS SDK以及HTTPDNS API接口的使用例程,这里我们通过使用Android SDK的例程演示如何实现0延迟的HTTPDNS服务。

public class NetworkRequestUsingHttpDNS {

    private static HttpDnsService httpdns;
    // 填入您的HTTPDNS accoutID信息,您可以从HTTPDNS控制台获取该信息
    private static String accountID = "100000";
    // 您的热点域名
    private static final String[] TEST_URL = {"http://www.aliyun.com", "http://www.taobao.com"};

    public static void main(final Context ctx) {
        try {
            // 设置APP Context和Account ID,并初始化HTTPDNS
            httpdns = HttpDns.getService(ctx, accountID);
            // DegradationFilter用于自定义降级逻辑
            // 通过实现shouldDegradeHttpDNS方法,可以根据需要,选择是否降级
            DegradationFilter filter = new DegradationFilter() {
                @Override
                public boolean shouldDegradeHttpDNS(String hostName) {
                    // 此处可以自定义降级逻辑,例如www.taobao.com不使用HttpDNS解析
                    // 参照HttpDNS API文档,当存在中间HTTP代理时,应选择降级,使用Local DNS
                    return hostName.equals("www.taobao.com") || detectIfProxyExist(ctx);
                }
            };
            // 将filter传进httpdns,解析时会回调shouldDegradeHttpDNS方法,判断是否降级
            httpdns.setDegradationFilter(filter);
            // 设置预解析域名列表,真正使用时,建议您将预解析操作放在APP启动函数中执行。预解析操作为异步行为,不会阻塞您的启动流程
            httpdns.setPreResolveHosts(new ArrayList<>(Arrays.asList("www.aliyun.com", "www.taobao.com")));
            // 允许返回过期的IP,通过设置允许返回过期的IP,配合异步查询接口,我们可以实现DNS懒更新策略
            httpdns.setExpiredIPEnabled(true);

            // 发送网络请求
            String originalUrl = "http://www.aliyun.com";
            URL url = new URL(originalUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            // 异步接口获取IP,当IP TTL过期时,由于采用DNS懒更新策略,我们可以直接从内存获得最近的DNS解析结果,同时HTTPDNS SDK在后台自动更新对应域名的解析结果
            ip = httpdns.getIpByHostAsync(url.getHost());
            if (ip != null) {
                // 通过HTTPDNS获取IP成功,进行URL替换和HOST头设置
                Log.d("HTTPDNS Demo", "Get IP: " + ip + " for host: " + url.getHost() + " from HTTPDNS successfully!");
                String newUrl = originalUrl.replaceFirst(url.getHost(), ip);
                conn = (HttpURLConnection) new URL(newUrl).openConnection();
            }
            DataInputStream dis = new DataInputStream(conn.getInputStream());
            int len;
            byte[] buff = new byte[4096];
            StringBuilder response = new StringBuilder();
            while ((len = dis.read(buff)) != -1) {
                response.append(new String(buff, 0, len));
            }
            Log.e("HTTPDNS Demo", "Response: " + response.toString());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 检测系统是否已经设置代理,请参考HttpDNS API文档。
     */
    public static boolean detectIfProxyExist(Context ctx) {
        boolean IS_ICS_OR_LATER = Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
        String proxyHost;
        int proxyPort;
        if (IS_ICS_OR_LATER) {
            proxyHost = System.getProperty("http.proxyHost");
            String port = System.getProperty("http.proxyPort");
            proxyPort = Integer.parseInt(port != null ? port : "-1");
        } else {
            proxyHost = android.net.Proxy.getHost(ctx);
            proxyPort = android.net.Proxy.getPort(ctx);
        }
        return proxyHost != null && proxyPort != -1;
    }
} 

对于使用HTTPDNS API接口的开发者,您可以在客户端自己定制更高效,并且符合您需求的HTTPDNS管理逻辑。

App域名劫持之DNS高可用 – 开源版HttpDNS方案详解

本文根据冯磊和赵星宇在“高可用架构”微信群所做的HttpDNS智能缓存库原理整理而成,转发请注明来自微信公众号ArchNotes。

冯磊,目前主要从事手机应用平台的构建,任职新浪网技术中国研发中心技术保障部架构师。5+年互联网,移动终端,游戏从业经验。历任软件工程师,高级软件工程师,技术经理。

赵星宇,HttpDNS的合作者。目前就职于新浪微博,从事手机微博的基础架构开发,任android高级研发工程师职位。

HttpDNS是使用HTTP协议向DNS服务器的80端口进行请求,代替传统的DNS协议向DNS服务器的53端口进行请求,绕开了运营商的Local
DNS,从而避免了使用运营商Local DNS造成的劫持和跨网问题。
(具体httpdns是什么?详细阅读见(【鹅厂网事】全局精确流量调度新思路-HttpDNS服务详解):http://mp.weixin.qq.com/s?__biz=MzA3ODgyNzcwMw==&mid=201837080&idx=1&sn=b2a152b84df1c7dbd294ea66037cf262&scene=2&from=timeline&isappinstalled=0&utm_source=tuicool)

鹅厂往事中提到

那么对于腾讯这样的域名数量在10万级别的互联网公司来讲,域名解析异常的情况到底有多严重呢?每天腾讯的分布式域名解析监测系统在不停地对全国所有的重点LocalDNS进行探测,腾讯域名在全国各地的日解析异常量是已经超过了80万条。这给腾讯的业务带来了巨大的损失。为此腾讯建立了专业的团队与各个运营商进行了深度沟通,但是由于各种原因,处理效率及效果均不能达到腾讯各业务部门的需求。除了和运营商进行沟通,有没有一种技术上的方案,能从根源上解决域名解析异常及用户访问跨网的问题呢?

HttpDNS主要解决三类问题:

  1. LocalDNS劫持

  2. 平均访问延迟下降

  3. 用户连接失败率下降

LocalDNS劫持:
由于HttpDNS是通过ip直接请求http获取服务器A记录地址,不存在向本地运营商询问domain解析过程,所以从根本避免了劫持问题。
(对于http内容tcp/ip层劫持,可以使用验证因子或者数据加密等方式来保证传输数据的可信度)

平均访问延迟下降: 由于是ip直接访问省掉了一次domain解析过程,(即使系统有缓存速度也会稍快一些‘毫秒级’)通过智能算法排序后找到最快节点进行访问。

用户连接失败率下降:
通过算法降低以往失败率过高的服务器排序,通过时间近期访问过的数据提高服务器排序,通过历史访问成功记录提高服务器排序。如果ip(a)访问错误,在下一次返回ip(b)或者ip(c)
排序后的记录。(LocalDNS很可能在一个ttl时间内(或多个ttl)都是返回记录

HttpDNSLib库组成

HttpDNSLib库主要由三个模块组成,查询模块,缓存模块,评估模块。

查询模块:

  1. 检查本地是否有对应的 domain 缓存

  2. 如果没有 则从本地LocalDNS获取然后从httpdns更新domain记录

  3. 有数据则检测是否过期 已过期则更新记录返回 LocalDNS 记录, 未过期则直接返回缓存层数据。

  4. 从HttpDNS 接口查询本次app开启后使用过的domain 记录定时访问,更新内存缓存,数据库缓存等记录

数据模块:

  1. 根据SP(或Wifi名)缓存域名信息

  2. 更具SP(或Wifi名)缓存服务器ip信息、优先级

  3. 记录服务器ip每次请求成功数、错误数

  4. 记录服务器ip最后成功访问时间、最后测速

  5. 添加 内存 -》数据库 之间的缓存层

评估模块:

  1. 根据本地数据,对一组IP排序

  2. 处理用户反馈回来的请求明细,入库

  3. 针对用户反馈是失败请求,进行分析上报预警

  4. 给HttpDns服务端智能分配A记录提供数据依据

HttpDNS交互流程

HttpDNS交互流程图:

从这张图中可以看出来
整个业务的交互流程,用户向查询模块传入一个URL地址,然后查询模块会检查缓存是否存在,不存在从httpdnsapi接口查询,
然后经过评估模块返回。在用户请求URL过程完毕时,需要将这次请求的结果反馈给 lib库的评估模块由评估模块入库记录本次质量数据。

HttpDns Lib库交互流程:

这张图就更深入的说了下 lib的工作原理。有两条竖线讲图片分为了三个区域,分别是左部分、中间部分 和 右部分。

左部分是app主线程操作的事情,中间部分是app调用者线程中处理lib库事件逻辑的事情,右面部分是新线程独立处理事件的逻辑。

开始是里客户端调用方,传入一个
url,获取domain信息后由查询模块查询domain记录,查询模块会从内存缓存层查询,内存缓存层没有数据会,查询数据库,如果数据库也没有数据会请求本地
LocalDNS。从三个环节中任何一个环节拿到数据后,
都会进入下一个环节,如果没有拿到数据返回null结束。进入评估模块,根据五个插件进行排序, 排序后返回数据给客户端。

lib模块设定定时器,根据ttl过期时间来检查domain是否需要更新。 定时器是独立线程, 不会影响app主线程。 httpdns
api请求数据, 先从自己配置的 httpdns api接口获取数据,如果获取不到会从 dnspod api接口获取如果也获取不到 直接从本地
localDNS获取数据,(从本地localDNS获取数据后期会改为发送UDP包封装dns协议从公共dns服务器直接获取,比如114dns等。dns服务器地址可自行设定。
)获取到数据后进入测速模块。 测速模块最新版本可以配置两种方式,一种是http空请求。 两个http头的交互,类似tcp首保耗时时间原理
,用来测试链路最快。 另一种是ping命令,(icmp协议)来尽量最小化流量的消耗,考虑倒可能有的服务器禁ping就使用空http测速即可。
测速后将数据插入本地 cache 即可。

代码结构

工程代码一共有八个主要package包,分别是cache、httpdns、log、model、query、score、speedtest、networktype。

cache包数据缓存层

IDnsCache是该包的对外主要接口。DnsCacheManager
实现该接口,封装了管理该包的所有逻辑调度,ConcurrentHashMap是内存缓存层的介质,当初使用过非线程安全的hashMap遇到了很多线程锁的问题,没有更好的办法自己控制锁管理,就替换成线程安全的concurrenthashmap对象了。DBConstants
设定了数据库名字表名字以及表字段,包含全部sql语句。 DNSCacheDatabaseHelper
用来操作数据库,所有和数据库交互的逻辑都在该类。

networktype包用来监控网络变化和检测当前网络状态

Constants 设定了网络状态的相关常量。 NetworkManager类也是这个包的主入口类所有网络状态的获取都是通过这个类来获取。 NetworkStateReceiver用来注册网络广播来接受网络发生变化的事件 。

httpdns包封装了所有HttpDNS api交互请求

IHttpDNS接口定义了该包和外部交互的所有数据格式,HttpDnsManager 实现了IHttpDNS接口。
HttpDnsConfig定义了使用到的常量配置, 以及dns api接口的开关,和顺序。
requests包里INetworkRequests接口轻量级的定义了 网络请求的实现,
目前使用ApacheHttp实现的该接口,如果用户有需求更换网络实现方式实现INetworkRequests 接口即可。 IJsonParser
接口定义了 httpdns api返回数据解析json的方式, 目前使用 android jsonObject实现。
如果需要扩展直接实现该接口即可。

log包实现了记录dnscachelib库记录日志倒文件的工具类

IDnsLog约定了写log和获取log的方法。 HttpDnsLogManager实现该接口,并管理log模块。该模块还有一个写文件的工具类。

model包封装了全部数据交互模型

DomainModel对应数据库domain表, IpModel对应数据库ip表。 HttpDnsPack是获取httpdns api接口数据的模型 。 ConnectFailModel用来记录所有异常错误。

query包是查询模块的入口

IQuery定义了该包对外的协议接口。 QueryManager实现该接口封装了所有查询相关操作。

Score包也是前面说的评估模块

IScore定义该包对外实现的接口,ScoreManager实现该接口。 PlugInManager用来管理所有评估插件。 所有的评估插件均实现 IPlugIn接口协议,规定输入输出。使用者可以自行添加评估插件。

speedtest包实现测速逻辑

ISpeedtest规定该包对外的接口协议, SpeedtestManager实现ISpeedtest接口。 封装了测速相关逻辑, 包括空http请求,以及ping命令测速。

另外场景包种有几个类简单介绍下。 DNSCache类是lib的主入口类,用户的所有操作均调用该入口类,该类是单利类直接获取实例调用即可,也是主场景。

由于内部model数据过于复杂,为用户专门封装DomainInfo模型。 该类仅返回用户使用的相关数据。

DNSCacheConfig 是httpdns库的全局配置文件, 可以直接修改该文件,也可以外部调用方法设置参数 。 该文件还封装了云端动态更新缓存配置。

代码结构如下图所示:

在编写该库的时候遇到最头疼的问题可能就是多线程同时访问导致遇到的数据异常错误。比如用户访问 api.weibo.cn
域名该域名目前数据库中没有缓存,内存中也没有缓存。在同时有多个请求以来来获取该域名的ip的时候, 因为没有数据会去请求api接口获取数据,
导致同时开启多个线程访问数据。 解决办法在请求api接口前增加正在请求队列,

任何需要请求数据的domain都先要在该队列检测是否有请求存在如果没有在继续进入后面流程如果有则丢掉本次请求指令。
另外在操作数据库的时候使用了 对象锁和 synchronized 方法锁,
导致了程序有锁死的情况,后改成全部使用对象锁就解决了该问题。全程的调试数据也是最头疼的环节,后直接编写测试程序,时时调试所有环节的数据(看到时时数据后发现了很多程序的bug,后都一一解决)。

以上为冯老师的分享,接下来是星宇跟大家分享下项目从研发倒现在所遇到的一些主要问题和大家有疑问的点。

开发过程中,常见的一些问题

1、手机网络从3G 切换到 Wifi下处理了什么?

NetworkStateReceiver类来监听网络是否发生变化,在网络有变化的时候,会刷新
NetworkManager类中的网络环境,在客户端内如果是手机网络可以知道网络类型(2G、3G、4G)也可以知道当前SP(移动、联通、电信),如果是Wifi网络环境可以知道SSID(wifi名字)在刷新网络环境后,会重新查询缓存内是否有当前链路下的最优A记录,如果没有则从LocalDNS获取第一次,然后马上更新httpdns记录。

2、网络发生变化后,返回的A记录还一样么?

数据库中缓存的数据,是根据当前sp来缓存的,也就是说当自身网络环境变化后,返回的a记录是不一样的 。手机网络下会根据当前sp来缓存
a记录服务器ip,如果是wifi网络环境下
根据当前ssid来缓存a记录,因为wifi环境下库自己没有办法明确判断出自己的运营商,但相同的ssid不会发生频繁的网络运营商变化。
所以在wifi下请求回来的a记录直接关联ssid名字即可,即使wifi sp发生变化,最多延迟一个ttl时间就更新成最新的a记录了。

3、怎样进行测速?

在从HttpDNS获取回来a记录的时候进行测速,测速的方式有两种:
ping和空的http请求。考虑倒有些服务器不支持ping来进行链路质量评判,可以使用空的http请求,仅仅是两个http头的流量开销,而ping的方式流量开销就更小了。
这个功能可以在库中自己配置。这里的测速其实是模拟首保接收的时间来做的,
同时对于流量控制严格的可以在库中配置测速频繁度,比如一台服务器在5分钟内有过测速记录则不进行测速。

4、域名ttl刚刚过期,库还没有从HttpDNS拉取回来数据怎么办?

ttl过期的前10秒去请求数据,在ttl过期后的10秒内库也认为当前a记录是有效的,会给你直接返回。

5、lib库目前只能使用 dnspod 服务商么? 支持dnspod 企业版本么?

目前库可以支持自定义的 HttpDNS api接口, 只需要实现IHttpDns接口类即可,在配置了dnspod企业key和id 的时候自动启用企业版本加密传输,支持企业版本。

6、使用这个库会不会降低应用请求网络的访问速度?

从目前的测试数据来看是不会的,HttpDNS库返回a记录的时间平均在5毫秒以内,有时会出现内存缓存中没有该域名记录,数据库中也没有的时候会从LocalDNS获取a记录,时间会稍长一些

一旦从LocalDNS获取后,会缓存倒内存中,在HttpDNS获取数据后会更新内存中得domain记录。
从库中获取a记录会比从LocalDNS获取a记录快一些。
在访问网络的时候由于是使用ip直链,可以起到一些加速效果,lib库获取domainA记录 + ip直接访问服务器 耗时小于
直接域名请求服务器。相关数据图片

下面给出一个测试系统的截图

7、lib库里面访问网络使用的是哪个网络库? json库用的是哪个?

考虑到该库的轻量级,使用的是android系统的org.apache.http.client.HttpClient库访问网络,如果需要切换到工程在使用的网络库可以实现
INetworkRequests 接口即可切换网络库。 json解析使用的也是android系统自带的org.json.JSONObject
如有需求切换json解析库,可直接实现IJsonParser接口即可切换。

8、使用该网络库会给我的app带来多大的体积增加?

目前该lib库没有引用任何外部的库文件。一切本着使用系统自身的api为原则,来保证库的轻量级,和兼容性。 目前lib库打包后70多k,代码在5千行左右。 测试工程代码在6千行左右。

9、lib库的配置必须要通过修改源代码的方式来进行配置么?

任何参数都可以在库调用方配置,DNSCacheConfig 类是整个库的配置文件。 并且支持云端动态更新配置 需要实现DNSCacheConfig.ConfigText_API 更新地址。 具体配置api接口请参考 dome工程中设置库的方式。

10、缓存domain记录是存储成文件还是数据库,或者android内部的一些存储方式?

lib缓存数据是通过数据库存储的。SQLiteDatabase, 具体的表接口和sql语句请参考 DBConstants 类文件。

11、 评估模块有什么功能?

评估模块目前由五个插件组成, 速度插件、推荐优先级插件、历史成功次数插件、历史错误数插件、最近一次成功时间插件 。
每一个a记录服务器ip,都会经过这五个插件进行评估排序后返回给使用者。 所有插件评估分值比重可以配置,
根据自己的需求以及不同的使用场景,调整出最合理的权重分配。

下面给出评估模块算法细节图

12、速度插件具体算法?

比如速度插件评分体系, 满分100分, 那么有3个服务器ip, 1号服务器http请求耗时10毫秒, 2号服务器20毫秒, 3号服务器30毫秒。 那么经过插件计算后 1号服务器100分, 2号服务器50分, 3号服务器25分。

13、优先级插件又是什么?

如果是自定义的服务器,可以返回服务器优先级字段,该的字段代表推荐使用该服务器的权重, 比如该字段服务端可以和监控系统结合起来,甚至是用来分流。 相应的权重值, 也会算出来不同的分值。

14、历史成功次数插件是什么? 历史错误次数插件是什么?

在当前sp当前链路下, 会记录访问过的该服务器ip的成功次数, 成功次数越多认为该服务器相对稳定。
会在排序的时候根据权重比值进行影响最终排序结果, 该插件权重不建议过高。
同理历史错误插件也是记录当前链路下服务器出过错误的次数,次数越高认为越不稳定。 排序尽量靠后。 同样该插件权重不建议过高。

15、最后一次成功时间?

如果该服务器在 很近的时间内访问过,那么评估系统则认为他链路是通的,则会给一个分值, 越接近现在的时间的服务器 分值越高。 24小时以前访问的分值为0 。

16、评估插件就这五个吗?

该五个插件属于抛砖引玉, 可以自定义插件 只需要实现 IPlugIn 接口即可。 所有的插件启用和停止都在 配置文件中可以修改,以及配置每个插件的权重比。

17、智能评估一定会带来好的效果吗?

首先httpdns返回的a记录已经是 经过当前地域和当前sp返回的最优记录结果集, 至少不会降低效率。

18、我可以不使用智能评估模块么?

可以的在配置文件中关掉智能评估即可,具体代码参照demo即可。 关掉智能评估模块后,会在多个a记录中随机排序返回。

19、该Lib库块兼容性如何?

使用testin兼容性测试 测试兼容性结果:99.49%。 Android平台全部由java代码开发,没有使用任何特殊特性,覆盖全部系统版本。

最后附几张测试工程效果图:

  • 模拟了客户端访问http请求,分别标识了每个任务的详细信息。

  • 这个页面全都是数据库相关配置,在代码中可以直接找到具体设置库文件的接口。

  • 数据报表入口,包含全部任务加速效果延迟效果数据记录, lib库耗时走向,每个ip直接访问请求和domain访问请求速度对比, 统计了服务器平局速度。

  • 缓存数据标签中包含了 当前库的所有状态, 能实时的看到内存缓存层的所有数据状态,包括数据库中得所有数据状态。
    每秒钟刷新一次。 在这里可以清空缓存层数据、数据层数据、已经当前测试工程的数据。 在这里你可以清楚的看到 ip 和 domain的对应关系,
    以及数据库表中 每项的关系。 和所有的domain 以及 ip 的状态。

全部代码 均已开源 https://github.com/SinaMSRE/HTTPDNSLib , 包括测试工程 也开源了 设计文档 和 流程图都在 git 上有。 测试工程的 ui psd 貌似也在git上

Q&A

Q1、每次请求url都需要去ping么?

不需要每次都ping的, 测试链路是否通畅 会在 从 httpdns api接口获取数据后, 在测试链路是否通畅, 每次请求 httpdns api间隔是一个 domain 设置的 ttl时间

ping 直发一个包, 最小化的减少 流量开销。

检测链路 如果配置成 http 空的请求, 也是同理 在 httpdns api请求结束后, 才会检测链路是否畅通。

Q2、前面提到的并发请求,被丢弃的请求是怎么处理的

并发请求是说 客户端请求 HttpDNS lib库 ,同时发 api.weibo.cn 的请求么?

因为 去问 HttpDNS api接口的时候 , 只需要有一个请求去问就可以了, 去问 HttpDNS api的时候 已经切换到
非客户端主线程, 在客户端调用的主线程中 如果没有缓存数据 就从 本地获取 dns 的a记录返回了。 所以直接丢弃这个访问 HttpDNS
api的请求即可, 不会影响到其他流程逻辑。

Q3、南北网络之间请求有特别处理么?

南方电信,北方网通,运营商ip不一样

首先 HttpDNS 返回的a记录会根据你的出口ip 来从权威的 dns 服务器问出来结果。 如果你是南方的ip 肯定给你的a记录
也是南方的, httpdns 返回的记录理论应该是和 传统的 dns 返回的 a 记录是一样的。 而去问 httpdns 的api 地址 是
bgp的机房。 所以 也是 兼容多链路 多地域。有遇见过 传统 dns 出口可能是 电信的, 但业务访问的 ip 出口是联通的情况。 所以
HttpDNS 访问 a记录 也能避免这类一部分错误。

Q4、用dnspod是用的他的接口么?如果dnspod上是配置的是cname,会递归解析出最终的ip缓存下来么?

会的。 这个依赖dnspod的返回结果, 同时也支持 cname 的返回结果。

比如 图片使用 cdn 如果返回的是 cname 结果。 那么数据库中记录的也是 cname 结果。 通过 cname 家 host 头来访问也是可以的。

Q5、数据库中记录的是cname,还是cname解析出的ip?

数据库中记录的是 cname , 并不是ip 。

因为测试过, 从一栋大楼走到另外一个大楼 里面 访问的最终ip可能都不相同。 所以如果返回的是cname 则直接存储cname 。 网络环境发生变化, 会重新拉取, 不会使用缓存的cname 。

Q6、那cname的情况下,httpdns就起不到实际的作用了?

不会的, 一般劫持的都是 业务的主要域名, 而cname域名的劫持相对较少, 从我们公司的业务来看啊。 而且 dnspod
返回cname 的情况 我目前还没看到。 都是解析倒ip 。 而我们自己做的 httpdns 服务器, 第一期目前会解析倒 cname 的节点。
跨域的ip解析 还没做 会放到二期。

Q7、我们遇到的问题是主域名解析没问题,cname的域名是amazon aws的域名,经常莫名其妙解析不通,怀疑是运营商搞鬼。当时也想自己做这个httpdns,但发现很麻烦,小厂没人力搞这个事情。

有这个可能,我觉得可以把你们的domain放到dnspod里面试下解析出来的是不是cname如果是直接的ip应该没问题。后期我们有计划加上udp直接发送dns协议包到公共的dns服务器节点来获取数据,也支持设置自己家的权威dns服务器。

想与高可用架构微信群内专家继续讨论DNS高可用话题,请关注公众号后,回复“arch”申请进群。

快网CloudXNS HttpDNS

1. 了解CloudXNS HttpDNS

CloudXNS的HttpDNS系统是在快网自主研发的授权DNS(CloudXNS)基础上开发的HttpDNS。 CloudXNS HttpDNS是为移动客户端或PC上软件量身定做的基于HTTP协议的域名解析方案,解决LDNS解析异常以及调度不准的问题。

虚线框是普通DNS工作方式:CloudXNS根据LDNS的IP地址解析域名www.cloudxns.net的A记录地址。

HttpDNS执行:(红线)
用户端通过HTTP协议访问CloudXNS HttpDNS,然后转换成DNS协议(+client=用户IP地址)访问CloudXNS,CloudXNS会根据用户的IP地址查找域名的A记录地址或者CNAME值返回。

2. 如何使用CloudXNS HttpDNS—明文接口

加密方式:明文传输

接口域名:httpdnsv3.ffdns.net, httpdnsv4.ffdns.net, httpdnsv5.ffdns.net 不同级别的域名调用不同的接口域名,详见域名配置页面

接口URL:http://httpdnsv3.ffdns.net/httpdns

接口调用:

a 调用参数

参数类型 参数名称 是否必须 具体描述
String dn true 域名,支持批量查询,域名之间‘,’分隔
String cip false 客户端IP地址,默认本机IP地址
String id false 用户id
String ttl false time to live (值只有为1时,才会返回ttl值)

b 接口示例:

http://接口域名/httpdns?dn=www.cloudxns.net
http://接口域名/httpdns?dn=www.cloudxns.net&ttl=1
http://接口域名/httpdns?dn=www.cloudxns.net&cip=6.6.6.6
http://接口域名/httpdns?dn=www.cloudxns.net&cip=6.6.6.6&ttl=1

c 接口结果示例:

[
{“dn”: “www.cloudxns.net”, “A”: [“61.174.9.79”]}
]
or
[
{“dn”: “www.cloudxn.net”, “A”: [“61.174.9.79″,”202.85.220.120”], “ttl”: “600”}
]

3. 如何使用CloudXNS HttpDNS—加密接口

加密方式:DES/ECB/PKCS5Padding

域名加密后的数据,支持批量查询:将要查询的多个域名以’,’逗号分割进行加密

接口域名:httpdnsv3.ffdns.net, httpdnsv4.ffdns.net, httpdnsv5.ffdns.net 不同级别的域名调用不同的接口域名,详见域名配置页面

接口URL:http://httpdnsv3.ffdns.net/httpdns

接口调用:

a 调用参数

参数类型 参数名称 是否必须 具体描述
String dn true 域名加密后的数据,支持批量查询:将要查询的多个域名以’,’逗号分割进行加密
String cip false 客户端IP地址,默认本机IP地址
String id true 用户id
String ttl false time to live (值只有为1时,才会返回ttl值)

b 接口示例:

http://接口域名/httpdns?dn=b97718f3d534ac7c0e93fa3e8e16d6bac6c356535e7a&id=3155
http://接口域名/httpdns?dn=b97718f3d534ac7c0e93fa3e8e16d6bac6c356535e7a&id=3155&cip=6.6.6.6&ttl=1

c 接口结果示例:

436bfd2c2f4ecb39042d4dc79256add449a9c66704de05d0f5d381d8c45cd679
用HttpDNS key解密后是
[
{“dn”: “www.cloudxns.net”, “A”: [“61.174.9.79”]}
]

HTTPDNS成为移动互联网的标配–原因与原理解析

DNS,作用就是将域名解析成IP。一个DNS查询,先从本地缓存查找,如果没有或者已经过期,就从DNS服务器查询,如果客户端没有主动设置DNS服务器,一般是从服务商DNS服务器上查找。这就出现了不可控。

DNS劫持

一些小服务商以及小地方的服务商非常喜欢干这个事情。根据腾讯给出的数据,DNS劫持率7%,恶意劫持率2%。网速给的劫持率是10-15%。

  1. 把你的域名解析到竞争对手那里,然后哭死都不知道,为什么流量下降了。
  2. 在你的代码当中,插入广告或者追踪代码。这就是为什么在淘宝或者百度搜索一下东西,很快就有人联系你。
  3. 下载APK文件的时候,替换你的文件,下载一个其他应用或者山寨应用。
  4. 打开一个页面,先跳转到广告联盟,然后跳转到这个页面。无缘无故多花广告钱,以及对运营的误导

智能DNS策略失效

智能DNS,就是为了调度用户访问策略,但是这些因素会导致智能DNS策略失效。

  1. 小运营商,没有DNS服务器,直接调用别的服务商,导致服务商识别错误,直接跨网传输,速度大大下降。
  2. 服务商多长NAT,实际IP,获得不了,结果没有就近访问。
  3. 一些运营商将IP设置到开卡地,即使漫游到其他地方,结果也是没有就近访问。

Http DNS 原理

客户端请求一个接口,例如http://154.58.*.*/dns?domain=www.woniubi.cn&client=xiaomi&screen=1024*960&gps=137,35&…等一些参数。服务器根据参数,给出最优的ip或者IP列表。APP就通过IP访问,不使用域名。

HTTP DNS 负载均衡

返回IP列表,客户端可以依靠策略,进行负载均衡。

  1. 客户端随机选取IP
  2. 客户端轮询使用IP

HTTP DNS缓存与更新

可以使用HTTP header进行方式进行缓存,参考这篇文章《http头部如何对缓存的控制》。也可以客户端自己设置过期时间,过期去服务端端拉取。

HTTP DNS不可用

两种解决方式

  1. 继续使用缓存
  2. 使用最初的DNS解析

IP不可用

HTTP DNS给的IP地址,发现不可用。

  1. 重试一定的次数,不成功换下一个IP。
  • 下一个IP也不可用,继续换下一个。

但是这样会造成下面一个问题。

雪崩

部分服务器不可用的时候,结果压力都转到其他可用的服务器上面,导致最后所有的服务器都不可用。

  1. 服务不可用的时候,随机加延时,分散压力。
  2. 重试一定次数的时候,就不继续重试,宁愿部分不可用,不能让所有的人不可用。

网站是否可用

浏览器都是通过域名访问,当然一些用户不可感知的服务,可以使用IP访问。

阿里HTTPDNS 详细价格信息

HTTPDNS商业化期提供流量包套餐、按量付费两种方式

购买流量包

计费说明

DDOS攻击防御

HTTPDNS提供20Gb的免费共享DDOS攻击防御(HTTPDNS所有客户共享此防御,举例来说,

假设两家客户同时受到10Gb的攻击,HTTPDNS可以成功防御;

假设某一时刻HTTPDNS遭受总量25Gb的DDOS攻击,服务可能会受影响)

如需更高等级的防御,请提工单申请

免费阈值

对于云解析付费用户,每自然月有3,000,000次免费域名解析次数阈值。

对于其他用户,每自然月有1,500,000次免费域名解析次数阈值。

https访问接口

httpdns支持通过https协议进行接口调用(https://203.107.1.1/{account_id}/d),每次通过https接口进行域名解析折算成5次http接口域名解析

举例来说,假设客户当日通过https接口进行了1,000,000次解析,系统会记录1,000,000次https解析,最终计费时,会换算成5,000,000次http类型解析,

并按照下文的资源包预付费或者按量后付费方式进行计量计费。

计费周期

当前每天结算一次,当日账单一般次日凌晨3点左右生成,请以账单实际生成时间为准。

对于流量包套餐,每天会尝试从流量包中扣除已消费域名解析次数,超出部分按照按量付费方式收费。对于按量付费,每天会根据实际消费的域名解析解析次数扣费。

计费方式转化

用户购买流量包后,优先从流量包中消费域名解析次数;

当流量包消费完毕以后,自动转入按量付费方式。

定价
流量包(预付费)
套餐编号 有效期(月) 流量包规格-域名解析次数(万次) 流量包价格(元) 单价(元) 折扣额度
1 3 500 16.8 0.0336 84折
2 3 5,000 150 0.03 75折
3 3 50,000 1,350 0.027 68折
4 3 200,000 4,800 0.024 6折
5 12 10,000 300 0.03 75折
6 12 100,000 2,500 0.025 63折
按量付费(后付费)
计量单位(万次域名解析) 价格(元)
1 0.04

退款说明

对于购买了HTTPDNS流量包的阿里云账户,在流量包截止期结束之前,如果核实未产生流量包消耗(HTTPDNS解析次数小于免费阈值)可以申请退款。其他情况不做退款处理。

申请退款请走工单系统申请人工处理。

OkHttp3应用[HTTP DNS的实现]

1. HTTP DNS 的介绍

HTTP DNS通过将域名查询请求放入http中的一种域名解析方式,而不用系统自带的libc库去查询运营商的DNS服务器,有更大的自由度。目前微信,qq邮箱、等业务均使用了HTTP DNS,详见这里

主要优点

  • 能够准确地将站点解析到离用户最近的CDN站点,方便进行流量调度
  • 解决部分运营商DNS无法解析国外站点的问题
  • TCP在一定程度可以防止UDP无校验导致的DNS欺诈(比如墙,运营商广告,404导航站),当然基于HTTP的话本质还是不安全的。

2. HTTP DNS 与 native DNS 的流程对比

使用http与native的区别主要在:

  • 需要实现native 自带的 LRU缓存(DNS默认是600s,原生的缓存在JNI内存中,而OkHttp缓存在硬盘中)
  • Socket请求拼装环节在java层,而不是在libc库中(当然有的项目非要做成JNI,也没办法)
  • 需要维护一个单独的HttpClient进行DNS查询

在进行实现前,我们先看下原生情况的查询方法的strace

2.1. Android下native DNS查询过程

//JDK层调用 InetAddress.lookupHostByName()(InetAddress.java); Libcore.os.android_getaddrinfo(InetAddress.java) //framework下调用 libcore.io.ForwardingOs.getaddrinfo(ForwardingOs.java) //framework层 libcore.io.Posix.getaddrinfo(Posix.java) //JNI层,此方法为java的代理 Posix_android_getaddrinfo(Posic.cpp) //调用BIONIC的libc标准库 android_getaddrinfofornet(getaddinfo.c) android_getaddrinfofornetcontext(getaddinfo.c)
...

发送socket包...

2.2. HTTP DNS流程

//拼装HTTP请求(OkHttp) client.newCall //JDK层,进行socket请求 Socket.connect(socket.java) //framework层 libcore.io.IoBridge.connect //进行tcp连接(JNI/C) ....

3. HTTP DNS的实现

在OkHttp中,提供了DNS的接口(这个接口基本没人知道,甚至网上都没有讨论),方便用户自己实现lookup方法。下文实现了两个OkHttpClient单例,一个负责dns,一个负责业务。

关于OkHttp,可以看以前的文章

经过考察第三方DNS服务,看起来只有腾讯系的DNSPod还是不错的,它直接返回一个ip地址,不需要解析json等乱七八糟的东西,文档在这里

DNSPod在返回的Header中,没有设置缓存,并主动断开了Socket连接,这样的话每次进行lookup时都会进行一个GET请求,相比以前原生多了40ms。我们都知道DNS默认的TTL时间一般是600s,因此我们可以通过复用OkHttp中的cache,在返回的response中加入Cache-Control的Header就可以实现再第二次lookup时避免再次访问网络。

static public synchronized OkHttpClient getHTTPDnsClient() { if (httpDnsclient == null) { final File cacheDir = GlobalContext.getInstance().getExternalCacheDir();
    httpDnsclient = new OkHttpClient.Builder() //消费者工作线程池 .dispatcher(getDispatcher()) //Logger拦截器  .addNetworkInterceptor(getLogger())
        .addNetworkInterceptor(new Interceptor() { @Override public Response intercept(Chain chain) throws IOException {
            Response originalResponse = chain.proceed(chain.request()); return originalResponse.newBuilder() //在返回header中加入缓存消息 //下次将不再发送请求 .header("Cache-Control", "max-age=600").build();
          }
        }) //5MB的文件缓存 .cache(new Cache(new File(cacheDir, "httpdns"), 5 * 1024 * 1024))
        .build();
  } return httpDnsclient;
}

max-age表示可以续600s;如果缓存命中将返回数据,反之抛出IOException。

通过测试,HTTP DNS工作效果如下:

  1. 在600s内,无论是否联网,都不会进行请求
  2. 在600s外,没联网时会抛出IOException,反之会进行HTTP请求

接着,我们实现了DNS查询接口,值得注意的一点就是第三方服务器可能会挂,所以一定要注意所有异常并留出后路。

static Dns HTTP_DNS = new Dns(){
  @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException { //防御代码 if (hostname == null) throw new UnknownHostException("hostname == null"); //dnspod提供的dns服务 HttpUrl httpUrl = new HttpUrl.Builder().scheme("http")
        .host("119.29.29.29")
        .addPathSegment("d")
        .addQueryParameter("dn", hostname)
        .build();
    Request dnsRequest = new Request.Builder().url(httpUrl).get().build(); try { String s = getHTTPDnsClient().newCall(dnsRequest).execute().body().string(); //避免服务器挂了却无法查询DNS if (!s.matches("\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b")) { return Dns.SYSTEM.lookup(hostname);
      } return Arrays.asList(InetAddress.getAllByName(s));
    } catch (IOException e) { return Dns.SYSTEM.lookup(hostname);
    }
  }
};

在真正的业务请求时,构建OkHttpClient时对DNS进行配置就ok了

//真正的调用客户端,供Retrofit与Picasso使用 static public synchronized OkHttpClient getClient() { if (client == null) { final File cacheDir = GlobalContext.getInstance().getExternalCacheDir(); client = new OkHttpClient.Builder().addNetworkInterceptor(getLogger())
        .cache(new Cache(new File(cacheDir, "okhttp"), 60 * 1024 * 1024))
        .dispatcher(getDispatcher()) //配置DNS查询实现 .dns(HTTP_DNS)
        .build();
  } return client;
}

4. 例子

Fork me on Github,有更多交流可以与我微信沟通,在深圳的开发者可以与我面基。

全局精确流量调度新思路-HttpDNS服务详解

小编:对于互联网,域名是访问的第一跳,而这一跳很多时候会“失足”,导致访问错误内容,失败连接等,让我们在互联网上畅游的爽快瞬间消失,而对于这关键的第一跳,鹅厂也在持续深入研究和思考对策,今天小编就邀请了我们负责这块域名解析的好伙伴—廖伟健同学跟我们做一个分享。同时,今天小编也非常希望了解大伙对这块内容的感受,所以今天文中加入了投票功能,希望您投上神圣的一票哦。事不延迟,我们启程 !

但凡使用域名来给用户提供服务的互联网企业,都或多或少地无法避免在有中国特色的互联网环境中遭遇到各种域名被缓存、用户跨网访问缓慢等问题。那么对于腾讯这样的域名数量在10万级别的互联网公司来讲,域名解析异常的情况到底有多严重呢?每天腾讯的分布式域名解析监测系统在不停地对全国所有的重点LocalDNS进行探测,腾讯域名在全国各地的日解析异常量是已经超过了80万条。这给腾讯的业务带来了巨大的损失。为此腾讯建立了专业的团队与各个运营商进行了深度沟通,但是由于各种原因,处理效率及效果均不能达到腾讯各业务部门的需求。除了和运营商进行沟通,有没有一种技术上的方案,能从根源上解决域名解析异常及用户访问跨网的问题呢?

一、问题根源:

要解决问题,我们得先得了解下现在国内各ISP的LocalDNS的基本情况。国内运营商LocalDNS造成的用户访问异常可以归为下三类:

1、域名缓存:

域名缓存很好理解,就是LocalDNS缓存了腾讯的域名的解析结果,不向腾讯权威DNS发起递归,示意图如下:

022251G8O.jpg

022251G8O.jpg

022251G8O.jpg

为何LocalDNS要把域名解析结果进行缓存呢?原因有以下几个:

(1)保证用户访问流量在本网内消化:国内的各互联网接入运营商的带宽资源、网间结算费用、IDC机房分布、网内ICP资源分布等存在较大差异。为了保证网内用户的访问质量,同时减少跨网结算,运营商在网内搭建了内容缓存服务器,通过把域名强行指向内容缓存服务器的IP地址,就实现了把本地本网流量完全留在了本地的目的。

(2)推送广告:有部分LocalDNS会把部分域名解析结果的所指向的内容缓存,并替换成第三方广告联盟的广告。

这种类型的行为就是我们常说的域名缓存,域名缓存会导致用户产生以下的访问异常:

A、仅对80端口的http服务做了缓存,如果域名是通过https协议或其它端口提供服务的,用户访问就会出现失败。比如支付服务、游戏通过指定端口连接connect server服务等。

B、缓存服务器的运维水平参差不齐,时有出现缓存服务器故障导致用户访问异常的问题。

2、解析转发:

除了域名缓存以外,运营商的LocalDNS还存在解析转发的现象。解析转发是指运营商自身不进行域名递归解析,而是把域名解析请求转发到其它运营商的递归DNS上的行为。正常的LocalDNS递归解析过程是这样的:

而部分小运营商为了节省资源,就直接将解析请求转发到了其它运营的递归LocalDNS上去了:

这样的直接后果就是腾讯权威DNS收到的域名解析请求的来源IP就成了其它运营商的IP,最终导致用户流量被导向了错误的IDC,用户访问变慢。

3、LocalDNS递归出口NAT:

LocalDNS递归出口NAT指的是运营商的LocalDNS按照标准的DNS协议进行递归,但是因为在网络上存在多出口且配置了目标路由NAT,结果导致LocalDNS最终进行递归解析的时候的出口IP就有概率不为本网的IP地址:

这样的直接后果就是GSLB DNS收到的域名解析请求的来源IP还是成了其它运营商的IP,最终导致用户流量被导向了错误的IDC,用户访问变慢。

二、现有的解决方案及存在的问题:

运营商的LocalDNS解析域名异常,给对用户访问腾讯业务的体验造成了非常大的损害。那么我们是如何处理这些域名解析异常的问题的呢?

1、实时监控+商务推动:

这种方案是目前腾讯的运营团队一直在使用的方案。这种方案就是周期比较长,毕竟通过行政手段来推动运营商来解决这个问题是比较耗时的。另外我们通过大数据分析,得出的结论是Top 3的问题用户均为移动互联网用户。对于这部分用户,我们有什么技术手段可以解决以上的问题呢?

2、绕过自动分配DNS,使用114dns或Google public DNS:

这个方案看上去很美好,114dns是国内最大的中立缓存DNS,而Google又是秉承不作恶理念的互联网工程帝国巨鳄,而且腾讯的权威DNS又支持edns-client-subnet功能,能直接识别使用Google publicDNS解析腾讯域名的用户的IP地址,不会出现流量调度失效。但是问题来了:

(1)如何在用户侧构造域名请求:对于PC端的客户端来说,构造一个标准的DNS请求包并不算什么难事。但在移动端要向一个指定的LocalDNS上发送标准的DNS请求包,而且要兼容各种iOS和android的版本的话,技术上是可行的,只是兼容的成本会很高。

(2)推动用户修改配置极高:如果要推动用户手动修改PC的DNS配置的话,在PC端和手机客户端的WiFI下面还算勉强可行。但是要用户修改在移动互联网环境下的DNS配置,其难度不言而喻。

3、完全抛弃域名,自建connectcenter进行流量调度:

如果要采用这种这种方案的话,首先你就得要拿到一份准确的IP地址库来判断用户的归属,然后再制定个协议搭个connect center来做调度,然后再对接入层做调度改造。这种方案和2种方案一样,不是不能做,只是成本会比较高,尤其对于腾讯这种业务规模如此庞大的公司而言。

三、利用HttpDNS解决用户域名解析异常:

既然上面的方案都存在那么多的问题,那有没有一种调度精准、成本低廉、配置方便的基于域名的流量调度系统呢?答案是肯定的。腾讯公司的GSLB 团队推出了一种全新的域名解析调度系统:HttpDNS。HttpDNS是为移动客户端量身定做的基于Http协议和域名解析的流量调度解决方案,专治LocalDNS解析异常以及流量调度不准。详细介绍如下:

(1)HttpDNS基本原理:

HttpDNS的原理非常简单,主要有两步:

A、客户端直接访问HttpDNS接口,获取业务在域名配置管理系统上配置的访问延迟最优的IP。(基于容灾考虑,还是保留次选使用运营商LocalDNS解析域名的方式)

B、客户端向获取到的IP后就向直接往此IP发送业务协议请求。以Http请求为例,通过在header中指定host字段,向HttpDNS返回的IP发送标准的Http请求即可。

(2)HttpDNS优势:

从原理上来讲,HttpDNS只是将域名解析的协议由DNS协议换成了Http协议,并不复杂。但是这一微小的转换,却带来了无数的收益:

A、根治域名解析异常:由于绕过了运营商的LocalDNS,用户解析域名的请求通过Http协议直接透传到了腾讯的HttpDNS服务器IP上,用户在客户端的域名解析请求将不会遭受到域名解析异常的困扰。

B、调度精准:HttpDNS能直接获取到用户IP,通过结合腾讯自有专利技术生成的IP地址库以及测速系统,可以保证将用户引导的访问最快的IDC节点上。

C、实现成本低廉:接入HttpDNS的业务仅需要对客户端接入层做少量改造,无需用户手机进行root或越狱;而且由于Http协议请求构造非常简单,兼容各版本的移动操作系统更不成问题;另外HttpDNS的后端配置完全复用现有权威DNS配置,管理成本也非常低。总而言之,就是以最小的改造成本,解决了业务遭受域名解析异常的问题,并满足业务精确流量调度的需求。

D、扩展性强:HttpDNS提供可靠的域名解析服务,业务可将自有调度逻辑与HttpDNS返回结果结合,实现更精细化的流量调度。比如指定版本的客户端连接请求的IP地址,指定网络类型的用户连接指定的IP地址等。

当然各位可能会问:用户将首选的域名解析方式切换到了HttpDNS,那么HttpDNS的高可用又是如何保证的呢?另外不同运营商的用户访问到同一个HttpDNS的服务IP,用户的访问延迟如何保证?

为了保证高可用及提升用户体验,HttpDNS通过接入了腾讯公网交换平台的BGP Anycast网络,与全国多个主流运营商建立了BGP互联,保证了这些运营商的用户能够快速地访问到HttpDNS服务;另外HttpDNS在多个数据中心进行了部署,任意一个节点发生故障时均能无缝切换到备份节点,保证用户解析正常。

四、接入效果及未来展望:

当前HttpDNS已在腾讯内部接入了多个业务,覆盖数亿用户,并已持续稳定运行超过一年时间。而接入了HttpDNS的业务在用户访问体验方面都有了非常大的提升。以某个接入HttpDNS的业务为例,该业务仅通过接入HttpDNS,在未做任何其它优化的情况下,用户平均访问延迟下降超过10%,访问失败率下降了超过五分之一,用户访问体验的效果提升非常显著。另外腾讯的HttpDNS服务除了在腾讯内部被广泛使用以外,也受到了业务同行的肯定。国内最大的publicDNS服务商114dns在受到腾讯DNS的启发下,也推出了HttpDNS服务。

在未来的日子里,腾讯GSLB团队将会在腾讯内部进一步推广HttpDNS服务,并将在实际业务的需求下对HttpDNS服务进行升级,如提供更为通用、安全、简单的接入协议,进一步提升接入用户的网络访问体验等等。希望HttpDNS能为各位在解决域名解析异常及全局流量调度失效方面提供一个简单、可行的思路,也欢迎各位业界同行与腾讯一起,就如何进行更精准的全局流量调度方面进行更为深入的讨论!

http://119.29.29.29/

接入文档
https://www.dnspod.cn/httpdns/guide

DEMO
https://www.dnspod.cn/httpdns/demo

腾讯DNSPod公共DNS 119.29.29.29 体验报告

腾讯DNSPod推出新Open DNS服务,公共DNS是119.29.29.29