App域名劫持之DNS高可用 - 開源版HttpDNS方案詳解(轉)

本文根據馮磊和趙星宇在「高可用架構」微信羣所作的HttpDNS智能緩存庫原理整理而成,轉發請註明來自微信公衆號ArchNotes。java

馮磊,目前主要從事手機應用平臺的構建,任職新浪網技術中國研發中心技術保障部架構師。5+年互聯網,移動終端,遊戲從業經驗。歷任軟件工程師,高級軟件工程師,技術經理。android

趙星宇,HttpDNS的合做者。目前就任於新浪微博,從事手機微博的基礎架構開發,任android高級研發工程師職位。git

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)github

鵝廠往事中提到面試

那麼對於騰訊這樣的域名數量在10萬級別的互聯網公司來說,域名解析異常的狀況到底有多嚴重呢?天天騰訊的分佈式域名解析監測系統在不停地對全國全部的重點LocalDNS進行探測,騰訊域名在全國各地的日解析異常量是已經超過了80萬條。這給騰訊的業務帶來了巨大的損失。爲此騰訊創建了專業的團隊與各個運營商進行了深度溝通,可是因爲各類緣由,處理效率及效果均不能達到騰訊各業務部門的需求。除了和運營商進行溝通,有沒有一種技術上的方案,能從根源上解決域名解析異常及用戶訪問跨網的問題呢?算法

HttpDNS主要解決三類問題:sql

  1. LocalDNS劫持數據庫

  2. 平均訪問延遲降低apache

  3. 用戶鏈接失敗率降低json

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,後都一一解決)。

以上爲馮老師的分享,接下來是星宇跟你們分享下項目從研發倒如今所遇到的一些主要問題和你們有疑問的點。

開發過程當中,常見的一些問題

一、手機網絡從3G 切換到 Wifi下處理了什麼?

NetworkStateReceiver類來監聽網絡是否發生變化,在網絡有變化的時候,會刷新 NetworkManager類中的網絡環境,在客戶端內若是是手機網絡能夠知道網絡類型(2G、3G、4G)也能夠知道當前SP(移動、聯通、電信),若是是Wifi網絡環境能夠知道SSID(wifi名字)在刷新網絡環境後,會從新查詢緩存內是否有當前鏈路下的最優A記錄,若是沒有則從LocalDNS獲取第一次,而後立刻更新httpdns記錄。

二、網絡發生變化後,返回的A記錄還同樣麼?

數據庫中緩存的數據,是根據當前sp來緩存的,也就是說當自身網絡環境變化後,返回的a記錄是不同的 。手機網絡下會根據當前sp來緩存 a記錄服務器ip,若是是wifi網絡環境下 根據當前ssid來緩存a記錄,由於wifi環境下庫本身沒有辦法明確判斷出本身的運營商,但相同的ssid不會發生頻繁的網絡運營商變化。 因此在wifi下請求回來的a記錄直接關聯ssid名字便可,即便wifi sp發生變化,最多延遲一個ttl時間就更新成最新的a記錄了。

三、怎樣進行測速?

在從HttpDNS獲取回來a記錄的時候進行測速,測速的方式有兩種: ping和空的http請求。考慮倒有些服務器不支持ping來進行鏈路質量評判,可使用空的http請求,僅僅是兩個http頭的流量開銷,而ping的方式流量開銷就更小了。 這個功能能夠在庫中本身配置。這裏的測速實際上是模擬首保接收的時間來作的, 同時對於流量控制嚴格的能夠在庫中配置測速頻繁度,好比一臺服務器在5分鐘內有過測速記錄則不進行測速。

四、域名ttl剛剛過時,庫尚未從HttpDNS拉取回來數據怎麼辦?

ttl過時的前10秒去請求數據,在ttl過時後的10秒內庫也認爲當前a記錄是有效的,會給你直接返回。

五、lib庫目前只能使用 dnspod 服務商麼? 支持dnspod 企業版本麼?

目前庫能夠支持自定義的 HttpDNS api接口, 只須要實現IHttpDns接口類便可,在配置了dnspod企業key和id 的時候自動啓用企業版本加密傳輸,支持企業版本。

