在前面的文章中,說了很多JVM和數(shù)據(jù)庫方面的東西,我所描述的內(nèi)容大多偏重于技術(shù)本身,和實際的業(yè)務(wù)系統(tǒng)結(jié)合的比較少,本文開始進入實際的系統(tǒng)設(shè)計中應(yīng)當注意的方方面面(文章偏重于訪問量高,但是每次訪問量并不是很大的系統(tǒng)),而偏重點在于性能和效率本身,由于這個知識涉及的基礎(chǔ)和面很廣,所以建議是先看下以前寫的內(nèi)容或自己有一定的基礎(chǔ)來才開始接觸比較好,另外本文也不能詮釋性能的關(guān)鍵,從一個應(yīng)用系統(tǒng)前端到后端涉及的部分非常多,本文也只會說明其中一部分,后續(xù)的部分我們再繼續(xù)說;下面我們想一下一個web應(yīng)用絕大部分請求的整個過程:client發(fā)出請求->server開始響應(yīng)并創(chuàng)建請求對象及反饋對象->如果沒有用戶對象就創(chuàng)建session信息->調(diào)用業(yè)務(wù)代碼->業(yè)務(wù)代碼分層組織數(shù)據(jù)->調(diào)用數(shù)據(jù)(從某個遠程或數(shù)據(jù)庫或文件等)->開始組織輸出數(shù)據(jù)->反饋數(shù)據(jù)開始通過模板引擎進行渲染->渲染完成未靜態(tài)文件向客戶端進行輸出->待客戶端接收完成結(jié)束掉請求對象(這種請求針對短連接,長連接有所區(qū)別)。 就從前端說起吧,說下一下幾個內(nèi)容: 1、線程數(shù)量 2、內(nèi)容輸出 3、線程上下文切換 4、內(nèi)存
1.首先說下線程數(shù)量,線程數(shù)量很多人認為在配置服務(wù)器的線程數(shù)量時認為越多越好,各大網(wǎng)站上很多人也給出了自己的測試數(shù)據(jù),也有人說了每個CPU配置多少線程為合適(比如有人說過每個CPU給25個線程較為合適),但是沒有一個明確的為什么,其實這個要和CPU本身的運行效率和上來說明,并非一概而論的,也需要考慮每個請求所持有的CPU開銷大小以及其處于非Running狀態(tài)的時間來說明,線程配置得過多,其實往往會形成CPU的征用調(diào)度問題,要比較恰當將CPU用滿才是性能的最佳狀態(tài)(說到線程就不得不說下CPU,因為線程就是消耗CPU的,其本身持有的內(nèi)存片段非常小,前面文章已經(jīng)說明了它的內(nèi)存使用情況,所以我們主要是討論它與CPU之間的關(guān)系)。 首先內(nèi)存到CPU的延遲在幾十納秒,雖然CPU內(nèi)部的三級緩存比這個更加小,但是幾乎對于我們所能識別的時間來講可以被忽略;另外內(nèi)存與CPU之間的帶寬也是以最少幾百M每秒的速度通信,所以對于內(nèi)存與CPU交互數(shù)據(jù)的時間開銷對于常規(guī)的高并發(fā)小請求的應(yīng)用客戶忽略掉,我們只計算本身的計算延遲開銷以及非計算的等待開銷,這些都一般會用毫秒來計算,相互之間是用10e6的級別來衡量,所以前者可以忽略,我們可以認為處于running的時間就是CPU實際執(zhí)行的時間,因為這種短暫的時間也很難監(jiān)控出來到底用了多久。 那么首先可以將線程的運行狀態(tài)劃分為兩大類,就是:運行與等待,我們不考慮被釋放的情況,因為線程池一般不會釋放線程,至于等待有很多種,我們都認為它是等待就可以了;為什么是這兩種呢,這兩種正好對應(yīng)了CPU是否在被使用,running狀態(tài)的線程就在持有CPU的占用,等待的就處于沒有使用CPU。 再明確一個概念,一個常規(guī)的web請求,后臺對應(yīng)一個線程對它的請求進行處理,同一個線程在同一個時間片上只能請求一個CPU為他進行處理,也就是說我們可以認為它不論請求過多少次CPU、不論請求了多少個CPU,只要這些CPU的型號是一樣的,我們就可以認為它是請求的一個CPU(注意這里的CPU不包含多個core的情況,因為多個core的CPU只能說明這個CPU的處理速度可以接近于多個CPU的速度,而真正對線程的請求來講,它認為這是一個CPU,在主板上也是一個插槽,所以計算CPU的時候不考慮多核心)。 最后明確線程在什么情況下會發(fā)生等待,比如讀取數(shù)據(jù)庫時,數(shù)據(jù)庫尚未反饋內(nèi)容之前,該線程是不會占用CPU的,只會處理等待;類似的是向客戶端輸出、線程為了去持有鎖的等待一系列的情況。 此時一個線程過來如果一個線程毫無等待(這種情況不存在,只是一種假設(shè)),不論它處理多久,處理時間長度多長,此時如果只有一個CPU,那么這個應(yīng)用服務(wù)器只需要一個1個線程就足以支撐,因為線程沒有等待,那么CPU就沒有停止運行,1個線程處理完這個請求后,接著就處理下一個請求,CPU一直是滿的,也幾乎沒有太大的征用,此時1個線程就是最佳的,如果是多個同型號的CPU,那么就是CPU數(shù)量的線程是最佳的;不過這個例子比較極端,在很多類似的情況下,大家喜歡用CPU+1或CPU-1來完成對類似情況的線程設(shè)置,為了保證一些特殊情況的發(fā)生。 那么考慮下實際的情況,如果有等待,這個等待不是鎖等待的(因為鎖等待有瓶頸,瓶頸在于CPU的個數(shù)對于他們無效),應(yīng)該如何考慮呢?我們此時來考慮下這個等待的時間長度應(yīng)該如何去考慮,假如等待的時間長度為100ms,而運行的時間長度為10ms,那么在等待的這100ms中,就可以有另外10個線程進來,對CPU進行占用,也就是說對于單個CPU來說,11個線程就可以占滿整個CPU的使用,如果是多個CPU當然在理論上可以乘以CPU的個數(shù),這里再次強調(diào),這里的CPU個數(shù)是物理的,而不算多核,多核在這里的意義比如以前一個CPU處理一個線程需要30ms,現(xiàn)在采用4個core,只需要處理10ms了,在這里體現(xiàn)了速度,所以計算是不要用它來計算。 那么對于鎖等待呢?這個有點麻煩了,因為這個和模塊有關(guān)系,這里也只能說明某個有鎖等待的模塊要達到最佳狀態(tài)的訪問效率可以配置的線程數(shù),首先要明確鎖等待已經(jīng)沒有CPU個數(shù)的概念,不論多少個CPU,只要運行到這段代碼,他們就是一個CPU,不然鎖就沒有存在的意義了;另外,假如訪問是非常密集的,那么當某個線程持有鎖并訪問的時候,其他沒有得到的運行到這個位置都會處于等待,我們將一個模塊的所有有鎖等待的時間集中在一起,只有當前一個線程將具有鎖的這段代碼運行完成后,下一個線程才可以繼續(xù)運行,所以它其他地方都沒有瓶頸,或者說其他地方理論配置的線程數(shù)都會很高,唯獨遇到這個地方就會很慢,假如一個線程從運行代碼時長為20ms,等待事件為100ms,鎖等待為20ms,此時假如該線程沒有受到任何等待就是140ms即可運行完成,而當多個線程同時并發(fā)到這里的時候,后續(xù)每個線程將會等待20*N的時間長度,當有7個線程的時候,恰好排滿運行的隊列,也就是當又7個線程訪問這個模塊的時候,理論上剛好達到每個線程順序執(zhí)行而且成流水線狀態(tài),但是這里不能乘以CPU的個數(shù)了,為什么,你懂的。
2.內(nèi)容輸出,其實內(nèi)容輸出有很多種方法,在java方面,你可以自己編寫OutputStream或者PrintWriter去輸出,也可以用渲染模板去渲染輸出,渲染的模板也有很多,最常見的就是JSP模板來渲染,也有velocity等各種各樣的渲染模板,當然對于頁面來講只能用渲染模板去做,不過異步請求你可以選擇,在選擇時要對應(yīng)選擇才能將效果做得比較好。 說到這里不得不說下velocity這個東西,也就是經(jīng)??吹降膙m的文件,這種文件和JSP一樣都是渲染模板的方法,只是語法格式有所區(qū)別,velocity是新出來的東西,很多人認為新的東西肯定很好,其實velocity是渲染效率很低的,在內(nèi)容較小的輸出上對性能進行壓力測試,其單位時間內(nèi)所能承受的訪問量,比JSP渲染模板要低好幾倍,不過對較大的數(shù)據(jù)輸出和JSP差不多,也就是頁面輸出使用velocity無所謂的,而且效果比JSP要好,但是類似ajax交互中的小數(shù)據(jù)輸出建議不要使用vm模板引擎,使用JSP模板引擎甚至于直接輸出是最佳的方式。 說到這里JSP模板引擎在輸出時是會被預(yù)先編譯為java的class文件,VM是解釋執(zhí)行的,所以小文件兩者性能差距很大,當遇到大數(shù)據(jù)輸出時,其實大部分時間在輸出文件的過程中,解釋時間幾乎就可以被忽略掉了。 那么JSP輸出小文件是不是最快的呢?未必,JSP的輸出其實是將JSP頁面的內(nèi)容組成字符串,最終使用PrintWriter流取完成,中間跳轉(zhuǎn)交互其實還是蠻多的,而且有部分容器在組裝字符串的時候竟然用+,這個讓我很是郁悶啊,所以很多時候小數(shù)據(jù)的輸出,我還是喜歡自己寫,經(jīng)過測試得到的結(jié)果是使用OutputStream的性能將會比PrintWriter高一些,(至于高多少,大家可以自己用工具或?qū)懘a測試下就知道了,這里可能單個處理速度幾乎看不出區(qū)別,要并發(fā)訪問看下平均每秒能處理的請求數(shù)就會有區(qū)別了),字符集方面,在獲取要輸出內(nèi)容的時候,指定byte的字符集,如:String.getByte(“字符集”),一般這類輸出也不會有表頭,只需要和接收方或者叫瀏覽器一致就可以了(有些接收方可能是請求方);其實OutputStream比PrintWriter快速的原因很簡單,在底層運行和傳輸?shù)倪^程中,始終采用二進制流來完成,即使是字符也需要轉(zhuǎn)換成byte格式,在轉(zhuǎn)換前,它需要去尋找很多的字符集關(guān)系,最終定位到應(yīng)該如何去轉(zhuǎn)換,內(nèi)部代碼看過一下就明白,內(nèi)部的方法調(diào)用非常多,一層套一層,相應(yīng)占用的CPU開銷也會升高。 總結(jié)起來說,如果你有vm模板引擎,在頁面請求時建議使用vm模板引擎來做,因為代碼要規(guī)范一些,而且也很好用;另外如果在簡單的ajax請求,返回數(shù)據(jù)較小的情況下,建議使用OutputStream直接輸出,這個輸出可以放在你的BaseAction的中,對實現(xiàn)類中是透明的,實現(xiàn)類只需要將處理的反饋結(jié)果數(shù)據(jù)放在一個地方,由父類完成統(tǒng)一的輸出即可,此處將Ajax類的調(diào)用可以獨立一個父親類出來,這樣繼承后就不用關(guān)心細節(jié)了。 輸出中文件和大數(shù)據(jù)將是一個問題,對于文件來說,尤其是大文件,在前面文章已經(jīng)說明,輸出時壓縮只能節(jié)省服務(wù)器輸出時和客戶端的流量,從而提高下載速度,但是絕對不會提高服務(wù)器端的性能,因為服務(wù)器端是通過消耗CPU去做動作,而且壓縮的這個過程是需要時間的,這種只會降低速度,而絕對不會提高;那么大文件的方法就是一種是將大文件提前壓縮好存放,如果實在太大,需要考慮采用斷點傳送,并將文件分解。 對大數(shù)據(jù)來講,和文件類似,不過數(shù)據(jù)可能對我們要好處理一點,需要控制訪問頻率甚至于直接在超過訪問頻率下拒絕訪問請求,每次請求的量也需要控制,如果對特殊大的數(shù)據(jù)量,建議采用異步方式輸出到文件并壓縮后,再由客戶端下載,這樣不論是客戶端還是服務(wù)器端都是有好處的。
3、線程上下文切換,對于線程的上下文切換,在一般的系統(tǒng)中基本遇不到,不過一些特殊應(yīng)用會遇到,比如剛才的異步導(dǎo)出的功能,請求的線程只是將事情提交上去,但是不是由它去下載,而是由其他線程再去處理這個問題,處理完成后再回寫某個狀態(tài)即可;在javaNIO中是非常的多,NIO是一種高性能服務(wù)器的解決方案,在有限的線程資源情況下,對極高并發(fā)的小請求,并存在很多推拉數(shù)據(jù)的情況下是很有效的,最大的要求就是服務(wù)器要有較好的連接支撐能力,NIO細節(jié)不用多說,理解上就是異步IO,把事情交給異步的一個線程去做,但是它也未必馬上做,它做完再反饋,這段時間交給你的這個線程不是等待而是去做其他的事情,充分利用線程的資源,處理完反饋結(jié)果的線程也未必是開始請求的線程,幾個來來回回是有很多的開銷的,總體其實效率上未必有單個請求好,但是對服務(wù)器的性能發(fā)揮是非常有效的。 線程之間的開銷大小也要看具體應(yīng)用情況以及配置情況決定,此時將任務(wù)和線程沒有做一個一對一的綁定,而是放一堆事情在隊列中,處理線程也有很多,誰有時間處理誰就處理它,每個線程都做自己這一類的事情,甚至于將一些內(nèi)容交給遠程去做,交互后就不管了,結(jié)果反饋的時候,這邊再由一個線程去處理結(jié)果請求即可。 在整個過程中會涉及到一次或多次的線程切換,這個過程中的開銷在某些時候也是不小的,關(guān)鍵還是要看應(yīng)用場景,不能一概而論。 4、內(nèi)存,最后還是內(nèi)存,其實這里我就不想多說了,因為前面幾篇文章說得太多了,不論是理論上還是實現(xiàn)上,以及經(jīng)驗上都說了非常多,不過可以說明的一點就是內(nèi)存的問題絕大部分來源于代碼,而代碼有很大一部分可能性來源于工程的程序員編寫或者框架,第三方包的內(nèi)存問題相對較少,一般被開源出來的包內(nèi)存溢出的可能性不大,但是不排除有寫得比較爛的代碼;二方包呢,一般指代公司內(nèi)部人員封裝的包,如果在經(jīng)過很多項目的驗證可以比較放心使用,要絕對放心的話還是需要看看源碼才行,至于JVM本身的BUG一般不要找到這個上面來,雖然也有這種可能性,不過這種問題除了升級JVM外也沒有太多的辦法,修改它的源碼的可能性不大,除非你真的太厲害了(這里在內(nèi)存上一般是指C或C++語言的源碼,java部分的基礎(chǔ)類包這些代碼如果真的有問題,還是比較容易修改的,但還是不建議自己刻意去修改,除非你能肯定有你更好的解決方案而且是穩(wěn)定有效的);在編寫代碼的時候?qū)⒛切┛梢蕴崆白龅氖虑樽隽耍ū热邕@個事情以后會反復(fù)做,重復(fù)做,而且都是一樣的,那么可以提前做一次,以后就不用做了),那些邏輯是可以省掉的,最后是如果你的應(yīng)用很特殊是不是更好的解決方案和算法來完成。
總結(jié)下,從今天提到的系統(tǒng)設(shè)計的角度來說,影響QPS的最關(guān)鍵的東西就是模板渲染,它會占據(jù)請求的很大一部分時間,而且這個東西可以做非常大的改進,比如:壓縮空白字符、重復(fù)對象的簡化和模板化、大數(shù)據(jù)和重復(fù)信息的CSS化、盡量將輸出轉(zhuǎn)化為網(wǎng)絡(luò)可以直接接受的內(nèi)容;而其次就是如何配置線程,配置得太少,CPU的開銷一直處于一種比較閑的狀態(tài),而配置得太多,CPU的征用情況比較嚴重,沒有建議值,只要最適合應(yīng)用場景的值,不過你的代碼如果沒有太多的同步,線程最少應(yīng)該設(shè)置為CPU的格式+1或-1個;上下文切換對常規(guī)應(yīng)用一般不要使用,對特殊的應(yīng)用要注意中間的切換開銷應(yīng)該如何降低;文件輸出上講提前做的壓縮提前做掉,注意控制訪問頻率和單次輸出量;最后內(nèi)存上多多注意代碼,配置上只需要控制好常規(guī)的幾個參數(shù),其余的在沒有特殊情況不要修改默認的配置。
擴展,那么關(guān)于一個系統(tǒng)的架構(gòu)中是不是就這么一點就完了呢,當然不是,這應(yīng)該說說出了一個常見的OLTP系統(tǒng)的一些常見的性能指標,但是還有很內(nèi)容,比如:緩存、宕機類異常處理、session切換、IO、數(shù)據(jù)庫、分布式、集群等都是這方面的關(guān)鍵內(nèi)容,尤其是IO也是當今系統(tǒng)中性能瓶頸的最主要原因之一;在后續(xù)的文章中會逐步說明一些相關(guān)的解決方案。 |
|