1. 程式人生 > >也談如何構建高效能服務端程式

也談如何構建高效能服務端程式

引子:我接觸過很多程式語言,接觸過各種各樣的伺服器端開發,Java,Go,Ruby,Javascript等語言,Spring,Node.js,Rails等等常見伺服器端框架和程式設計模型都有接觸。這裡談一下我個人對高效能伺服器端程式的一些看法,希望給各位讀者一些認識。這片文章提到的內容也是 Coding(https://coding.net) 程式碼託管乃至整站都在使用的一些概念和技術。

此外,閱讀這篇文章,有如下幾個前提:不談硬體,不評論程式語言以及框架的好壞,不談高階演算法,可拍磚,拒絕噴子

三個關鍵詞

Cache,Asynchronous,Concurrent我們一個一個來講。

Cache

Cache 翻譯成中文就是快取,臺灣的叫法叫做快取,其本質是將獲取緩慢或者計算緩慢的資料結果暫時儲存起來,以便以後再次獲取或者計算同樣的資料可以直接從儲存中取得結果,從而可能提升效能的一種手段。Cache 最早是應用在計算機的 CPU 中,這篇文章不談硬體,所以有需要了解 CPU 的快取的同學可自行搜尋。

可以想象,如果讓一個人一遍一遍的從 1+2+3+4+…+99+100=? 這樣去算,他加到最後發現等於5050,而這個過程耗費了他大量的時間,耗費了大量的腦力,在此期間,他可能把所有精力都放在這個計算上面而無暇顧及其他事情。等到他累得滿頭大汗,加完了結果,他告訴你是 5050。沒過多久,你又讓他做同樣的事情,我相信這傢伙會不加思索的再次告訴你 5050。為什麼?你會笑我說,人又不是傻子,這為同學肯定記得這個結果是5050啊。

可是,計算機不一樣,計算機就是你上面要嘲笑的那個傻子,他傻到,完全不會記得剛在做了什麼事情,他會傻乎乎的再重新算一遍告訴你結果。沒錯如果你問他一萬遍,這頭沒有腦子的機器會算一萬遍的。雖然上面這個從1加到100這個例子對於一款現代化的計算機來講簡直是小菜一碟,但是計算機往往面臨的計算難題是我們人類所無法企及的。

Cache 就是為了來解決這個事情的,因為事情往往是這樣的:你會發現一些非常複雜的過程的計算結果是可重用的,而且把這個結果暫時儲存在某些地方,查詢起來也是極為方便的。

所以,現在你理解了快取,那可以來思考一些快取的設計策略了。這裡做一點說明,不同的快取策略跟具體的業務系統關係非常大,制定快取策略需要根據具體的情況來分析。常用的策略:

  • 最終結果型快取。這種快取往往提升效能效果最為明顯,但是命中率卻低,也就是可重用性不高。
  • 中間結果型快取。還拿上面的例子來說,1加到100,你可以構建出是個快取分別是1加到10,10加到20,20加到30 … 一直到 90加到100 這9個快取。好處是你如果被請求到 1加到60 的時候,仍然可以使用這些快取結果。可壞處也很明顯,你取到幾個快取的結果後不得不再進行一次運算。所以實際情況,往往是在最終結果和中間結果之間找到平衡點,或者是兩者配合使用。

不知不覺中,你有沒有發現,1+2+3+4+…+99+100=5050 是個永遠都成立的事實,這也就意味著,它永遠不用被清除。可事實是往往是,快取是有有效期的,例如需要快取今天的天氣情況,今天是 2014年11月16日,到了明天就是 11月17日,天氣就不一樣了。再例如需要快取 Coding 的最新冒泡列表,當有人釋出了新的冒泡,那麼這個列表就得被更新。從這個角度來看,快取的策略又有如下常見的幾種:

  • 永久式快取:結果在任何情況下都不發生改變,無需清除或者更新
  • 有有效期的快取:在特定時間點或者時間段後失效
  • 觸發式失效快取:當某一事件產生時,快取失效,當然有有效期式快取也可以理解成時間點和時間段到期為觸發條件的觸發式失效快取

嗯,既然提到了快取的更新或者清除,那麼就牽扯到快取的更新策略。例子永遠好過大段的理論:假如我們要快取 Coding 的冒泡列表。有這麼一種策略:當用戶請求時我們檢查下是否已存在這樣的快取,如果有直接返回快取資料,否則我們生成這個列表(計算機的計算過程),返回給使用者並且把冒泡列表(計算結果)儲存起來,以便以後的使用者訪問時直接獲取。當用戶釋出了一個新的冒泡的時候,我們清除這個快取,再有使用者請求時將重複以上過程。這是其中一種完整的快取清除策略。另外一種是,每當我們收到一個使用者釋出的冒泡時,都重新構建這個快取,使用者每次檢視冒泡列表都是取的快取資料。這兩種快取分別稱之為:

  • 被動式快取:需要用到時才構建
  • 主動式快取:預先構建

關於 Cache 還有很多很多需要注意和設計上的思路和策略,這裡不再一一贅述。這些快取在不同的維度有不同的策略,我們需要根據具體的業務情況來選擇合適的策略。Coding 的很多業務中使用了上述很多種策略,例如我們常見的分支列表和標籤列表就是使用觸發式失效快取,我們的廣場專案列表就是使用主動式快取構建。

Asynchronous

Asynchronous 的意思是非同步。什麼是非同步呢?就是不在第一時間告知呼叫者結果,告訴他我已經收到這個任務了,我會處理,處理完畢後通知你結果,如果你不是等不到結果就無法進行下去的話,你完全可以先幹別的事情。嗯,好像我描述的比較拉雜。還是例子:你去咖啡廳點一杯咖啡,服務員告訴你現磨咖啡需要15分鐘才可做好,那麼在咖啡做好之前,你不可能盯著服務員或者咖啡師15分鐘,你肯定會幹點別的,比如說玩手機上一下網,或者跟你女朋友商量下去看電影什麼的,總之你不會傻乎乎等著的。等到咖啡做好了,服務員會記得給你端過來的。這就是非同步過程,你的大腦不必為一個漫長的過程卡住,可以繼續其他的事情。

服務端程式設計往往也是這樣,在你等待一個很緩慢的過程的時候,如果你不是必須要得到這個過程的結果才能繼續下去,你完全可以先進行別的過程,等到那個緩慢的過程執行完畢後,它會通知你結果的。

非同步已經在現在的各種程式設計領域有了很廣泛的應用,例如 Ajax 技術,就是一種非同步的手段,在瀏覽器和伺服器互動的時候,完全不影響你在網頁上的其他操作。

非同步在各種程式語言和框架中都有相應的支援,這裡簡單介紹一下 Javascript 的非同步支援。熟悉它的人的人請無視這段。它使用回撥的方式支援非同步,大致意思是,A 交代給 B 一個任務,並且告知 B 任務完成後繼續執行哪段程式(往往包裝成一個匿名function),B執行完任務後,執行這個匿名的 function,這樣來完成非同步過程。在 Javascript 中大量的使用這種回撥的非同步方案,已經不再侷限於對一個緩慢的過程了,可以對幾乎所有的過程都採用非同步處理。

在服務端程式中,除了使用執行緒,協程,回撥之外,另外一種常見的非同步的支援方式就是訊息佇列。其原理是,生產者傳送訊息到訊息佇列中,消費者從中取出訊息,做出相應處理,並把結果儲存起來或者通過某種方式告知生產者。

非同步在很多時候可以運用現代化計算機 CPU 的多核特性和分散式計算特性,能顯著的提升應用的效能,但是一個前提就是,非同步的任務的結果必須是主程序進行下一步操作所不依賴的,否則主程序必須等待,直到這個任務執行結束,拿到結果再進行下一步,這時就變成了傳統的同步計算了。

非同步操作在 Coding 中也有非常廣泛的應用。例如當用戶執行完一次 Push,Coding 需要生成一條 Push 的動態,需要清理掉相應的快取,需要觸發相關的 WebHook 等等,這些操作都是通過訊息佇列來非同步完成的。因為這些操作非常的耗時,而且完全不需要即時完成,所以使用者在 Push 的時候等待著這些操作完成是很不合理的。非同步操作在這裡即展示出了其應用多核和多臺伺服器的優勢,在某種程度上還能提升使用者體驗。

Golang 是 Google 2009 年釋出的一門現代化語言,其語言特性對非同步提供了良好的支援。這裡舉個例子體現一下非同步的魅力:

//一個結構體
type project struct {
    //引數Channel
    name chan string
    result chan string
}