門戶級UGC系統的技術進化路線
新聞門戶網站的評論系統,或者稱為跟帖、留言板,是所有門戶網站的核心標準服務組件之一。與論壇、博客等其他互聯網UGC系統相比,評論系統雖然從產品功能角度衡量相對簡單,但是因為需要能夠在突發熱點新聞事件時,在沒有任何預警和準備的前提下支撐住短短幾分鐘內上百倍甚至更高的訪問量暴漲,而評論系統既無法像靜態新聞內容業務那樣通過CDN和反向代理等中間緩存手段化解沖擊,也不可能在平時儲備大量冗余設備應對突發新聞,所以如何在有限的設備資源條件下提升系統的抗壓性和伸縮性,也是對一個貌似簡單的UGC系統的不小考驗。
(圖0:2008年劉翔因傷退賽后的每分鐘系統發帖量曲線)
本人曾負責新浪網評論系統多年,新浪評論系統不僅服務于新浪的門戶新聞業務,還包括簽名、調查、辯論、投票等產品,我經歷了系統從單機到多機再到集群,從簡單到復雜再回歸簡單,從突發熱點事件時經常宕機到后來承受與新浪新聞靜態池(新浪新聞業務的全國性內容分發與緩存系統)相近流量壓力的架構演變過程,負責了前后多個版本的設計和開發工作。有心得收獲也有經驗教訓,摘要總結一下供各位同行批評和參考。
另外我也希望能看到各位同行更多的經驗分享,在互聯網系統架構設計領域,失敗遠比成功更寶貴。
新聞評論系統的起源
新浪網很早就在新聞中提供了評論功能,最開始是使用Perl語言開發的簡單腳本,目前能找到的最早具備評論功能的新聞是2000年4月7日的(鏈接見附錄),經過多次系統升級,14年前的評論地址已經失效了,但是數據仍保存在數據庫中。直到今天,評論仍是國內所有新聞網站的標配功能。
評論系統3.0
大約2003年左右,我接手負責評論系統,系統版本3.0。當時的評論系統運行在單機環境,一臺X86版本Solaris系統的Dell 6300服務器提供了全部服務,包括MySQL和Apache,以及所有前后臺CGI程序,使用C++開發。
(圖1:3.0系統流程和架構)
3.0系統的緩存模塊設計的比較巧妙,以顯示頁面為單位緩存數據,因為評論頁面是依照提交時間降序排列,每新增一條新評論,所有帖子都需要向下移動一位,所以緩存格式設計為每兩頁數據一個文件,前后相鄰的兩個文件有一頁的數據重復,最新的緩存文件通常情況下不滿兩頁數據。
(圖2:頁面緩存算法示意圖)
上圖是假設評論總數95條,每頁顯示20條時的頁面緩存結構,此時用戶看到的第一頁數據讀取自“緩存頁4”的95~76,第二頁數據讀取自緩存頁3的75~56,以此類推。
這樣發帖動作對應的緩存更新可以簡化為一次文件追加寫操作,效率最高。而且可以保證任意評論總量和顯示順序下的翻頁動作,都可以在一個緩存文件中讀到所需的全部數據,而不需要跨頁讀取再合并。缺點是更新評論狀態比如刪除時,需要清空自被刪除貼子開始的所有后續緩存文件。緩存模塊采取主動+被動更新模式,發帖為主動更新,每次發帖后觸發一次頁面緩存追加寫操作。更新評論狀態為被動更新,所涉及緩存頁面文件會被清空,直到下一次用戶讀取頁面緩存時再連接數據庫完成查詢,然后更新頁面緩存,以備下次讀取。這個針對發帖優化的頁面緩存算法繼續沿用到了后續版本的評論系統中。
此時的評論系統就已經具備了將同一專題事件下所有新聞的評論匯總顯示的能力,在很長一段時間內這都是新浪評論系統的獨有功能。
雖然3.0系統基本滿足了當時的產品需求,但是畢竟是單機系統,熱點新聞時瞬間涌來的大量發帖和讀取操作,經常會壓垮這臺當時已屬高配的4U服務器,頻繁顯示資源耗盡的錯誤頁面。我接手后的首要任務就是盡量在最短時間內最大限度的降低系統的宕機頻率,通過觀察分析確定主要性能瓶頸在數據庫層面。
3.0系統里每個新聞頻道的全部評論數據都保存在一張MyISAM表中,部分頻道的數據量已經超過百萬,在當時已屬海量規模,而且只有一個數據庫實例,讀寫競爭非常嚴重。一旦有評論狀態更新會導致很多緩存頁面失效,瞬間引發大量數據庫查詢,進一步加劇了讀寫競爭。當所有CGI進程都阻塞在數據庫環節無法退出時,殃及Apache,進而導致系統Load值急劇上升無法響應任何操作,只有重啟才能恢復。
解決方案是增加了一臺FreeBSD系統的低配服務器用于數據庫分流,當時MySQL的版本是3.23,Replication主從同步還未發布,采取的辦法是每天給數據表減肥,把超過一周的評論數據搬到二號服務器上,保證主服務器的評論表數據量維持在合理范圍,在這樣的臨時方案下3.0系統又撐了幾個月。
在現在看來相當簡陋的系統架構下,新浪評論系統3.0與中國互聯網產業的門戶時代一起經歷了南海撞機、911劫機、非典、孫志剛等新聞事件。
(截圖1~4:南海撞機、911劫機事件、非典、孫志剛事件)
評論系統4.0啟動
2004年左右,運行了近三年的3.0系統已經無法支撐新浪新聞的持續流量上漲,技術部門啟動了4.0計劃,核心需求就是三個字:不宕機。
因為當時我還負責了新浪聊天系統的工作,不得不分身應對新舊系統的開發維護和其他項目任務,所以在現有評論系統線上服務不能中斷的前提下,制定了數據庫結構不變,歷史數據全部保留,雙系統逐步無縫切換,升級期間新舊系統并存的大方針。
評論系統4.0第一階段
- 文件系統代替數據庫,基于ICE的分布式系統
既然3.0系統數據庫結構不可變,除了把數據庫升級到MySQL 4.0啟用Repliaction分解讀寫壓力以外,最開始的設計重點是如何把數據庫與用戶行為隔離開。
解決方案是在MySQL數據庫和頁面緩存模塊之間,新建一個帶索引的數據文件層,每條新聞的所有評論都單獨保存在一個索引文件和一個數據文件中,期望通過把對數據庫單一表文件的讀寫操作,分解為文件系統上互不干涉可并發執行的讀寫操作,來提高系統并發處理能力。在新的索引數據模塊中,查詢評論總數、追加評論、更新評論狀態都是針對性優化過的高效率操作。從這時候起,MySQL數據庫就降到了只提供歸檔備份和內部管理查詢的角色,不再直接承載任何用戶更新和查詢請求了。
同時引入了數據庫更新隊列來緩解數據庫并發寫操作的壓力,因為當時消息隊列中間件遠不如現在百花齊放,自行實現了一個簡單的文件方式消息隊列模塊,逐步應用到了4.0系統的各個模塊間異步通信場合中。
(圖3:4.0系統流程)
(圖4:4.0索引緩存結構)
選用了ICE作為RPC組件,用于所有的模塊間調用和網絡通信,這大概是剛設計4.0系統時唯一沒做錯的選擇,在整個4.0系統項目生命周期中,ICE的穩定性和性能表現從未成為過問題。(Wiki:Internet Communications Engine)
4.0系統開發語言仍為C++,因為同時選用了MySQL 4.0、ICE、Linux系統和新文件系統等多項應用經驗不足的新技術,也為后來的系統表現動蕩埋下了伏筆(新浪到2005年左右才逐步從FreeBSD和Solaris遷移到了CentOS系統)
(圖5:4.0系統架構)
此時的4.0評論系統已從雙機互備擴容到五機集群,進入小范圍試用階段,雖然扛過了劉翔第一次奪金時創記錄的發帖高峰,但是倒在了2004年亞洲杯中國隊1:3敗于日本隊的那個夜晚。
(截圖5:劉翔首次奪金)
當時系統在進入宕機之前的最高發帖速度大約是每分鐘千帖量級,在十年前還算得上是業界同類系統的峰值,最終確認問題出在文件系統的IO負載上。
設計索引緩存模塊時的設想過于理想化,雖然把單一數據表的讀寫操作分解到了文件系統的多個文件上,但是不可避免的帶來了對機械磁盤的大量隨機定位讀寫操作,在CentOS默認的ext3文件系統上每個新聞對應兩個文件的的設計(2004年新浪新聞總量千萬左右),雖然已經采取了128x256的兩層目錄HASH來預防單目錄下文件過多隱患,但剛上線時還表現良好的系統,稍過幾個月后就把文件系統徹底拖垮了。
既然ext3無法應對大數量文件的頻繁隨機讀寫,當時我們還可以選擇使用B*樹數據結構專為海量文件優化的ReiserFS文件系統,在與系統部同事配合反復對比測試,解決了ReiserFS與特定Linux Kernel版本搭配時的kswapd進程大量消耗CPU資源的問題之后,終于選定了可以正常工作的Kernel和ReiserFS對應版本,當然這也埋下了ReiserFS作者殺妻入獄后新裝的CentOS服務器找不到可用的ReiserFS安裝包這個大隱患。(Wiki:ReiserFS)
評論系統4.0第二階段
- 全系統異步化,索引分頁算法優化
直到這個階段,新浪評論系統的前端頁面仍是傳統的Apache+CGI模式,隨著剩余頻道的逐步切換,新浪評論系統升級為靜態HTML頁面使用XMLHTTP組件異步加載XML數據的AJAX模式,當時跨域限制更少的JSON還未流行。升級為當時剛剛開始流行的AJAX模式并不是盲目追新,而是為了實現一個非常重要的目標:緩存被動更新的異步化。
(圖6:異步緩存更新流程)
隨著消息隊列的普遍應用,4.0系統中所有的數據庫寫操作和緩存主動更新(即后臺程序邏輯觸發的更新)都已經異步化了,當時已經在實踐中證明,系統訪問量大幅波動時,模塊間異步化通信是解決系統伸縮性和保證系統響應性的唯一途徑。但是在CGI頁面模式下由用戶動作觸發的緩存被動更新,只能阻塞在等待狀態,直到查詢數據和更新緩存完成后才能返回,會導致前端服務器Apache CGI進程的堆積。
使用AJAX模式異步加載數據,可以在幾乎不影響用戶體驗的前提下完成等待和循環重試動作,接收緩存更新請求的支持優先級的消息隊列還可以合并對同一頁面的重復請求,也隔離了用戶行為對前端服務器的直接沖擊,極大提高了前端服務器的伸縮性和適應能力,甚至連低硬件配置的客戶端電腦在AJAX模式加載數據時都明顯更順暢了。前端頁面靜態化還可以把全部的數據組裝和渲染邏輯,包括分頁計算都轉移到了客戶端瀏覽器上,充分借用用戶端資源,唯一的缺點是對SEO不友好。
通過以上各項措施,此時的4.0系統抗沖擊能力已經有明顯改善,但是接下來出現了新的問題。在3.0系統時代,上萬條評論的新聞已屬少見,隨著業務的增長,類似2005年超女專題或者體育頻道NBA專題這樣千萬評論數級別的巨無霸留言板開始出現。
為了提高分頁操作時定位和讀取索引的效率,4.0系統的算法是先通過mmap操作把一個評論的索引文件加載到內存,然后按照評論狀態(通過或者刪除)和評論時間進行快速排序,篩選出通過狀態的帖子并且按照時間降序排列好,這樣讀取任意一頁的索引數據,都是內存里一次常量時間成本的偏移量定位和讀取操作。幾百條或者幾千條評論時,上述方案運作的很好,但是在千萬留言數量的索引文件上進行全量排序,占用大量內存和CPU資源,嚴重影響系統性能。曾經嘗試改用BerkeleyDB的Btree模式來存儲評論索引,性能不升反降。
為了避免大數據量排序操作的成本,只能改為簡單遍歷方式,從頭開始依次讀取直到獲取到所需的數據。雖然可以通過從索引文件的兩端分別作為起點,來提升較新和較早頁面的定位效率,但是遍歷讀取本身就是一個隨著請求頁數增大越來越慢的線性算法,并且隨著4.0系統滑動翻頁功能的上線,原本用戶無法輕易訪問到的中間頁面數據也開始被頻繁請求,所以最終改為了兩端精確分頁,中間模糊分頁的方式。模糊分頁就是根據評論帖子的通過比例,假設可顯示帖子均勻分布,一步跳到估算的索引偏移位置。畢竟在數十萬甚至上百萬頁的評論里,精確計算分頁偏移量沒有太大實際意義。
(截圖6:4.0滑動翻頁)
2005年非常受關注的日本申請加入聯合國常任理事國事件,引發了各家網站的民意沸騰,新浪推出了征集反日入常簽名活動并在短短幾天內征集到2000多萬簽名。因為沒有預計到會有如此多的網民參與,最開始簡單實現的PHP+MySQL系統在很短時間內就無法響應了,然后基于4.0評論系統緊急加班開發了一個簽名請愿功能,系統表現穩定。
(截圖7:反日入常簽名)
評論系統4.0第三階段
- 簡化緩存策略,進一步降低文件系統IO
到了這個階段,硬件資源進一步擴容,評論系統的服務器數量終于達到了兩位數,4.0系統已經達到了當初的“不宕機”設計目標,隨著網站的改版所有新聞頁面包括網站首頁都開始實時加載和顯示最新的評論數量和最新的帖子列表,此時4.0系統承受的Hits量級已經接近新浪新聞靜態池的水平。并且從這時起,新浪評論系統再沒有因為流量壓力宕機或者暫停服務過。
(圖7:4.5系統架構)
前面提到,新裝的CentOS系統很難找到足夠新版本的ReiserFS安裝包,甚至不得不降級系統版本,一直困擾性能表現的文件系統也接近了優化的極限,這時候memcached出現了。
2006年左右memcached取代了4.0系統中索引緩存模塊的實體數據部分(主要是評論正文),索引緩存模塊在文件系統上只存儲索引數據,評論文本都改用memcached存儲,極大降低了文件系統的IO壓力。因為系統流量與熱點事件的時間相關性,僅僅保存最近幾周的評論就足以保證系統性能,極少量過期數據訪問即使穿透到MySQL也問題不大,當然服務器宕機重啟和新裝服務器上線時要非常留意數據的加載預熱。
(圖8:4.5系統流程)
之后4.0系統進入穩定狀態,小修小補,又堅持服役了若干年,并逐步拓展到股票社區、簽名活動、三方辯論、專家答疑、觀點投票等產品線,直到2010年之后5.0系統的上線。
2008年5月12日當天,我發現很多網友在地震新聞評論中詢問親友信息,就立即開發了基于評論系統的地震尋親功能并于當晚上線。大約一周后為了配合Google發起的尋親數據匯總項目,還專門為Google爬蟲提供了非異步加載模式的數據頁面以方便其抓取。
(截圖8:汶川地震尋親)
2004年上線的4.0系統,2010~2011年后被5.0系統取代逐步下線,從上線到下線期間系統處理的用戶提交數據量變化趨勢如下圖。
(截圖9:4.0系統流量圖)
高訪問量UGC系統設計總結
縱觀整個4.0系統的設計和優化過程,在硬件資源有限的約束下,依靠過度設計的多層緩沖,完成了流量劇烈波動時保障服務穩定的最基本目標,但是也確實影響到了UGC系統最重要的數據更新實時性指標,數據更新的實時性也是之后5.0系統的重點改進方向。
總結下來,一般UGC系統的設計方針就是通過降低系統次要環節的實時一致性,在合理的成本范圍內,盡量提高系統響應性能,而提高響應性能的手段歸根結底就是三板斧:隊列(Queue)、緩存(Cache)和分區(Sharding):
- 隊列:可以緩解并發寫操作的壓力,提高系統伸縮性,同時也是異步化系統的最常見實現手段;
- 緩存:從文件系統到數據庫再到內存的各級緩存模塊,解決了數據就近讀取的需求;
- 分區:保證了系統規模擴張和長期數據積累時,頻繁操作的數據集規模在合理范圍。
關于數據庫:
- 區分冷熱數據,按照讀寫操作規律合理拆分存儲,一般UGC系統近期數據才是熱點,歷史數據是冷數據;
- 區分索引和實體數據,索引數據是Key,易變,一般用于篩選和定位,要保證充分的拆分存儲,極端情況下要把關系數據庫當NoSQL用;實體數據是Value,一般是正文文本,通常不變,一般業務下只按主鍵查詢;兩者要分開;
- 區分核心業務和附加業務數據,每一項附加的新業務數據都單獨存儲,與核心業務數據表分開,既可以降低核心業務數據庫的變更成本,還可以避免新業務頻繁調整上下線時影響核心業務。
目前的互聯網系統大都嚴重依賴MySQL的Replication主從同步來實現系統橫向擴展,雖然MySQL在新版本中陸續加入了RBR復制和半同步等機制,但是從庫的單線程寫操作限制還是最大的的制約因素,至少到現在還沒有看到很理想的革新性的解決方案。
關于緩存,從瀏覽器到文件系統很多環節都涉及到緩存,這里主要說的是應用系統自己的部分:
最好的緩存方案是不用緩存,緩存帶來的問題往往多于它解決的問題;
只有一次更新多次讀取的數據才有必要緩存,個性化的的冷數據沒必要緩存;
緩存分為主動(Server推)和被動(Client拉)兩種更新方式,各自適用于不用場景;
主動更新方式一般適用于更新頻率較高的熱數據,可以保證緩存未命中時,失控的用戶行為不會引發系統連鎖反應,導致雪崩;
被動更新方式一般適用于更新頻率相對較低的數據,也可以通過上文提到的異步更新模式,避免連鎖反應和雪崩;
緩存的更新操作盡量設計為覆蓋方式,避免偶發數據錯誤的累積效應。
一個UGC系統流量剛開始上漲時,初期的表面性能瓶頸一般會表現在Web Server層面,而實際上大多是數據庫的原因,但是經充分優化后,最終都會落在文件系統或者網絡通信的IO瓶頸上。直接承載用戶訪問沖擊的前端服務器最好盡量設計為無狀態模式,降低宕機重啟后的修復工作量。
順帶提及,我在新浪聊天和評論系統的開發過程中,逐步積累了一個Web應用開發組件庫,在新浪全面轉向PHP之前,曾用于新浪的內容管理(CMS)、用戶注冊和通行證、日志分析和論壇等使用C++的系統,目前發布于https://github.com/pi1ot/webapplib。
評論系統5.0方案
2010年后針對4.0系統的缺陷,啟動了5.0系統工作。因為工作的交接,5.0系統我只負責了方案設計,具體開發是交給其他同事負責的,線上的具體實現與原始設計方案可能會有區別。5.0系統極大簡化了系統層次,在保證抵抗突發流量波動性能的前提下,數據更新的及時性有了明顯的提高。
(圖9:5.0系統流程)
設計方案上的主要變化有:
- 評論帖子ID從數據庫自增整數改為UUID,提交時即可確定,消除了必須等待主庫寫入后才能確定評論ID的瓶頸,對各個層面的緩存邏輯優化有極大幫助;
- 重新設計數據庫結構,通過充分的數據切分,保證了所有高頻業務操作都可以在一個有限數據量的數據表中的一次簡單讀取操作完成,索引和文本數據隔離存儲,在數據庫中實現了原4.0系統中索引模塊的功能,取消了4.0系統的索引緩存層;
- 改用內存NoSQL緩存用戶頻繁讀取的最新10到20頁數據,取消了原4.0系統文件方式的頁面緩存層;
- 系統運行環境遷移到新浪云的內部版本:新浪動態平臺,設備資源富裕度有了極大改善;
- 改為Python語言開發,不用再像4.0系統那樣每次更新時都要等待半個小時的編譯過程,也不用再打包幾百兆的執行文件同步到幾十臺服務器上,而語言層面的性能損失可以忽略不計。
新聞評論產品總結
新聞評論作為微博之前最能反映輿情民意的UGC平臺,長期承載了國內互聯網用戶對時事新聞的匿名表達欲望,曾經一度成為上到政府下到網民的關注焦點。雖然面臨了相對其他社區系統更為嚴厲的管控力度,也錯過了實施實名制改造時邁向社區化的最佳時機,但是無論如何,在21世界的前十年,國內門戶網站的新聞評論服務,都是中國互聯網產品和技術發展歷史上絕對不能錯過的一筆。