聊聊HTTPS環境DNS優化:美圖App請求耗時節約近半案例

DNS 服務作用於網絡連接之前,將域名解析爲 IP 地址供後續流程進行連接。DNS 查詢時,會先在本地緩存中嘗試查找,如果不存在或是記錄過期,就繼續向 DNS 服務器發起遞歸查詢,這裏的 DNS 服務器一般就是運營商的 DNS 服務器。在這過程中,會產生一些不可控的問題。

美圖的移動端產品在實際用戶環境下會面臨 DNS 劫持、耗時波動等問題,這些 DNS 環節的不穩定因素,導致後續網絡請求被劫持或是直接失敗, 對產品的用戶體驗產生不好的影響。 爲此,我們對移動端產品的 DNS 解析進行了優化探索,產生了相應的 SDK。在這過程中,我們參考借鑑了業內的主流方案,也進行了一些實踐上的思考。下面的內容會主要以 Android 平臺來進行說明。
Local DNS VS HTTP DNS

在長期的實踐中,互聯網公司發現 LocalDNS 會存在如下幾個問題:

域名緩存: 運營商 DNS 緩存域名解析結果,將用戶導向網內緩存服務器;

解析轉發 & 出口 NAT: 運營商 DNS 轉發查詢請求或是出口 NAT 導致流量調度策略失效;

爲了解決 LocalDNS 的這些問題,業內也催生了 HTTP DNS 的概念,它的基本原理如下:

原本用戶進行 DNS 解析是向運營商的 DNS 服務器發起 UDP 報文進行查詢,而在 HTTP DNS 下,我們修改爲用戶帶上待查詢的域名和本機 IP 地址直接向 HTTP WEB 服務器發起 HTTP 請求,這個 HTTP WEB 將返回域名解析後的 IP 地址。

比如 DNSPod 的實現原理如下:

相比 LocalDNS, HTTP DNS 會具備如下優勢:

根治域名解析異常: 繞過運營商的 DNS,向具備 DNS 解析功能的 HTTP WEB 服務器發起查詢;

調度精準: HTTP DNS 能夠直接獲取到用戶的 IP 地址,從而實現準確導流;

擴展性強: 本身基於 HTTP 協議,可以實現更強大的功能擴展;

那麼,是否直接全部走 HTTP DNS 呢?
美圖移動端 DNS 優化策略探索

HTTP DNS 相比 LocalDNS 存在一些優勢, 然而 HTTP DNS 本身也是存在一定的成本問題。

美圖的產品線豐富,涉及的域名也較爲廣泛,爲了適應各產品的實際場景,在實踐中我們設計了較爲靈活的策略控制。

首先,在策略上我們並未完全放棄 LocalDNS。

一個 App 涉及的域名衆多,在策略上我們能夠配置其核心 API 域名走 HTTP DNS,而對於非核心請求我們仍希望它先嚐試走 LocalDNS, 在異常情況下才升級走 HTTP DNS。

那麼如何判斷 LocalDNS 的異常情況呢?

我們選擇了幾個指標來衡量一個 DNS 服務器的質量情況:

IP 記錄的 TTL 時間: 在 DNS 劫持發生的情況下,返回的 TTL 可能會有非常大的值;

解析耗時: 如果一個 DNS 服務器解析耗時不理想,那麼它也不是我們希望的;

返回的 IP 的可連接性: 對返回的 IP 進行質量測試,如果連接狀況不佳,那麼這個 DNS 服務器有劫持的可疑;

在 Android 平臺上,通過系統方法獲得的解析結果信息是非常有限的,上面的指標有的將無法獲取,因此在實踐中我們會自己去構造 DNS 查詢報文,向運營商的多個 DNS 服務器發起查詢。

通過上面幾個指標的綜合評定,當 LocalDNS 表現不佳的時候,策略上我們將升級走 HTTP DNS,嘗試讓用戶獲取更好的 DNS 解析效果。

在 DNS 解析環節,還有一個我們比較關心的指標,那就是 DNS 解析的耗時:

LocalDNS 在過期的情況下,會發起遞歸查詢,這個時間是不可控的,在部分情況下甚至能達到數秒級別; HTTP DNS 相對會好一些,但正常來看,也會有200ms 左右的耗時。 這個時間能否再優化一些呢?

我們 SDK 在本地構建了自己的記錄緩存池,每次通過 LocalDNS 或是 HTTP DNS 解析得到記錄都存在緩衝池中。

當然,這個是普遍的做法,系統底層的 netdb 庫也是這樣實現。

區別在於我們做了一個小改動:對於過期的記錄我們採用懶更新的策略,當查到過期的緩存記錄時,先返回過期記錄給用戶,同時再異步重新發起 DNS 查詢更新緩存記錄。

這個小改動能夠保證我們二次解析時都能命中本地緩存,極大地降低 DNS 解析耗時,不過它也帶來了一定的風險性。

因此實踐中,我們也會添加異步定期的 DNS 記錄緩存池掃描功能,及時發現緩存中的過期記錄並進行更新,也降低 App 命中過期記錄的情況。
無侵入的 SDK 接入方式探索

