1. 程式人生 > >高併發系統設計與時間和空間的平衡

高併發系統設計與時間和空間的平衡

 

 

高併發系統設計與時間和空間的平衡

高可用上文我們已經講過了,可當前網際網路時代,怎麼少的了高併發呢?高併發和高可用一樣, 已經變成各個系統的標配了,如果你的系統QPS沒有個大幾千上萬,都不好意思跟人打招呼,雖然可能每天的呼叫量不超過100。

 

高併發這個詞,我個人感覺是從電商領域開始往外流傳的,特別是電商領域雙11那種藐視全球的流量,再把技術架構出來分享一把,現在搞得全網際網路都在說高併發,而且你注意回憶一下所有你看到的高併發系統,往往都逃不開一個核心概念,那就是快取+雜湊,一切都是以這個概念和基礎的,彷彿這就是高併發的核心技術了。

 

快取+雜湊=高併發?

 

(一)我們看到的高併發技術

 

圍繞這個核心技術,通常我們看到的各種高併發的架構系統,在部落格,論壇,現場分享出來的高併發系統,都跑不出以下幾個方面的東西。

 

1資源靜態化

 

就是那種單個頁面流量巨大無比,每秒的QPS幾十萬上百萬的系統,確實併發高的系統,核心解決方案就是靜態化,靠機器和頻寬去抗,假如沒有CDN的話,所有流量都落到同一個IP下面的話,基本上也就是用Nginx的檔案靜態化了,單機的承受能力主要取決於頻寬和單機的效能,要再多的話,那就LVS(或者F5)+叢集了,這種的典型場景就是搞活動時候的首頁,活動頁面了,還有就是引流搜尋引擎的著陸頁了,一般都是現成的圖片和文字靜態化,當然,這種還有很多前端的技巧和技術了,這一點我不是很瞭解,就不得瑟了,就中後臺來說,大部分情況下直接Nginx搞定了,核心還是使用了快取技術

 

2讀寫分離和分庫分表

 

讀寫分離是大家看到的第二個高併發的架構了,也很常規,因為一般情況下讀比寫要多得多,所以資料庫的主庫寫,從庫們提供讀操作,一下就把資料庫的併發效能提高了。

 

如果還不夠,那麼分庫分表把,把資料分到各個資料庫的各個機器上,進一步的減少單臺機器的壓力,從而達到高併發的目的。

 

如果是分庫分表,有時候使用的就是雜湊技術了,以某個欄位雜湊一下然後來分庫分表,讀寫分離的讀操作,基本也是通過雜湊技術把讀落到不同的機器上去減輕單機壓力。

 

3萬能的快取

 

說到高併發,不得不說快取了,現在各種快取的工具也很多也很成熟,memcache,redis

之類的KV資料庫作為快取已經非常成熟了,而且基本上都可以叢集化部署,操作起來也很簡單,簡直變成了一個高併發的代言詞了,核心就是快取技術了,而memcache和redis只是用來實現這個快取技術的工具而已。

 

4無敵的雜湊

 

但凡大資料處理,高併發系統,必言雜湊,隨機插入,時間複雜度O(1),隨便查詢,時間複雜度O(1),除了耗費點空間以外,幾乎沒什麼缺點了,在現在這個記憶體廉價的時代,雜湊表變成了一個高併發系統的標配。

 

5正面的例子

 

我們來看個例子,看看一些個大家眼中標準的高併發系統的設計,這些設計大家應該都看過,無非就是上面的幾個要點,最重要的就是快取+雜湊,這兩個東西的組合好像無所不能。

 

活動秒殺頁面

 

活動秒殺頁面,這是個標準的高併發吧,到了搞活動的那個時刻,單頁面的訪問量是天量資料了,但這種系統有個特點:邏輯簡單,只要頻寬和效能夠,就一定能提供穩定的服務服務。

 

能迅速的返回資料即可,沒有什麼計算邏輯,這種高併發系統的設計基本上就是在怎麼壓榨機器的IO效能了,如果有CDN絕對使用CDN,能在本機讀取的絕不走網路獲取,能讀取到記憶體中絕不放在硬碟上,把系統的磁碟IO和網路IO都儘可能的壓榨,使用快取+雜湊技術,只要設計合理,99%的情況能搞定。活動頁面的衝擊感實在太強,想象一下幾千萬人同時訪問網站還沒有掛,讓很多人覺得高併發應該就是這樣子,這估計也是高併發這次經常在電商技術中出現的原因吧,因為搞個活動就可以搞出一個高併發事件。

 

