維護幾十種語言和站點,愛奇藝國際站 WEB 端網頁優化實踐

1. 前言

愛奇藝國際站(www.iq.com)提供了優質的視頻給海外各國用戶,自上線以來,現已支持幾十個國際站點,並且在東南亞多個國家保證了海量用戶高速觀看體驗。

國際站業務的特點是用戶在境外訪問,後端服務器也是部署在國外。這樣就面臨着比較複雜的客觀條件:每個國家的網絡及安全政策都不太一樣,各國用戶的網絡建設水平不一。國內互聯網公司出海案例不多,愛奇藝國際站的建設也都是在摸索中前進。

爲給海外用戶提供更好的使用體驗,愛奇藝後端團隊在這段時間做了不少性能優化的工作,我們也希望將這些探索經驗留存下來,與同行溝通交流。

在這篇文章中,我們將針對其中的亮點內容詳細解析,包括但不限於:

2. 技術調研

都說緩存和異步是高併發兩大殺器。而一般做技術性能優化,技術方案無外乎如下幾種:

並且性能優化是個系統性工程,涉及到後端、前端、系統網絡及各種基礎設施,每一塊都需要做各自的性能優化。比如前端就包含減少 Http 請求,使用瀏覽器緩存,啓用壓縮,CDN 加速等等,後端優化就更多了。本文會挑選愛奇藝國際站後端團隊做的優化工作及取得的階段性成果進行更詳細的介紹。

注:當分析系統性能問題時,可以通過以下指標來衡量:

3. 業務背景

在介紹優化過程之前,需要簡要介紹下愛奇藝國際站的特有業務特點,以及這些業務特點帶來的難點和挑戰。

3-1 模式語言

愛奇藝國際站業務有其特殊性,除中國大陸,世界上有二百多個國家,運營的時候,有些不同國家會統一運營,比如馬來西亞和新加坡;有的國家獨立運營,比如泰國。這種獨立於國家之上的業務概念,愛奇藝稱之爲模式(也可叫做站點)。業務運營時,會按照節目版權地區,分模式獨立運營。這並不同於國內,所有人看到的非個性化推薦內容都是一樣的。

還有個特殊性是多語言,不同國家語言不同,用戶的語言多變,愛奇藝需要維護幾十種語種的內容數據

並且在國際站,用戶屬性和模式強綁定,用戶模式和語言會寫在 cookie 裏,輕易不能改變。

3-2 服務端渲染

既然做國際站業務,那必不可少做 google SEO,搜索引擎的結果是愛奇藝很大的流量入口,而 SEO 也是一個龐大的工程,這裏不多描述,但是這個會給愛奇藝前端技術選型帶來要求,所以前端頁面內容是服務端渲染的。與傳統 SPA (單頁應用程序 (Single-Page Application)) 相比,服務器端渲染 (SSR) 的優勢主要在於:

4. 優化步驟

總體來說,CDN 和服務端頁面渲染這塊有其他團隊也在一直做技術改進,國際站後端團隊的核心工作點在前端緩存優化和後端服務優化上。主要包括以下內容:

  1. 瀏覽器緩存優化

  2. 壓縮優化

  3. 服務端緩存優化

4-1 網頁緩存服務

WEB 端一個頁面通常會渲染幾十個節目,如果每次都去請求後端 API,響應速度肯定會變慢很多,所以必須要添加緩存。但是緩存有利有弊,並且如何做好緩存其實並不是個容易的課題。

魯迅先生曾經說過,一切脫離業務的技術空談都是耍流氓。所以在結合業務做好緩存這件事上,道阻且長。

國際站 WEB 端首版本上線後,簡要架構如下:

愛奇藝國際站有 Google SEO 的要求,所以節目相關的數據都會在服務端渲染。可以看到客戶端瀏覽器直接和前端 SSR 服務器交互(中間有 CDN 服務商等),前端渲染 node 服務器會有短暫的本地緩存。

版本上線後,表現效果不理想。在業務背景的時候介紹過,提供給用戶是分站點(國家)、語言的節目內容,這些存放在 cookie 裏,不方便在 CDN 服務做強緩存。所以,做了一次架構優化,優化後如下:

可以看到,增加了一層網頁緩存服務,該服務爲後端 Java 服務,職責是把前端 node 渲染的頁面細粒度進行緩存,並使用 redis 集中式緩存。上線後,緩存命中率得到極大提高。

4-2 AB 方案

