也談如何構(gòu)建高性能服務(wù)端程序
當(dāng)前位置:點(diǎn)晴教程→知識(shí)管理交流
→『 技術(shù)文檔交流 』
引子 我接觸過(guò)很多編程語(yǔ)言,接觸過(guò)各種各樣的服務(wù)器端開(kāi)發(fā),Java,Go,Ruby,Javascript等語(yǔ)言,Spring,Node.js,Rails等等常見(jiàn)服務(wù)器端框架和編程模型都有接觸。這里談一下我個(gè)人對(duì)高性能服務(wù)器端程序的一些看法,希望給各位讀者一些認(rèn)識(shí)。這片文章提到的內(nèi)容也是 Coding(https://coding.net) 代碼托管乃至整站都在使用的一些概念和技術(shù)。 此外,閱讀這篇文章,有如下幾個(gè)前提:不談?dòng)布?,不評(píng)論編程語(yǔ)言以及框架的好壞,不談高級(jí)算法,可拍磚,拒絕噴子 三個(gè)關(guān)鍵詞 Cache,Asynchronous,Concurrent CacheCache 翻譯成中文就是緩存,臺(tái)灣的叫法叫做快取,其本質(zhì)是將獲取緩慢或者計(jì)算緩慢的數(shù)據(jù)結(jié)果暫時(shí)存儲(chǔ)起來(lái),以便以后再次獲取或者計(jì)算同樣的數(shù)據(jù)可以直接從存儲(chǔ)中取得結(jié)果,從而可能提升性能的一種手段。Cache 最早是應(yīng)用在計(jì)算機(jī)的 CPU 中,這篇文章不談?dòng)布杂行枰私?CPU 的緩存的同學(xué)可自行搜索。 可以想象,如果讓一個(gè)人一遍一遍的從 1+2+3+4+…+99+100=? 這樣去算,他加到最后發(fā)現(xiàn)等于5050,而這個(gè)過(guò)程耗費(fèi)了他大量的時(shí)間,耗費(fèi)了大量的腦力,在此期間,他可能把所有精力都放在這個(gè)計(jì)算上面而無(wú)暇顧及其他事情。等到他累得滿頭大汗,加完了結(jié)果,他告訴你是 5050。沒(méi)過(guò)多久,你又讓他做同樣的事情,我相信這家伙會(huì)不加思索的再次告訴你 5050。為什么?你會(huì)笑我說(shuō),人又不是傻子,這為同學(xué)肯定記得這個(gè)結(jié)果是5050啊。 可是,計(jì)算機(jī)不一樣,計(jì)算機(jī)就是你上面要嘲笑的那個(gè)傻子,他傻到,完全不會(huì)記得剛在做了什么事情,他會(huì)傻乎乎的再重新算一遍告訴你結(jié)果。沒(méi)錯(cuò)如果你問(wèn)他一萬(wàn)遍,這頭沒(méi)有腦子的機(jī)器會(huì)算一萬(wàn)遍的。雖然上面這個(gè)從1加到100這個(gè)例子對(duì)于一款現(xiàn)代化的計(jì)算機(jī)來(lái)講簡(jiǎn)直是小菜一碟,但是計(jì)算機(jī)往往面臨的計(jì)算難題是我們?nèi)祟愃鶡o(wú)法企及的。 Cache 就是為了來(lái)解決這個(gè)事情的,因?yàn)槭虑橥沁@樣的:你會(huì)發(fā)現(xiàn)一些非常復(fù)雜的過(guò)程的計(jì)算結(jié)果是可重用的,而且把這個(gè)結(jié)果暫時(shí)存儲(chǔ)在某些地方,查找起來(lái)也是極為方便的。 所以,現(xiàn)在你理解了緩存,那可以來(lái)思考一些緩存的設(shè)計(jì)策略了。這里做一點(diǎn)說(shuō)明,不同的緩存策略跟具體的業(yè)務(wù)系統(tǒng)關(guān)系非常大,制定緩存策略需要根據(jù)具體的情況來(lái)分析。常用的策略:
不知不覺(jué)中,你有沒(méi)有發(fā)現(xiàn),1+2+3+4+…+99+100=5050 是個(gè)永遠(yuǎn)都成立的事實(shí),這也就意味著,它永遠(yuǎn)不用被清除??墒聦?shí)是往往是,緩存是有有效期的,例如需要緩存今天的天氣情況,今天是 2014年11月16日,到了明天就是 11月17日,天氣就不一樣了。再例如需要緩存 Coding 的最新冒泡列表,當(dāng)有人發(fā)布了新的冒泡,那么這個(gè)列表就得被更新。從這個(gè)角度來(lái)看,緩存的策略又有如下常見(jiàn)的幾種:
嗯,既然提到了緩存的更新或者清除,那么就牽扯到緩存的更新策略。例子永遠(yuǎn)好過(guò)大段的理論:假如我們要緩存 Coding 的冒泡列表。有這么一種策略:當(dāng)用戶請(qǐng)求時(shí)我們檢查下是否已存在這樣的緩存,如果有直接返回緩存數(shù)據(jù),否則我們生成這個(gè)列表(計(jì)算機(jī)的計(jì)算過(guò)程),返回給用戶并且把冒泡列表(計(jì)算結(jié)果)存儲(chǔ)起來(lái),以便以后的用戶訪問(wèn)時(shí)直接獲取。當(dāng)用戶發(fā)布了一個(gè)新的冒泡的時(shí)候,我們清除這個(gè)緩存,再有用戶請(qǐng)求時(shí)將重復(fù)以上過(guò)程。這是其中一種完整的緩存清除策略。另外一種是,每當(dāng)我們收到一個(gè)用戶發(fā)布的冒泡時(shí),都重新構(gòu)建這個(gè)緩存,用戶每次查看冒泡列表都是取的緩存數(shù)據(jù)。這兩種緩存分別稱之為:
關(guān)于 Cache 還有很多很多需要注意和設(shè)計(jì)上的思路和策略,這里不再一一贅述。這些緩存在不同的維度有不同的策略,我們需要根據(jù)具體的業(yè)務(wù)情況來(lái)選擇合適的策略。Coding 的很多業(yè)務(wù)中使用了上述很多種策略,例如我們常見(jiàn)的分支列表和標(biāo)簽列表就是使用觸發(fā)式失效緩存,我們的廣場(chǎng)項(xiàng)目列表就是使用主動(dòng)式緩存構(gòu)建。 Asynchronous Asynchronous 的意思是異步。什么是異步呢?就是不在第一時(shí)間告知調(diào)用者結(jié)果,告訴他我已經(jīng)收到這個(gè)任務(wù)了,我會(huì)處理,處理完畢后通知你結(jié)果,如果你不是等不到結(jié)果就無(wú)法進(jìn)行下去的話,你完全可以先干別的事情。 服務(wù)端程序設(shè)計(jì)往往也是這樣,在你等待一個(gè)很緩慢的過(guò)程的時(shí)候,如果你不是必須要得到這個(gè)過(guò)程的結(jié)果才能繼續(xù)下去,你完全可以先進(jìn)行別的過(guò)程,等到那個(gè)緩慢的過(guò)程執(zhí)行完畢后,它會(huì)通知你結(jié)果的。 異步已經(jīng)在現(xiàn)在的各種編程領(lǐng)域有了很廣泛的應(yīng)用,例如 Ajax 技術(shù),就是一種異步的手段,在瀏覽器和服務(wù)器交互的時(shí)候,完全不影響你在網(wǎng)頁(yè)上的其他操作。 異步在各種編程語(yǔ)言和框架中都有相應(yīng)的支持,這里簡(jiǎn)單介紹一下 Javascript 的異步支持。熟悉它的人的人請(qǐng)無(wú)視這段。它使用回調(diào)的方式支持異步,大致意思是,A 交代給 B 一個(gè)任務(wù),并且告知 B 任務(wù)完成后繼續(xù)執(zhí)行哪段程序(往往包裝成一個(gè)匿名function),B執(zhí)行完任務(wù)后,執(zhí)行這個(gè)匿名的 function,這樣來(lái)完成異步過(guò)程。在 Javascript 中大量的使用這種回調(diào)的異步方案,已經(jīng)不再局限于對(duì)一個(gè)緩慢的過(guò)程了,可以對(duì)幾乎所有的過(guò)程都采用異步處理。 在服務(wù)端程序中,除了使用線程,協(xié)程,回調(diào)之外,另外一種常見(jiàn)的異步的支持方式就是消息隊(duì)列。其原理是,生產(chǎn)者發(fā)送消息到消息隊(duì)列中,消費(fèi)者從中取出消息,做出相應(yīng)處理,并把結(jié)果存儲(chǔ)起來(lái)或者通過(guò)某種方式告知生產(chǎn)者。 異步在很多時(shí)候可以運(yùn)用現(xiàn)代化計(jì)算機(jī) CPU 的多核特性和分布式計(jì)算特性,能顯著的提升應(yīng)用的性能,但是一個(gè)前提就是,異步的任務(wù)的結(jié)果必須是主進(jìn)程進(jìn)行下一步操作所不依賴的,否則主進(jìn)程必須等待,直到這個(gè)任務(wù)執(zhí)行結(jié)束,拿到結(jié)果再進(jìn)行下一步,這時(shí)就變成了傳統(tǒng)的同步計(jì)算了。 異步操作在 Coding 中也有非常廣泛的應(yīng)用。例如當(dāng)用戶執(zhí)行完一次 Push,Coding 需要生成一條 Push 的動(dòng)態(tài),需要清理掉相應(yīng)的緩存,需要觸發(fā)相關(guān)的 WebHook 等等,這些操作都是通過(guò)消息隊(duì)列來(lái)異步完成的。因?yàn)檫@些操作非常的耗時(shí),而且完全不需要即時(shí)完成,所以用戶在 Push 的時(shí)候等待著這些操作完成是很不合理的。異步操作在這里即展示出了其應(yīng)用多核和多臺(tái)服務(wù)器的優(yōu)勢(shì),在某種程度上還能提升用戶體驗(yàn)。 Golang 是 Google 2009 年發(fā)布的一門(mén)現(xiàn)代化語(yǔ)言,其語(yǔ)言特性對(duì)異步提供了良好的支持。這里舉個(gè)例子體現(xiàn)一下異步的魅力: //一個(gè)結(jié)構(gòu)體 這一段程序涉及到了 Golang 的 goroutine 和 channel,不了解的可以去查一下相關(guān)資料。 ConcurrentConcurrent 的意思是并行?,F(xiàn)代化的 CPU 往往具有多個(gè)核心,而且有些 CPU 也具有超線程能力。如果我們可以將單個(gè)過(guò)程拆分成小的任務(wù),交給 CPU 的多個(gè)核心,或者是分布式計(jì)算系統(tǒng)的多個(gè)計(jì)算節(jié)點(diǎn),就可以充分利用并行計(jì)算來(lái)提升性能。前提是這些任務(wù)相互之間不要有相互依賴的關(guān)系。依然是例子:需要計(jì)算網(wǎng)站上某一批用戶的活躍度積分,傳統(tǒng)的,我們會(huì)查出這一批用戶,然后寫(xiě)一個(gè)循環(huán),然后輪流計(jì)算他們的積分,最后得到結(jié)果。其實(shí)每個(gè)用戶的積分的計(jì)算都是獨(dú)立的,相互不依賴,那么我們就可以利用這一點(diǎn)來(lái)并行化這個(gè)計(jì)算。 下面給出一段 Coding 代碼托管中的程序,這段程序是指定條件獲取一個(gè)提交列表,使用了并行計(jì)算的一種 并發(fā)循環(huán): public List<Commit> getCommits(String objectId, String path, int offset, int maxCount) { 這段程序是一個(gè)并發(fā)循環(huán)的例子,例子中需要根據(jù)一些參數(shù)查詢到 Commit 的列表,而 repo.getCommit 這個(gè)過(guò)程完全不需要一個(gè)一個(gè)輪流查詢,因?yàn)樗麄兪峭耆?dú)立的,所以可以使用 Java 的 Cocurrent 包來(lái)做并發(fā)循環(huán),充分利用多核來(lái)盡快得到執(zhí)行結(jié)果。 總結(jié) 關(guān)于高性能服務(wù)器程序需要關(guān)注的點(diǎn)還有很多,這里只是簡(jiǎn)單的介紹了下三個(gè)利器(Cache,Asynchronous,Concurrent)。而即便是這三個(gè)利器,我的介紹也只是冰山一角,但是請(qǐng)相信你看懂了我介紹的這些東西,重新去思考服務(wù)端編程會(huì)獲得不少收獲的。 最后再給一些小提示:
該文章在 2014/12/2 23:51:37 編輯過(guò) |
關(guān)鍵字查詢
相關(guān)文章
正在查詢... |