六、使用這個庫會不會下降應用請求網絡的訪問速度?

從目前的測試數據來看是不會的,HttpDNS庫返回a記錄的時間平均在5毫秒之內,有時會出現內存緩存中沒有該域名記錄,數據庫中也沒有的時候會從LocalDNS獲取a記錄,時間會稍長一些

一旦從LocalDNS獲取後,會緩存倒內存中,在HttpDNS獲取數據後會更新內存中得domain記錄。 從庫中獲取a記錄會比從LocalDNS獲取a記錄快一些。 在訪問網絡的時候因爲是使用ip直鏈,能夠起到一些加速效果,lib庫獲取domainA記錄 + ip直接訪問服務器 耗時小於 直接域名請求服務器。相關數據圖片

下面給出一個測試系統的截圖

七、lib庫裏面訪問網絡使用的是哪一個網絡庫? json庫用的是哪一個?

考慮到該庫的輕量級,使用的是android系統的org.apache.http.client.HttpClient庫訪問網絡,若是須要切換到工程在使用的網絡庫能夠實現 INetworkRequests 接口便可切換網絡庫。 json解析使用的也是android系統自帶的org.json.JSONObject 若有需求切換json解析庫,可直接實現IJsonParser接口便可切換。

八、使用該網絡庫會給個人app帶來多大的體積增長?

目前該lib庫沒有引用任何外部的庫文件。一切本着使用系統自身的api爲原則,來保證庫的輕量級,和兼容性。 目前lib庫打包後70多k,代碼在5千行左右。 測試工程代碼在6千行左右。

九、lib庫的配置必需要經過修改源代碼的方式來進行配置麼?

任何參數均可以在庫調用方配置,DNSCacheConfig 類是整個庫的配置文件。 而且支持雲端動態更新配置 須要實現DNSCacheConfig.ConfigText_API 更新地址。 具體配置api接口請參考 dome工程中設置庫的方式。

十、緩存domain記錄是存儲成文件仍是數據庫,或者android內部的一些存儲方式?

lib緩存數據是經過數據庫存儲的。SQLiteDatabase, 具體的表接口和sql語句請參考 DBConstants 類文件。

十一、 評估模塊有什麼功能?

評估模塊目前由五個插件組成, 速度插件、推薦優先級插件、歷史成功次數插件、歷史錯誤數插件、最近一次成功時間插件 。 每個a記錄服務器ip,都會通過這五個插件進行評估排序後返回給使用者。 全部插件評估分值比重能夠配置, 根據本身的需求以及不一樣的使用場景,調整出最合理的權重分配。

下面給出評估模塊算法細節圖

十二、速度插件具體算法?

好比速度插件評分體系, 滿分100分, 那麼有3個服務器ip, 1號服務器http請求耗時10毫秒, 2號服務器20毫秒, 3號服務器30毫秒。 那麼通過插件計算後 1號服務器100分, 2號服務器50分, 3號服務器25分。

1三、優先級插件又是什麼?

若是是自定義的服務器,能夠返回服務器優先級字段,該的字段表明推薦使用該服務器的權重, 好比該字段服務端能夠和監控系統結合起來,甚至是用來分流。 相應的權重值, 也會算出來不一樣的分值。

1四、歷史成功次數插件是什麼? 歷史錯誤次數插件是什麼?

在當前sp當前鏈路下, 會記錄訪問過的該服務器ip的成功次數, 成功次數越多認爲該服務器相對穩定。 會在排序的時候根據權重比值進行影響最終排序結果, 該插件權重不建議太高。 同理歷史錯誤插件也是記錄當前鏈路下服務器出過錯誤的次數,次數越高認爲越不穩定。 排序儘可能靠後。 一樣該插件權重不建議太高。

1五、最後一次成功時間?

若是該服務器在 很近的時間內訪問過,那麼評估系統則認爲他鏈路是通的,則會給一個分值, 越接近如今的時間的服務器 分值越高。 24小時之前訪問的分值爲0 。

1六、評估插件就這五個嗎?

該五個插件屬於拋磚引玉, 能夠自定義插件 只須要實現 IPlugIn 接口便可。 全部的插件啓用和中止都在 配置文件中能夠修改,以及配置每一個插件的權重比。

