美圖App的移動端DNS優化實踐:HTTPS請求耗時減少近半

本文引用了顏向羣發表於高可用架構公衆號上的文章《聊聊HTTPS環境DNS優化:美圖App請求耗時節約近半案例》的部份內容,感謝原做者。php

一、引言

移動互聯網時代,APP 廠商之間的競爭很是激烈,而良好的用戶體驗是必須優先考慮的,美圖產品以高顏值著稱,對產品的用戶體驗很是重視。從技術的角度來看,客戶端的體驗優化當中 DNS 優化是很是關鍵的一環,怎麼下降 DNS 的耗時、怎麼減小域名劫持等問題,都是你們須要重點解決的研發問題。html

本文介紹美圖APP在移動端DNS優化的實踐(主要針對HTTPS協議),文章內容從DNS問題、原理到最終優化效果,講解的較全面,值得學習和借鑑。程序員

另外:如您想詳細瞭解移動端DNS的各類雜症及主流解決方案,推薦詳讀《全面瞭解移動端DNS域名劫持等雜症:原理、根源、HttpDNS解決方案等》。算法

(原文連接:http://www.52im.net/thread-2172-1-1.html編程

二、相關文章

TCP/IP詳解 卷1:協議 - 第14章 DNS:域名系統緩存

網絡編程懶人入門(七):深刻淺出,全面理解HTTP協議安全

現代移動端網絡短鏈接的優化手段總結:請求速度、弱網適應、安全保障服務器

移動端IM開發者必讀(一):通俗易懂,理解移動網絡的「弱」和「慢」網絡

移動端IM開發者必讀(二):史上最全移動弱網絡優化方法總結session

全面瞭解移動端DNS域名劫持等雜症:原理、根源、HttpDNS解決方案等

三、內容概述

 

DNS 服務做用於網絡鏈接以前,將域名解析爲 IP 地址供後續流程進行鏈接(原理詳見:《TCP/IP詳解 卷1:協議 - 第14章 DNS:域名系統》)。

DNS 查詢時,會先在本地緩存中嘗試查找,若是不存在或是記錄過時,就繼續向 DNS 服務器發起遞歸查詢,這裏的 DNS 服務器通常就是運營商的 DNS 服務器。

在這過程當中,會產生一些不可控的問題。

美圖的移動端產品在實際用戶環境下會面臨 DNS 劫持、耗時波動等問題(詳見:《全面瞭解移動端DNS域名劫持等雜症:原理、根源、HttpDNS解決方案等》),這些 DNS 環節的不穩定因素,致使後續網絡請求被劫持或是直接失敗, 對產品的用戶體驗產生很差的影響。 

爲此,咱們對移動端產品的 DNS 解析進行了優化探索,產生了相應的 SDK。在這過程當中,咱們參考借鑑了業內的主流方案,也進行了一些實踐上的思考。

下面的內容會主要以 Android 平臺來進行說明。

四、LocalDNS VS  HTTP DNS

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

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

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

什麼是LocalDNS?通常來講,LocalDNS就是指本地ISP運營商的DNS:

 

▲ 圖中「局部DNS服務器」便是LocalDNS

爲了解決 LocalDNS 的這些問題,業內也催生了 HTTP DNS 的概念(注:如您對LocalDNS、HTTP DNS這些概念還不瞭解,請務必先閱讀《全面瞭解移動端DNS域名劫持等雜症:原理、根源、HttpDNS解決方案等》)。

HTTP DNS的基本原理以下:

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

好比 DNSPod 的實現原理以下:

 

相比 LocalDNS,HTTP DNS 會具有以下優點: 

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

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

3)擴展性強:自己基於 HTTP 協議,能夠實現更強大的功能擴展。

那麼,是否直接所有走 HTTP DNS 呢?

五、美圖APP的DNS 優化策略探索

HTTP DNS 相比 LocalDNS 存在一些優點, 然而 HTTP DNS 自己也是存在必定的成本問題。

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

首先,在策略上咱們並未徹底放棄 LocalDNS。

一個 App 涉及的域名衆多,在策略上咱們可以配置其核心 API 域名走 HTTP DNS,而對於非核心請求咱們仍但願它先嚐試走 LocalDNS, 在異常狀況下才升級走 HTTP DNS。

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

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

1)IP 記錄的 TTL 時間:在 DNS 劫持發生的狀況下,返回的 TTL 可能會有很是大的值;

2)解析耗時:若是一個 DNS 服務器解析耗時不理想,那麼它也不是咱們但願的;

3)返回的 IP 的可鏈接性:對返回的 IP 進行質量測試,若是鏈接情況不佳,那麼這個 DNS 服務器有劫持的可疑。

