計算系統的設計其實就是在玩約束條件的游戲。一個設計良好的系統就像是個定制化的運輸箱:它的外部有鎖扣和手柄,但從里面裝的東西來看其內部設計,卻往往給人印象不佳。如果要設計一個優雅簡潔的容器,你應該聚焦在那些最重要的因素上:尺寸、重量、;平衡和移動。這些因素以及它們之間的相關影響就構成了設計中的有效約束條件。
在這篇文章中,我將描述了一種對系統數據的形狀和流的約束條件進行分析的方法,并將它應用在真實世界的例子中。基于約束條件的方法和物流領域中的格德拉特(Goldratt)理論相似。根據我的經驗,最成功的系統設計師和容量規劃師傾向于依據具體的約束條件來思考計算系統,即使他們可能沒有說出聲來。
相比從基準(benchmark)外推,或者爭論“并發用戶”的定義,使用約束條件方法要有用的多。它能在編碼之前揭示可能的系統瓶頸及其相互的依賴關系。這些知識對于理性地選擇設計方案至關重要。通過實踐你甚至可以對別人,比如競爭對手的系統做出敏銳的猜測,而這也很簡單,只需要你觀察它是如何運行的。
這個技巧在于先建立數據的尺寸和重量,然后聚焦于它是如何流動的。計算機其實只做兩件事情:讀入數據和寫出數據。性能決定了多少數據可以移動,以及它們移動到哪里來完成任務。說起來似乎很容易,但這需要掌握基礎的計算理論。每個計算機等同于一個圖靈機(Turing Machine),而所有圖靈機做的事情就是在磁帶上移動符號,所以它的吞吐量取決于符號移動的速度。從小的微米級的CPU內部到大的橫跨世界的分布式數據庫,這個推論仍然成立。幸運的是,這里的數學方法簡單明了。
可以迅速丟棄壞的方案,而無需實際創建它們,這種能力對于好的系統編程有決定性的意義。它部分是靠直接,還有一些經驗,但最主要的是算法。
工作集尺寸(Working set size):這是系統在正常操作時需要處理的一組數據。復雜系統通常會有多個不同的工作集,但其中一至兩個是最主要的。在類似郵件或者是新聞饋入的流應用中,當前處理的工作集要比總體的工作集小得多。人們幾乎很少會去訪問幾周前的消息,所以把它們考慮成由另一個系統處理也許更好。更多時候,情況并不如此簡單,很難在“熱”數據和“冷”數據之間畫一條清晰的界限。所以,用概率帶(probability band)來思考這個問題會很有幫助:比如過一段給定的時間后,不同部分的數據被使用的概率會是多少?會有多少個概率帶?它們是如何分布的?在初始分析中,你可以只關注工作集的大體尺寸,而不是它的細節特征。然而,這些細節會在后面主動找你的。
平均事務尺寸(Average transaction size):這個可以理解為系統執行單個事務時的工作集。那么系統完成一個事務要接觸到多少數據呢?下載一個圖片和運行一個網絡搜索,其發送到客戶端的回答包括同等規模的數據。但是,它們在后臺處理時接觸到的數據卻差別很大。注意我使用了“事務”這個詞來表示不相同的工作,這個觀點同樣適用于大的分析作業。
請求速率(Request rate): How many transactions are expected per hour / minute / second? 每個小時/分鐘/秒會有多少個事務發生呢?會有峰值時間,或者比較穩定的需求? 如果是搜索引擎,每分鐘每個人可能會有5到10次的查詢需要處理。如果是在線電子書,它可能是一個持續但較低的流量。如果是游戲,那每個人每秒種可能會有多個事務要處理。簡單說,這些都是預期的并發量。并發量和事務尺寸的結合 決定了系統數據流的主要因素。
更新速率(Update rate): 這是來衡量數據增加、刪除和編輯的頻率。郵件系統有高的增加速率,低的刪除速率和幾乎是零的編輯速率。而廣告拍賣的用例在這三種速率上都異乎尋常的高。衡量更新速率是否值得擔心有個有用的方法,就是把它和讀取的并發量做一個對比。
數據增長的速率和工作集大小或數據保留策略也是相關的。0.1%的增長速率意味著數據要保留大概三年(365 * 3大約是1,000)),反之亦然。而1%的增長率則意味著數據保留100天。
一致性(Consistency): 一次更新必須在多快的時間內傳播到整個系統?對于關鍵字廣告報價,幾分鐘的傳播時間是可以接受的,而股票交易系統必須在毫秒級完成. 一個評論系統通常期望在一兩秒內顯示新的評論, 這需要后臺瘋狂的工作,以給評論者產生即時性的錯覺。當更新速度是請求速度中最主要的部分時,一致性就是個關鍵因素。當傳播的更新對于業務特別重要時也同樣如此,比如賬號注冊或者價格和存貨發生了變化。
位置(Locality): 一個請求需要訪問工作集中的哪些數據? 這部分的數據如何去定義? 不同請求是否有重疊的部分?極端情況下會使用搜索引擎來回答這些問題: 即用戶可以從系統中任何地方來查詢到想要的數據。在郵件應用中,用戶被確保只能訪問他們的收件箱,相對整體數據,這是個小的且定義良好的數據片段。在另一個實例中,你可能需要無重復數據的存儲來保存郵件附件,并讓其成為熱點。
計算(Computation):你需要用什么樣的數學方法來運行數據?數據可以預先計算和緩存嗎?你會在大數據集上進行交叉嗎?你是將計算貼近數據,還是相反?為什么?
時延(Latency):事務多快 可以返回成功或失敗呢?用戶在搜索航班或者進行信用卡交易時,似乎可以接受幾秒鐘的處理時間。網絡搜索必須要在幾百毫秒內可以返回;而系統調用外部依賴的API時則希望可以在100毫秒或更少的時間內返回結果。除了時延,考慮方差也是很重要的。正如論證的那樣,90%的查詢在0.1秒以內,剩余的在2秒以內的情況要比所有請求都在0.2秒內完成的情況要差。
找到瓶頸
找到一個你想分析的系統,然后填寫出上述的那些約束條件,并且草擬一個方案能滿足這些條件。下來問自己最后一個問題:決定性的操作是什么?換句話說,最基本的瓶頸在什么地方?哪些部分必須仔細考量?
當你大聲說出來答案時,雖然聽起來可能平淡無奇,但是確定一個系統的真正瓶頸對于 認知事物有極大的幫助。偶發性的瓶頸往往會形成堆積,修正一個會激活另一個,但是它們不會對你設計的基本前提形成顛覆性的威脅。基礎性的瓶頸則難于修復,也很難識別,因為它們是由創建系統時基于的一些自然事實或者一個強假定引起的。一個無窮快的網址仍然受限于光的速度,火箭也會受限于提升自身燃料重量的需求。保持這種思考性的實驗直到你完全理解最基礎的瓶頸是什么?以及為什么?
我們可以打個比方,你有個披薩店,想賺更多的錢。如果購買的人排長隊,你可以把訂單登記的數量增加一倍。如果是披薩送貨時遲到,你可以規劃一個更好的工作節奏,或者為送貨員配備車輛。甚至你可以嘗試提高烤箱的溫度。但從根本上說,披薩店的瓶頸是烤箱的尺寸。即使你把其他事情都做好,如果烤箱容量不擴張,你也無法生產出更多的披薩。當然你也可以再購買一個烤箱,不過你必須事情想清楚,把這個新家伙放到什么地方。
如果你不能清晰地發現系統的基本瓶頸,那么就去改變系統的約束條件,看看它的響應有什么變化。比如你必須將延遲降低10倍,看看會發生什么?比如你只用一半數量的計算機?如果放松一致性的約束,又要靠什么樣的技巧才能僥幸成功?通常認為初始約束是真實和靜止的,但實際中它們很少如此。問題中的創造力遠比答案中的創造力更有利用價值。
如果你想了解某件事情,把它放在極端條件下觀察或者檢查它的對立面。
——Col. John Boyd
影視流用例
讓我們將上面的方法應用到比披薩店更復雜的案例中,即視頻流服務。可以想象它是一個小規模的Hulu或Netflix,視頻庫容量為十萬個,平均60分鐘的時長,500 kbps的比特率。在高峰時間,大約會有一百萬人觀看。
工作集尺寸: (100 k視頻)*(60分鐘)*(60秒)*(500 kb /秒)/( 8位字節),算出來大約超過20 TB的數據。公式中最敏感的數字是比特率,改變比特率可以縮小或膨脹總的數據大小。
工作集的概率帶依賴于視頻受歡迎程度的分布,它(可能)是個長尾。假設所有視頻中,受歡迎程度前100個的視頻占播放總數的10%,排名在前1000個的視頻占20%,排名在前10000個的視頻占35%。而倒數第三的視頻的占比很可能已經無足輕重,你甚至不必去留意它。所以,人們很容易完全忽略長尾。然而,正如我們后面將看到的那樣,這將是一個mistake。
平均請求尺寸(Average request size): 大約是(3600秒)*( 64 kbps),或者幾百MB。在實踐中,流的平滑取決于能夠把數據以略高于其消耗的速度推到客戶端去,并且很可能都是以小數據塊來完成。為了使問題簡化,我們特意忽略了發送到客戶端過程中,涉及流量整形的那些繁雜的問題。
請求速率(Request rate): 和較小請求尺寸的系統相比,其請求速率會相當的低,但是仍會有高并發的請求。可以期望用戶瀏覽是多個短脈沖,而媒體流則是長時間持續的。高峰時段系統每秒排出的數據將達到每秒0.5TB。
更新速率(Update rate): 和請求速率相比,更新速率近乎于零,這是因為視頻基本上都是專業制作。但如果這是YouTube,那么將會是一個完全不同的故事。
一致性(Consistency): 因為在很大程度上這是個只讀的系統,所以這一點可以被忽略掉。
位置:用戶可以觀看任何一個電影,當然同一時刻只能有一個。從相反的方向來看這個問題,你會發現來自不同的地方的用戶會在同一時刻訪問相同的電影。
計算(Computation): 計算量的大小取決于視頻是否是動態生成編碼。如果不是這樣,那么計算的主要工作就是把數據放到管道中。
延遲(Latency): 這是個有趣的問題。最壞的情況是不停的進行頻道切換或者在視頻播放時中不停的跳躍播放位置。在實際的影片服務中,您可能已經注意到,切換不同的媒體流或者在一個視頻中跳躍播放位置需要在一兩秒鐘內完成,長于這個時間將是不可接受的。
現在讓我們來概述一下可以滿足這些約束條件的系統。總共20TB需要保存的數據量,看上去不算太大;500Gbps的請求速率,看上去數據服務量還是挺多。如果我們使用當前(2015年)16核的文件服務器,它可以輕松完成7 Gbps的有效網絡數據吞吐,所以我們需要至少50個這樣的服務器,每個上面配置128 GB的RAM,還有2 TB的存儲,這樣很容易容納整個的工作集數據。
雖然我們可以把數據存儲起來,但我們可以移動它們嗎?讓我們再來看看歡迎度曲線,前100個視頻占了總需求的10%,所以你會想要在每個服務器存儲這100個視頻的副本。實際中你可能會對前一個視頻都這樣做,它們占了總帶寬的三分之一,但只用掉了10%的存儲空間。如果有足夠的RAM和一點運氣,那么受歡迎的視頻幾乎完全可以從內存中提供服務。
這樣,還剩下9萬個視頻,處于長尾右端的3萬個視頻觀看次數很少,所以無關緊要。但長尾中間的6萬個視頻占了總業務流量的2/。所以需要考慮如何為它們服務,同時保持延遲條件的約束?由于RAM已經被最受歡迎的視頻占用,所以接下來就要算算存儲層額可以支持多少500 kbps的業務流量并發。具體的設計可能要進行測試后才會出來,但最終很可能是幾百個磁盤驅動器并行工作來達到好的結果。
從這里我們可以得出結論:視頻服務的基本瓶頸是隨機尋道時間,在設計上這等同于那些小的金屬機械臂可以來回移動的速度。起控制作用的約束條件是人們觀看視頻的歡迎度曲線,這個是你無法改變的。
還有其他的設計方案可能會解決這個問題。例如,在每個服務器上配置1 TB的RAM,而不是2 TB的磁盤。使用今天或明天的SSD技術也可以圓滿的解決這個問題。這里用到的數字可能不是很準確,而且肯定會隨著時間的推移而改變。關鍵是發現那些最重要的具體細節,并重點討論它們。
人臉識別用例
有時候,系統會被一個特定操作嚴重主導,以至于幾乎其它操作相比它都無足輕重。可以舉一個有趣的案例,即人臉識別。記住不是人臉檢測,人臉檢測僅僅是在圖像中找到人臉,而決定在給定照片中的人是誰,則需要和照片庫中照片進行比較。
人臉識別首先需要對已知人群的照片進行預處理,以便對每個人的基本特性生成一種抽象描述,這種抽象描述有個非常奇怪的名字,叫“特征臉(eigenface)”。一個特征臉是一個固定大小的數據塊,通常小于100 kB,它可以由一些聰明的算法生成。生成特征臉的主要好處是可以相對低廉的計算任意兩個人臉之間的相似度得分。得分越高,兩個人臉輪廓特征就越接近,從而你識別出這個人是誰的可能性更高。
假設你有一個對應五千萬個體的人臉特征庫,它們可能來自國民護照,或者駕照數據庫,或者是世界上最大的年鑒。系統每秒會有數百個查詢照片流請求進來,這些實時地流請求來自邊境機構的護照控制需求。必須在1秒或更少的時間內為每張照片找到相似度排名前十的匹配者。好了,我們開始分析:
工作集尺寸:工作集尺寸為:5千萬 * 100KB,總共5TB。熱數據和冷數據短期內無法區分,并且可能并不存在這樣的區分。
平均請求尺寸: 每個請求系統進來的數據是一張照片,他可以被縮減成100KB固定尺寸的特征臉,但每個請求需要訪問的系統數據可能會非常高。
請求速率:每秒300個。
更新速率: 具體情況暫時還不清楚,但是可能次數較低,而且應該是在預處理時的批處理操作。
一致性:在更新速率低的情況下,這個可以暫時忽略。
位置:我們必須掃描庫中的5千萬資料,并且給匹配度排名,這個想法有些不切實際。我們需要找到一種方法盡可能多得省略一些工作。
計算:閱讀實際的人臉檢測算法是相當迷人的,但對于這個練習卻并不是很重要。我們做一個假設即可,即完整的比較兩個特征臉需要10毫秒的CPU時間。
延遲:必須在1秒或更少的時間內完成,這是個剛性需求。
滿足低延遲需求的唯一方法是在執行在給定的搜索時,大幅減少特征臉全面對比的數量。但正如腸道檢查那樣,這涉及到可能性的范疇,即在這個問題上是否可以投入足夠的計算資源?答案很可能是No。每秒特征臉對比的次數=5000萬* 10毫秒* 300,這大約需要每秒5000 CPU-years的計算能力。我看過一些大型集群,但都達不到這樣的規模要求。
所以我們需要用些聰明的方法來減少工作。這個領域中比較活躍的研究是一種快速搜索【特征臉索引】的方案,但是通過摘要我們知道其加速線性而不是對數的(所以可能無法滿足我們的性能需求)。谷歌有一個通用的圖像相似度搜索方案,這個理論上也許是可行的,但是不一定是可用的技術。現在我們先停止調查,嘗試一下別的東西吧。也許我們可以基于人的眼睛顏色、年齡或性別來消除大部分的候選人。舉個例子來說,對比一個30歲的女人和一個男孩子的照片是沒有意義的。
假設可以通過啟發式和低成本的技巧來縮小候選人的數量,比如對于一個給定的查詢照片只需要對比1000個候選人。這相當于用10個CPU來解決10,000毫秒(譯者注:1000個候選人乘以10毫秒)的計算問題以讓延遲控制在1秒,如果添加更多的資源,完成任務就沒有問題。如果每個用12個CPU,那么300 * 12意味著我們總共需要3600個CPU來處理請求,這大約是250臺服務器或是六個機架。這樣的計算機集群其重量大概是三噸,其計算能力以任何標準衡量都是相當高的,但為了這個重要的項目還是值得的。
我們現在可以計算它了,但是我們可以保存數據嗎?5TB對于RAM來說是相當多的。但從另一方面來說,我們有服務器集群來提供這些內存。將庫中的特征臉數據分布到集群的RAM中有點類似Memcached的方案。
好了,現在讓它動起來吧。噢。它看起來走不了。
我們可以存儲它,但是我們可以移動它嗎?我們忘記更新平均請求尺寸了。如果單個請求訪問1000個特征臉,這些特征臉數據是隨機分布的,這相當于將100 MB的數據從它所在的Memcached服務器移動到另一個負責特征臉對比的CPU所在的服務器上。假如那個時間每秒有300個請求,那我們就會面臨240gbps網絡帶寬消耗,這是個致命的風險。對于每臺服務器來說,這接近于1gbps這個令人不安的數據,而且此時CPU已經忙于特征臉的比較而無法應對這么高的網絡傳輸負載。
我們需要將計算放在數據存儲的地方,而不是將數據傳輸到計算節點。在開始比較之前,我們準確地知道哪1000個特征臉需要對比。Memcached對特征臉鍵值采用直接哈希的方式,所以特征臉存儲的服務器是很容易得知的,下來就可以將具體的請求路由到這個特定的服務器上。每個服務器基于本地數據會做4或5次比較,并返回它們的相似度得分。整體的相似度得分的列表可以很容易地組裝起來并進行排序。這種方案下,唯一的網絡流量將是查詢時特征臉照片所占的100 KB,乘以服務器的數量250,估算成300倍,總共60gbps,這個數字雖然很高,但是是可行的。進一步,可以使用更聰明的數據分布方案來避免數據查詢展開到整個集群中。
系統的基本瓶頸就是掃描那些特征臉,到目前這點可能看得還不是很清晰。這是個比較難的問題,我們幾乎沒找到一個合理的解決方案,甚至我們還沒有考慮識別的準確性是否足夠有用。這種人臉識別的分析在重要細節幾乎肯定會出錯,但從一些懂行的人給出的評論來看,方案大致是正確的。我故意選擇了這個我不太了解的用例來演示如何約束分析如何幫助你分析系統,即使(特別)你不是一個專家。
想象一個專家過來告訴你“為什么不利用GPU呢?他們可以將特征臉的對比提速十倍!”你可以回答“聽上去很有趣,但是它對網絡帶寬問題有何幫助呢?”(實際上可能會有幫助,如果它能減少機器數量)。如果有人說“我有一個辦法來索引特征臉!”你可以回答“這很有趣,但前提是它能幫助我執行不到1000次完整的比較。順便說一下,你應該和懂GPU的那幫家伙聊聊。”你也知道應該密切注意說這樣話的一些人,即 “我可以使用比100KB更小的尺寸來準確地代表一個人的臉。”
這其是基于約束條件分析方法的真正價值,即幫助你更好理解問題的物理意義,從而揭示出那些需要創造性的地方。像特征臉技巧那樣,它允許你通過標準形式來呈現復雜的圖片,從而可以在對比中很快地接受或拒絕。
科學不僅僅是探索為什么(Why),而是為什么不(Why not)?
— Cave Johnson
結束語
類似這種大數據系統分析的工作有幾個技巧值得去練習。能夠通過不完整的數據進行快速評估是至關重要的。例如,谷歌擁有多少臺服務器呢?(可以按照電力消耗來估算)。另一個很好的技能是能丟棄推理中那些有缺陷的思路,并及時對大腦中的想法進行刷新。Jeff Dean有許多非常好的在線談論包括“你應該知道的數字”以及如何考慮大型系統的設計。
可以在現有系統上進行約束條件分析,然后來查找答案,這對于掌握技能是個很好的方式。在本文中,我回避了那些有高更新速率或高一致性約束條件的案例。但你可以花一個小時左右的時間來分析一些類似的商業系統以增強了解,比如2004年左右時的AdWords、2005年時的YouTube或者2005年的Flickr。關于這些系統都有一些很好的評論和討論,而且是系統的創建者寫的。但是建議你在獨立的解答之前先不要看這些文章。