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