這樣的場景再擴充套件一點,就是凡是能提前提供資料的併發訪問,就可以用快取+雜湊來搞定併發。

 

12306

 

接下來,我們再看看這個星球併發量最瘋狂的網站,瞬間的訪問量碾壓其他網站的12306,這種場景下的高併發也有個特點,那就是雖然量大,但其實無法給每個使用者提供服務。

 

類似的其實還有商品的搶購系統,商品和車票一共就1000張,你100萬的人搶,你係統做得再好,也無法給100萬人提供服務,之前12306剛剛上線的時候很多人噴,說如果讓某某公司來做肯定能做好,但大家很多隻看到了表面,讓某很厲害的公司來做,最多也只能做到大家訪問的時候不會掛掉,你買不到車票還是買不到,而且現在的12306體驗也已經做得很好了,也不卡了,但是還是很多人罵,為什麼?還不是因為買不到票。

 

對於這樣的系統,設計關注的就不僅僅是提高效能了,因為效能瓶頸已經擺在那了,就1000張票,做得更多的是分流和限流了,除了快取+雜湊來保證使用者體驗以外,出現了奇葩驗證碼,各個站點分時間點放票。然後通過排隊系統來以一種非同步的方式提供最終的服務。

 

我們給這樣的場景再擴充套件一下,凡是不能提前提供資料的,可以通過快取+雜湊來提高使用者體驗,然後通過非同步方式來提供服務。

 

(二)高併發系統如何設計

 

如果把上面兩個場景的情況合併一下,彷彿快取+雜湊變成萬能的了,很多人眼中的高併發就是上面的場景的組合,認為快取+雜湊就可以解決高併發的問題,其他的場景中,加上快取提高讀寫速度,在加上雜湊提供分流技術,再通過一個非同步提供最終服務,高併發就這麼搞定了,但實際上是不是這樣呢?顯然沒那麼簡單,那如何來設計一個高併發的系統呢?

 

1合理的資料結構

 

舉個例子來說吧,搜尋提示功能大家都知道吧,就是下面這個圖的東西。

 

 

如果是google,baidu這種大型搜尋系統,或者京東淘寶這種電商系統,搜尋提示的呼叫量是搜尋服務本身呼叫量的幾倍,因為你每輸入一個鍵盤,就要呼叫一次搜尋提示服務,這算得上是個標準的高併發系統吧?那麼它是怎麼實現的呢?

 

可能很多人腦子裡立刻出現了快取+雜湊的系統,把搜尋的搜尋提示詞存在redis叢集中,每次來了請求直接redis叢集中查詢key,然後返回相應的value值就行了,完美解決,雖然耗費點記憶體,但是空間換時間嘛,也能接受,這麼做行不行?恩,我覺得是可以的,但有人這麼做嗎?沒有。

 

瞭解的人應該知道,沒有人會這麼來實現,這種搜尋提示的功能一般用trie樹來做,耗費的記憶體不多,查詢速度為O(k),其中k為字串的長度,雖然看上去沒有雜湊表的O(1)好,但是少了網路開銷,節約了很多記憶體,並且實際查詢時間還要不比快取+雜湊慢多少,一種合適當前場景的核心資料結構才是高併發系統的關鍵,快取+雜湊如果也看成一種資料結構,但這種資料結構並不適用於所有的高併發場景,所以高併發系統的設計,關鍵在合理的資料結構的設計,而不在架構的套用。

 

2不斷的程式碼效能優化

 

有了上面的資料結構,並且設計出了系統了,拿到線上一跑,效果還行,但感覺沒達到極限,這時候可千萬不能就直接上外部工具(比如快取)提升效能,需要做的是不斷的程式碼效能的優化,簡單的說,就是不斷的review你的程式碼,不斷的找出可以優化的效能點,然後進行優化,因為之前設計的時候就已經通過理論大概能算出來這個系統的併發量了,比如上面那個搜尋提示,如果我們假定平均每個搜尋詞6個字元,檢索一次大約需要查詢6次,需要2-3毫秒,這樣的話,如果8核的機器,多執行緒程式設計方式,一秒鐘最多能接受3200次請求(1000ms/2.5ms*8),如果沒達到這個量級,那麼肯定是程式碼哪裡有問題。

 