在 DNS 優化的實踐中,我們遇到較大的問題,倒不是策略層面設計問題,而是我們的 DNS SDK 運用到實際 App 產品業務上的姿勢問題。

業內對 HTTP DNS 在實際業務中的接入方式多采用 IP 直連的形式,

即原本直接請求 http://www.meitu.com,現在我們先調用 SDK 進行域名解析,拿到 IP 地址比如 1.1.1.1,然後替換域名爲: http://1.1.1.1/

這樣操作之後, 由於 URL 中 HOST 已經是 IP 地址,網絡請求庫將跳過域名解析環節,直接向 1.1.1.1 服務器發起 HTTP 請求。

在實際操作中,對於 IP 直連的方案我們踩了不少的坑。

首先,對於 HTTP 請求,採用 IP 直連的方案後,我們還是需要進行的一個操作是手動配置 Header 中的 HOST :

  URL htmlUrl = new  URL("http://1.1.1.1/");
  HttpURLConnection connection =  (HttpURLConnection) htmlUrl.openConnection();
  connection.setRequestProperty("Host","www.meitu.com");

HTTP 協議相對比較容易,只需要處理 HOST,那麼 HTTPS 呢?

發起HTTPS請求首先需要進行 SSL/TLS 握手,其流程如下:

1、客戶端發送 Client Hello,攜帶隨機數、支持的加密算法等信息;

2、服務端收到請求後,選擇合適的加密算法,連同公鑰證書、隨機數等信息返回給客戶端;

3、客戶端檢驗服務端證書的合法性,計算產生隨機數並用證書公鑰加密發送給服務端;

4、服務端通過私鑰獲取隨機數信息,基於之前的交互信息計算得到協商密鑰並通知給客戶端;

5、客戶端驗證服務端發送的數據和密鑰,通過後雙方握手完成,開始進行加密通信;

在我們採用 IP 直連的形式後,上述 HTTPS 的第三步會發生問題, 客戶端檢驗服務端下發的證書這動作包含兩個步驟:

客戶端用本地保存的根證書解開證書鏈,確認服務端的證書是由可信任的機構頒發的。

客戶端需要檢查證書的 Domain 域和擴展域是否包含本次請求的 HOST。

證書的驗證需要這兩個步驟都檢驗通過才能夠進行後續流程,否則 SSL/TLS 握手將在這裏失敗結束。

由於在 IP 直連下,我們給網絡請求庫的 URL 中 host 部分已經被替換成了 IP 地址,因此證書驗證的第二步中,默認配置下 “本次請求的 HOST” 會是一個 IP 地址,這將導致 domain 檢查不匹配,最終 SSL/TLS 握手失敗。
那麼該如何解決這個問題?

解決 SSL/TLS 握手中域名校驗問題的方法在於我們重新配置 HostnameVerifier, 讓請求庫用實際的域名去做域名校驗,

代碼示例如下:

  final URL htmlUrl = new  URL("https://1.1.1.1/");
  HttpsURLConnection connection =  (HttpsURLConnection) htmlUrl.openConnection();
  connection.setRequestProperty("Host","www.meipai.com");
  connection.setHostnameVerifier(new  HostnameVerifier() {
  @Override
   public boolean  verify(String hostname, SSLSession session) {
  return  HttpsURLConnection.getDefaultHostnameVerifier()
  .verify("www.meipai.com",session);

}
});

我們又解決了一個問題,那麼 IP 直連下, HTTPS 的問題都搞定了嗎?

沒有,HTTPS 還有 SNI 的場景要特殊處理。

SNI(Server Name Indication)是爲了解決一個服務器使用多個域名和證書的SSL/TLS擴展。它的基本工作原理如下:

服務端配置有多個域名和對應的證書。客戶端在與服務器建立SSL鏈接之時,先發送自己要訪問站點的域名。服務器根據這個域名返回一個合適的證書。

跟上面 Domain 校驗的情況類似,這裏的網絡請求庫默認發送給服務端的 "要訪問站點的域名" 就是我們替換後的 IP 地址。

服務端在收到這樣一個 IP 地址形式的域名後將是一臉懵逼,找不到對應的證書,最後只好下發一個默認的域名證書回來。

接下來發生的是,客戶端在檢驗證書的 Domain 域時,怎麼也檢查不通過,因爲服務端下發的證書本來就不是對應該域名的。

最後 SSL/TLS 握手失敗告終。

上述這個 SNI 場景下的問題,我們是否有辦法解決呢?

可以解決,需用客戶端重新定製 SSLSocketFactory , 不過修改的代碼相對較多,這裏就不列舉了。

如果我們 SDK 要接入到 App 實際業務中,到 HTTPS SNI 場景處理這裏,相信很多同學都崩潰了,接入的工作量其實也不低。

很多情況下可能就做了妥協,只有 Okhttp 場景才使用這個 SDK,因爲 Okhttp 本身支持 DNS 替換,沒有上面那些問題。

