閱讀450 返回首頁    go 阿裏雲 go 技術社區[雲棲]


淘寶首頁“有一套”


image


淘寶首頁是淘寶的門麵,承載著幾乎淘係所有業務的入口,流量很大,量級單位為億。近幾年無線端崛起,業務重點開始向無線終端偏移(目前不能叫偏移,基本以無線為主了),所以淘寶 PC 端首頁的流量也有削減,不過即便如此,它的日均 PV 依然相當高。

淘寶首頁一向是內部平台和技術的試驗田,它一直在變化著。最新的框架和係統都會找淘寶首頁試點,可以試想下,如果某一項需要推動的升級或者優化措施在淘寶首頁已經上線,並且拿到了良好的數據和穩定性,其他業務還有什麼理由不去嚐試和更迭呢?同時,去年一年身在淘寶前端的技術架構組,自然而然也會主動去 push 一些實驗性的內容到業務上。

淘係的站點頁麵包括首頁、其他頻道頁和活動頁等,這些頁麵並不都由淘寶前端一行一行的代碼碼出來,業務如此之多,這種玩法即便人數 double 也忙不過來。事實上,大多數頁麵都是依托內部的搭建平台——運營或者前端通過模塊搭建的方式——構建的,而前端 focus 的重點在於搭建平台的建設自身以及模塊的通用性和複用率的保障,當然,還有一些工程化的東西。

使用搭建平台搭建的頁麵,前端隻需要考慮組成頁麵的原子模塊的開發,整體的渲染由搭建平台提供的統一腳本全權負責。而在淘寶首頁上,考慮到頁麵模塊數量巨多,加上還有少量跨部門、跨團隊的溝通,渲染模型略微不同。


背景中提到,淘寶首頁依托於內部搭建平台,它的變遷自然也是跟著搭建係統的變化而變化的。

1. PHP 下的淘寶首頁

接手淘寶首頁不久,便遇到了一年一度的改版,那時它還運行在 PHP 環境中。這裏需要說明的是,淘寶首頁的所有代碼完全由前端掌控,前端不會直接跟數據庫打交道,其數據來源分為兩部分。

數據來源

一是 運營填寫的數據。 采用前端挖坑的形式,預留坑位讓運營獲取填寫數據,如(偽代碼):

<?php $info = Person('name:String:姓名,age:Number:年齡', '個人信息坑位填寫');?>

<div>
<?php $info.forEach(index) { ?>
  Name: <?= info[index].name ?>, Age: <?= info[index].age ?>
<?php } ?>
</div>

上麵的代碼會產生一份 PHP 的模板和 info 字段對應的表單坑位,這個過程簡稱「挖坑」。


image


運營填寫這些坑位就會產生這份 PHP 模板對應的數據,最後渲染出來就是一個完整的 HTML 片段(實時性渲染)。


.
├── data.json   # 運營數據的來源
└── index.php   # 裝載運營數據的 PHP 模板

舊版搭建係統中就是通過這種方式構造一個子模塊。我描述得十分簡單,但作為一個平台它需要考慮的東西還有很多很多的,比如數據順序的控製、定時發布、回滾機製、過濾機製、篩選機製、數據的同步、數據的更新、版本控製、權限控製、其他係統的引用等等。

二是 後端或者個性化平台提供的數據。 不同的業務有不同的訴求。一些業務有自己的後端,他們要求使用自己業務產出的數據;有的業務希望用戶看到的內容不一樣,千人千麵,期望接入算法;一些業務跟賣家直接打交道,期望使用招商數據;而有些業務期望采用運營從數據池篩選出來的數據…總之,淘寶首頁需要對接形形色色的係統,接口繁多。後麵會提到對動態數據源的整合。

並且這些係統對應的域名是不一樣的,JSONP 格式自然也就成了首選。但一些特殊的係統,比如廣告,它的渲染並不是一個簡單的 JSONP 請求,可能它還要幹預整個廣告的渲染流程,比如加載他們的 JS,把渲染的控製權交過去。

頁麵的架構

上麵介紹了數據的來源和子模塊的結構,那麼整個頁麵又是如何構成的呢?模塊的搭建分為兩種,一種是可視化搭建,運營或者前端可以將開發好的模塊(或者模塊庫中選取的模塊)拖拽到容器內,形成一個頁麵,

image


當然,上圖也隻是一個模型,作為一個係統需要考慮的問題還有很多很多,如頁麵的布局、多終端適配、模塊的臨時隱藏、位置調整、皮膚選擇、模塊的複製等等。

也可以通過如下源碼搭建的方式(偽代碼):


