1. 程式人生 > >美圖App的移動端DNS優化實踐:HTTPS請求耗時減小近半

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

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

1、引言

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

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

另外:

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

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

2、相關文章

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

網路程式設計懶人入門(七):深入淺出,全面理解HTTP協議

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

移動端IM開發者必讀(一):通俗易懂,理解行動網路的“弱”和“慢”

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

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

3、內容概述

DNS 服務作用於網路連線之前,將域名解析為 IP 地址供後續流程進行連線(原理詳見:《TCP/IP詳解 卷1:協議 - 第14章 DNS:域名系統》)。

DNS 查詢時,會先在本地快取中嘗試查詢,如果不存在或是記錄過期,就繼續向 DNS 伺服器發起遞迴查詢,這裡的 DNS 伺服器一般就是運營商的 DNS 伺服器。

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

美圖的移動端產品在實際使用者環境下會面臨 DNS 劫持、耗時波動等問題(詳見:《全面瞭解移動端DNS域名劫持等雜症:原理、根源、HttpDNS解決方案等

》),這些 DNS 環節的不穩定因素,導致後續網路請求被劫持或是直接失敗, 對產品的使用者體驗產生不好的影響。 

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

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

4、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 呢?

5、美圖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 命中過期記錄的情況。

5、美圖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 優化效果。

6、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