在美圖的實踐中,我們不僅僅希望 Okhttp 的請求才進行這個 DNS 優化,我們希望在 App H5 頁面加載、播放器播放等場景也能應用相應的優化。

在這樣的需求下,IP 直連的接入方案帶來的接入工作量其實不低,甚至需要改動到部分輪子。

在最初的實踐中,我們也的確嘗試了落實 IP 直連 到各個模塊,然而即使克服了改造的工作量問題,實際運行上還是會有不少坑。

那麼,有沒有更合適的一種技術方案,能夠降低 我們 DNS SDK 的接入工作量,也能兼顧各種使用場景,比如 HTTPS、RTMP 協議等?

基於這樣的目標,我們在實踐中嘗試探索了一種對業務集成友好的無侵入式 DNS SDK 集成方案。下面我們以 Android 平臺進行說明。

我們知道在 Java 層面上進行 DNS 解析的基本方式是調用如下方法:

InetAddress.getAllByName("www.meipai.com");

Android 平臺上常用的 Okhttp、HttpUrlConnection 等網絡請求庫都會依賴這個形式的 DNS 解析。

我們深入分析 InetAddress 的運行流程,其大致如下:

在上述流程中我們可以知道,InetAddress 會有到 AddressCache 嘗試獲取已緩存記錄的動作,而這裏 AddessCache 是一個 static 的 map 結構變量。

因此,在這裏我們來對它做點小手腳 :模仿系統的 AddressCache 構造一個我們自己的 AddressCahce 結構,不過它的 get 方法被替換爲從我們 SDK 獲取解析記錄。

通過反射的形式用我們修改後的 AddressCache 替換掉系統的 AddressCache 變量。

這個偷天換日的操作之後,HttpsUrlConnection 等 Java 層網絡請求在進行 DNS 解析時就會是這樣一個流程:

通過這個形式,我們能夠完美解決 Java 層的 DNS SDK 接入問題,對於業務方來說,他們並不需要做任何 URL 替換操作,對應的 HTTPS 場景下的問題也不復存在。

Java 層的接入解決了, 那麼 Native 層呢?

我們知道在 Android 平臺上,像 WebView、播放器等模塊他們進行網絡連接的操作都是在 native 層進行的,並不會調用到 Java 層的 InetAddress 方法。

首先在 C/C++ 層,我們知道進行 DNS 解析會使用 getaddrinfo 或是 gethostbyname2 這兩個函數。

另外我們還知道,在 Android 等 Linux 系統下,對於 .so 這類可共享對象文件會是 ELF 的文件格式。

因此從這些已知信息,我們可以得到下列一些情況: 我們的 App 中 a.so 中直接使用到了系統 libc.so 中的 getaddrinfo 函數,那麼根據 ELF 文件規範,在 a.so 的 .rel.plt 表中會有如下關係定義: getaddrinfo ==> 0xFFFFFF 。.rel.plt 表中的映射關係爲 a.so 的運行指出了 getaddrinfo 這個外部符號在當前內存空間中的地址。

正常情況下,a.so 中執行到 getaddrinfo 的函數流程是這樣的:
http://attachbak.dataguru.cn/attachments/portal/201711/30/155225tcd9zzpndq19pz11.png

那麼在這裏,我們是否可以手動修改這個映射表內容,把 getaddrinfo 的內存地址替換成我們的 my_getaddrinfo 地址呢?

這樣,a.so 在實際運行時會被拐到我們的 my_getaddrinfo 中?

實際上,確實是可行的。我們嘗試在 SDK 啓動後,對 a.so 的 .rel.plt 表進行修改,達到接管 a.so DNS 的目的,修改後的 a.so 運行流程如下:
http://attachbak.dataguru.cn/attachments/portal/201711/30/155226ostke46esz6skej6.jpeg

通過上面的方式,我們能夠比較完美地接管 App 在 Java 層 和 Native 層 DNS 過程,實現業務方無任何額外改動的情況下運用我們的 DNS SDK 優化效果。

SDK 上線後的效果表現

在實際運用中,我們取得了比較好的效果。得益於 DNS SDK 在命中本地緩存率上的策略優化,我們的移動端產品在網絡請求中 DNS 解析環節耗時得到降低,

從實際監控數據來看,完整網絡請求的耗時也能夠降低 100ms 左右。
http://attachbak.dataguru.cn/attachments/portal/201711/30/155226h7jp1i1p713w7x4x.png

通過 HTTP DNS 的引入和 LocalDNS 優化升級策略,我們的網絡請求成功率有提升,在未知主機等具體錯誤率表現出下降的趨勢。

由於 SDK 層面本身做好了靈活的策略配置,我們通過線上監控和配置也讓各產品在效益和成本之間取得一個較佳的平衡點。

沃通CA推薦網站服務器部署超安EV SSL證書,在瀏覽器上顯示綠色地址欄及單位名稱,讓網站的顯示內容具有唯一性,更難以被複制仿冒,有效防止各類網絡釣魚***。此外,弱口令密碼極易被***以釣魚***方式獲取,建議網站爲每項服務啓用雙因素身份驗證,提高在線賬戶的安全性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章