<body>
  <?= loadModule(Mod1ID) ?>
  <?= loadModule(Mod2ID) ?>
  <?= loadModule(Mod3ID, 'lazyload') ?>
  <?= loadModule(Mod4ID, 'lazyload') ?>
  <?= loadModule(Mod5ID, 'lazyload') ?>
</body>

通過模塊 id 將模塊引入,並且添加一些類似 lazyload 的標記,方便控製渲染節奏和數據入口。源碼搭建和模塊搭建的區別在於,前者更易於控製模塊的結構以及模塊的渲染順序。

動態數據源

首頁麵對一大堆接口和平台,對接幾十個業務方,接口是個很大的問題,由於後台係統的差異,基本沒有辦法統一數據源的格式,一旦運營哪天心血來潮要換一個他自己覺得用的更爽的或者數據更好的係統,前後端估計又得溝通和對接幾次。所以出現了下麵這張圖:


image


平台具備數據源接入的能力,也就是說我們挖的坑不僅僅可以讓運營填數據,還可以從各種數據源中直接導入數據,當然,這裏需要進行一次數據字段的映射轉換。後端提供的接口是這樣的:


{
  "data": [{
    "item_name": "name",
    "item_url": "https://xxx",
    "item_pic": "https://xxx"
  }]
}

前端約定的接口形式是:


{
  "info": [{
    "name": "name",
    "url": "https://xxx"
  }]
}

那麼係統必須提供這種映射的綁定策略:


info/name -> data/item_name
info/url -> data/item_url

綁定之後,數據既可以同步輸出,也可以異步輸出,這些都是平台提供的能力。這個方案基本上解決了後端係統/接口變化的問題,並且減少了前後端之間的溝通成本。

不過這裏需要注意的是,雖然頁麵上的接口都通過平台統一梳理了一次,這也意味著,頁麵所有的請求會先流經平台,然後分發到各個後端,平台的抗壓能力要求很高。

2. PHP 到 Node 的變遷

淘寶首頁日均請求的這個量級,不可能是十幾二十台台服務器抗得住的,支撐它必須有一個服務集群。

image


每一個 CDN 節點上都具備 PHP 渲染的能力,當頁麵發布時,我們把該頁麵所有的模塊和數據同步到全部 CDN 節點上,基本模式大概就是如此了。看起來還挺不錯,但是經過一段時間的運維,很多安全、性能問題都慢慢浮現出來了:

性能問題。 每個 PHP 頁麵包含多個子模塊,而子模塊也有可能引用了其他的子模塊,PHP 的 include 操作是存在消耗的,每一次引用都是一次磁盤 IO,一個渲染節點上跑了成千上萬個類似淘寶首頁的 PHP 頁麵,並發一高其效率可想而知。

推送機製問題。 文件同步(圖中的 sync 動作)是一種比較惡心的機製,首先,時間上沒法控製,一個文件同步到所有的節點,快則幾秒鍾,慢的話耗時會超過一兩分鍾;並且同步過程還有可能失敗,健康檢測的成本也是相當高的。發布比較緊湊時,需要同步的文件也很多,很容易造成隊列堆積,加劇同步差的體驗。

實時性強需求問題。 文件在推送之前,還可能經過一些前置係統,發布鏈路越長,線上生效時間越慢,慢的時候大約五分鍾才生效,這樣的延時對於實時性要求很高(如秒殺)的需求來說是完全不能接受的。

當然,還有很多其他問題,如運維成本增高、安全風險增高、PHP 資深人才儲備不足等等。所以 PHP 渲染容器的命運,就是,被幹掉。


image

上圖改變了下玩法,服務集群為 Cache CDN,它隻有靜態文件處理能力,沒有 PHP/Node 的渲染能力,所以處理效率高,性能也好,抗壓能力相當強,並且扛不住的時候還可以花錢買服務,拓展 Cache 集群。

用戶訪問時,Nginx 轉到 Cache CDN,如果命中緩存則直接返回,沒有命中便回源到源站服務器。源站服務器是具備模塊渲染能力的 Node 服務,它可以做很多事情:

  • 控製 Cache 響應頭,通過 max-age 和 s-maxage 控製頁麵在客戶端的緩存時間以及在 Cache 上的緩存時間,這個緩存時間可以根據需求隨時做調整,比如大促的時候調長一些
  • 控製內外網環境,和 AB 測試狀態
  • 融合前端相關的工具鏈,比如檢測、壓縮、過濾等等

它的優勢有很多,這裏不一一列舉了。這個模式中還添加了一層容災,源站服務器每隔一段時間將數據推送到於 Cache 同機房的備份服務器,一點源站掛了,還能夠自動容災到備份數據上。