這個階段可能需要藉助一些個工具了,JAVA有JAVA的效能優化工具,大家都有自己覺得好使的,我本身JAVA用得很少,也沒啥可推薦的,如果是Golang的話,自帶的go tool pprof就能很好的進行效能優化。

 

或者最簡單的,就是把各個模組的時間打印出來,壓測一遍,看看哪個模組耗時,然後再去仔細review那個模組的程式碼,進行演算法和資料結構的優化,我個人比較推崇這個辦法,雖然比較笨,但是比較實在,效能差就是差,比較直觀能看出來,也能看出需要優化的點,而且比較貼近程式碼,少了外部工具的干擾,可能也比較裝逼吧。

 

這個過程是一個長期的過程,也是《重構:改善程式碼的既有設計》中提到的,一個優秀的系統需要不斷的進行程式碼級別的優化和重構,所以高併發系統的實現,就是不斷的優化你程式碼的效能,不斷逼近你設計時的理論值。

 

3再考慮外部通用方法

 

以上兩個都完成了,併發量也基本達到理論值了,但是還有提升的需求,這時候再來考慮外部的通用方法,比如加一個LRU快取,把熱詞的查詢時間變成O(1),進一步提高效能。

 

說起LRU,多說一句,這是個標準的快取技術了,實現起來程式碼也不復雜,就是個雜湊表+連結串列的資料結構,一個合格的開發人員,即便沒有聽說過,給定一個場景,應該也能自己設計出來,我見過很多簡歷都說自己有大型高併發系統的開發經驗,能熟練運用各種快取技術,也對快取技術有深入的瞭解,但是一面試的時候我讓他寫個LRU,首先有50%的人沒聽說過,OK,沒聽過沒關係,我描述一下,然後給一個場景,硬碟上有N條資料,並且有一個程式包,提供GET和SET方法,可以操作磁碟讀寫資料,但是速度太慢,請設計一個記憶體中的資料結構,也提供GET和SET方法,儲存最近訪問的前100條資料,這個資料結構就是一個LRU了,讓面試者實現出來,如果覺得寫程式碼麻煩,可以把資料結構設計出來描述一下就行了,就這樣,還很多人不會,這怎麼能說是對快取技術有深入瞭解呢?就這樣,怎麼能說有過大型高併發系統的經驗呢?這只是開源工具的使用經驗罷了。

 

在沒把系統的效能壓榨完全之前,不要使用外部的通用方法,因為使用了以後就沒有太多進一步優化空間了。

 

4最後靠運維技術了

 

上面幾種都已經弄完了,還需要提升效能,這時候再考慮運維的技術了,比如常規的加負載均衡,部署成叢集之類的,通過運維和部署的方法提高服務的併發性。

 

高併發系統只是相對的,沒有什麼無上限的高併發,流量的洪流來了,再高的高併發一樣掛,新浪微博的高併發應該做得很好吧?但是林心如發條微博說她和霍建華談戀愛了,一樣把微博搞掛了(非官方訊息啊,我猜測的,呵呵,那天下午新浪微博正好掛了),呵呵,你說要是TFBOY明天過生日,微博是不是要連夜加幾個redis叢集啊?如果有微博的朋友,留個言溜溜唄:)

 

3、總結

 

羅裡吧嗦說了這麼多,其實我就想表達一個意思,不管是前面的高可用,還是今天的高併發。

 

程式碼才是關鍵,架構都是錦上添花的東西,既然是錦上添花的,必然坑多,沒有什麼捷徑。

 

程式碼的健壯性決定了高可用,這些印度人就能做到,而高效能,高併發需要的不僅僅是程式碼的健壯性,還有資料結構的設計和程式碼的調優能力了。架構模式是大家總結出來的,和你的系統可能關係不是很大,學習太多的架構,腦袋會亂,還不如實打實的看幾本書,然後對著架構多推敲練習,很多人對資料結構嗤之以鼻,覺得對於現有的開發來說,資料結構沒那麼重要了,但對於後端開發來說,資料結構是很重要的技能,雖然面試的時候不會讓你去翻轉一棵二叉樹,但二叉樹是什麼,什麼場景下用還是應該知道的吧?

 

