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