後端網頁緩存上線後,想繼續對服務進行優化。但是後端優化分步驟進行,如何最快查看準確的優化效果?一般比較會有兩種緯度:橫向和縱向。縱向即時間驗證結果,可以使用 Google Cloud Platform 爲應用開發者們(特別是全棧開發)推出的應用後臺服務。藉助 Firebase,應用開發者們可以快速搭建應用後臺,集中注意力在開發 client 上,並且有實時可觀測的數據庫,有時間緯度的網頁性能數據,根據優化操作的上線時間點,就可以看到時間緯度的性能變化。但是上面也提到,網頁性能影響因素過多,CDN 及前端團隊也都在做優化,時間緯度並不能準確看到優化成果。

那就是要使用橫向比較,怎麼做呢?

答案還是 firebase,在 firebase 上新增項目 B,網頁緩存服務會把優化的流量更新爲項目 B 投遞,這樣橫向比較項目 A 和 B 的性能,就能直接準確表現出優化效果。具體如下圖:

PlanB 爲灰度優化方案,判斷方案 B 的方式有很多種,但是需要確保該用戶兩次訪問時,都會命中同一個方案,以免無法命中緩存。愛奇藝國際站目前採用按照 IP 進行灰度,確保用戶在 IP 不變更情況下,灰度策略不調整時他的灰度方案是不變的。第二節有提到緩存 key 裏的 B 的作用,就是這裏的 PlanB。

詳細的比較方式和流程見下圖,後續的所有優化策略,都是通過這個流程來判斷是否有效:

  1. 瀏覽器請求到後端服務,服務器獲取端 IP

  2. 根據配置中心配置的灰度比例,計算當前請求是 plan A or plan B

  3. 如果是灰度方案 B,則走優化邏輯

  4. 並且 SSR 會根據灰度方案返回不同的 firebase 配置

  5. firebase 進行分開數據投遞,控制檯拿到兩種對比的性能數據

  6. 分析數據,比較後得到優化結果

可以看到,這樣的流程下來,實現了橫向對比,能較準確地拿到性能對比結果,便於持續優化。

4-3 瀏覽器緩存優化

增加了網頁緩存服務後,會緩存 5min 的前端渲染頁面,5min 後緩存自動失效。這個時候會觸發請求到 SSR 服務,返回並寫入緩存。

絕大多數情況下,頁面並沒有更新,而用戶可能在刷新頁面,這種數據不會發生變化,適合使用瀏覽器協商緩存:

協商緩存:瀏覽器與服務器合作之下的緩存策略協商緩存依賴於服務端與瀏覽器之間的通信。協商緩存機制下,瀏覽器需要向服務器去詢問緩存的相關信息,進而判斷是重新發起請求、下載完整的響應,還是從本地獲取緩存的資源。

如果服務端提示緩存資源未改動(Not Modified),資源會被重定向到瀏覽器緩存,這種情況下網絡請求對應的狀態碼是 304(not modified)。具體流程如下:

使用 Etag 的方式實現瀏覽器協商緩存,上線後,304 的請求佔比升至 4%,firebase 灰度方案 B 性能提高 **5%** 左右,網頁性能提高。

4-4 壓縮優化

Google 認爲互聯網用戶的時間是寶貴的,他們的時間不應該消耗在漫長的網頁加載中,因此在 2015 年 9 月 Google 推出了無損壓縮算法 Brotli。Brotli 通過變種的 LZ77 算法、Huffman 編碼以及二階文本建模等方式進行數據壓縮,與其他壓縮算法相比,它有着更高的壓塑壓縮效率。啓用 Brotli 壓縮算法,對比 Gzip 壓縮 CDN 流量再減少 20%。

根據 Google 發佈的研究報告,Brotli 壓縮算法具有多個特點,最典型的是以下 3 個:

並且從日誌中看到,愛奇藝的用戶瀏覽器大多支持 br 壓縮。之前,後臺服務是支持 gzip 壓縮的,具體如下:

可以看到,是 nginx 服務支持了 gzip 壓縮。

並且後端網頁服務的 redis 存儲的是壓縮後的內容,並且使用自定義序列化器,即讀取寫入不做處理,減少 cpu 消耗,redis 的 value 就是壓縮後的字節數組。

nginx 支持 brotli

原始 nginx 並不直接支持 brotli 壓縮,需要進行重新安裝編譯:

網頁緩存項目支持 br 壓縮

http 協議中,客戶端是否支持壓縮及支持何種壓縮,是根據頭 Accept-Encoding 來決定的,一般支持 br 的 Accept-Encoding 內容是 “gzip,br”。

nginx 服務支持 br 壓縮後,網頁緩存服務需要對兩種壓縮內容進行緩存。邏輯如下:

從上圖可以看到,當服務端需要支持 Br 壓縮和 gzip 壓縮,並且需要支持灰度方案時,他的業務複雜度變成指數增長。

