作者:vivo 網際網路運維團隊- Hu Tao
本文介紹了vivo短視訊使用者存取體驗優化的實踐思路,並簡單講解了實踐背後的幾點原理。
我們平時在看抖音快手視訊的時候,如果滑動到某個視訊畫面一直幾s不動的時候,大概率就會划走了,所以在短視訊專案中,畫面卡頓是非常影響使用者體驗的,啟播速度越快,就越能留住使用者。
啟播速度簡單來說就是從呼叫開始播放到首幀上屏的時間,大致可分為兩部分:
視訊檔下載耗時
視訊解碼耗時
本文主要從運維排查問題的角度,從網路這部分的各個環節入手,結合vivo短視訊的具體案例,給大家分享下優化過程。
我們先梳理下一次完整的網路請求過程,以使用者端視角為例,如下圖所示:
在接入CDN的情況下,可分為幾個階段:
DNS域名解析:獲取伺服器的IP地址。
TCP連線建立:與伺服器IP建立連線即tcp三次握手。
TLS握手:使用者端向伺服器索要並驗證伺服器的公鑰,雙方協商生產「對談祕鑰」並進行加密通訊。
CDN響應:將內容資源分發到位於多個地理位置機房中的伺服器上並返回給使用者端。
針對以上階段,分別講下vivo短視訊是如何進行優化的。
我們在上網的時候,通常使用的方式域名,而不是 IP 地址,因為域名方便人類記憶。那麼實現這一技術的就是 DNS 域名解析,DNS 可以將域名網址自動轉換為具體的 IP 地址。
DNS 中的域名都是用句點來分隔的,比如 www.server.com,這裡的句點代表了不同層次之間的界限。在域名中,越靠右的位置表示其層級越高。根域是在最頂層,它的下一層就是 com 頂級域,再下面是 server.com。
所以域名的層級關係類似一個樹狀結構:
根DNS伺服器
頂級域 DNS 伺服器(com)
權威 DNS 伺服器(server.com)
根域的 DNS 伺服器資訊儲存在網際網路中所有的 DNS 伺服器中。這樣一來,任何 DNS 伺服器就都可以找到並存取根域 DNS 伺服器了。
因此,使用者端只要能夠找到任意一臺 DNS 伺服器,就可以通過它找到根域 DNS 伺服器,然後再一路順藤摸瓜找到位於下層的某臺目標 DNS 伺服器。
瀏覽器首先看一下自己的快取裡有沒有,如果沒有就向作業系統的快取要,還沒有就檢查本機域名解析檔案 hosts,如果還是沒有,就會 DNS 伺服器進行查詢,查詢的過程如下:
使用者端首先會發出一個 DNS 請求,問 www.server.com 的 IP 是啥,並行給本地 DNS 伺服器(也就是使用者端的 TCP/IP 設定中填寫的 DNS 伺服器地址)。
本地域名伺服器收到使用者端的請求後,如果快取裡的表格能找到 www.server.com,則它直接返回 IP 地址。如果沒有,本地 DNS 會去問它的根域名伺服器:「老大, 能告訴我 www.server.com 的 IP 地址嗎?」 根域名伺服器是最高層次的,它不直接用於域名解析,但能指明一條道路。
根 DNS 收到來自本地 DNS 的請求後,發現後置是 .com,說:「www.server.com 這個域名歸 .com 區域管理」,我給你 .com 頂級域名伺服器地址給你,你去問問它吧。」
本地 DNS 收到頂級域名伺服器的地址後,發起請求問「老二, 你能告訴我 www.server.com 的 IP 地址嗎?」
頂級域名伺服器說:「我給你負責 www.server.com 區域的權威 DNS 伺服器的地址,你去問它應該能問到」。
本地 DNS 於是轉向問權威 DNS 伺服器:「老三,www.server.com對應的IP是啥呀?」 server.com 的權威 DNS 伺服器,它是域名解析結果的原出處。為啥叫權威呢?就是我的域名我做主。
權威 DNS 伺服器查詢後將對應的 IP 地址 X.X.X.X 告訴本地 DNS。
本地 DNS 再將 IP 地址返回使用者端,使用者端和目標建立連線,同時本地 DNS 快取該 IP 地址,這樣下一次的解析同一個域名就不需要做 DNS 的迭代查詢了。
至此,我們完成了 DNS 的解析過程。現在總結一下,整個過程畫成了一個圖。
DNS 域名解析的過程蠻有意思的,整個過程就和我們日常生活中找人問路的過程類似,只指路不帶路。
弄清了域名解析的工作流程,將vivo域名和頭部廠商域名進行對比分析,發現vivo短視訊域名解析耗時不穩定,波動範圍很大,懷疑是某些地區使用者存取量少,本地DNS伺服器快取命中率低導致的,因此我們的優化思路就是 提高本地DNS快取命中率。
如上圖所示,提高DNS快取命中率一個簡單的辦法就是新增全國範圍內的撥測任務,來進行DNS加熱。
通過對調整前後的DNS解析時間進行比較,可以看到耗時降低了30ms左右。
這裡簡單對比下 HTTP/1、HTTP/2、HTTP/3 的效能。
HTTP 協定是基於 TCP/IP,並且使用了「請求 - 應答」的通訊模式,所以效能的關鍵就在這兩點裡。
1. 長連線
早期 HTTP/1.0 效能上的一個很大的問題,那就是每發起一個請求,都要新建一次 TCP 連線(三次握手),而且是序列請求,做了無謂的 TCP 連線建立和斷開,增加了通訊開銷。
為了解決上述 TCP 連線問題,HTTP/1.1 提出了長連線的通訊方式,也叫持久連線。這種方式的好處在於減少了 TCP 連線的重複建立和斷開所造成的額外開銷,減輕了伺服器端的負載。
持久連線的特點是,只要任意一端沒有明確提出斷開連線,則保持 TCP 連線狀態。
2. 管道網路傳輸
HTTP/1.1 採用了長連線的方式,這使得管道(pipeline)網路傳輸成為了可能。
即可在同一個 TCP 連線裡面,使用者端可以發起多個請求,只要第一個請求發出去了,不必等其回來,就可以發第二個請求出去,可以減少整體的響應時間。
舉例來說,使用者端需要請求兩個資源。以前的做法是,在同一個TCP連線裡面,先傳送 A 請求,然後等待伺服器做出迴應,收到後再發出 B 請求。管道機制則是允許瀏覽器同時發出 A 請求和 B 請求。
但是伺服器還是按照順序,先回應 A 請求,完成後再回應 B 請求。要是 前面的迴應特別慢,後面就會有許多請求排隊等著。這稱為「隊頭堵塞」。
3. 隊頭阻塞
「請求 - 應答」的模式加劇了 HTTP 的效能問題。
因為當順序傳送的請求序列中的一個請求因為某種原因被阻塞時,在後面排隊的所有請求也一同被阻塞了,會招致使用者端一直請求不到資料,這也就是「隊頭阻塞」。好比上班的路上塞車。
使用 TCP 長連線的方式改善了 HTTP/1.0 短連線造成的效能開銷。
支援 管道(pipeline)網路傳輸,只要第一個請求發出去了,不必等其回來,就可以發第二個請求出去,可以減少整體的響應時間。
但 HTTP/1.1 還是有效能瓶頸:
請求 / 響應頭部(Header)未經壓縮就傳送,首部資訊越多延遲越大。只能壓縮 Body 的部分;
傳送冗長的首部。每次互相傳送相同的首部造成的浪費較多;
伺服器是按請求的順序響應的,如果伺服器響應慢,會招致使用者端一直請求不到資料,也就是隊頭阻塞;
沒有請求優先順序控制;
請求只能從使用者端開始,伺服器只能被動響應。
針對上面的 HTTP/1.1 的效能瓶頸,HTTP/2 做了一些優化。而且因為 HTTP/2 協定是基於 HTTPS 的,所以 HTTP/2 的安全性也是有保障的。
1. 頭部壓縮
HTTP/2 會壓縮頭(Header)如果你同時發出多個請求,他們的頭是一樣的或是相似的,那麼,協定會幫你消除重複的部分。
這就是所謂的 HPACK 演演算法:在使用者端和伺服器同時維護一張頭資訊表,所有欄位都會存入這個表,生成一個索引號,以後就不傳送同樣欄位了,只傳送索引號,這樣就提高速度了。
2. 二進位制格式
HTTP/2 不再像 HTTP/1.1 裡的純文字形式的報文,而是全面採用了二進位制格式。
頭資訊和資料體都是二進位制,並且統稱為幀(frame):頭資訊幀和資料框。
這樣雖然對人不友好,但是對計算機非常友好,因為計算機只懂二進位制,那麼收到報文後,無需再將明文的報文轉成二進位制,而是直接解析二進位制報文,這增加了資料傳輸的效率。
3. 資料流
HTTP/2 的封包不是按順序傳送的,同一個連線裡面連續的封包,可能屬於不同的迴應。因此,必須要對封包做標記,指出它屬於哪個迴應。
每個請求或迴應的所有封包,稱為一個資料流(Stream)。
每個資料流都標記著一個獨一無二的編號,其中規定使用者端發出的資料流編號為奇數, 伺服器發出的資料流編號為偶數。
使用者端還可以指定資料流的優先順序。優先順序高的請求,伺服器就先響應該請求。
4. 多路複用
HTTP/2 是可以在一個連線中並行多個請求或迴應,而不用按照順序一一對應。
移除了 HTTP/1.1 中的序列請求,不需要排隊等待,也就不會再出現「隊頭阻塞」問題,降低了延遲,大幅度提高了連線的利用率。
舉例來說,在一個 TCP 連線裡,伺服器收到了使用者端 A 和 B 的兩個請求,如果發現 A 處理過程非常耗時,於是就迴應 A 請求已經處理好的部分,接著迴應 B 請求,完成後,再回應 A 請求剩下的部分。
5. 伺服器推播
HTTP/2 還在一定程度上改善了傳統的「請求 - 應答」工作模式,服務不再是被動地響應,也可以主動向用戶端傳送訊息。
舉例來說,在瀏覽器剛請求 HTML 的時候,就提前把可能會用到的 JS、CSS 檔案等靜態資源主動發給使用者端,減少延時的等待,也就是伺服器推播(Server Push,也叫 Cache Push)。
HTTP/2 通過頭部壓縮、二進位制編碼、多路複用、伺服器推播等新特性大幅度提升了 HTTP/1.1 的效能,而美中不足的是 HTTP/2 協定是基於 TCP 實現的,於是存在的缺陷有三個。
TCP 與 TLS 的握手時延遲;
隊頭阻塞;
網路遷移需要重新連線。
1. TCP 與 TLS 的握手時延遲
對於 HTTP/1 和 HTTP/2 協定,TCP 和 TLS 是分層的,分別屬於核心實現的傳輸層、openssl 庫實現的表示層,因此它們難以合併在一起,需要分批次來握手,先 TCP 握手,再 TLS 握手。
發起 HTTP 請求時,需要經過 TCP 三次握手和 TLS 四次握手(TLS 1.2)的過程,因此共需要 3 個 RTT 的時延才能發出請求資料。
另外, TCP 由於具有「擁塞控制」的特性,所以剛建立連線的 TCP 會有個「慢啟動」的過程,它會對 TCP 連線產生"減速"效果。
2. 隊頭阻塞
HTTP/2 實現了 Stream 並行,多個 Stream 只需複用 1 個 TCP 連線,節約了 TCP 和 TLS 握手時間,以及減少了 TCP 慢啟動階段對流量的影響。不同的 Stream ID 才可以並行,即使亂序傳送幀也沒問題,但是同一個 Stream 裡的幀必須嚴格有序。另外,可以根據資源的渲染順序來設定 Stream 的優先順序,從而提高使用者體驗。
HTTP/2 通過 Stream 的並行能力,解決了 HTTP/1 隊頭阻塞的問題,看似很完美了,但是 HTTP/2 還是存在「隊頭阻塞」的問題,只不過問題不是在 HTTP 這一層面,而是在 TCP 這一層。
HTTP/2 多個請求是跑在一個 TCP 連線中的,那麼當 TCP 丟包時,整個 TCP 都要等待重傳,那麼就會阻塞該 TCP 連線中的所有請求。
因為 TCP 是位元組流協定,TCP 層必須保證收到的位元組資料是完整且有序的,如果序列號較低的 TCP 段在網路傳輸中丟失了,即使序列號較高的 TCP 段已經被接收了,應用層也無法從核心中讀取到這部分資料,從 HTTP 視角看,就是請求被阻塞了。
舉個例子,如下圖:
圖中傳送方傳送了很多個 packet,每個 packet 都有自己的序號,可以認為是 TCP 的序列號,其中 packet 3 在網路中丟失了,即使 packet 4-6 被接收方收到後,由於核心中的 TCP 資料不是連續的,於是接收方的應用層就無法從核心中讀取到,只有等到 packet 3 重傳後,接收方的應用層才可以從核心中讀取到資料,這就是 HTTP/2 的隊頭阻塞問題,是在 TCP 層面發生的。
3. 網路遷移需要重新連線
一個 TCP 連線是由四元組(源 IP 地址,源埠,目標 IP 地址,目標埠)確定的,這意味著如果 IP 地址或者埠變動了,就會導致需要 TCP 與 TLS 重新握手,這不利於移動裝置切換網路的場景,比如 4G 網路環境切換成 WIFI。
這些問題都是 TCP 協定固有的問題,無論應用層的 HTTP/2 在怎麼設計都無法逃脫。
要解決這個問題,HTTP/3 就將傳輸層協定從 TCP 替換成了 UDP,並在 UDP 協定上開發了 QUIC 協定,來保證資料的可靠傳輸。
無隊頭阻塞,QUIC 連線上的多個 Stream 之間並沒有依賴,都是獨立的,也不會有底層協定限制,某個流發生丟包了,只會影響該流,其他流不受影響;
建立連線速度快,因為 QUIC 內部包含 TLS1.3,因此僅需 1 個 RTT 就可以「同時」完成建立連線與 TLS 金鑰協商,甚至在第二次連線的時候,應用封包可以和 QUIC 握手資訊(連線資訊 + TLS 資訊)一起傳送,達到 0-RTT 的效果。
連線遷移,QUIC 協定沒有用四元組的方式來「繫結」連線,而是通過連線 ID 來標記通訊的兩個端點,使用者端和伺服器可以各自選擇一組 ID 來標記自己,因此即使移動裝置的網路變化後,導致 IP 地址變化了,只要仍保有上下文資訊(比如連線 ID、TLS 金鑰等),就可以「無縫」地複用原連線,消除重連的成本。
另外 HTTP/3 的 QPACK 通過兩個特殊的單向流來同步雙方的動態表,解決了 HTTP/2 的 HPACK 隊頭阻塞問題。
不過,由於 QUIC 使用的是 UDP 傳輸協定,UDP 屬於「二等公民」,大部分路由器在網路繁忙的時候,會丟掉 UDP包,把「空間」讓給 TCP 包,所以 QUIC 的推廣之路應該沒那麼簡單。期待,HTTP/3 正式推出的那一天!
隨著vivo短視訊發展的不同時期,我們做了不同的優化:
1.使用HTTP/1.1:使用者端將首幀圖片域名和評論頭像域名進行合併,TCP連結複用率提高4%,平均圖片載入耗時下降40ms左右;
2.使用HTTP/2:使用者端在部分域名上灰度使用H2,卡頓率下降0.5%
3.使用QUIC:使用者端針對弱網場景,優先使用QUIC協定;同時針對短視訊業務特點,專項優化QUIC效能。
CDN 的全稱叫 Content Delivery Network,中文名叫「內容分發網路」,它是解決由於長距離而網路存取速度慢的問題。
簡單來說,CDN 將內容資源分發到位於多個地理位置機房中的伺服器上,這樣我們在存取內容資源的時候,不用存取源伺服器。而是直接存取離我們最近的 CDN 節點 ,這樣一來就省去了長途跋涉的時間成本,從而實現了網路加速。
CDN 加速的是內容資源是靜態資源。
所謂的「靜態資源」是指資料內容靜態不變,任何時候來存取都是一樣的,比如圖片、音訊。與之相反的「動態資源」,是指資料內容是動態變化的,每次存取都不一樣,比如使用者資訊等。不過,動態資源如果也想被快取加速,就要使用動態 CDN,其中一種方式就是將資料的邏輯計算放在 CDN 節點來做,這種方式就被稱為邊緣計算。
CDN 加速策略有兩種方式,分別是「推模式」和「拉模式」。
大部分 CDN 加速策略採用的是「拉模式」,當用戶就近存取的 CDN 節點沒有快取請求的資料時,CDN 會主動從源伺服器下載資料,並更新到這個 CDN 節點的快取中。可以看出,拉模式屬於被動快取的方式,與之相反的 「推模式」就屬於主動快取的方式。如果想要把資源在還沒有使用者存取前快取到 CDN 節點,則可以採用「推模式」,這種方式也叫 CDN 預熱。通過 CDN 服務提供的 API 介面,把需要預熱的資源地址和需要預熱的區域等資訊提交上去,CDN 收到後,就會觸發這些區域的 CDN 節點進行回源來實現資源預熱。
找到離使用者最近的 CDN 節點是由 CDN 的全域性負載均衡器(Global Sever Load Balance,GSLB)負責的。那 GSLB 是在什麼時候起作用的呢?在回答這個問題前,我們先來看看在沒有 CDN 的情況下,存取域名時發生的事情。在沒有 CDN 的情況下,當我們存取域名時,DNS 伺服器最終會返回源伺服器的地址。比如,當我們在瀏覽器輸入 www.server.com 域名後,在本地 host 檔案找不到域名時,使用者端就會存取本地 DNS 伺服器。
這時候:
如果本地 DNS 伺服器有快取該網站的地址,則直接返回網站的地址;
如果沒有就通過遞迴查詢的方式,先請求根 DNS,根 DNS 返回頂級 DNS(.com)的地址;再請求 .com 頂級 DNS 得到 server.com 的域名伺服器地址,再從 server.com 的域名伺服器中查詢到 www.server.com 對應的 IP 地址,然後返回這個 IP 地址,同時本地 DNS 快取該 IP 地址,這樣下一次的解析同一個域名就不需要做 DNS 的迭代查詢了。
但加入 CDN 後就不一樣了。
會在 server.com 這個 DNS 伺服器上,設定一個 CNAME 別名,指向另外一個域名 www.server.cdn.com,返回給本地 DNS 伺服器。接著繼續解析該域名,這個時候存取的就是 server.cdn.com 這臺 CDN 專用的 DNS 伺服器,在這個伺服器上,又會設定一個 CNAME,指向另外一個域名,這次指向的就是 CDN 的 GSLB。接著,本地 DNS 伺服器去請求 CDN 的 GSLB 的域名,GSLB 就會為使用者選擇一臺合適的 CDN 節點提供服務,選擇的依據主要有以下幾點:
看使用者的 IP 地址,查表得知地理位置,找相對最近的 CDN 節點;
看使用者所在的運營商網路,找相同網路的 CDN 節點;
看使用者請求 URL,判斷哪一臺伺服器上有使用者所請求的資源;
查詢 CDN 節點的負載情況,找負載較輕的節點。
GSLB 會基於以上的條件進行綜合分析後,找出一臺最合適的 CDN 節點,並返回該 CDN 節點的 IP 地址給本地 DNS 伺服器,然後本地 DNS 伺服器快取該 IP 地址,並將 IP 返回給使用者端,使用者端去存取這個 CDN 節點,下載資源。
通過我們分析後發現,部分接入CDN的域名在全國一些省份沒有就近存取, CDN邊緣節點跨地區覆蓋問題比較嚴重。於是我們找CDN廠商針對性的做了調整,調整後平均請求耗時降到300ms左右,首包耗時也降到100多。
隨著業務的發展,提升使用者體驗也會變得越來越重要,使用者存取體驗優化將是一個永無止境的過程,除了上面說的這些方法之外,還有一些優化也是我們嘗試過的,比如:
連線優化:視訊播放上下滑動會頻繁的斷開連線,無法複用連線。
預快取檔案:減少啟動耗時。
內容優化:減少檔案傳輸大小。
希望本文能為大家在日常工作中優化使用者存取體驗問題帶來幫助。