也談如何構建高性能服務端程序

  引子算法

  我接觸過不少編程語言,接觸過各類各樣的服務器端開發,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 的最新冒泡列表,當有人發佈了新的冒泡,那麼這個列表就得被更新。從這個角度來看,緩存的策略又有以下常見的幾種:async

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

  嗯,既然提到了緩存的更新或者清除,那麼就牽扯到緩存的更新策略。例子永遠好過大段的理論:假如咱們要緩存 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 } //addProject func addProject(u user, p project) { //檢查用戶權限  checkPermission(u) //啓動協程  go func() { //獲取輸入 name := <-p.name //訪問數據庫,輸出結果通道 q.result <- "add project :" + name }() } //主進程 func main() { //初始化project p := project{ make(chan string, 1), make(chan string, 1) } //某位用戶 u := user{} //執行addProject,注意執行的時候還不須要告知要建立的項目名字  addProject(u,p) //準備參數 p.name <- "an-asynchronous-project" //獲取結果 fmt.Println(<-p.result) }

  這一段程序涉及到了 Golang 的 goroutine 和 channel,不瞭解的能夠去查一下相關資料。
  這段程序實現了 在還爲準備好參數時就已經調用一個 function 。當咱們調用 addProject 的時候還不知道項目的名字,可是這徹底不影響咱們去檢查用戶權限。程序徹底能夠一邊去檢查權限,一邊去獲取項目名字,當程序執行到不得不拿到項目的名字才 能繼續的時候,它將阻塞,直到咱們告訴他項目名字。

  Concurrent

  Concurrent 的意思是並行。現代化的 CPU 每每具備多個核心,並且有些 CPU 也具備超線程能力。若是咱們能夠將單個過程拆分紅小的任務,交給 CPU 的多個核心,或者是分佈式計算系統的多個計算節點,就能夠充分利用並行計算來提高性能。前提是這些任務相互之間不要有相互依賴的關係。依然是例子:須要計 算網站上某一批用戶的活躍度積分,傳統的,咱們會查出這一批用戶,而後寫一個循環,而後輪流計算他們的積分,最後獲得結果。其實每一個用戶的積分的計算都是 獨立的,相互不依賴,那麼咱們就能夠利用這一點來並行化這個計算。

  下面給出一段 Coding 代碼託管中的程序,這段程序是指定條件獲取一個提交列表,使用了並行計算的一種 併發循環

public List<Commit> getCommits(String objectId, String path, int offset, int maxCount) { List<String> shas = getCommitsSha(this, objectId, path, offset, maxCount); List<Commit> commits = new ArrayList<>(); if (shas != null) { List<GetCommit> getCommits = new ArrayList<>(); for (String sha : shas) { getCommits.add(new GetCommit(this, sha)); } //聲明一個自適應的線程池 ExecutorService executor = Executors.newFixedThreadPool(8); List<Future<Commit>> futureList = null; //併發的調用getCommit futureList = executor.invokeAll(getCommits); executor.shutdown(); for (Future<Commit> future : futureList) { Commit commit = future.get(); commits.add(commit); } } return commits; } //Java 是一個囉嗦的語言,還要聲明一個類來包裝一下這個過程。 class GetCommit implements Callable<Commit> { private Repo repo; private String sha; public GetCommit(Repo repo, String sha) { this.repo = repo; this.sha = sha; } @Override public Commit call() throws Exception { return repo.getCommit(sha); } }

  這段程序是一個併發循環的例子,例子中須要根據一些參數查詢到 Commit 的列表,而 repo.getCommit 這個過程徹底不須要一個一個輪流查詢,由於他們是徹底獨立的,因此可使用 Java 的 Cocurrent 包來作併發循環,充分利用多核來儘快獲得執行結果。

  總結

  關於高性能服務器程序須要關注的點還有不少,這裏只是簡單的介紹了下三個利器 (Cache,Asynchronous,Concurrent)。而即使是這三個利器,個人介紹也只是冰山一角,可是請相信你看懂了我介紹的這些東西, 從新去思考服務端編程會得到很多收穫的。
  這三者也是相輔相成的關係,不少時候都是配合着使用才能起到很好的效果。異步和並行在某種程度上是有重疊的,而咱們常用異步的方式去主動構建緩存。

  最後再給一些小提示:

  • 不要讓 CPU 閒着(CPU 正常狀況下壓力大的時候天然不會閒着,這裏指的是CPU負載低谷時,可讓他主動的構建緩存,或者作一些準備工做等等。)
  • 提高 CPU 效率,即不要總讓 CPU 作重複的勞動,用空間換時間的理念去減輕 CPU 的壓力
  • 不要讓可有可無的附屬的任務卡住主進程,讓他們在後臺慢慢作
  • 能夠提早作好準備工做,這個比較抽象,可是舉例子就很明白,鏈接池,主動緩存,以及我舉得那個 Golang 的例子都是很好的例子
相關文章
相關標籤/搜索