上圖的業務都存在上文圖中的 “(後端)網頁緩存服務”。以及後面也會重點對這個服務進行優化。

該功能灰度一週後,firebase 上方案 B 和方案 A 的數據對比發現,br 壓縮會使頁面大小下降 30%,FCP 性能上升 6% 左右。


4-5 服務端緩存優化

經過瀏覽器緩存優化和內容壓縮優化後,整體網頁性能得到不少提升。把優化目標放到服務端緩存模塊,這也是此次分享的重點內容。

本地緩存 + redis 二級緩存

對於緩存模塊,首先增加了本地緩存。本地緩存使用了更加前沿優秀的本地緩存框架 caffeine,它使用了 W-TinyLFU 算法,是一個更高性能、高命中率的本地緩存框架。這樣就形成了如下架構:

可以看到就是很常見的二級緩存,本地和 redis 緩存失效時間都是 5 分鐘。本地緩存的空間大小和 key 數量有限,命中淘汰策略後的緩存 key,會請求 redis 獲取數據。

增加本地緩存後,請求 redis 的網絡 IO 變少,優化了後端性能

本地緩存 + redis 二級主動刷新緩存

上面方案運行一段時間後,數據發現,5min 的本地緩存和 redis 命中率並不高,結果如下:

看起來緩存命中率還有較大的優化空間。那緩存失效是因爲緩存時間太短,能否延長緩存失效時間呢?有兩種方案:

  1. 增加緩存失效時間

  2. 增加後臺主動刷新,主動延長緩存失效時間

方案 1 不可取,因爲業務上 5 分鐘失效已經是最大限度了。那方案 2 如何做呢?最開始嘗試針對所有緩存,創建延遲任務,主動刷新緩存。上線後發現下游壓力非常大,cpu 幾乎打滿。

分析後發現,還是因爲 key 太多,同樣的頁面,可能會離散出幾十個 key,主動刷新的 qps 超過了本身請求的好多倍。這種影響後臺本身性能的緩存業務肯定不可取,但是在不影響下游的情況下,如何提高緩存命中率呢?

然後把請求進行統計後發現,大多數請求集中在頻道頁和熱劇上,統計結果大致如下:

上圖藍色和綠色區域爲首頁訪問和熱劇訪問,可以看到,這兩種請求佔了 50% 以上的流量,可以稱之爲熱點請求。

然後針對這種數據結果,分析後做了以下架構優化:

可以看到,增加了 refresh-task 模塊。會針對業務熱點內容,進行主動刷新,並嚴格監控並控制 QPS。保證頁面緩存長期有效。詳細流程如下:

  1. 緩存服務接收到頁面請求,獲取緩存

  2. 如果沒有命中,則從 SSR 獲取數據

  3. 判斷是否是熱點頁面

  4. 如果是熱點頁面,發送延時消息到 rockmq

  5. job 服務消費延時消息,根據 key 獲取請求頭和請求體,刷新緩存內容

上線後看到,熱點頁面的緩存命中率基本達到 100%。firebase 上的性能數據 FCP 也提高了 20%。

本地緩存(更新)+redis 二級實時更新緩存

大家知道愛奇藝是做視頻內容網站,保持最新的優質內容纔會有更多的用戶,而技術團隊就是要做好技術支撐保證更好的用戶體驗。

而從上面的緩存策略上看,還有一個重大問題沒有解決,就是節目更新會有最大 5 分鐘的時差。果然,收到不少前臺運營反饋,WEB 端節目更新延遲情況比較嚴重。設身處地地想想,內容團隊緊鑼密鼓地準備字幕等數據就趕在 21:00 準時上線 1 集內容,結果後臺上線後,WEB 端過 5min 才更新這一集,肯定無法接受。

所以,從業務上分析,雖然是純展示服務,也就是 CRUD 裏基本只有 R(Read),並不像交易系統那樣有很多的寫操作,但是愛奇藝展示的內容,有 5% 左右的內容是強更新的,即需要及時更新,這就需要做到實時更新。

但是如果僅僅是監聽消息,更新緩存,當有多臺實例的時候,一次調用只會選擇一臺實例進行更新本地緩存,其他實例的本地緩存還是沒有被更新,這就需要用到廣播。一般會想到用消息隊列去實現,比如 activeMq 等等,但是會引入其他第三方中間價,給業務帶來複雜度,給運維帶來負擔。

調研後發現,Redis 通過 PUBLISH、SUBSCRIBE 等命令實現了訂閱與發佈模式,這個功能提供兩種信息機制,分別是訂閱 / 發佈到頻道和訂閱 / 發佈到模式。SUBSCRIBE 命令可以讓客戶端訂閱任意數量的頻道,每當有新信息發送到被訂閱的頻道時,信息就會被髮送給所有訂閱指定頻道的客戶端。可以看到,用 redis 的發佈 / 訂閱功能,能實現本地緩存的更新同步。