找準合適的資料結構,不斷的優化程式碼,這樣來提升你的系統性能,這樣的系統才是你可控的,才有不斷優化的空間,更好的高併發,如果一開始就上外部的快取技術,很可能如果效能達不到要求,就沒有優化空間了,因為要修改外部的系統還是很困難的。

 

時間和空間的平衡

 

最後我們來說說架構中時間和空間的平衡吧,這裡的時間指代比較廣,可能是開發時間,但大部分指的是執行時間,也就是演算法的時間複雜度了,而空間就是演算法中經常說的空間換時間中的空間了,一個好的系統,設計出來必然是各種時間複雜度和空間複雜度平衡出來的結果,架構設計的過程,並不僅僅是模組的堆疊,在走到岔路口的時候,更多的是時間和空間平衡之後選的一個技術方案,這一篇,我會用一個搜尋提示服務設計的實際例子,來說一下架構設計的過程中,時間和空間的各種矛盾,怎麼分析,怎麼選擇,最後淌過這些時空的坑。

 

1. 搜尋提示是什麼

 

搜尋提示是搜尋引擎的重要組成部分,雖然一般是作為一個單獨的服務來對外提供服務,但在一個搜尋系統中,搜尋提示是非常重要的組成部分,我還沒看到哪個比較成熟的搜尋引擎沒有搜尋提示功能的。首先,我們看看搜尋提示是什麼,大家肯定都用過,就是下面這些個東西:

 

 

2. 搜尋提示的場景和目的

 

搜尋提示一般情況下是為了提高使用者的搜尋體驗,更快的選擇合適的搜尋詞,提高檢索的效率的,但是因為搜尋框的流量實在是太大了,所以搜尋提示也扮演著廣告變現的責任,網際網路嘛,有流量就有變現,比如下面這個圖,明顯就是一個廣告啦。


 

3. 初步技術選型

 

1搜尋提示的需求

 

要實現一個搜尋提示系統,首先需要確定的是需要提示出來什麼東西,有兩種提示方式。

 

  • 一種是提示出其他的搜尋詞,這也是大部分的搜尋提示所做的,提示出其他使用者的類似搜尋詞。

  • 還有一種是提示出現有的結果集有的東西,這種實現方式比較少見,比如一個生鮮類的電商網站,商品數量比較少,那麼沒必要去提示一些使用者的搜尋詞,直接把商品名稱(比如蘋果,桃子,橘子)提示出來就行了,這種提示方式我們這裡不討論,因為實現起來比較簡單。

 

2技術棧

 

既然知道需求了,那麼開始選擇技術棧了。

 

  • 首先,既然有其他使用者的搜尋詞,那麼必然有一個離線的資料收集和處理的系統來完成其他使用者的搜尋日誌處理,生成需要的資料。

  • 其次,需要一個單獨的API服務,來提供搜尋提示的功能,輸入為不完整的搜尋詞,輸出為根據這個搜尋詞提示出來的其他搜尋詞,檢索方式的話,一般都是使用字首匹配的方式了,這個大家都比較認可。

  • 最後,需要前端有個js程式碼來實時呼叫後臺的API,這個不在我們的討論範圍內。

 

整個系統的結構圖應該是下面這個樣子,離線模組處理完日誌資料以後,推送到API模組中,給前面的前端提供服務。

 

 

好了,框框設計好了。也就是架構圖完成了哦,真是牛逼的架構啊,三個框,離線,線上,前端全齊了。接下來,我們來看看線上API部分的設計吧,我們先假設離線資料都已經準備好了,就是一堆使用者的搜尋詞,如何快速的字首匹配這些詞就成了API設計部分的關鍵了,有這麼幾種實現方式。

 

  • 粗暴的短平快方式

 

用redis儲存所有資訊,每條資訊類似

 

{KEY:北 VALUE:北京,北京大學,北大,北京遇上西雅圖}

{KEY:北京 VALUE:北京,北京大學,北京遇上西雅圖}....

 

每次來了請求的話,直接查詢redis給出結果返回,就是佔點空間,最好還需要一臺單獨的伺服器。

 

  • 優雅點的實現方式

 

字首匹配嘛,最先想到的資料結構就是Trie樹了,所以所有的Key可以用Trie樹來儲存和檢索,速度也挺快的,而且空間佔用比較少。

 

  • 複雜點的實現方式

