一、越來(lái)越多的并發(fā)連接數(shù)
現(xiàn)在的Web系統(tǒng)面對(duì)的并發(fā)連接數(shù)在近幾年呈現(xiàn)指數(shù)增長(zhǎng),高并發(fā)成為了一種常態(tài),給Web系統(tǒng)帶來(lái)不小的挑戰(zhàn)。以最簡(jiǎn)單粗暴的方式解決,就是增加Web系統(tǒng)的機(jī)器和升級(jí)硬件配置。雖然現(xiàn)在的硬件越來(lái)越便宜,但是一味地通過(guò)增加機(jī)器來(lái)解決并發(fā)量的增長(zhǎng),成本是非常高昂的。結(jié)合技術(shù)優(yōu)化方案,才是更有效的解決方法。
并發(fā)連接數(shù)為什么呈指數(shù)增長(zhǎng)?實(shí)際上,從這幾年的用戶基數(shù)上看,這個(gè)數(shù)量并沒(méi)有出現(xiàn)指數(shù)增長(zhǎng),因此它并非主要原因。主要原因,還是web變得更復(fù)雜,交互更豐富所導(dǎo)致的。
1. 頁(yè)面元素增多,交互復(fù)雜
Web頁(yè)面元素越來(lái)越多,更為豐富。更多的資源元素,意味著更多的下載請(qǐng)求。Web系統(tǒng)的交互越來(lái)越復(fù)雜,交互場(chǎng)景和次數(shù)也大幅增加。以“www.qq.com”的首頁(yè)為例子,刷新一次,大概會(huì)有244個(gè)請(qǐng)求。并且,在頁(yè)面打開完成之后,還會(huì)有一些定時(shí)的查詢或者上報(bào)請(qǐng)求持續(xù)運(yùn)作。
目前的Http請(qǐng)求,為了減少反復(fù)的創(chuàng)建和銷毀連接行為,通常都建立長(zhǎng)連接(Connection keep-alive)。一經(jīng)建立,這個(gè)連接會(huì)被保持住一段時(shí)間,被后續(xù)請(qǐng)求復(fù)用。然而,它也帶來(lái)了另一個(gè)新的問(wèn)題,連接的保持是會(huì)占用Web系統(tǒng)服務(wù)端資源的,如果不充分使用這個(gè)連接,會(huì)導(dǎo)致資源浪費(fèi)。長(zhǎng)連接被創(chuàng)建后,首批資源傳輸完畢,之后幾乎沒(méi)有數(shù)據(jù)交互,一直到超時(shí)時(shí)間,才會(huì)自動(dòng)釋放長(zhǎng)連接占據(jù)的系統(tǒng)資源。
除此之外,還有一些Web需求本身就需要長(zhǎng)期保持連接的,例如Web socket。
2. 主流的本瀏覽器的連接數(shù)在增加
面對(duì)越來(lái)越豐富的Web資源,主流瀏覽器并發(fā)連接數(shù)也在增加,同一個(gè)域下,早期的瀏覽器一般只有1-2個(gè)下載連接,而目前的主流瀏覽器通常在2-6個(gè)。增加瀏覽器并發(fā)連接數(shù)目,在需要下載資源比較多的場(chǎng)景下,可以加快頁(yè)面的加載速度。更多的連接對(duì)瀏覽器加載頁(yè)面元素是有好處的,在某些連接遭遇“網(wǎng)絡(luò)阻塞”的情況下,其他正常的下載連接可以繼續(xù)工作。
這樣自然無(wú)形增加了Web系統(tǒng)后端的壓力,更多的下載連接意味著占據(jù)了更多的Web服務(wù)器的資源。而在用戶訪問(wèn)高峰期,自熱而然就形成了“高并發(fā)”場(chǎng)景。這些連接和請(qǐng)求,占據(jù)了服務(wù)器的大量CPU和內(nèi)存等資源。尤其在資源數(shù)目超過(guò)100+的網(wǎng)站頁(yè)面中,使用更多的下載連接,非常有必要。
二、Web前端優(yōu)化,降低服務(wù)端壓力
在緩解“高并發(fā)”的壓力,需要前端和后端的共同配合優(yōu)化,才能達(dá)到最大效果。在用戶第一線的Web前端,可以起到減少或者減輕Http請(qǐng)求的效果。
1. 減少Web請(qǐng)求
常用的實(shí)現(xiàn)方法是通過(guò)Http協(xié)議頭中的expire或max-age來(lái)控制,將靜態(tài)內(nèi)容放入瀏覽器的本地緩存,在之后的一段時(shí)間里,不再請(qǐng)求Web服務(wù)器,直接使用本地資源。還有HTML5中的本地存儲(chǔ)技術(shù)(LocalStorage),也被作為一個(gè)強(qiáng)大的數(shù)據(jù)本地緩存。
這種方案緩存后,根本不發(fā)送請(qǐng)求到Web服務(wù)器,大幅降低服務(wù)器壓力,也帶來(lái)了良好的用戶體驗(yàn)。但是,這種方案,對(duì)首次訪問(wèn)的用戶無(wú)效,同時(shí),也影響部分Web資源的實(shí)時(shí)性。
2. 減輕Web請(qǐng)求
瀏覽器的本地緩存是存在過(guò)期時(shí)間的,一旦過(guò)期,就必須重新向服務(wù)器請(qǐng)求。這個(gè)時(shí)候,會(huì)有兩種情形:
(1)服務(wù)器的資源內(nèi)容沒(méi)有更新,瀏覽器請(qǐng)求Web資源,服務(wù)器回復(fù)“可以繼續(xù)使用本地緩存”。(發(fā)生通信,但是Web服務(wù)器只需要做簡(jiǎn)單“回復(fù)”)
(2)服務(wù)器的文件或者內(nèi)容已經(jīng)更新,瀏覽器請(qǐng)求Web資源,Web服務(wù)器通過(guò)網(wǎng)絡(luò)傳輸新的資源內(nèi)容。(發(fā)生通信,Web服務(wù)器需要完成復(fù)雜的傳輸工作)
這里的協(xié)商方式是通過(guò)Http協(xié)議的Last-Modified或Etag來(lái)控制,這個(gè)時(shí)候請(qǐng)求服務(wù)器,如果是內(nèi)容沒(méi)有發(fā)生變更的情況,服務(wù)器會(huì)返回304 Not Modified。這樣的話,就不需要每次請(qǐng)求Web服務(wù)器都做復(fù)雜的傳輸完整數(shù)據(jù)文件的工作,只要簡(jiǎn)單的http應(yīng)答就可以達(dá)到相同的效果。
雖然上述請(qǐng)求,起到“減輕”Web服務(wù)器的壓力,但是連接仍然被建立,請(qǐng)求也發(fā)生了。
3. 合并頁(yè)面請(qǐng)求
如果是比較老一些的Web開發(fā)者,應(yīng)該會(huì)更有印象,在ajax盛行之前。頁(yè)面大部分都是直接輸出的,并沒(méi)有這么多的ajax請(qǐng)求,Web后端將頁(yè)面內(nèi)容完全拼湊好了,再返回給前端。那個(gè)時(shí)候,頁(yè)面靜態(tài)化,是一個(gè)挺廣泛的優(yōu)化方式。后來(lái),被交互更友好的ajax漸漸替代了,一個(gè)頁(yè)面的請(qǐng)求也變得越來(lái)越多。
由于移動(dòng)端的網(wǎng)絡(luò)(2G/3G)比起PC寬帶差很多,并且部分手機(jī)配置比較低,面對(duì)一個(gè)超過(guò)100個(gè)請(qǐng)求的網(wǎng)頁(yè),加載的速度會(huì)緩慢很多。于是,優(yōu)化的方向又重新回到合并頁(yè)面元素,減少請(qǐng)求數(shù)量:
(1)合并HTML展示內(nèi)容。將CSS和JS直接嵌入到HTML頁(yè)面內(nèi),不通過(guò)連接的方式引入。
(2)Ajax動(dòng)態(tài)內(nèi)容合并請(qǐng)求。對(duì)于動(dòng)態(tài)內(nèi)容,將10次Ajax請(qǐng)求合并為1次的批量信息查詢。
(3)小圖片合并,通過(guò)CSS的偏移量技術(shù)Sprites,將很多小圖片合并為一張。這個(gè)優(yōu)化方式,在PC端的Web優(yōu)化中,也非常常見。
合并請(qǐng)求,減少了傳輸數(shù)據(jù)的次數(shù),也就是相當(dāng)于將它們從一個(gè)一個(gè)地請(qǐng)求,變?yōu)橐淮蔚?#8220;批量”請(qǐng)求。上述優(yōu)化方法,到達(dá)“減輕”Web服務(wù)器壓力的目的,減少了需要建立的連接。
三、 節(jié)約Web服務(wù)端的內(nèi)存
前端的優(yōu)化完成,我們就需要著眼于Web服務(wù)端本身。內(nèi)存是Web服務(wù)器非常重要的資源,更多的內(nèi)存通常意味著可以同時(shí)放入更多的工作任務(wù)。就Web服務(wù)占用內(nèi)存而言,可以粗略劃分:
(1)用來(lái)維持連接的基本內(nèi)存,進(jìn)程初始化時(shí),會(huì)載入一些基礎(chǔ)模塊到內(nèi)存。
(2)被傳輸?shù)臄?shù)據(jù)內(nèi)容載入到各個(gè)緩沖區(qū),占據(jù)的內(nèi)存。
(3)程序執(zhí)行過(guò)程中,申請(qǐng)和使用的內(nèi)存。
如果維持一個(gè)連接,能夠盡可能少占用內(nèi)存,那么我們就可以維持更多的并發(fā)連接,從而讓W(xué)eb服務(wù)器支持更多的并發(fā)連接數(shù)。
Apache(httpd)是一個(gè)成熟并且古老的Web服務(wù),而Apache的發(fā)展和演變,一直在追求做到這一點(diǎn),它試圖不斷減少服務(wù)占據(jù)的內(nèi)存,以支持更大的并發(fā)量。以Apache的工作模式的演變?yōu)橐暯牵覀円黄饋?lái)看看,它們是如何優(yōu)化內(nèi)存的問(wèn)題的。
1. prefork MPM,多進(jìn)程工作模式
prefork是Apache最成熟和穩(wěn)定的工作模式,即使是現(xiàn)在,仍然被廣泛使用。主進(jìn)程生成后,它先完成基礎(chǔ)的初始化工作,然后,通過(guò)fork預(yù)先產(chǎn)生一批的子進(jìn)程(子進(jìn)程會(huì)復(fù)制父進(jìn)程的內(nèi)存空間,不需要再做基礎(chǔ)的初始化工作)。然后等待服務(wù),之所以預(yù)先生成,是為了減少頻繁創(chuàng)建和銷毀進(jìn)程的開銷。多進(jìn)程的好處,是進(jìn)程之間的內(nèi)存數(shù)據(jù)不會(huì)相互干擾,同時(shí),某個(gè)進(jìn)程異常終止也不會(huì)影響其他進(jìn)程。但是,就內(nèi)存而言,每個(gè)httpd子進(jìn)程占用了很多的內(nèi)存,因?yàn)樽舆M(jìn)程的內(nèi)存數(shù)據(jù)是復(fù)制父進(jìn)程的。我們可以粗略認(rèn)為,這里存在大量的“重復(fù)數(shù)據(jù)”被放在內(nèi)存中。最終,導(dǎo)致我們能夠生成的子進(jìn)程最大數(shù)量是很有限。在面對(duì)高并發(fā)時(shí),因?yàn)橛胁簧貹eep-alive的長(zhǎng)連接,將這些子進(jìn)程“霸占”住,很可能導(dǎo)致可用子進(jìn)程耗盡。因此,prefork并不太適合高并發(fā)場(chǎng)景。
- 優(yōu)點(diǎn):成熟穩(wěn)定,兼容所有新老模塊。同時(shí),不需要擔(dān)心線程安全的問(wèn)題。(例如,我們常用的mod_php,將PHP編譯為Apache的子模塊,就不需要支持線程安全)
- 缺點(diǎn):一個(gè)服務(wù)進(jìn)程占用很多內(nèi)存。
2. worker MPM,多進(jìn)程和多線程的混合模式
worker模式比起prefork,是使用了多進(jìn)程和多線程的混合模式。它也預(yù)先f(wàn)ork了幾個(gè)子進(jìn)程(數(shù)量很少),然后每個(gè)子進(jìn)程創(chuàng)建一些線程(其中包括一個(gè)監(jiān)聽線程)。每個(gè)請(qǐng)求過(guò)來(lái),會(huì)被分配到1個(gè)線程來(lái)服務(wù)。線程比起進(jìn)程會(huì)更輕量,因?yàn)榫€程通常會(huì)共享父進(jìn)程的內(nèi)存空間,因此,內(nèi)存的占用會(huì)減少一些。在高并發(fā)的場(chǎng)景下,因?yàn)楸绕餻refork更省內(nèi)存,因此會(huì)有更多的可用線程。
但是,它并沒(méi)有解決Keep-alive的長(zhǎng)連接“霸占”線程的問(wèn)題,只是對(duì)象變成了比較輕量的線程。
有些人會(huì)覺(jué)得奇怪,那么這里為什么不完全使用多線程呢,還要引入多進(jìn)程?因?yàn)檫€需要考慮穩(wěn)定性,如果一個(gè)線程掛了,會(huì)導(dǎo)致同一個(gè)進(jìn)程下其他正常的子線程都掛了。如果全部采用多線程,某個(gè)線程掛掉,就導(dǎo)致整個(gè)Apache服務(wù)“全軍覆沒(méi)”。而目前的工作模式,受影響的只是Apache的一部分服務(wù),而不是整個(gè)服務(wù)。
線程共享父進(jìn)程的內(nèi)存空間,減少了內(nèi)存的占用,卻又引起了新的問(wèn)題。就是“線程安全”,多個(gè)線程修改共享資源導(dǎo)致的“競(jìng)爭(zhēng)行為”,又強(qiáng)迫我們所使用的模塊必須支持“線程安全”。因此,它有一定程度上增加Web服務(wù)的不穩(wěn)定性。例如,mod_php所使用的PHP拓展,也同樣需要支持“線程安全”,否則,不能在該模式下使用。
- 優(yōu)點(diǎn):占據(jù)更少的內(nèi)存,高并發(fā)下表現(xiàn)更優(yōu)秀。
- 缺點(diǎn):必須考慮線程安全的問(wèn)題,同時(shí)鎖的引入又增加了CPU的開銷。
3. event MPM,多進(jìn)程和多線程的混合模式,引入Epoll
這個(gè)是Apache中比較新的模式,在現(xiàn)在的版本(Apache 2.4.10)已經(jīng)是穩(wěn)定可用的模式。它和worker模式很像,最大的區(qū)別在于,它解決了keep-alive場(chǎng)景下,長(zhǎng)期被占用的線程的資源浪費(fèi)問(wèn)題。event MPM中,會(huì)有一個(gè)專門的線程來(lái)管理這些keep-alive類型的線程,當(dāng)有真實(shí)請(qǐng)求過(guò)來(lái)的時(shí)候,將請(qǐng)求傳遞給服務(wù)線程,執(zhí)行完畢后,又允許它釋放。它減少了“占據(jù)”連接而又不使用的資源浪費(fèi),增強(qiáng)了高并發(fā)場(chǎng)景下的請(qǐng)求處理能力。因?yàn)闇p少了“閑等”的線程,線程的數(shù)量減少,同等場(chǎng)景下,內(nèi)存占用會(huì)下降一些。
event MPM在遇到某些不兼容的模塊時(shí),會(huì)失效,將會(huì)回退到worker模式,一個(gè)工作線程處理一個(gè)請(qǐng)求。新版Apache官方自帶的模塊,全部是支持event MPM的。注意一點(diǎn),event MPM需要Linux系統(tǒng)(Linux 2.6+)對(duì)EPoll的支持,才能啟用。Apache的三種模式中在真實(shí)應(yīng)用場(chǎng)景中,event MPM是最節(jié)約內(nèi)存的。
4. 使用比較輕量的Nginx作為Web服務(wù)器
雖然Apache的不斷優(yōu)化,減少了內(nèi)存占用,從而增加了處理高并發(fā)的能力。但是,正如前面所說(shuō),Apache是一個(gè)古老而成熟的Web服務(wù),同時(shí),集成很多穩(wěn)定的模塊,是一個(gè)比較重的Web服務(wù)。Nginx是個(gè)比較輕量的Web服務(wù),占據(jù)的內(nèi)存天然就少于Apache。而且,Nginx通過(guò)一個(gè)進(jìn)程來(lái)服務(wù)于N個(gè)連接。所使用的方式,并不是Apache的增加進(jìn)程/線程來(lái)支持更多的連接。對(duì)于Nginx來(lái)說(shuō),它少創(chuàng)建了大量的進(jìn)程/線程,減少了很多內(nèi)存的開銷。
靜態(tài)文件的QPS性能壓測(cè)結(jié)果,Nginx性能大概3倍于Apache對(duì)靜態(tài)文件的處理。PHP等動(dòng)態(tài)文件的QPS,Nginx的做法通常是通過(guò)FastCGI的方式和PHP-FPM通信的方式完成,PHP作為一個(gè)與之無(wú)關(guān)的外部服務(wù)存在。而Apache通常將PHP編譯為自己的字模塊(新版的Apache也支持FastCGI)。PHP動(dòng)態(tài)文件,Nginx的表現(xiàn)略遜于Apache。
5. sendfile節(jié)約內(nèi)存
Apache、Nginx等不少Web服務(wù),都帶有sendfile支持的。sendfile可以減少數(shù)據(jù)到“用戶態(tài)內(nèi)存空間”(用戶緩沖區(qū))的拷貝,進(jìn)而減少內(nèi)存的占用。當(dāng)然,很多同學(xué)第一個(gè)反應(yīng)當(dāng)然是問(wèn)Why?為了盡可能清楚講述這個(gè)原理,我們就先回Linux內(nèi)核態(tài)和用戶態(tài)的存儲(chǔ)空間的交互。
一般情況下,用戶態(tài)(也就是我們的程序所在的內(nèi)存空間)是不會(huì)直接讀寫或者操作各種設(shè)備(磁盤、網(wǎng)絡(luò)、終端等),中間通常用內(nèi)核作為“中間人”,來(lái)完成對(duì)設(shè)備的操作或者讀寫。
以最簡(jiǎn)單的磁盤讀寫例子,從磁盤中讀取A文件,寫入到B文件。A文件數(shù)據(jù)是從磁盤開始,然后載入到“內(nèi)核緩沖區(qū)”,然后再拷貝到“用戶緩沖區(qū)”,我們才可以對(duì)數(shù)據(jù)進(jìn)行處理。寫入的時(shí)候,也同理,從“用戶態(tài)緩沖區(qū)”載入到“內(nèi)核緩沖區(qū)”,最后寫入到磁盤B文件。
這樣寫文件很累吧,于是有人覺(jué)得這里可以跳過(guò)“用戶緩沖區(qū)”的拷貝。其實(shí),這就是MMP(Memory-Mapping,內(nèi)存映射)的實(shí)現(xiàn),建立一個(gè)磁盤空間和內(nèi)存的直接映射,數(shù)據(jù)不再?gòu)?fù)制到“用戶態(tài)緩沖區(qū)”,而是返回一個(gè)指向內(nèi)存空間的指針。于是,我們之前的讀寫文件例子,就會(huì)變成,A文件數(shù)據(jù)從磁盤載入到“內(nèi)核緩沖區(qū)”,然后從“內(nèi)核緩沖區(qū)”復(fù)制到B文件的“內(nèi)核緩沖區(qū)”,B文件再?gòu)?#8221;內(nèi)核緩沖區(qū)“寫回到磁盤中。這個(gè)過(guò)程,減少了一次內(nèi)存拷貝,同時(shí)也少內(nèi)存占用。
好了,回到sendfile的話題上來(lái),簡(jiǎn)單的說(shuō),sendfile的做法和MMP類似,就是減少數(shù)據(jù)從”內(nèi)核態(tài)緩沖區(qū)“到”用戶態(tài)緩沖區(qū)“的內(nèi)存拷貝。
默認(rèn)的磁盤文件讀取,到傳輸給socket,流程(不使用sendfile)是:
使用sendfile之后:
這種方式,不僅節(jié)省了內(nèi)存,而且還有CPU的開銷。
四、節(jié)約Web服務(wù)器的CPU
對(duì)Web服務(wù)器而言,CPU是另一個(gè)非常核心的系統(tǒng)資源。雖然一般情況下,我們認(rèn)為業(yè)務(wù)程序的執(zhí)行消耗了我們主要CPU。但是,就Web服務(wù)程序而言,多線程/多進(jìn)程的上下文切換,也是比較消耗CPU資源的。一個(gè)進(jìn)程/線程通常不能長(zhǎng)期占有CPU,當(dāng)發(fā)生阻塞或者時(shí)間片用完,就無(wú)法繼續(xù)占用CPU,這個(gè)時(shí)候,就會(huì)發(fā)生上下文切換,CPU時(shí)間片從老進(jìn)程/線程切換到新的。除此之外,在并發(fā)連接數(shù)目很高的場(chǎng)景下,對(duì)這些用戶建立的連接(socket文件描述符)狀態(tài)的輪詢和檢測(cè),也是比較消耗CPU的。
而Apache和Nginx的發(fā)展和演變,也在努力減少CPU開銷。
1. Select/Poll(Apache早期版本的I/O多路復(fù)用)
通常,Web服務(wù)都要維護(hù)很多個(gè)和用戶通信的socket文件描述符,I/O多路復(fù)用,其實(shí)就是為了方便對(duì)這些文件描述符的管理和檢測(cè)。Apache早期版本,是使用select的模式,簡(jiǎn)單的說(shuō),就是將這些我們關(guān)注的socket文件描述符交給內(nèi)核,讓內(nèi)核告訴我們,那些描述符可操作。Poll與select原理基本相同,因此放在一起,它們之間的區(qū)別,就不贅敘了哈。
select/poll返回的是一個(gè)我們之前提交的文件描述符集合(內(nèi)核將其中可讀、可寫或者異常狀態(tài)的socket文件描述符的標(biāo)識(shí)位修改了),我們需要通過(guò)輪詢檢查才能獲得我們可以操作的文件描述符。在這個(gè)過(guò)程中,不斷重復(fù)執(zhí)行。在實(shí)際應(yīng)用場(chǎng)景中,大部分被我們監(jiān)控的socket文件描述符,都是”空閑的“,也就是說(shuō),不能操作。我們對(duì)整個(gè)集合輪詢,就是為了找了少部分我們可以操作的socket文件描述符。于是,當(dāng)我們監(jiān)控的socket文件描述符越多(用戶并發(fā)連接數(shù)越來(lái)越多),這個(gè)輪詢工作,也就越來(lái)越沉重,進(jìn)而導(dǎo)致增大了CPU的開銷。
如果我們監(jiān)控的socket文件描述符,幾乎都是”活躍的“,反而使用這種模式更合適一點(diǎn)。
2. Epoll(新版的Apache的event MPM,Nginx等支持)
Epoll是Linux2.6開始正式支持的I/O多路復(fù)用,我們可以理解為它是對(duì)select/poll的改進(jìn)。首先,我們同樣將我們關(guān)注的socket文件描述符集合告訴給內(nèi)核,同時(shí),給它們注冊(cè)”回調(diào)函數(shù)“,如果某個(gè)socket文件準(zhǔn)備好了,就通過(guò)回調(diào)函數(shù)通知我們。于是,我們就不需要專門去輪詢整個(gè)全量的socket文件描述符集合,直接可以得到已經(jīng)可操作的socket文件描述符。那么,那些大部分”空閑“的描述符,我們就不遍歷了。即使我們監(jiān)控的socket文件描述越來(lái)越多,我們輪詢的也只是”活躍可操作“的socket文件描述符。
其實(shí),有一種極端點(diǎn)的場(chǎng)景,就是我們?nèi)课募枋龇麕缀醵际?#8221;活躍“的,這樣反而導(dǎo)致了大量回調(diào)函數(shù)的執(zhí)行,又增加了CPU的開銷。但是,就Web服務(wù)的真實(shí)場(chǎng)景,絕大部分時(shí)候,都是連接集合中都存在很多”空閑“連接。
3. 線程/進(jìn)程的創(chuàng)建銷毀和上下文切換
通常,Apache某一個(gè)時(shí)間內(nèi),是一個(gè)進(jìn)程/線程服務(wù)于一個(gè)連接。于是,Apache就有很多的進(jìn)程/線程,服務(wù)于很多的連接。Web服務(wù)在高峰期,會(huì)建立很多的進(jìn)程/線程,也就帶來(lái)很多的上下文切換開銷。而Nginx,它通常只有1個(gè)master主進(jìn)程和幾個(gè)worker子進(jìn)程,然后,1個(gè)worker進(jìn)程服務(wù)很多個(gè)連接,進(jìn)而節(jié)省了CPU的上下文切換開銷。
兩種模式雖然不同,但實(shí)際上不能直接出分好壞,綜合來(lái)說(shuō),各有各自的優(yōu)勢(shì),就不妄議了哈。
4. 多線程下的鎖對(duì)CPU的開銷
Apache中的worker和event模式,都有采用多線程。多線程因?yàn)楣蚕砀高M(jìn)程的內(nèi)存空間,在訪問(wèn)共享數(shù)據(jù)的時(shí)候,就會(huì)產(chǎn)生競(jìng)爭(zhēng),也就是線程安全問(wèn)題。因此通常會(huì)引入鎖(Linux下比較常用的線程相關(guān)的鎖有互斥量metux,讀寫鎖rwlock等),成功獲取鎖的線程可以繼續(xù)執(zhí)行,獲取失敗的通常選擇阻塞等待。引入鎖的機(jī)制,程序的復(fù)雜度往往增加不少,同時(shí)還有線程“死鎖”或者“餓死”的風(fēng)險(xiǎn)(多進(jìn)程在訪問(wèn)進(jìn)程間共享資源的時(shí)候,也有同樣的問(wèn)題)。
死鎖現(xiàn)象(兩個(gè)線程彼此鎖住對(duì)方想要獲取的資源,相互阻塞等待,永遠(yuǎn)無(wú)法達(dá)不到滿足條件):
餓死現(xiàn)象(某個(gè)線程,一直獲取不到它想要鎖資源,永遠(yuǎn)無(wú)法執(zhí)行下一步):
為了避免這些鎖導(dǎo)致的問(wèn)題,就不得不加大程序的復(fù)雜度,解決方案一般有:
(1)對(duì)資源的加鎖,根據(jù)約定好的順序,大家都先對(duì)共享資源X加鎖,加鎖成功之后才能加鎖共享資源Y。
(2)如果線程占有資源X,卻加鎖資源Y失敗,則放棄加鎖,同時(shí)也釋放掉之前占有的資源X。
在使用PHP的時(shí)候,在Apache的worker和event模式下,也必須兼容線程安全。通常,新版本的PHP官方庫(kù)是沒(méi)有線程安全方面的問(wèn)題,需要關(guān)注的是第三方擴(kuò)展。PHP實(shí)現(xiàn)線程安全,不是通過(guò)鎖的方式實(shí)現(xiàn)的。而是為每個(gè)線程獨(dú)立申請(qǐng)一份全局變量的副本,相當(dāng)于線程的私人內(nèi)存空間,但是這樣做相對(duì)消耗多一些內(nèi)存。不過(guò),這樣的好處,是不需要引入復(fù)雜的鎖機(jī)制實(shí)現(xiàn),也避免了鎖機(jī)制對(duì)CPU的開銷。
這里順便提到一下,經(jīng)常和Nginx搭配工作的PHP-FPM(FastCGI)使用的是多進(jìn)程,因此不會(huì)有線程安全的問(wèn)題。
五、 小結(jié)
可能有些同學(xué)看完之后,會(huì)得出結(jié)論,Nginx+PHP-FPM的工作方式,似乎是最節(jié)省系統(tǒng)資源的Web系統(tǒng)工作方式。某種程度上說(shuō),的確是可以這么說(shuō)的,但是Web系統(tǒng)的搭建,需要從實(shí)際業(yè)務(wù)應(yīng)用的角度出發(fā),具體問(wèn)題需要具體分析,尋求最合適的技術(shù)方案。
Web服務(wù)的不斷演變和發(fā)展,努力地追求用盡可能少的系統(tǒng)資源,來(lái)支撐更多的用戶請(qǐng)求,這是一條波瀾壯闊的前進(jìn)之路。這些技術(shù)方案,匯聚了很多值得學(xué)習(xí)和借鑒的解決問(wèn)題的思路。
php的安全模式是個(gè)非常重要的內(nèi)嵌的安全機(jī)制,能夠控制一些php中的函數(shù),比如system(),
同時(shí)把很多文件操作函數(shù)進(jìn)行了權(quán)限控制,也不允許對(duì)某些關(guān)鍵文件的文件,比如/etc/passwd,
但是默認(rèn)的php.ini是沒(méi)有打開安全模式的,我們把它打開:
safe_mode = on
(2) 用戶組安全
當(dāng)safe_mode打開時(shí),safe_mode_gid被關(guān)閉,那么php腳本能夠?qū)ξ募M(jìn)行訪問(wèn),而且相同
組的用戶也能夠?qū)ξ募M(jìn)行訪問(wèn)。
建議設(shè)置為:
safe_mode_gid = off
如果不進(jìn)行設(shè)置,可能我們無(wú)法對(duì)我們服務(wù)器網(wǎng)站目錄下的文件進(jìn)行操作了,比如我們需要
對(duì)文件進(jìn)行操作的時(shí)候。
(3) 安全模式下執(zhí)行程序主目錄
如果安全模式打開了,但是卻是要執(zhí)行某些程序的時(shí)候,可以指定要執(zhí)行程序的主目錄:
safe_mode_exec_dir = D:/usr/bin
一般情況下是不需要執(zhí)行什么程序的,所以推薦不要執(zhí)行系統(tǒng)程序目錄,可以指向一個(gè)目錄,
然后把需要執(zhí)行的程序拷貝過(guò)去,比如:
safe_mode_exec_dir = D:/tmp/cmd
但是,我更推薦不要執(zhí)行任何程序,那么就可以指向我們網(wǎng)頁(yè)目錄:
safe_mode_exec_dir = D:/usr/www
(4) 安全模式下包含文件
如果要在安全模式下包含某些公共文件,那么就修改一下選項(xiàng):
safe_mode_include_dir = D:/usr/www/include/
其實(shí)一般php腳本中包含文件都是在程序自己已經(jīng)寫好了,這個(gè)可以根據(jù)具體需要設(shè)置。
(5) 控制php腳本能訪問(wèn)的目錄
使用open_basedir選項(xiàng)能夠控制PHP腳本只能訪問(wèn)指定的目錄,這樣能夠避免PHP腳本訪問(wèn)
不應(yīng)該訪問(wèn)的文件,一定程度上限制了phpshell的危害,我們一般可以設(shè)置為只能訪問(wèn)網(wǎng)站目錄:
open_basedir = D:/usr/www
(6) 關(guān)閉危險(xiǎn)函數(shù)
如果打開了安全模式,那么函數(shù)禁止是可以不需要的,但是我們?yōu)榱税踩€是考慮進(jìn)去。比如,
我們覺(jué)得不希望執(zhí)行包括system()等在那的能夠執(zhí)行命令的php函數(shù),或者能夠查看php信息的
phpinfo()等函數(shù),那么我們就可以禁止它們:
disable_functions = system,passthru,exec,shell_exec,popen,phpinfo,escapeshellarg,escapeshellcmd,proc_close,proc_open,dl,show_source,get_cfg_var
如果你要禁止任何文件和目錄的操作,那么可以關(guān)閉很多文件操作
disable_functions = chdir,chroot,dir,getcwd,opendir,readdir,scandir,fopen,unlink,delete,copy,mkdir, rmdir,rename,file,file_get_contents,fputs,fwrite,chgrp,chmod,chown
以上只是列了部分不叫常用的文件處理函數(shù),你也可以把上面執(zhí)行命令函數(shù)和這個(gè)函數(shù)結(jié)合,
就能夠抵制大部分的phpshell了。
(7) 關(guān)閉PHP版本信息在http頭中的泄漏
我們?yōu)榱朔乐购诳瞳@取服務(wù)器中php版本的信息,可以關(guān)閉該信息斜路在http頭中:
expose_php = Off
比如黑客在 telnet www.12345.com 80 的時(shí)候,那么將無(wú)法看到PHP的信息。
(8) 關(guān)閉注冊(cè)全局變量
在PHP中提交的變量,包括使用POST或者GET提交的變量,都將自動(dòng)注冊(cè)為全局變量,能夠直接訪問(wèn),
這是對(duì)服務(wù)器非常不安全的,所以我們不能讓它注冊(cè)為全局變量,就把注冊(cè)全局變量選項(xiàng)關(guān)閉:
register_globals = Off
當(dāng)然,如果這樣設(shè)置了,那么獲取對(duì)應(yīng)變量的時(shí)候就要采用合理方式,比如獲取GET提交的變量var,
那么就要用$_GET['var']來(lái)進(jìn)行獲取,這個(gè)php程序員要注意。
(9) 打開magic_quotes_gpc來(lái)防止SQL注入
SQL注入是非常危險(xiǎn)的問(wèn)題,小則網(wǎng)站后臺(tái)被入侵,重則整個(gè)服務(wù)器淪陷,
所以一定要小心。php.ini中有一個(gè)設(shè)置:
magic_quotes_gpc = Off
這個(gè)默認(rèn)是關(guān)閉的,如果它打開后將自動(dòng)把用戶提交對(duì)sql的查詢進(jìn)行轉(zhuǎn)換,
比如把 ' 轉(zhuǎn)為 \'等,這對(duì)防止sql注射有重大作用。所以我們推薦設(shè)置為:
magic_quotes_gpc = On
(10) 錯(cuò)誤信息控制
一般php在沒(méi)有連接到數(shù)據(jù)庫(kù)或者其他情況下會(huì)有提示錯(cuò)誤,一般錯(cuò)誤信息中會(huì)包含php腳本當(dāng)
前的路徑信息或者查詢的SQL語(yǔ)句等信息,這類信息提供給黑客后,是不安全的,所以一般服務(wù)器建議禁止錯(cuò)誤提示:
display_errors = Off
如果你卻是是要顯示錯(cuò)誤信息,一定要設(shè)置顯示錯(cuò)誤的級(jí)別,比如只顯示警告以上的信息:
error_reporting = E_WARNING & E_ERROR
當(dāng)然,我還是建議關(guān)閉錯(cuò)誤提示。
(11) 錯(cuò)誤日志
建議在關(guān)閉display_errors后能夠把錯(cuò)誤信息記錄下來(lái),便于查找服務(wù)器運(yùn)行的原因:
log_errors = On
同時(shí)也要設(shè)置錯(cuò)誤日志存放的目錄,建議根apache的日志存在一起:
error_log = D:/usr/local/apache2/logs/php_error.log
注意:給文件必須允許apache用戶的和組具有寫的權(quán)限。
【一、在服務(wù)器端配置】
我們php手手工安裝的,php的默認(rèn)配置文件在 /usr/local/apache2/conf/php.ini,我們最主要就是要配置php.ini中的內(nèi)容,讓我們執(zhí)行 php能夠更安全。整個(gè)PHP中的安全設(shè)置主要是為了防止phpshell和SQL Injection的攻擊,一下我們慢慢探討。我們先使用任何編輯工具打開 /etc/local/apache2/conf/php.ini,如果你是采用其他方式安裝,配置文件可能不在該目錄。
(1) 打開php的安全模式
php的安全模式是個(gè)非常重要的內(nèi)嵌的安全機(jī)制,能夠控制一些php中的函數(shù),比如system(),
同時(shí)把很多文件操作函數(shù)進(jìn)行了權(quán)限控制,也不允許對(duì)某些關(guān)鍵文件的文件,比如/etc/passwd,
但是默認(rèn)的php.ini是沒(méi)有打開安全模式的,我們把它打開:
safe_mode = on
(2) 用戶組安全
當(dāng)safe_mode打開時(shí),safe_mode_gid被關(guān)閉,那么php腳本能夠?qū)ξ募M(jìn)行訪問(wèn),而且相同
組的用戶也能夠?qū)ξ募M(jìn)行訪問(wèn)。
建議設(shè)置為:
safe_mode_gid = off
如果不進(jìn)行設(shè)置,可能我們無(wú)法對(duì)我們服務(wù)器網(wǎng)站目錄下的文件進(jìn)行操作了,比如我們需要
對(duì)文件進(jìn)行操作的時(shí)候。
(3) 安全模式下執(zhí)行程序主目錄
如果安全模式打開了,但是卻是要執(zhí)行某些程序的時(shí)候,可以指定要執(zhí)行程序的主目錄:
safe_mode_exec_dir = D:/usr/bin
一般情況下是不需要執(zhí)行什么程序的,所以推薦不要執(zhí)行系統(tǒng)程序目錄,可以指向一個(gè)目錄,
然后把需要執(zhí)行的程序拷貝過(guò)去,比如:
safe_mode_exec_dir = D:/tmp/cmd
但是,我更推薦不要執(zhí)行任何程序,那么就可以指向我們網(wǎng)頁(yè)目錄:
safe_mode_exec_dir = D:/usr/www
(4) 安全模式下包含文件
如果要在安全模式下包含某些公共文件,那么就修改一下選項(xiàng):
safe_mode_include_dir = D:/usr/www/include/
其實(shí)一般php腳本中包含文件都是在程序自己已經(jīng)寫好了,這個(gè)可以根據(jù)具體需要設(shè)置。
(5) 控制php腳本能訪問(wèn)的目錄
使用open_basedir選項(xiàng)能夠控制PHP腳本只能訪問(wèn)指定的目錄,這樣能夠避免PHP腳本訪問(wèn)
不應(yīng)該訪問(wèn)的文件,一定程度上限制了phpshell的危害,我們一般可以設(shè)置為只能訪問(wèn)網(wǎng)站目錄:
open_basedir = D:/usr/www
(6) 關(guān)閉危險(xiǎn)函數(shù)
如果打開了安全模式,那么函數(shù)禁止是可以不需要的,但是我們?yōu)榱税踩€是考慮進(jìn)去。比如,
我們覺(jué)得不希望執(zhí)行包括system()等在那的能夠執(zhí)行命令的php函數(shù),或者能夠查看php信息的
phpinfo()等函數(shù),那么我們就可以禁止它們:
disable_functions = system,passthru,exec,shell_exec,popen,phpinfo
如果你要禁止任何文件和目錄的操作,那么可以關(guān)閉很多文件操作
disable_functions = chdir,chroot,dir,getcwd,opendir,readdir,scandir,fopen,unlink,delete,copy,mkdir, rmdir,rename,file,file_get_contents,fputs,fwrite,chgrp,chmod,chown
以上只是列了部分不叫常用的文件處理函數(shù),你也可以把上面執(zhí)行命令函數(shù)和這個(gè)函數(shù)結(jié)合,
就能夠抵制大部分的phpshell了。
(7) 關(guān)閉PHP版本信息在http頭中的泄漏
我們?yōu)榱朔乐购诳瞳@取服務(wù)器中php版本的信息,可以關(guān)閉該信息斜路在http頭中:
expose_php = Off
比如黑客在 telnet www.12345.com 80 的時(shí)候,那么將無(wú)法看到PHP的信息。
(8) 關(guān)閉注冊(cè)全局變量
在PHP中提交的變量,包括使用POST或者GET提交的變量,都將自動(dòng)注冊(cè)為全局變量,能夠直接訪問(wèn),
這是對(duì)服務(wù)器非常不安全的,所以我們不能讓它注冊(cè)為全局變量,就把注冊(cè)全局變量選項(xiàng)關(guān)閉:
register_globals = Off
當(dāng)然,如果這樣設(shè)置了,那么獲取對(duì)應(yīng)變量的時(shí)候就要采用合理方式,比如獲取GET提交的變量var,
那么就要用$_GET['var']來(lái)進(jìn)行獲取,這個(gè)php程序員要注意。
(9) 打開magic_quotes_gpc來(lái)防止SQL注入
SQL注入是非常危險(xiǎn)的問(wèn)題,小則網(wǎng)站后臺(tái)被入侵,重則整個(gè)服務(wù)器淪陷,
所以一定要小心。php.ini中有一個(gè)設(shè)置:
magic_quotes_gpc = Off
這個(gè)默認(rèn)是關(guān)閉的,如果它打開后將自動(dòng)把用戶提交對(duì)sql的查詢進(jìn)行轉(zhuǎn)換,
比如把 ' 轉(zhuǎn)為 \'等,這對(duì)防止sql注射有重大作用。所以我們推薦設(shè)置為:
magic_quotes_gpc = On
(10) 錯(cuò)誤信息控制
一般php在沒(méi)有連接到數(shù)據(jù)庫(kù)或者其他情況下會(huì)有提示錯(cuò)誤,一般錯(cuò)誤信息中會(huì)包含php腳本當(dāng)
前的路徑信息或者查詢的SQL語(yǔ)句等信息,這類信息提供給黑客后,是不安全的,所以一般服務(wù)器建議禁止錯(cuò)誤提示:
display_errors = Off
如果你卻是是要顯示錯(cuò)誤信息,一定要設(shè)置顯示錯(cuò)誤的級(jí)別,比如只顯示警告以上的信息:
error_reporting = E_WARNING & E_ERROR
當(dāng)然,我還是建議關(guān)閉錯(cuò)誤提示。
(11) 錯(cuò)誤日志
建議在關(guān)閉display_errors后能夠把錯(cuò)誤信息記錄下來(lái),便于查找服務(wù)器運(yùn)行的原因:
log_errors = On
同時(shí)也要設(shè)置錯(cuò)誤日志存放的目錄,建議根apache的日志存在一起:
error_log = D:/usr/local/apache2/logs/php_error.log
注意:給文件必須允許apache用戶的和組具有寫的權(quán)限。
MYSQL的降權(quán)運(yùn)行
新建立一個(gè)用戶比如mysqlstart
net user mysqlstart fuckmicrosoft /add
net localgroup users mysqlstart /del
不屬于任何組
如果MYSQL裝在d:\mysql ,那么,給 mysqlstart 完全控制 的權(quán)限
然后在系統(tǒng)服務(wù)中設(shè)置,MYSQL的服務(wù)屬性,在登錄屬性當(dāng)中,選擇此用戶 mysqlstart 然后輸入密碼,確定。
重新啟動(dòng) MYSQL服務(wù),然后MYSQL就運(yùn)行在低權(quán)限下了。
如果是在windos平臺(tái)下搭建的apache我們還需要注意一點(diǎn),apache默認(rèn)運(yùn)行是system權(quán)限,
這很恐怖,這讓人感覺(jué)很不爽.那我們就給apache降降權(quán)限吧。
net user apache fuckmicrosoft /add
net localgroup users apache /del
ok.我們建立了一個(gè)不屬于任何組的用戶apche。
我們打開計(jì)算機(jī)管理器,選服務(wù),點(diǎn)apache服務(wù)的屬性,我們選擇log on,選擇this account,我們填入上面所建立的賬戶和密碼,
重啟apache服務(wù),ok,apache運(yùn)行在低權(quán)限下了。
實(shí)際上我們還可以通過(guò)設(shè)置各個(gè)文件夾的權(quán)限,來(lái)讓apache用戶只能執(zhí)行我們想讓它能干的事情,給每一個(gè)目錄建立一個(gè)單獨(dú)能讀寫的用戶。
這也是當(dāng)前很多虛擬主機(jī)提供商的流行配置方法哦,不過(guò)這種方法用于防止這里就顯的有點(diǎn)大材小用了。
【二、在PHP代碼編寫】
if (!get_magic_quotes_gpc()) {
$lastname = addslashes($_POST[‘lastname’]);
} else {
$lastname = $_POST[‘lastname’];
}
最好對(duì)magic_quotes_gpc已經(jīng)開放的情況下,還是對(duì)$_POST[’lastname’]進(jìn)行檢查一下。
再說(shuō)下mysql_real_escape_string和mysql_escape_string這2個(gè)函數(shù)的區(qū)別:
mysql_real_escape_string 必須在(PHP 4 >= 4.3.0, PHP 5)的情況下才能使用。否則只能用 mysql_escape_string ,兩者的區(qū)別是:mysql_real_escape_string 考慮到連接的
當(dāng)前字符集,而mysql_escape_string 不考慮。
總結(jié)一下:
* addslashes() 是強(qiáng)行加\;
* mysql_real_escape_string()
* mysql_escape_string不考慮連接的當(dāng)前字符集。
-------------------------------------------------------------------------------------------------
在PHP編碼的時(shí)候,如果考慮到一些比較基本的安全問(wèn)題,首先一點(diǎn):
1. 初始化你的變量
為什么這么說(shuō)呢?我們看下面的代碼:
PHP代碼
PHP代碼
2. 防止SQL Injection (sql注射)