由此變更了緩存架構,變更後的架構如下:

可以看到,相比之前增加了本地緩存同步更新的功能邏輯,具體實現方式就是用 redis 的 pub/sub。流程如下

  1. 服務收到更新消息

  2. 更新 redis 緩存

  3. 發送 pub 消息

  4. 各本地實例訂閱且收到消息,從 redis 更新或者清除本地緩存

可以看到,這種方案可以保證分佈式多實例場景下,各實例的本地緩存都能被更新,保證端上拿到的是最新的數據。

上線後,能保證節目更新在可接受時間範圍內,避免了之前因引入緩存導致的 5 分鐘延遲。

Tips:Redis 5.0 後引入了 Stream 的數據結構,能夠使發佈 / 訂閱的數據持久化,有興趣的讀者可以使用新特性替換。

本地緩存(更新)+redis 二級實時更新緩存 + 緩存預熱

衆所周知,後端服務的發佈啓動是日常操作,而本地緩存隨服務關閉而消失。那麼在啓動後的一個時間段裏,就會存在本地緩存沒有的空窗期。而在這個時間裏,往往就是緩存擊穿的重災區間。愛奇藝國際站類似於創業項目,迭代需求很多,發佈頻繁,精彩會在發佈啓動時出現慢請求,這裏是否有優化空間呢?

能否在服務啓動後,健康檢查完成之前,把其他實例的本地緩存同步到此實例,從而避免這個緩存空窗期呢?基於這個想法,對緩存功能做了如下更新。

具體流程如下:

  1. 新實例啓動時發佈初始化消息

  2. 其他實例收到訂閱消息後,獲取本地可配置數量,通過 caffeine 的熱 key 算法,獲取緩存 keys,發送更新消息

  3. 新實例收到訂閱消息後,從 redis 或者從遠程服務新增本地緩存。

  4. 這樣能使 new client 變 "warm"(即預熱)

這樣的預熱操作在健康檢查之前,就可以保證在流量進來之前,服務已經預熱完成。

預熱功能新增後,服務的啓動後 1 分鐘內的本地緩存命中率大大提升,之前冷啓動導致的慢請求基本不復存在。

本地緩存(更新)+redis 二級實時更新緩存 + 緩存預熱 + 兜底緩存

在迭代過程中,會發現在業務增長期,前後端迭代需求很多,運營這邊也一直在操作後臺。偶爾會出現 WEB 端頁面不可用的情況出現,這個時候,並沒有可靠的降級方案。

經過對現有方案的評估和覆盤,發現讓 redis 緩存數據失效時間變長,當作備份數據。當 SSR 不可用或者報錯時,緩存擊穿後拿不到數據,可以用 redis 的兜底數據返回,雖然兜底數據的時效行不強,但是能把頁面渲染出來,不會出現最差的渲染失敗的情況。經過設計,架構調整如下:

可以看到,並沒有對主體的二級緩存方案做變更,只是讓 redis 的數據時效時間變長,正常讀緩存時,還是會拿 5min 的新鮮數據。當 SSR 服務降級時,會取 24 小時時效的兜底數據返回,只是增加了 redis 的存儲空間,但是服務可用性得到大大提高。

4-6 二級緩存工具

從上面看到,針對服務端二級緩存做了很多操作,而且有業務經驗的同學會發現,這些實際上是可以複用的,很多業務上都能有這些功能,比如二級緩存、緩存同步、緩存預熱、緩存主動刷新等等。

由此,基於開源框架進行二次開發,結合了 caffeine 和 redis 的自有 API,研發了二級緩存工具。

更多功能還在持續開發中。

如果業務方需要二級緩存中的這些功能,無需大量另外開發,引入工具包,只需進行少量配置,就能支持業務中的各種緩存需求。

5. 優化成果

經過不懈努力,咱們國際站 WEB 端的性能得到大大提升,可以看看數據:

這只是其中一項 FCP 數據,還有後端服務的緩存命中率和服務指標,都有顯著的變化。Amazon 十年前做的一項研究表明,網頁加載時間減少 100 毫秒,收入就會增加 1%。放在現在這個要求恐怕更高,所以優化的成果還是很顯著的。

但是我們並沒有停下腳步,也還在嘗試後端服務進行 GC 優化、服務響應式改造等,這也是性能優化的另一大課題,期待後續的優化成果。

作者:

Peter Lee 愛奇藝海外事業部後端開發

Isaac Gao  愛奇藝海外事業部後端開發經理

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。