既然是檢索嘛,就直接用搜索引擎的倒排索引技術來實現嘛,速度也夠,而且資料量也可以支援得很大。

 

4. 時間與空間的平衡一

 

實際工程應用中,這三種實現方式我都見過,而且有些實現方式是把這三種結合起來使用了,後面的文章我會說到。具體使用哪一種需要看你的實際場景,這三種實現方式差不多正好對應三種場景。

 

  • 如果你是個小型的電商或者論壇之類的,每天的搜尋量也不是很大,而且在可見的未來也不會變得很大,而且也不差錢,那麼直接第一種,說不定一天就能擼出來,速度還不錯,但是這種有一些缺陷,首先,value值不能太複雜,影響效率,所以可擴充套件性不是很強,而現在的電商搜尋提示中往往還有很多其他資訊需要儲存,redis作為快取伺服器提供高併發服務的前提是資料量比較小,最好在2K以內,這樣的話用redis就有點不合適了。這種方案是個存空間的選擇了,用空間換取了檢索時間和開發時間,多虧有redis這種神器。

 

  • 如果是個大型的搜尋引擎或者電商,搜尋日誌已經是巨量了,而且搜尋詞多種多樣,那麼第三種倒排索引技術為基礎的實現方式可能是更好的選擇,而且既然是大搜,技術都是現成的,索引分片,叢集都是現成的,直接改了上就是。這種方式用長期的開發時間和檢索速度上稍微的降低換取了記憶體空間,如果從頭開始做的話,時間成本比較高。

 

  • 大部分時候,第二種實現方式是大家都採用的方式,首先沒有第一種那麼粗暴,並且能完成方案一的所以功能,單機就能達到較好的效果,也不用索引分片,也不用叢集,所以工程複雜性不是很高,也能在較短的時間內實現出來。其次第二種方案可擴充套件性較強,後面掛個倒排檔案就可以變成簡化版的第三方案。這種方式用演算法換取了記憶體空間,用O(n)替代了O(1),換取了記憶體空間,也是標準的計算機領域的時間換空間了。

 

通過一番分析下來,決定使用第二種實現方式,就是Trie樹的方式了,好了,API的基本選型確定了,那麼開始設計,準備寫程式碼吧。

 

5. Trie樹的多種結構

 

既然確定了Trie樹的實現方式,那麼首先要了解一下Trie樹吧,以及Trie樹的各種結構,看看具體用哪個吧。

 

1基本Trie樹

 

Trie樹又叫字典樹,本質上是一個多叉樹,每一個節點就是一個多叉的結構,如果是英文的匹配,那麼是一個26叉樹,每個節點一個26長度的陣列,每個節點的資料結構如下:

 

 

而Trie樹畫出來就是下面這個樣子。


 

從畫出來的圖,很直觀的可以看出來這棵樹的構造方法和遍歷方法,如果是純英文的話,每個節點都有一個26長度的陣列,來了一個字元,通過字元的編號直接就可以遍歷到下一個節點,查詢的時候複雜度就是O(K),K表示查詢的字串長度,這種資料結構簡單明瞭,實現起來也很容易。

 

2優化後的Trie樹

 

基本Trie樹的資料結構有個問題,就是記憶體使用得太多了,如果是中文查詢的話,需要把所有的中國字都編號到這個陣列中,記憶體就爆了,於是有一種優化方法,就是把陣列變成變長的,這種Trie樹的節點資料結構變成下面的樣子了,節點查詢變成一個順序查詢或者二分查找了。

 

 

3雙陣列Trie樹

 

所謂雙陣列Trie樹,當然就是通過兩個陣列來實現這棵樹了,這兩個陣列分別叫base陣列和check陣列,一個是基礎陣列,一個是檢查陣列。

 

Trie樹實際上是一種有限狀態機,通過狀態轉移矩陣在各個狀態之間跳轉,雙陣列Trie樹極大的節省了空間,大致就是下面這個樣子,我後面會有一篇專門的文章來說Trie樹實現的,這裡就不詳細展開了,實在等不及的可以自己先搜尋一下相關資料看看雙陣列Trie樹吧。

 

6. 時間與空間的平衡二

 