在 Android 平臺上,經過系統方法得到的解析結果信息是很是有限的,上面的指標有的將沒法獲取,所以在實踐中咱們會本身去構造 DNS 查詢報文,向運營商的多個 DNS 服務器發起查詢。

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

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

1)LocalDNS 在過時的狀況下,會發起遞歸查詢,這個時間是不可控的,在部分狀況下甚至能達到數秒級別;

2)HTTP DNS 相對會好一些,但正常來看,也會有200ms 左右的耗時。

這個時間可否再優化一些呢? 

咱們 SDK 在本地構建了本身的記錄緩存池,每次經過 LocalDNS 或是 HTTP DNS 解析獲得記錄都存在緩衝池中。

固然,這個是廣泛的作法,系統底層的 netdb 庫也是這樣實現。

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

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

所以實踐中:咱們也會添加異步按期的 DNS 記錄緩存池掃描功能,及時發現緩存中的過時記錄並進行更新,也下降 App 命中過時記錄的狀況。

五、美圖APP無侵入的 SDK 接入方式探索

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

5.1 IP直連方案及各類坑

業內對 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 的第三步會發生問題,。

客戶端檢驗服務端下發的證書這動做包含兩個步驟: 

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

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

證書的驗證須要這兩個步驟都檢驗經過纔可以進行後續流程,不然 SSL/TLS 握手將在這裏失敗結束。

因爲在 IP 直連下,咱們給網絡請求庫的 URL 中 host 部分已經被替換成了 IP 地址,

所以證書驗證的第二步中,默認配置下 「本次請求的 HOST」 會是一個 IP 地址,這將致使 domain 檢查不匹配,最終 SSL/TLS 握手失敗。

那麼該如何解決這個問題? 

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

代碼示例以下: 

finalURL htmlUrl = newURL("https://1.1.1.1/");

HttpsURLConnection connection = (HttpsURLConnection) htmlUrl.openConnection();

connection.setRequestProperty("Host","www.meipai.com");

connection.setHostnameVerifier(newHostnameVerifier() {

      @Override

      publicbooleanverify(String hostname, SSLSession session) {

          returnHttpsURLConnection.getDefaultHostnameVerifier()

                    .verify("www.meipai.com",session);

      }

});

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

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

SNI(Server Name Indication)是爲了解決一個服務器使用多個域名和證書的SSL/TLS擴展。

它的基本工做原理以下:

1)服務端配置有多個域名和對應的證書。客戶端在與服務器創建SSL連接之時,先發送本身要訪問站點的域名;

2)服務器根據這個域名返回一個合適的證書。

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

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

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

最後 SSL/TLS 握手失敗了結。

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

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

若是咱們 SDK 要接入到 App 實際業務中,到 HTTPS SNI 場景處理這裏,相信不少同窗都崩潰了,接入的工做量其實也不低。

不少狀況下可能就作了妥協,只有 Okhttp 場景才使用這個 SDK,由於 Okhttp 自己支持 DNS 替換,沒有上面那些問題。

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

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

在最初的實踐中,咱們也的確嘗試了落實 IP 直連 到各個模塊,然而即便克服了改造的工做量問題,實際運行上仍是會有很多坑。

5.2 美圖最終使用的無侵入式 DNS SDK 集成方案

那麼,有沒有更合適的一種技術方案,可以下降 咱們 DNS SDK 的接入工做量,也能兼顧各類使用場景,好比 HTTPS、RTMP 協議等?

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

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

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

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

咱們深刻分析 InetAddress 的運行流程,其大體以下: 

 

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

所以,在這裏咱們來對它作點小手腳 :

1)模仿系統的 AddressCache 構造一個咱們本身的 AddressCahce 結構,不過它的 get 方法被替換爲從咱們 SDK 獲取解析記錄;

2)經過反射的形式用咱們修改後的 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 的函數流程是這樣的:

 

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

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

實際上,確實是可行的。 咱們嘗試在 SDK 啓動後,對 a.so 的 .rel.plt 表進行修改,達到接管 a.so DNS 的目的。

修改後的 a.so 運行流程以下:

 

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

六、SDK 上線後的效果表現

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

從實際監控數據來看,完整網絡請求的耗時也可以下降 100ms 左右:

 

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

因爲 SDK 層面自己作好了靈活的策略配置,咱們經過線上監控和配置也讓各產品在效益和成本之間取得一個最佳的平衡點。

附錄:更多網絡通訊方面的精華文章

TCP/IP詳解 - 第11章·UDP:用戶數據報協議

TCP/IP詳解 - 第17章·TCP:傳輸控制協議