1七、智能評估必定會帶來好的效果嗎?

首先httpdns返回的a記錄已是 通過當前地域和當前sp返回的最優記錄結果集, 至少不會下降效率。

1八、我能夠不使用智能評估模塊麼?

能夠的在配置文件中關掉智能評估便可,具體代碼參照demo便可。 關掉智能評估模塊後,會在多個a記錄中隨機排序返回。

1九、該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

Q一、每次請求url都須要去ping麼?

不須要每次都ping的, 測試鏈路是否通暢 會在 從 httpdns api接口獲取數據後, 在測試鏈路是否通暢, 每次請求 httpdns api間隔是一個 domain 設置的 ttl時間

ping 直髮一個包, 最小化的減小 流量開銷。

檢測鏈路 若是配置成 http 空的請求, 也是同理 在 httpdns api請求結束後, 纔會檢測鏈路是否暢通。

Q二、前面提到的併發請求,被丟棄的請求是怎麼處理的

併發請求是說 客戶端請求 HttpDNS lib庫 ,同時發 api.weibo.cn 的請求麼?

由於 去問 HttpDNS api接口的時候 , 只須要有一個請求去問就能夠了, 去問 HttpDNS api的時候 已經切換到 非客戶端主線程, 在客戶端調用的主線程中 若是沒有緩存數據 就從 本地獲取 dns 的a記錄返回了。 因此直接丟棄這個訪問 HttpDNS api的請求便可, 不會影響到其餘流程邏輯。

Q三、南北網絡之間請求有特別處理麼?

南方電信,北方網通,運營商ip不同

首先 HttpDNS 返回的a記錄會根據你的出口ip 來從權威的 dns 服務器問出來結果。 若是你是南方的ip 確定給你的a記錄 也是南方的, httpdns 返回的記錄理論應該是和 傳統的 dns 返回的 a 記錄是同樣的。 而去問 httpdns 的api 地址 是 bgp的機房。 因此 也是 兼容多鏈路 多地域。有碰見過 傳統 dns 出口多是 電信的, 但業務訪問的 ip 出口是聯通的狀況。 因此 HttpDNS 訪問 a記錄 也能避免這類一部分錯誤。

Q四、用dnspod是用的他的接口麼?若是dnspod上是配置的是cname,會遞歸解析出最終的ip緩存下來麼?

會的。 這個依賴dnspod的返回結果, 同時也支持 cname 的返回結果。

好比 圖片使用 cdn 若是返回的是 cname 結果。 那麼數據庫中記錄的也是 cname 結果。 經過 cname 家 host 頭來訪問也是能夠的。

Q五、數據庫中記錄的是cname,仍是cname解析出的ip?

數據庫中記錄的是 cname , 並非ip 。

由於測試過, 從一棟大樓走到另一個大樓 裏面 訪問的最終ip可能都不相同。 因此若是返回的是cname 則直接存儲cname 。 網絡環境發生變化, 會從新拉取, 不會使用緩存的cname 。

Q六、那cname的狀況下,httpdns就起不到實際的做用了?

不會的, 通常劫持的都是 業務的主要域名, 而cname域名的劫持相對較少, 從咱們公司的業務來看啊。 並且 dnspod 返回cname 的狀況 我目前還沒看到。 都是解析倒ip 。 而咱們本身作的 httpdns 服務器, 第一期目前會解析倒 cname 的節點。 跨域的ip解析 還沒作 會放到二期。

Q七、咱們遇到的問題是主域名解析沒問題,cname的域名是amazon aws的域名,常常莫名其妙解析不通,懷疑是運營商搞鬼。當時也想本身作這個httpdns,但發現很麻煩,小廠沒人力搞這個事情。

有這個可能,我以爲能夠把大家的domain放到dnspod裏面試下解析出來的是否是cname若是是直接的ip應該沒問題。後期咱們有計劃加上udp直接發送dns協議包到公共的dns服務器節點來獲取數據,也支持設置本身家的權威dns服務器。

http://www.tuicool.com/articles/7nAJBb

相關文章
相關標籤/搜索