OK,三種Trie樹的實現方式都說了,現在要開始抉擇了,我們先看看這三種資料結構的時間和空間。

 

  • 第一種空間佔用大,特別是中文的情況,檢索的時間效率為O(n),其中n為每次請求的字串的長度,這種實現方式基本上屬於新人練手的水平,純粹為了瞭解這個資料結構或者大學生做做課程設計,工程化的可能性幾乎為0。

  • 第二種空間基本不浪費,但檢索的時間效率如果按照二分進行每個節點的查詢的話,每個節點的查詢時間變成了O(lg(n)),整體的查詢時間變成K*O(log(n)),同樣插入效率也變低了。

  • 第三種情況空間不浪費,時間效率也為O(n)。

 

初看,肯定選第三種了,但是!!第三種實現方式有個致命的缺陷,就是無法向下遍歷(具體可以自己看看雙陣列的實現方式),也就是說我輸入北京,找不到北京大學,北京愛上西雅圖,因為它已經不是一個樹型結構了,無法向下遍歷了。所以如果不對第三種結構進行改造的話,是無法滿足我們的功能的。要改造,最簡單的辦法就是在每個詞後面掛一個連結串列,表示這個詞的後繼詞都是什麼,像下圖這樣。

 

 

如果按上圖那麼來的話,需要輔助的空間來儲存後繼詞,那麼問題又來了,又是一次時間和空間的抉擇了,是選擇K*O(log(n))的第二種方案,然後後繼詞實時遍歷樹來獲取(又要耗費一定的時間),還是選擇選擇第三種方案,用空間換取時間呢?

 

好,既然這樣,我們來仔細算算這個賬,我們以每個節點都存一箇中文來算,雖然常用的漢字大概2500個,但其中最常用的才500左右。先看第二種方案,那麼我們大概估算出,每個節點的平均陣列長度大概600(實際上除了第一層的節點,後面的節點陣列長度完全達不到這個量級,用600屬於極限估算了),600的二分查詢大約需要7到8次,取個平均值4次,那麼每次查詢的時間就是4*K(K是字串的長度),如果我們定好最長的提示詞不超過8個字(太長也沒意義),那麼首先這個樹的高度就是8了,如果50萬的詞量的話,使用多少記憶體大概能算出來,然後每次遍歷下級節點的時間就是600^(8-K)(如果陣列的每個元素都有值),我去,這麼大,嚇死了,好,我們即便假設每個節點的陣列長度平均為60,要遍歷完也要60^(8-K),也嚇尿了,所以實時遍歷所有子節點的方式不可取,而且後繼詞最多也就提示出10個,遍歷出這麼多詞還要排序,遍歷全部節點實在是沒有必要,所以,第二種方案要麼放棄,要麼也要改造,如何改造呢?

 

因為詞基本上都是離線算好的,稍微把節點的資料結構優化一下,在節點中加一個欄位,表示哪個子節點有需要的資料(排序前10的詞),這樣往下遍歷的時候就直接遍歷相應的下標就可以了,就能把60^(8-K)這種遍歷減少到幾十次,從而找到10個提示詞,我們把這個結構叫二次優化的Trie樹

 

這一輪的時間和空間的比拼,第三個方案感覺就要勝利了,但第二個方案的優化版貌似也還能接受,一個耗費空間,查詢速度快,一個節省空間,查詢速度慢點。

 

這裡多說一下,其實上面只是預估的辦法比較搓,這麼寫是為了說預估的技能,最直接的就是拿著日誌統計一遍,得到一堆不超過8位長度的搜尋詞,同時也能演算法兩個方案的記憶體使用規模和大概的查詢效率,這樣的預估辦法最準確,但是在大部分時候我們並沒有這麼多資料,所以只能做一些基本的預估。

 

7. 離線資料處理

 

好了,我們先把檢索部分放一放,來看看離線資料處理部分吧。我們先要確定一下什麼東西需要在離線部分算好,什麼東西需要線上處理?

 

  • 首先,日誌的清洗肯定是離線部分了,我們先要把沒有搜尋結果的詞去掉,然後去掉太長的詞(假定超過8的都不要),然後保留有一定熱度的詞(比如每天搜尋量超過10次的詞),等等一些規則以後,假如剩下了50萬的詞,那這50萬就是我們的基礎資料了。

     

  • 其次,Trie樹的構建是離線構建好還是實時往服務推送由服務端去構建呢?

     

  • 還有,排序的時候是離線給每個搜尋詞打個分,然後實時排序呢?還是離線把序都排好,服務端直接使用結果呢?

 