TCP/IP詳解 - 第18章·TCP鏈接的創建與終止

TCP/IP詳解 - 第21章·TCP的超時與重傳

技術往事:改變世界的TCP/IP協議(珍貴多圖、手機慎點)

通俗易懂-深刻理解TCP協議(上):理論基礎

通俗易懂-深刻理解TCP協議(下):RTT、滑動窗口、擁塞處理

理論經典:TCP協議的3次握手與4次揮手過程詳解

理論聯繫實際:Wireshark抓包分析TCP 3次握手、4次揮手過程

計算機網絡通信協議關係圖(中文珍藏版)

UDP中一個包的大小最大能多大?

P2P技術詳解(一):NAT詳解——詳細原理、P2P簡介

P2P技術詳解(二):P2P中的NAT穿越(打洞)方案詳解

P2P技術詳解(三):P2P技術之STUN、TURN、ICE詳解

通俗易懂:快速理解P2P技術中的NAT穿透原理

高性能網絡編程(一):單臺服務器併發TCP鏈接數到底能夠有多少

高性能網絡編程(二):上一個10年,著名的C10K併發鏈接問題

高性能網絡編程(三):下一個10年,是時候考慮C10M併發問題了

高性能網絡編程(四):從C10K到C10M高性能網絡應用的理論探索

高性能網絡編程(五):一文讀懂高性能網絡編程中的I/O模型

高性能網絡編程(六):一文讀懂高性能網絡編程中的線程模型

鮮爲人知的網絡編程(一):淺析TCP協議中的疑難雜症(上篇)

鮮爲人知的網絡編程(二):淺析TCP協議中的疑難雜症(下篇)

鮮爲人知的網絡編程(三):關閉TCP鏈接時爲何會TIME_WAIT、CLOSE_WAIT

鮮爲人知的網絡編程(四):深刻研究分析TCP的異常關閉

鮮爲人知的網絡編程(五):UDP的鏈接性和負載均衡

鮮爲人知的網絡編程(六):深刻地理解UDP協議並用好它

鮮爲人知的網絡編程(七):如何讓不可靠的UDP變的可靠?

網絡編程懶人入門(一):快速理解網絡通訊協議(上篇)

網絡編程懶人入門(二):快速理解網絡通訊協議(下篇)

網絡編程懶人入門(三):快速理解TCP協議一篇就夠

網絡編程懶人入門(四):快速理解TCP和UDP的差別

網絡編程懶人入門(五):快速理解爲何說UDP有時比TCP更有優點

網絡編程懶人入門(六):史上最通俗的集線器、交換機、路由器功能原理入門

網絡編程懶人入門(七):深刻淺出,全面理解HTTP協議

網絡編程懶人入門(八):手把手教你寫基於TCP的Socket長鏈接

網絡編程懶人入門(九):通俗講解,有了IP地址,爲什麼還要用MAC地址?

技術掃盲:新一代基於UDP的低延時網絡傳輸層協議——QUIC詳解

讓互聯網更快:新一代QUIC協議在騰訊的技術實踐分享

現代移動端網絡短鏈接的優化手段總結:請求速度、弱網適應、安全保障

聊聊iOS中網絡編程長鏈接的那些事

移動端IM開發者必讀(一):通俗易懂,理解移動網絡的「弱」和「慢」

移動端IM開發者必讀(二):史上最全移動弱網絡優化方法總結

IPv6技術詳解:基本概念、應用現狀、技術實踐(上篇)

IPv6技術詳解:基本概念、應用現狀、技術實踐(下篇)

從HTTP/0.9到HTTP/2:一文讀懂HTTP協議的歷史演變和設計思路

腦殘式網絡編程入門(一):跟着動畫來學TCP三次握手和四次揮手

腦殘式網絡編程入門(二):咱們在讀寫Socket時,究竟在讀寫什麼?

腦殘式網絡編程入門(三):HTTP協議必知必會的一些知識

腦殘式網絡編程入門(四):快速理解HTTP/2的服務器推送(Server Push)

腦殘式網絡編程入門(五):天天都在用的Ping命令,它究竟是什麼?

腦殘式網絡編程入門(六):什麼是公網IP和內網IP?NAT轉換又是什麼鬼?

以網遊服務端的網絡接入層設計爲例,理解實時通訊的技術挑戰

邁向高階:優秀Android程序員必知必會的網絡基礎

全面瞭解移動端DNS域名劫持等雜症:技術原理、問題根源、解決方案等

美圖App的移動端DNS優化實踐:HTTPS請求耗時減少近半

>> 更多同類文章 ……

(原文連接:http://www.52im.net/thread-2172-1-1.html

相關文章
相關標籤/搜索