模式的變化不僅在運維上有了突破,CDN 被攻擊時的安全風險也低了很多,同時也省卻了 sync 所需的各種檢測機製,每年節約成本也是百萬以上,優勢還是相當明顯。

3. Node,不一樣的模式

上麵 PHP 模塊中,我們隻說了 HTML 和數據部分,用心的讀者應該已經發現,CSS 和 JS 這些靜態資源都沒提到,那頁麵是如何渲染的呢?

舊版 PHP 頁麵中,我們是直接引入了一個 CSS 和一個 JS,淘寶這邊采用的是 git 版本迭代發布,這些靜態資源都是直接放在一個 git 倉庫中。也就是這樣:


<head>
  <link rel="stylesheet" href="//cdn/@VERSION@/index.css">
  <script src="//cdn/@VERSION@/index.js"></script>
</head>
<body>
  <?= loadModule(Mod1ID) ?>
  <?= loadModule(Mod2ID) ?>
  <?= loadModule(Mod3ID, 'lazyload') ?>
  <?= loadModule(Mod4ID, 'lazyload') ?>
  <?= loadModule(Mod5ID, 'lazyload') ?>
</body>

每次發布完 git 文件,再修改 PHP 的版本號,然後發布 PHP 代碼。當然,也做了相關的優化,比如發布 git 時自動更新版本號等。

而新版搭建平台的頁麵渲染模式與 PHP 的模式不太一樣。

image


一個模塊的 CSS/JS 和模板放在一起,CSS/JS 與頁麵其他模塊的靜態資源是相互獨立的,目的就是希望單個模塊也能夠完整的跑起來,更加利於模塊的複用。

而模塊的挖坑,也從模板中獨立了出來,采用 JSON Schema 的形式定義數據格式,


.
├── index.css    # 模塊樣式
├── index.js     # 模塊渲染腳本
├── schema.json  # schema 配置
└── index.xtpl   # 模塊的模板

搭建平台通過這個 JSON Schema 解析成 圖一 的坑位。那麼一個模塊的渲染就變成了index.xtpl 和挖坑數據之間的拚裝了。

模塊之間相互獨立隔離,所以會存在一定程度的冗餘,不過模塊解偶帶來的收益要比這點冗餘要多得多。事實上,我們是通過一個倉庫去管理單個模塊的。頁麵的渲染就比較簡單了,源站 Node 容器會將所有的 index.xtpl 合並成一個 page.xtpl,為減少頁麵請求,css 和 js 也會 combo 成一個文件,如上圖所示的 https://cdn/??mod1.css,mod2.css,mod3.css。

任何模塊的更新,頁麵都會有感知,下次進入係統時,就會提示是否需要升級模塊和頁麵。


首頁模塊眾多,如果一口氣吐出來,DOM 數量必然超過 4k 個,其結果就是首屏時間極長。按照 TMS 的開發規範,每個 TMS 模塊都包含一個 index.js 和 index.css,最後展示出來兩個 combo 的 js 和 css。首頁加載的時候也不會一口氣執行所有 index.js,否則剛開始頁麵阻塞會十分嚴重。

頁麵的渲染邏輯

image


首頁框架的加載邏輯,大致上圖所示:

  • 遍曆所有 TMS 模塊(包含一個 J_Module 的鉤子)
  • 部分 TMS 模塊無 JS 內容,但是加載了一個 index.js,為模塊添加 tb-pass 的 class,用於跳過該模塊 JS 的執行
  • 將頁麵分為兩塊,首屏為一塊,非首屏整體為第二塊,先將首屏模塊加入到懶加載監控
  • 待首屏模塊加載完成,或者用戶處理了頁麵交互時(滾動、鼠標移動等),將非首屏模塊加入到懶加載監控
  • 處理一些特殊模塊,它們會在進入視窗之前幾百像素就開始加載
  • 監控滾動,按照以上邏輯,渲染模塊

部分模塊即便是被執行了,也不一定渲染出來,因為它的優先級不高,在模塊內部加了事件監聽,比如等到 mouseover/onload 事件觸發的時候再渲染這些內容。

之前寫過性能優化相關的文章,複製就沒必要了,直接貼地址:

  • 《淘寶首頁性能優化實踐》

代碼的性能優化是一個精細活,如果你要在一個龐大的未經優化的頁麵上做性能優化,可能會麵臨一次重構代碼。

上麵的文章提到的是頁麵內部的細節優化,但是在開發流程中做的規範化、標準化,以及線上訪問通路中的各個環節優化還沒有提及。


