标签归档:域名劫持

移动互联网时代,如何优化你的网络 —— 域名解析篇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方便开发者进行终端上的集成和使用,推荐大家可以尝试一下。

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”申请进群。