《Web服務(wù)端性能提升實踐》要點:
本文介紹了Web服務(wù)端性能提升實踐,希望對您有用。如果有疑問,可以聯(lián)系我們。
隨著互聯(lián)網(wǎng)的不斷發(fā)展,日常生活中越來越多的需求通過網(wǎng)絡(luò)來實現(xiàn),從衣食住行到金融教育,從口袋到身份,人們無時無刻不依賴著網(wǎng)絡(luò),而且越來越多的人通過網(wǎng)絡(luò)來完成自己的需求.
作為直接面對來自客戶請求的Web服務(wù)端,無疑要同時承受更多的請求,并為用戶提供更好的體驗.這個時候Web端的性能常常會成為業(yè)務(wù)發(fā)展的瓶頸,提升性能刻不容緩.本文作者在開發(fā)過程中總結(jié)了一些提升Web服務(wù)端性能的經(jīng)驗,與大家分享.
對于Web服務(wù)端性能,首先我們分析一下相關(guān)指標(biāo).從用戶角度講,用戶調(diào)用Web服務(wù)時,請求返回時間越短,用戶體驗越好.從服務(wù)端角度講,同一時間能承載用戶請求量越大,服務(wù)端性能就越強.綜合兩方面,我們總結(jié)性能優(yōu)化的兩個方向:
1. 增加服務(wù)端所能支撐并發(fā)請求的最大數(shù)量;
2. 提高每個請求處理速度.
明確了優(yōu)化方向,首先介紹一種服務(wù)端通常的架構(gòu)模式,即來自瀏覽器或者App的Web一個請求,在服務(wù)端經(jīng)過哪幾層結(jié)構(gòu)被處理并返回的.
架構(gòu)模式:IP負載均衡->緩存服務(wù)器->反向代理->應(yīng)用服務(wù)器->數(shù)據(jù)庫
如圖1所示,為了說明方便,我們來舉個實際的例子: LVS(Keepalived)->Squid->nginx->Go->MySQL
圖1:服務(wù)端架構(gòu)
我們對請求在每層做分發(fā)處理,這樣可以使下一級結(jié)構(gòu)有多個分支同時工作,來提高總體的最大并發(fā)數(shù).
結(jié)合架構(gòu),我們來分析通常有哪些問題在拖了性能的后腿,以及找出對應(yīng)的解決方法.
正常情況下,IP負載均衡,緩存服務(wù)器和nginx代理這幾層主要是集群穩(wěn)定性問題.容易出現(xiàn)性能瓶頸的地方往往是應(yīng)用服務(wù)器層和數(shù)據(jù)庫層,我們下面來列舉幾個例子:
大部分Web請求都是阻塞性質(zhì)的,當(dāng)一個請求被處理時,進程就會被掛起(占用CPU)直至請求完成.在大多數(shù)情況下,Web請求完成的足夠快,所以這個問題并不被關(guān)注.然而,對于那些響應(yīng)時間來完成的請求(像返回數(shù)據(jù)量大的請求或外部API),這意味著應(yīng)用程序被鎖定直至處理結(jié)束,這期間,其他的請求不會被處理,很明顯,這些無效的等待時間浪費掉了,并且占用系統(tǒng)資源,嚴重的影響了我們可以負擔(dān)的并發(fā)請求的數(shù)量.
Web服務(wù)端在等待上一個請求處理的過程中,我們可以讓I/O循環(huán)打開以便處理其他應(yīng)用請求,直到處理完成時啟動一個請求并給予反饋,而不再是等待請求完成的過程中掛起進程.這樣,我們可以節(jié)省一些沒有必要的等待時間,用這些時間去處理更多的請求,這樣我們就可以大大增加請求的吞吐量,也就是在宏觀上提高了我們可處理的并發(fā)請求數(shù).
這里我們用Python的一款Web框架Tornado來具體說明改變阻塞方式提高并發(fā)性能.
場景:我們構(gòu)建一個向遠端(某個十分穩(wěn)定的網(wǎng)站)發(fā)送HTTP請求的簡單Web應(yīng)用.這期間,網(wǎng)絡(luò)傳輸穩(wěn)定,我們不考慮網(wǎng)絡(luò)來帶的影響.
在這個例子中,我們使用Siege(一款壓力測試軟件)對服務(wù)端在10秒內(nèi)執(zhí)行大約10個并發(fā)請求.
如圖2所示,我們可以很容易看出,這里的問題是無論每個請求自身返回多么快,服務(wù)器對遠端的訪問請求往返都會產(chǎn)生足夠大的滯后,因為進程直到請求完成并且數(shù)據(jù)被處理前都一直處于強制掛起狀態(tài).當(dāng)一兩個請求時這還不是一個問題,但達到100個(甚至10個)用戶時,這意味著整體變慢.如圖,不到10秒時間10個相似用戶的平均響應(yīng)時間達到了1.99秒,共計29次.這個例子只展示了非常簡單的邏輯.如果你要添加其他業(yè)務(wù)邏輯或數(shù)據(jù)庫的調(diào)用的話,結(jié)果會更糟糕.增加更多的用戶請求時,同時可被處理的請求就會增長緩慢,甚至有些請求會發(fā)生超時或失敗.
圖2:阻塞式響應(yīng)
下面我們用Tornado執(zhí)行非阻塞的HTTP請求.
如圖3所示,我們從每秒3.20個事務(wù)提升到了12.59,在相同的時間內(nèi)總共提供了118次請求.這真是一個非常大的改善!正如你所想象的,隨著用戶請求增多和測試時間增長時,它將能夠提供更多連接,并且不會遇到上面版本遭受的變慢的問題.從而穩(wěn)定的提高了可負載的并發(fā)請求數(shù).
圖3:非阻塞式響應(yīng)
先來介紹一下基礎(chǔ)知識:一個應(yīng)用程序是運行在機器上的一個進程;進程是一個運行在自己內(nèi)存地址空間里的獨立執(zhí)行體.一個進程由一個或多個操作系統(tǒng)線程組成,這些線程其實是共享同一個內(nèi)存地址空間的一起工作的執(zhí)行體.
傳統(tǒng)計算方式單線程運行,效率低,計算能力弱.
一種解決辦法就是完全避免使用線程.例如,可以使用多個進程將重擔(dān)交給操作系統(tǒng)來處理.但是,有個劣勢就是,我們必須處理所有進程間通信,通常這比共享內(nèi)存的并發(fā)模型有更多的開銷.
另一種辦法是用多線程工作,不過,公認的,使用多線程的應(yīng)用難以做到準確,同步不同的線程,對數(shù)據(jù)加鎖,這樣同時就只有一個線程可以變更數(shù)據(jù).不過過去的軟件開發(fā)經(jīng)驗告訴我們這會帶來更高的復(fù)雜度,更容易使代碼出錯以及更低的性能.
其中最主要的問題是內(nèi)存中的數(shù)據(jù)共享,它們會被多線程以無法預(yù)知的方式進行操作,導(dǎo)致一些無法重現(xiàn)或者隨機的結(jié)果(稱作“競態(tài)”).所以這個經(jīng)典的方法明顯不再適合現(xiàn)代多核/多處理器編程:thread-per-connection 模型不夠有效.在諸多比較合適的范式中,有個被稱作 Communicating Sequential Processes(順序通信處理)(CSP, C. Hoare 發(fā)明的)還有一個叫做 message-passing-model(消息傳遞)(已經(jīng)運用在了其他語言中,比如 Erlang).
我們這里使用辦法是利用并行的架構(gòu)來處理任務(wù),一個并發(fā)程序可以在一個處理器或者內(nèi)核上使用多個線程來執(zhí)行任務(wù),但是只有同一個程序在某個時間點同時運行在多核或者多處理器上才是真正的并行.
并行是一種通過使用多處理器以提高速度的能力.所以并發(fā)程序可以是并行的,也可以不是.
并行模式可以同時使用多線程、多核、多處理器,甚至多計算機,這無疑可以調(diào)動更多資源,從而壓縮響應(yīng)時間,提升運算效率,極大地增強了服務(wù)端的性能.
這里用Go語言中的Goroutine來具體說明.
在Go語言中,應(yīng)用程序并發(fā)處理的部分被稱作 goroutines(協(xié)程),它可以進行更有效的并發(fā)運算.在協(xié)程和操作系統(tǒng)線程之間并無一對一的關(guān)系:協(xié)程是根據(jù)一個或多個線程的可用性,映射(多路復(fù)用,執(zhí)行于)在他們之上的;協(xié)程調(diào)度器在 Go?運行時很好的完成了這個工作.協(xié)程是輕量的,比線程更輕.它們痕跡非常不明顯(使用少量的內(nèi)存和資源):使用 4K 的棧內(nèi)存就可以在堆中創(chuàng)建它們.因為創(chuàng)建非常廉價,必要的時候可以輕松創(chuàng)建并運行大量的協(xié)程(在同一個地址空間中 100,000 個連續(xù)的協(xié)程).并且它們對棧進行了分割,從而動態(tài)的增加(或縮減)內(nèi)存的使用;棧的管理是自動的,但不是由垃圾回收器管理的,而是在協(xié)程退出后自動釋放.協(xié)程可以運行在多個操作系統(tǒng)線程之間,也可以運行在線程之內(nèi),讓你可以很小的內(nèi)存占用就可以處理大量的任務(wù).由于操作系統(tǒng)線程上的協(xié)程時間片,你可以使用少量的操作系統(tǒng)線程就能擁有任意多個提供服務(wù)的協(xié)程,而且 Go?運行時可以聰明地意識到哪些協(xié)程被阻塞了,暫時擱置它們并處理其他協(xié)程.甚至,程序可以在不同的處理器和計算機上同時執(zhí)行不同的代碼段.
我們通常想將一個長計算過程切分成幾塊,然后讓每個goroutine各自負責(zé)一塊工作,這樣對于單一請求的響應(yīng)時間有成倍的提升.
舉個例子,有一個任務(wù)分3個階段,a階段去數(shù)據(jù)庫a中取數(shù)據(jù),b階段去數(shù)據(jù)庫b中取數(shù)據(jù),c階段合并數(shù)據(jù)返回.我們啟動goroutine以后a、b階段可以一起進行,極大地縮短了響應(yīng)時間.
說白了就是部分計算過程由串行轉(zhuǎn)換為并行,一個任務(wù)不需要等待其他無關(guān)的任務(wù)執(zhí)行完在執(zhí)行,實際計算中程序的并行執(zhí)行會更有用處.
關(guān)于這部分佐證的數(shù)據(jù)就不在這邊過多敘述了,感興趣的同學(xué)可以自己看一下這方面的資料.比如Web服務(wù)端由Ruby切換為Go性能提升15倍的老故事(Ruby使用的是綠色線程,即只有一個CPU得到利用).雖然這個故事可能有點夸大,但是并行帶來的性能提升是毫無疑問的.(Ruby切換為Go:http://www.vaikan.com/how-we-went-from-30-servers-to-2-go/).
磁盤讀取數(shù)據(jù)靠的是機械運動,每次讀取數(shù)據(jù)花費的時間可以分為尋道時間、旋轉(zhuǎn)延遲、傳輸時間三個部分,尋道時間指的是磁臂移動到指定磁道所需要的時間,主流磁盤一般在5ms以下;旋轉(zhuǎn)延遲就是我們經(jīng)常聽說的磁盤轉(zhuǎn)速,比如一個磁盤7200轉(zhuǎn),表示每分鐘能轉(zhuǎn)7200次,也就是說1秒鐘能轉(zhuǎn)120次,旋轉(zhuǎn)延遲就是1/120/2 = 4.17ms;傳輸時間指的是從磁盤讀出或?qū)?shù)據(jù)寫入磁盤的時間,一般在零點幾毫秒,相對于前兩個時間可以忽略不計.那么訪問一次磁盤的時間,即一次磁盤I/O的時間約等于9ms(5ms+4.17ms)左右,聽起來還挺不錯的,但要知道一臺500 -MIPS的機器每秒可以執(zhí)行5億條指令,因為指令依靠的是電的性質(zhì),換句話說執(zhí)行一次I/O的時間可以執(zhí)行40萬條指令,數(shù)據(jù)庫動輒十萬百萬乃至千萬級數(shù)據(jù),每次9毫秒的時間,顯然是個災(zāi)難.
磁盤I/O對服務(wù)器性能的影響沒有根本的解決辦法,除非你把磁盤扔掉,換成別的東西.我們能在網(wǎng)上搜到各種存儲介質(zhì)的響應(yīng)速度與價格,如果你有錢,你就可以任性的更換存儲介質(zhì).
在不更換存儲介質(zhì)的條件下,我們可以減少應(yīng)用程序?qū)Υ疟P的訪問次數(shù),比如設(shè)置緩存,還可以把部分磁盤I/O放到請求周期外,比如用隊列和棧來處理數(shù)據(jù)的I/O等.
隨著業(yè)務(wù)開發(fā)模式的變化,敏捷式開發(fā)被越來越多的團隊采用,周期越來越短,很多數(shù)據(jù)庫查詢語句都是按照業(yè)務(wù)邏輯來寫,時間久了常常就忽略了SQL查詢的格式問題,造成數(shù)據(jù)庫壓力的增加,使數(shù)據(jù)庫查詢的響應(yīng)變慢.這里簡單介紹MySQL數(shù)據(jù)庫中,幾條被我們忽略的常見問題和優(yōu)化方式:
from_unixtime(create_time) = ’2014-05-29’
就不能使用到索引,原因很簡單,b+樹中存的都是數(shù)據(jù)表中的字段值,但進行檢索時,需要把所有元素都應(yīng)用函數(shù)才能比較,顯然成本太大.所以語句應(yīng)該寫成create_time = unix_timestamp(’2014-05-29’)
;應(yīng)盡量避免在 where 子句中對字段進行 null 值判斷,否則將導(dǎo)致引擎放棄使用.select id from t where num is null
可以在num上設(shè)置默認值0,確保表中num列沒有null值,然后這樣查詢:
select id from t where num=0
select id from t where num=10 or num=20
select id from t where num=10 union all select id from t where num=20?
select id from t where name like ‘%abc%’
select id from t where num in(1,2,3)
select id from t where num between 1 and 3
文章來自微信公眾號:互聯(lián)網(wǎng)架構(gòu)師
轉(zhuǎn)載請注明本頁網(wǎng)址:
http://www.snjht.com/jiaocheng/3740.html