8. 時間與空間的平衡三

 

雖然是離線處理,但一樣有時間和空間的選擇。我們先來看構建部分,Trie樹的構建是離線構建好還是實時往服務推送由服務端去構建,首先我們需要確定的是這個搜尋提示服務需不需要實時更新,一般情況下,搜尋提示沒有那麼強的實時性要求,一般一天或者兩天更新一次體驗也不會太差,所以做實時更新的搜尋提示,要不就是你實在是太蛋疼了,要不就是遇到了一個特別讓人蛋疼的產品經理(臥槽,黑了一下產品經理啊)。所以我們使用離線構建的方式構建好兩個陣列和輔助的資料結構,都存在磁碟上,服務端啟動的時候讀取檔案就行了,這是用離線時間換取的服務端的時間,是很划得來的。

 

再來看看排序的部分,很明顯,排序離線做好也比較合適,排序的位置基本不會有太大的變化,但是如果排序離線做好的話,那麼輔助的資料結構就會比較大了,因為每個字首後面跟著的10個詞都要排好序放在輔助結構中,但如果我們只是把每個詞打個分(比如就按熱度給個分),然後用第二個方案(優化的Trie樹)的儲存方式,線上的時候去排序,那麼輔助結構就會小很多,兩種情況的結構大概就是下面這樣的區別。

 

 

左邊的是全排序好了的,直接使用,雙陣列Trie樹+輔助結構方式;右邊的是隻是打了分的,優化的Trie樹,遍歷出結果以後實時排序的。離線排序的空間佔用大,即便優化一下,把詞都放一個地方單獨存著,輔助結構中只儲存詞的編號,一樣也比較佔地方,但是查詢速度快啊。線上排序的方式不怎麼佔地方,就是每個節點多了一個分數的欄位,需要實時排序一下,雖然是實時排序,但個數就10個,不管是快排還是堆排,都很快的,所以時間效率也慢不到哪去。

 

9. 整體的時空平衡

 

綜合衡量一看,我個人覺得兩種方式都能接受,具體選哪一個就仁者見仁了。

 

  • 如果搜尋詞的量比較穩定,不會有太大的變化,那麼使用雙陣列Trie樹+輔助資料結構+離線構建Trie樹+離線排序的方式更合適。

     

  • 如果搜尋詞雖然現在是50萬,但很可能會增加得比較多,或者像下圖一樣,搜尋提示的頁面還會承載很多其他的資料的話,那麼使用二次優化的Trie樹+離線構建Trie樹+離線打分+實時排序的實現方式更合適,因為能節省更多的記憶體給後續擴充詞語用或者給其他資料用。

     

  • 還有如果對速度要求苛刻,那麼就第一種,如果沒那麼苛刻,那就第二種。

 

架構設計沒有好壞,只有合適不合適。

 

10. 總結

 

上面分析了這麼一大堆,淌過三個的時間與空間的坑,終於基本確定了技術方案了,這其實也是系統架構設計中經常會要遇到的選擇了,架構師們把這些選擇做完以後,可以開始細分模組設計開發了,所以,一個小小的系統就這麼多選擇,各種空間和時間的平衡,你說架構師哪那麼好當?呵呵,你以為就畫完這篇文章的第一圖就架構結束了啊。

 

這裡只是用搜尋提示作為一個例子來說明系統設計的時候需要時時刻刻關注時間和空間這兩個因素的平衡,現在很多人設計系統的時候基本上不太關注時間,因為高配的伺服器,幾十上百GB的記憶體隨便用,所以大多數都把設計往空間上去靠,用更多的空間來換取執行效率,這本身並沒有什麼問題,誰不希望更快啊,但是有時候預估一下,有可能雖然犧牲了一點時間效率,但是換來了不少的空間,這樣的系統在資料量變大時有更多的可擴充套件空間,我覺得是非常值得的交換。

 

再有,對資料結構和演算法的瞭解以及預估算能力其實是平衡時間和空間的重要技能,也是架構設計中避坑的基本技能,所以有公司的面試題會出現請你估算一下黃河出海口的面積這類估算題,因為預估算能力也是重要的架構技能吧。