在大流量下,任何小問題都會被放大成大問題,所以開發環節遇到的任何偶發性問題都需要引起重視。不過很多偶發性問題在我們的測試環境中是找不到的,比如與地域相關的問題(如上海的某個 CDN 節點掛了),用戶屬性問題(如 nickname 最後一個為字母 s 的用戶頁麵天窗),瀏覽器插件問題,運營商廣告注入問題等等。

難以在上線之前把所有問題考慮周全,但是有兩點是必須做好的:兜底容災 + 監控預警。

1. 兜底容災機製

兜底容災有兩個層麵的考慮:

  • 異步接口請求錯誤,包括接口數據格式錯誤,接口請求超時等
  • 同步渲染,源站頁麵渲染出錯

異步接口請求,主要涉及到的是後台係統,對接係統較多,各個係統的穩定性和抗壓能力各不相同,這方麵的保障有多種方案,下麵是最常見的:

image


每次數據請求都緩存到本地,並且為每個接口都提供一個硬兜底。還有一種方案是「重試」,請求一次不成功那就請求第二次。這方麵的討論具體可以看看之前寫的這篇文章:《淘寶首頁兜底容災方案》。

對於同步渲染,它隻需要頁麵模板和同步數據,兩者中任一種存在錯誤,源站都會報錯,此時回源返回的內容就是一個 error 頁麵,狀態碼為 5xx。這個錯誤不一定是開發者造成的,有可能是係統鏈路出現同步異常或者斷路問題。針對這種問題,我給淘寶首頁做了一個鏡像頁:

image


一旦源站任何異常,Nginx 都會轉到與 Cache CDN 同機房的首頁鏡像上去,這個鏡像內容就是淘寶首頁的 HTML 備份源碼。

2. 監控預警機製

監控也有兩個層麵:

  • 模塊級別的監控,接口請求布點、模塊天窗檢測等
  • 頁麵的監控,在頁麵上添加特殊標記,定時回歸所有 CDN 節點,查看特殊標記是否存在

模塊層麵的監控,內容還是相當多的,監控的點越多越詳細,到最後定位問題的效率就會越高,比如在一個稍微複雜的模塊上,我會埋下這些監控:

接口請求格式錯誤、請求失敗、請求超時,至少三個埋點

  • 硬兜底數據請求失敗埋點
  • 模塊 5s 內沒有渲染完成統計埋點
  • 模塊內鏈接和圖片黑白名單匹配埋點


image

其中部分監控還會自動處理明確的錯誤,比如 https 頁麵下出現了 http 的圖片,會立即自動處理掉這些問題。

3. 上線前的自動化檢測

這屬於淘寶整個工程化環境的一部分,前端自動化測試。一般會在上線之前處理這些問題:

  • 檢測 HTML 是否符合規範
  • 檢測 https 升級情況
  • 檢測鏈接合法性
  • 檢測靜態資源合法性
  • 檢測 JavaScript 報錯
  • 檢測頁麵加載時是否有彈出框
  • 檢測頁麵是否調用 console.*
  • 頁麵 JS 內存記錄

當然,也可以自己添加測試用例,比如檢測接口數據格式、模塊天窗問題等。自動化檢測也可以設定定時回歸,還是比較有保障的。


1. 健康檢查

頁麵模塊眾多,為了能夠追蹤頁麵上每一個小點的變化,我在請求、渲染的每一個環節都做了詳細的統計,如下圖所示:

image


一旦接口請求失敗,或者接口走了容災邏輯,或者模塊渲染超過 5s,控製台都會有黃色警報,當然此時,也已經向服務器發送了警報統計。

2. 接口 Hub

接口 Hub 是對數據請求的管理工具,如下圖所示:

image


頁麵很多模塊的渲染都需要一個以上的數據源,一旦運營反饋頁麵渲染數據異常,可以直接通過 Hub 找到數據,加速 Bug 定位效率。同時 Hub 也可以用來切換環境,將一個接口的請求切換到日常或者預發環境的接口之中,它是調試的利器。

3. 快捷通道

我在頁麵腳本執行前後都放了一個快捷操作通道,一旦遇到緊急線上問題,比如樣式錯亂溢出、接口報錯導致天窗等,可以通過快捷通道直接修改頁麵的 CSS 和 JS,兩分鍾內上線。

不過這類通道隻適合緊急問題的修複,畢竟隨意插入 JS 代碼是存在很大風險的。

原文鏈接

最後更新:2017-06-28 18:02:12

  上一篇:go  樹莓派使用 DHT11 溫濕度傳感器
  下一篇:go  百度外賣如何做到前端開發配置化