在企业运维下, DNS 劫持是一个很让人头疼的问题,会带来很多问题。比如说,访问不正常,访问百度可能打开的不是百度网站,或者访问了一个 http 的协议,通过劫持,可能访问不到我最终要访问的那台服务器。
因为有这样的问题,给大家也带来很多困扰。所以,我和我的小伙伴一起研发了一个移动端的 Lib 库,这个 Lib 库完完全全可以解决手机移动上的 DNS 劫持的问题。
域名缓存
如果有一个电信的用户,他查询了 api.weibo.cn,由一个 LocalDNS 服务器访问到新浪权威,查询应该访问哪一台服务器,权威给到 Local 服务器, Local 反馈给这个用户。问题出现在什么环节呢?如果当 LocalDNS ,也就是运营商的服务器不去访问这样一个权威的服务器,就不去访问新浪了,或者以前访问过这样一个权威,然后现在把这个访问解决一直缓存。
当权威将域名和 IP 发生改变之后, LocalDNS 并没有及时的更新,这样导致用户访问不到服务器,或者访问不到资源。我想这样的一个问题,在开发的同学们当中应该多多少少都会遇到。这是一种域名缓存,由 LocalDNS 把刚才的指向结果缓存在本地,用户请求这样一个指向的时候,当前的运营商直接把结果反馈给用户。
运营商在网间结算的时候,会尽量控制它的出口带宽,控制成本问题。比如把百度的首页缓存到他自己的服务器上,当这样一个园区网络的用户都访问百度的时候,真正的数据并没有访问到百度自己的服务器,而是从当前的缓存服务器拿到了数据,丢给用户,这样带来的问题是当前的页面可能不是最新,但是这个数据是可以访问得到的。
另外,推送广告,很多无耻的运营商,会把运营解析指向到他们的一个缓存服务器上,并且把网页里面的广告替换成他们自己的,当一个网站比较大的时候,如果广告都替换成他们自己的一些广告 ID ,属于坐收渔翁之利,很多网站流量大的时候,都会多多少少遇到这样的问题,同时客户端也会遇到这样的问题。
如果他们做的好,没有问题。如果他们做的不好会带来什么问题?运营商一般对 80 端口的 http 服务做了缓存,如果域名是通过 https 的协议或者其他端口提供服务,用户访问就会出现失败,比如支付服务,游戏服务,都可能会出现这一类的问题。另外,一些缓存服务器的运营水平参差不齐,发生缓存服务器故障,导致用户没有办法正常访问到网络结果,这是最直接会影响到我们运营的产品上。
域名转发
这是运营商玩的小猫腻。电信的用户在查询 api.weibo.cn 的时候,会到当前的运营商服务器查,联通的用户也会到当前运营商服务器查,他们两个 LocalDNS 都会访问我们新浪权威的服务器,由权威将指向反馈给正确的用户。
转发解析这个里面会出现什么样的问题呢?比如当前的电信 LocalDNS 不访问新浪权威的 DNS 服务器,而是直接访问了联通 LocalDNS 服务器,为什么有一些小的运营商这么做,他自己要控制他的流量成本,会直接将 DNS 的规则递归到他相关或者是同网的 DNS 服务器上做解析,由 LocalDNS 查询了权威,在权威看到 IP 的结果,访问的结果是一个联通用户访问的,给它的指向肯定是一个联通的结果,这个时候电信的 LocalDNS 拿到的结果实际上是联通去请求的权威的指向结果。
大家说,对于电信的用户访问一个联通的服务器是不是会很慢?我想大家以前多多少少会遇到这样的问题,有些用户会抱怨,比如打客服,为什么我访问你们家这么慢?其实很多问题我们以前做过一系列监控,甚至和用户做连调,发现了这样一类问题,他们的 LocalDNS 是一个安全的 Local ,但是访问上来的服务器是另外的一个安全出口。这样一个指向是错误的,导致当前的电信用户访问的是联通,甚至没有办法访问到运营商的网络。而部分小运营商为了节省资源,就直接将解析请求转发到其他运营商递归 LocalDNS 上去了,这样导致用户没有办法正常的请求数据。
LocalDNS 递归出口 NAT
比如这个图,由于是一个配置错误, LocalDNS 如果都是正常的指向,配置错误会从另外的一个网卡去查询,联通的用户的指向结果是客户端, Local 客户端是多网络的,由于它的配置错误,没有正确的向联通的查询,而是访问了电信权威的服务器,导致拿到的指向结果就是错的,这也是很常见的一类问题。这里是由于它的网络网卡的配置错误,导致没有办法正确的访问,正常的 IDC 也同样的问题,会导致用户访问变慢,甚至导致用户没有访问通过。
现有的解决方案及存在的问题
第一、实时监控 商务推动。第二、绕过自动分配的 DNS ,直接使用 114 DNS ,或者 Google 的 DNS , 114 DNS ,大家可能会把网卡的网址设成 114 的 DNS ,它不太有不准的问题,这是一个办法。另外,完全抛弃这样一个域名,直接自己建立这样一个流量调度系统。比如一个用户直接访问是一个 IP ,大家写接口的时候都会知道,我访问的是一个域名,由域名做各种调度,最终分配到服务器,也可以由客户端直接访问持续一个 IP ,由 IP 做各种调度,最终告诉你,我该服务哪一台服务器。如果让一个用户在他的电脑上设置 114 ,或者 8.8 ,这个成本是很大的。
如果我们自己的技术人员帮助用户自己发一个 DNS 的请求包,这个事情是可以做,但是如果要去适配 N 多 Android 的版本,还要适配 iOS ,甚至各种端,我这个问题会让大家很头疼,它不是不能做,技术上可行,成本会很高。另外,推动用户修改配置,这是一个很麻烦的事情。
基于前面这样的一系列的问题,今年的时候,有了这样一个概念,叫做 HttpDNS ,实际上很简单,一个联通的用户通过一个 HttpDNS 的协议去访问了 dns.sina.com ,这是一个域名,访问到我们新浪 http 的服务器,这是一个 http 的 web 服务器,由 httpWeb 服务器向新浪权威的 DNS 服务器查,会查询到当前的这个用户,因为访问的时候带 IP ,当前这个用户应该给一个记录的指向,由 http 查出来推给用户,这里的协议是 http 的协议,并不是传统的 DNS 的协议。
如果当 http 的 DNS 服务器挂掉怎么办?备选方案还是走传统的向运营商查询去问, httpDNS 的原理就是将 DNS 的协议,变成 http 的协议,绕过了 DNS 的协议请求,解决了劫持问题。
大家可能都听到了, HttpDNS 的原理很简单:第一步,客户端直接访问 httpDNS 的接口,获取业务在域名配置管理系统上配置的访问延迟最优的 IP 。同时,基于容灾考虑,保留次选使用运营商 LocalDNS 解析域名的方式。第二步,客户端使用获取到的 IP ,直接往此 IP 发送业务协议请求,以 http 请求为例,通过在 header 中制定 host 字段,向 httpDNS 返回的 IP 发送标准的 http 请求即可。
HttpDNS 的优势
从原理上来讲, HttpDNS 只是将域名解析的协议,由 DNS 的协议换成 http 的协议,其实它一点都不复杂。这个很微小的转换,却带来无数的收益。
第一、它根治了域名解析的异常,由 http 的协议直接打到 httpDNS 服务器的 IP 上,用户在客户端解析请求的时候,不会遭受到域名解析异常这些问题;第二,它的调度非常精准,有些包括 DNS 做的不太好,它多个网卡,甚至多个链路。一个电信的用户访问上来,可能是联通的出口, HttpDNS 也可以面临这样的问题,因为它访问服务器的时候直接走业务的 IP , http 服务器抓到的 IP 也是业务的 IP ,可以保证 IP 地址非常精准,后面的业务来说,也是可以分配到最好的一个服务器的 IP 地址。
实现成本非常低廉,目前 Android 版本 SDK 已经开源了,代码已经托管,大家可以去使用。 iOS 版本在最后测试阶段,估计近期也会开源。同时,它的扩张性非常强, http 可以接入多方的 HttpDNS ,甚至可以直接访问公共的 DNS 服务器。有的人会说,我没有 HttpDNS 服务器怎么办?目前已知可以使用的 DNS 服务器, DNSPod D ,目前开放了 httpDNS 接口,免费使用,大家可以直接用它。
同时, 114 也可以做,但是还没有正式的开放。阿里内部也已经有很多项目使用了自己家的 DNS 服务器。新浪目前也是在做自己的 HttpDNS 服务器,具体是否会对外使用,还没有收到消息。目前来说,大家可以选择的有 DNSPod D ,同时也可以在我们这样的 SDK 里面,可以直接发 UDP 的包,直接到 114 ,或者直接到 8.8 ,甚至一些其他的公共的 DNS 服务器上直接进行查询,也就回答了大家说没有 HttpDNS 服务器能不能用,是可以的。
HttpDNS Lib 库目前的现状,存在的问题,以及解决的问题
HttpDNS 这样一个 Lib 库解决了三类问题:第一、 Local DNS 的劫持。第二、平均访问延迟下降。第三、用户连接失败率下降。目前已知用户延迟访问下降可以超过 10 % ,访问失败率下降超过 1/5 。当然这个也要看大家用的是什么样的一个服务。
Local DNS 劫持,由于 HttpDNS 通过 IP 直接请求,可以直接拿到这样的记录。比如在 tcp/ip 层进行劫持,我们目前也会加入一些使用因子,或者数据加密的方式,保证数据的可信度,目前这一类的事情已经在做,后期我想可能很快也会开源。
为什么朋友访问延迟可以下降,由于 IP 直接访问省掉了一次 domain 解析过程,以前访问一个域名,由本地系统先查询一下,当前的这个域名应该访问哪一台服务器。因为去请求 A 记录的时候和业务是一个异步的操作,不会给业务带来任何影响。目前, SDK 去测试,获取到一个 A 记录平均时间会在 5 毫秒以内。
用户连接失败率下降,这个里面有一个智能排序的算法。比如说,我们分析一个用户的行为,一个用户每天可能是在家、路上、公司,很多用户可能是这样几个点,同时有其他的用户不太遵循这样的规律。在家的时候,他所访问的服务器正常一般是固定的,因为他的家是固定的,在公司的时候一般访问的服务器也是固定的。我们本地在 SDK 的算法会把他访问的历史记录缓存下来,同时有一系列的优化算法。比如他经常访问这台服务器,这台服务器能频繁的给它正确的返回结果,我们在算法里面排分的时候,会把这台服务器打分打的比较高。也就是通过一些历史的数据,个性化的定制了用户应该去访问哪些服务器,可以排除一部分的错误。
用户要去访问一个域名,到缓存查询模块,如果缓存里没有,会上 HttpDNSLib 库查,如果没有数据,会从本地拿一个这样的结果先给用户,缓存再返回数据,会由缓存这个模块把数据进行缓存,还有评估模块,给我一个接口,查询模块和缓存模块沟通,会和评估模块沟通,用户使用完,可以随用户的意愿,是否把你使用的结果反馈给评估模块,如果你给了我一个使用的结果,我会给你一个使用更好的 IP 地址,主要有三个模块的组成,查询模块、数据模块和病故模块。查询模块,检测本地是否有相应的域名的缓存,如果没有,当地的 LocalDNS 获取,然后从 HttpDNS 更新 domain 记录。
有数据检测,过期则更新记录返回 LocalDNS 记录,未过期则直接返回缓存层数据。这个里面是查询模块简单做的事情。数据模块会根据当前的 sp 进行关联的记录。缓存域名的信息,缓存服务器的信息以及服务器的优先级记录每个服务器的 IP 请求成功次数和错误次数,也会记录 IP 最后的访问时间和最后的测速,这些数据都是为了后面的评估模块做智能打分的一些数据的依据。同时,还添加了内存到数据库之间的缓存层,为了让用户更快速的拿到一个结果。
评估模块根据本地的数据,会对整个 IP 排序,这是一个智能排序。然后,会给 HttpDNS 的服务器智能分配 A 记录提供数据依据。比如大家自己的 HttpDNS 服务器,当那些服务器挂掉的时候, HttpDNS 服务器会自动、智能把挂掉的服务器绕掉。同时也会给服务器进行一系列的反馈,如果不同会有相应的作用,下一批不会给用户反馈不通的 IP 了。处理用户反馈回来的明细请求,也是为了更好的智能化。针对用户反馈失败请求,进行预警监控,这也是一种机制。
智能排序会根据五个因子:第一、本次测速,速度最快的 IP 会打分,打的分也是最高。第二、反馈结果有一个官方推荐,比如这些服务器的一个压力,很大的服务器可能打分打的偏低一点,让用户尽量不要选择这些压力大的服务器,然后给一些分数高的,压力比较小的,让负载均衡去摊开,也是一种做法。第三、历史成功的次数,该 ip 历史访问成功次数,比如找一个朋友办一件事情,这个朋友找他办了 100 次事情,都是很漂亮的帮我办了,另外一个朋友,我也找他帮我办了 100 次,但是他只办成 1 次,大家也可能很清楚,我肯定找靠谱的人帮助我办事。第四、历史错误次数,该 ip 历史访问错误次数。第五、最后的成功时间,这台服务器,刚刚够访问它通过了,请求了它的一次这样的数据,它就给了我结果。我现在再去找他问数据的时候,其实他还是同样能够给我结果的,因为他刚刚是正常的,现在我也认为他是正常的,这里面会有一个阀值, 24 小时,如果超过 24 小打分就是 0 分了,如果 24 小时之内,这一组 IP 哪一个最接近我的峰值使用,就会排序最高。智能排序是基于一系列历史的插件,历史数据做的一系列的插件。同时,这些插件大家可以自己进行扩展,如果想要深度的去使用这样一个库,里面都是接口化的,插件化的,大家可以直接进行扩展,自己需要一系列的插件。
首先,用 SDK 肯定要获取 A 记录,调用 SDK 进入一个白名单的过滤流程,如果你访问的 IP ,这个域名存在这个端,我们会接着走下去,如果没有配置这个域名,我们这个库对于你发的域名是不支持的。白名单如果配置是空的,意味着全域名的支持。白名单之后,我们会查询对应的导列,先从内存槽进行查询,如果没有,可能是因为刚刚退出了,所以接着查 DB , DB 有,再同步回内槽,返回给查询的导列。就是访问 HttpDNS Server 。
拿到数据做数据校验,如果 IP 并非是最新的,我们会把它添加到更新队列,以保证下次过来的时候,一定会反馈给你最新的。接下来就是记录一些 Log ,最后经过一个过滤器,转换成应用层,应用层对应的一个数据模型,返回给业务层,这是获取 A 记录的一个交互流程。
怎么保证数据是最新的呢?我们有一个定时器的逻辑。定时器做了三件事情:第一、更新国企的 DoMain 数据,第二、对这一组返回的 A 记录做一个测速的逻辑。第三、日志管理,每个环节进行一些日志的记录,会在定时器进行轮回。
A 记录的更新策略。拿着 DoMain 去 HttpDNS Server 请求回来 A 记录,请求回来 A 记录,首先会同步到数据层,更新成功之后,同步到内存缓存,业务方再去调用 SDK 的时候,就会从业务层取 DoMain 对应的 A 记录。
网络发生变化怎么办?首先, SDK 里面有一个监听的模块,会监听网络的变化,判断当前网络发生的变化,比如从 WIFI 变到 3G ,从 3G 变到 WIFI ,这个时候会清除内存的缓存,保证下次取的时候不会取错。 WIFI 下,我们走的可能是联通的运营商,如果我们手机是移动的,跟 SDK 是不一样的,如果你是联通的,给你返回一个电信的 IP ,可能就会比较慢,这是网络变化的一个策略。
Lib 库的主要功能点。首先,初始化。初始化 SDK 配置,然后初始网络状态信息,注册网络的监听变化。同时, SDK 会支持你的一些预加载的行为。比如你已经很明确,你一定会解析这个域名,我们在初始化之后,就会紧跟着去调用一下预加载的这个函数,它会找预加载的策略,在后台帮你把对应的域名的一些记录在后台都做完了,当你之后再去获取 A 记录的时候,它在数据层就已经有了,这是预加载。
其次,更新 A 记录,我们用一个插件化的思想,我们认为提供给我们的数据源,不仅仅有自己的 HttpDNS Server ,同时还可以用 DNSPOD 。备选方案也跟其他并列, LocalDNS ,这些都可以进行热插拔和优先级排序。
最后,要进行测速,其实我们可以理解成为是一个最优链路。测速的目的是为了找到一个高可用的 IP ,如果 HttpDNS Server 返回给咱们一组 A 记录有三个,我们怎么判断这三个可用呢?有两种方式:第一、用 Socket 连接你的 80 端口,这样直接对应到你的业务接口,如果他们连得通,说明这个业务肯定是通的。我们取建立连接的时间作为它的一个 RTT ,对于三个 A 记录来说,哪个时间最短,我们认为这个链路不仅通而且更快。除了这个方式,我们可以可以采用 Ping 的方式。
排序模块,也是基于插件化的一个实现。插件有五个因子,分别是:第一、速度,基于 ping 或者 socket 得出的 RTT 。第二、优先级,根据监控系统、链路反馈、负载均衡得出的优先级。第三、成功次数。第四、错误次数,如果我们使用的时候一直没有失败,我们就认为这个 IP 是非常可靠的 IP ,权重越高,得分越高。第四、失败次数,同上。第五、最后成功时间,上一秒钟有三个 A 记录, A 记录是成功的,这一秒钟也认为它是成功的,占一个模块。最终会根据五个插件和不同的权重计算出一个最终的得分,如果哪个得分最高,我们认为这个 IP 是真正一个高可用的 IP 。
查询模块,其实是内存层和 DB 。先去内存层查,如果没有去 DB 查,如果 DB 没有,返回默认的 Local ,如果 DB 有,返回回来,同步内存再回去,这是查询模块。
配置模块。使用 SDK 的时候,支持进行一个分布的自定义。首先,开关的类别,我们支持 HttpDNSLib 库的总开关的方式,开了就能用,关了你去调用,我们还是给你打了一个 Local ,相当于是没有开的。第二、智能排序算法,五个插件归属在智能排序这块,如果开了这个开关,它才会进行排序,进行一个得分和机损,如果没有开,默认是 Http Server 返回给默认。如果没有太多时间做 Http Server ,可以直接用 DNSPod ,还有 UDP ,有些人可能会问为什么没有 LocalDNS ?如果把这三个都关了,好像就没有用了。所以我们默认 LocalDNS 是一个兜底的服务。
配置模块的参数比较多。首先,自定义 HttpDNS API 接口地址,我们得有自己的一个地址,同样用到 DNS Pod ,也需要配置它的 API ,如果是企业级的配置方式,需要配置企业的 KEY 和企业 ID 。 UDP 请求 DNS 服务器 IP 地址,比如 8.8 , 114 ,刚才提到域名的白名单,如果不配,默认是全域名支持,如果配了,就只支持你已经配了得域名。包括日志的采样率,日志的上传时间,定期是轮询间隔时间, IP 测速间隔时间,都是可以自定义, A 记录 TTL 差值时间,排序算法的插件权重值。
主要围绕这么几点:第一、智能动态节点加速。第二、和监控系统进行整合,可以进行动态节点的切换。第三、故障之后的负载均衡切换。比如现在的 HTTP2 整合。
一路走来,掉进去的坑还是不少的,最早的时候,更新 A 记录的时候会有并发,比如说,我们在业务方,不同的线程并发的调用函数,会产生并发的问题。当 SDK 这个库还没有缓存域名的时候,我们后台并发调用了很多解析过程,后来发现有这个问题,就把它解决了。其实我们的目的是要得到一个高可用的 IP ,但是当时定的名字叫做测速,后来发现这好像是一个偷换概念的方式。首先,流量的方式,大家知道下载邮件,首先窗口出现这种问题,后来发现无线电不直接取 RTT ,最终我们采用了测速机制变革以及插件化的改革。我们做的时候,配置不同 Server 的时候都插件化了,允许自由更换。
The End