高性能分布式計算與存儲系統設計概要(上篇)
2012年底,末日之后,看到大家都在寫年末總結,我也忍不住想一試。工作已經3年半了,頭一次寫總結。雖然到現在仍是無名小碼農一名,但工作這些年,技術着實有不少積累。成長最大的,當然就是這篇文章標題提到的——高性能分布式計算與存儲系統的設計和研發過程,這也是我自2010年供職於國內最大的某著名網站之后,和這個系統一起成長,親眼見證和伴隨着它的發展,從一個嬰兒一樣的"Demo"程序,成長為現在可以處理千萬級日PV的強大系統,直到2012年我離開。我也順勢積累了Unix/Linux服務器、多線程、I/O、海量數據處理、注重高性能與效率的C/C++編程等寶貴的碼農財富,當然,遺憾和不足,仍然是有許多的。
2012年,其實是自工作以來,技術積淀最多的一年。因為,在2012年,我終於學會了獨立思考,我不再像以前一樣,許許多的技術只是需要用到的時候,匆忙的google(有時候還要先匆忙的先翻牆),我發現,“好記性不如爛筆頭”,古訓確實毋庸置疑,有大量的、瑣碎的技術經驗、編程細節、技巧,需要積淀下來,可能單條的細節與技巧,並不會對一個人的職業生涯產生什么影響,但把它們都積聚起來,就會強大許多,很實際的,帶來的技術提升,能帶來更高的Offer。所以,2012年,我開始到博客園寫技術博客,和眾多園友分享我對技術的一知半解,共同進步;也終於耐下心,為自己做了一個簡單的個人主頁,雖然10年前,我就可以做出這樣的東西……;我成為了更忠實的蘋果粉,所以我嘗試去做iOS創業,雖然這和我的主要工作研究方向並不一致,當我看到自己做的demo在自己iPhone 4s上跑起來,我突然又有一了一種久違的興奮——那是每一個程序員,都體會過的,小小的成就感;2012年,我開始接觸和了解許多以前從來不懂的技術:Hadoop、GoogleFS、JVM、XCode、ARC……小到如何將vim打造成一個IDE……。
接下來,該進入這篇文章的正題了, 就是簡單地談談,我這兩年,主要做的東西——高性能分布式計算與存儲系統。
這個系統看名字十分牛比,所涉足的目前互聯網最領先的技術領域。具體有什么用途? 在我之前供職的公司,它主要是作為中間層,給網站頁面提供緩存服務的,並且,它對付的難題,是大數據、海量數據,相信,每一個日PV超過千萬級的網站,都必須會有類似的系統存在,如果,你曾經看過,博客園里的《淘寶技術發展》等類似文章,就一定不會對我接來將要提到的許多概念和術語感到陌生。對於這樣大流量,需要處理大數據的網站而言,由Web的邏輯直接調用管理數據存儲,是非常不科學的,實際上也是不可能的,大數據、高並發的對數據庫進行讀寫,通常數據庫都會掛掉,從而使網站也掛掉,必須要在Web和數據庫之間,通過技術手段實現一種“轉換”或“控制”,或“均衡”或“過渡”,我不知道這樣用詞是否正確,你只要明白其中的意思就好了。這樣的技術手段有許多,所實現的東東也有許多,我們用到的,就是被稱為“中間層”的一個邏輯層,在這個層,將數據庫的海量數據抓出來,做成緩存,運行在服務器的內存中,同理,當有新的數據到來,也先做成緩存,再想辦法,持久化到數據庫中,就是這樣簡單的思路,但實現起來,從零到有,可以說難如登天,但是,任何事物,都是在曲折中,不斷發展前進的,這是中學我們就學過的哲學理論。這個系統,就被我們稱簡為“緩存系統”,它最大的好處,就是砍掉了每天上千萬次的數據庫讀寫操作,取代而之的,是讀取服務器中提供緩存服務的進程所控制的內存,所以你知道,這里面節省了多少的資源申請、競爭、I/O……當然,后面你也會發現,它會帶來許多新的問題,最顯著的問題,就是數據的同步和一致性,后面我會講到。
現在,讓我們先看看, 這個系統,發展到我離開它的時候,長什么樣子?(由於涉及到商業機密,具體的技術不能提供)
就是這樣的一張架構圖,代表着可以處理每日上千萬PV的系統,涉及到許多的技術,讓我們一個部分一個部分解讀它。
首先,從當我有一個web請求到達時,將會發生怎樣的事情說起。比如,我是一個用戶,我在這個網站登陸,我的“個人”頁面上,將會加載許許多多的東西,有許許多多的圖片、文字、消息等,我們舉其中一個例子,我將要得到我的好友列表——friend list。通過常識可以知道,這個friend list,不是隨機的、臨時的,而肯定是一個(一組)持久化存儲於數據庫里的數據,我們就是一個用戶請求得到他的friend list說起,來解讀這張架構圖。如果我的網站流量很小,每天不超過10萬PV,峰值可能就幾百個上千個用戶,同時請求他們的friend list,那么,現今任何一種語言配上任何一種數據庫的搭配,只要稍做處理,都可以很好的完成這個工作——從數據庫中,讀出該用戶的friend list,然后訪回給web,如果用戶對好友列表作了任何修改,web馬上將修改內容寫入數據庫,形成新的friend list。然而,當訪問流量持續提升,達到千萬級、甚至億級PV的時候,剛才說的方法就不可行了。因為,同時可能有幾十萬甚至上百萬用戶,通過web請求從數據庫中讀(如果寫將會更糟糕)上百條萬數據,數據庫將不堪重負,形成巨大的延遲甚至掛掉。通過上面的系統,來解決這樣的問題。
現在,我們要設計和研發的上述系統,當一個web頁面提交一個獲取friend list的請求后,它首先將根據一定的規則,通過負載均衡,然后到達相應的master節點。上面我們提到的是DNS負載均衡,這得眾多負載均衡技術中的一種方法。也就是說,我有許許多多的master節點(上圖的scalabe表明,我是可擴展的,只要有條件,可隨意橫向擴展節點,以提高速度、容災、容量等指標),每個master節點的IP地址(域名)當然不一樣,通過DNS負載均衡,合理地把該請求,送到相對“空閑”的master節點服務器。現在解釋一下master節點服務器和slave節點服務器的功能:slave節點,主要用於"Running services",即,實際處理請求的緩存服務進程,通常運行在slave節點上;master節點,主要用於分發通過負載均衡的請求(當然,master節點上也可以運行一些“緩存服務進程”,即並發流量不高、較輔助的一些服務),找到用於處理實際請求的合適的slave節點,將該請求交給它處理,再次實現了一道“負載均衡”,同時,需要分布式計算的內容,將可能同時分發到幾個slave節點,之后再對結果進行合並返回(Map-Reduce原理)。
好了,現在我們已經知道,一個friend list請求已經通過DNS負載均衡、通過master節點進行分配,到達了相應的slave節點上。我們還知道,所說的“緩存” ,正是slave節點中所運行的services進程中所管理的內存,提供同樣功能的service可能會有很多份,同時運行在不同slave節點上,以提供高並發和分布式計算的功能。例如,獲得friend list就是這樣的service,因為這個功能太常用了,所以,在我們的系統中,這樣的服務可能同時提供5份、10份甚至更多,那么我這個獲取friend list的請求,究竟被分配到哪個slave節點上的service處理呢?這正是剛才提到的master節點來完成這一工作。再比如,我現在需要獲取“二度關系”的列表(關於六度人脈理論,可google),所謂“二度關系”,就是好友的好友,那么我要取這樣的列表,即friend's every friend list,這樣的請求,將會把取每個friend list分配(Map)到不同slave節點上去做(根據一定的規則),然后再進行合並(Reduce)(當然,熟悉算法的同學可能已經發現,這樣去獲取請求,非常的笨拙,有沒有更好的方法呢?當然有!因為好友的好友,其實就是好友的friend list與我和好友的共同好友common friend list的“差集”,對嗎?,所以我不用去取好友的每個好友的friend list,而只用取2次就可以通過計算完成請求,這又節省了多少資源呢?假如我有100個好友,1000個,10000萬個?會節省多少次計算呢?這也證明,一個良好的算法,對改善程序性能,有多么大的幫助!)
好,我們繼續。現在,我的獲取friend list的請求,已經在被某個slave節點中的負責這一功能的service進程處理,它將根據一定規則,給出兩種可能的處理方式:
1、 我這個用戶非常活躍,經常登陸網站(一定的規則,認為緩存未到過期時間),且我這個slave節點自上次“重建緩存”(即重新從數據庫中讀取數據,建立緩存,后面會談)后,沒有發生過down機重啟行為(又一定的規則),我也沒有收到過master節點發送過來要求更新緩存(即從數據庫中比較數據並更新)的Notification(通知),或是在一定條件下我這個slave節點對它掌握的緩存數據版本(版本管理系統原理,思考一下svn的工作原理)和數據庫進行了一次比較(注意,比較數據版本可認為只是一個int值,且是原子操作,這和比較整條數據是否一致在性能上有天壤之別)發現是最新的數據版本,那么,我這個slave節點將直接返回緩存數據,而沒有任何數據庫讀操作,也就是說,我這一次獲取friend list的請求,得到的是緩存數據,當然,這個緩存數據肯定是最新的、正確的、和數據庫中的持久化數據是一致的,后面會提到怎樣來盡量保證這一點;
2、第1點中的“一定規則”不滿足時,即我這個slave節點的緩存和數據庫中的數據可能存在不一致的沒有其它辦法,我必須從數據庫中讀取數據,更新緩存,然后再返回。但同時注意,slave節點中的service服務進程,將認為此用戶現在活躍,可能還會請求一些相關、類似的數據(如馬上可能進行添加好友、刪除好友等操作),所以去數據庫讀取數據的時候,將不會只讀friend list,可能與用戶有關的其它一部分數據,會被同時讀取並更新緩存,如果負責這一部分數據的緩存服務並不是當前的service進程,或在其它slave節點,或同時還有幾份service進程在工作,那么slave節點將提交“更新緩存”請求給master節點,通過master節點發出Notification給相關slave節點的相關service進程,從而,盡可能使每一次讀取數據庫的作用最大化,而如果稍后用戶果然進行了我們猜測的行為(可認為cache命中),結果將同第1點,直接通過緩存返回數據而且保證了數據的正確和一致性。
好了,剛剛提到的都是“讀操作”,相比“寫操作”, 其數據一致性更容易保證,之后我們將講述“寫操作”的工作原理。現在,讓我們先跳過這一部分,繼續看架構圖。slave節點之后,就是實際的數據存儲了,使用了MySQL、Redis,MySQL主從之間的協同是DBA的工作,不在此篇討論,Redis主要存儲K-V鍵值對數據,比如用戶id和用戶昵稱,是最常用的K-V對之一,通過Redis進行存儲,再結合上述的工作過程,可保證這個系統的高性能。而架構圖最右下角的Hadoop與MongoDB,是可選的MySQL替代方案,其實,正是未來的主要發展方向。如果slave節點中的service服務進程與Hadoop良好結合,系統的性能將更上一層樓。順便說一句,master、slave節點都是由C++開發的。Why C++?可參考酷殼上的一篇文章《C++ Performance per $》。
好了,上篇就講到這里,余下的問題,我們在下篇進行討論,謝謝大家。