1、愈來愈多的併發鏈接數php
如今的Web系統面對的併發鏈接數在近幾年呈現指數增加,高併發成爲了一種常態,給Web系統帶來不小的挑戰。以最簡單粗暴的方式解決,就是增長 Web系統的機器和升級硬件配置。雖然如今的硬件愈來愈便宜,可是一味地經過增長機器來解決併發量的增加,成本是很是高昂的。結合技術優化方案,纔是更有 效的解決方法。前端
併發鏈接數爲何呈指數增加?實際上,從這幾年的用戶基數上看,這個數量並無出現指數增加,所以它並不是主要緣由。主要緣由,仍是web變得更復雜,交互更豐富所致使的。web
1. 頁面元素增多,交互複雜ajax
Web頁面元素愈來愈多,更爲豐富。更多的資源元素,意味着更多的下載請求。Web系統的交互愈來愈複雜,交互場景和次數也大幅增長。以 「www.qq.com」的首頁爲例子,刷新一次,大概會有244個請求。而且,在頁面打開完成以後,還會有一些定時的查詢或者上報請求持續運做。後端
目前的Http請求,爲了減小反覆的建立和銷燬鏈接行爲,一般都創建長鏈接(Connection keep-alive)。一經創建,這個鏈接會 被保持住一段時間,被後續請求複用。然而,它也帶來了另外一個新的問題,鏈接的保持是會佔用Web系統服務端資源的,若是不充分使用這個鏈接,會致使資源浪 費。長鏈接被建立後,首批資源傳輸完畢,以後幾乎沒有數據交互,一直到超時時間,纔會自動釋放長鏈接佔據的系統資源。瀏覽器
除此以外,還有一些Web需求自己就須要長期保持鏈接的,例如Web socket。緩存
2. 主流的本瀏覽器的鏈接數在增長安全
面對愈來愈豐富的Web資源,主流瀏覽器併發鏈接數也在增長,同一個域下,早期的瀏覽器通常只有1-2個下載鏈接,而目前的主流瀏覽器一般在2-6 個。增長瀏覽器併發鏈接數目,在須要下載資源比較多的場景下,能夠加快頁面的加載速度。更多的鏈接對瀏覽器加載頁面元素是有好處的,在某些鏈接遭遇「網絡 阻塞」的狀況下,其餘正常的下載鏈接能夠繼續工做。服務器
這樣天然無形增長了Web系統後端的壓力,更多的下載鏈接意味着佔據了更多的Web服務器的資源。而在用戶訪問高峯期,自熱而然就造成了「高併發」 場景。這些鏈接和請求,佔據了服務器的大量CPU和內存等資源。尤爲在資源數目超過100+的網站頁面中,使用更多的下載鏈接,很是有必要。網絡
2、Web前端優化,下降服務端壓力
在緩解「高併發」的壓力,須要前端和後端的共同配合優化,才能達到最大效果。在用戶第一線的Web前端,能夠起到減小或者減輕Http請求的效果。
1. 減小Web請求
經常使用的實現方法是經過Http協議頭中的expire或max-age來控制,將靜態內容放入瀏覽器的本地緩存,在以後的一段時間裏,再也不請求 Web服務器,直接使用本地資源。還有HTML5中的本地存儲技術(LocalStorage),也被做爲一個強大的數據本地緩存。
這種方案緩存後,根本不發送請求到Web服務器,大幅下降服務器壓力,也帶來了良好的用戶體驗。可是,這種方案,對首次訪問的用戶無效,同時,也影響部分Web資源的實時性。
2. 減輕Web請求
瀏覽器的本地緩存是存在過時時間的,一旦過時,就必須從新向服務器請求。這個時候,會有兩種情形:
(1)服務器的資源內容沒有更新,瀏覽器請求Web資源,服務器回覆「能夠繼續使用本地緩存」。(發生通訊,可是Web服務器只須要作簡單「回覆」)
(2)服務器的文件或者內容已經更新,瀏覽器請求Web資源,Web服務器經過網絡傳輸新的資源內容。(發生通訊,Web服務器須要完成複雜的傳輸工做)
這裏的協商方式是經過Http協議的Last-Modified或Etag來控制,這個時候請求服務器,若是是內容沒有發生變動的狀況,服務器會返 回304 Not Modified。這樣的話,就不須要每次請求Web服務器都作複雜的傳輸完整數據文件的工做,只要簡單的http應答就能夠達到相同 的效果。
雖然上述請求,起到「減輕」Web服務器的壓力,可是鏈接仍然被創建,請求也發生了。
3. 合併頁面請求
若是是比較老一些的Web開發者,應該會更有印象,在ajax盛行以前。頁面大部分都是直接輸出的,並無這麼多的ajax請求,Web後端將頁面 內容徹底拼湊好了,再返回給前端。那個時候,頁面靜態化,是一個挺普遍的優化方式。後來,被交互更友好的ajax漸漸替代了,一個頁面的請求也變得愈來愈 多。
因爲移動端的網絡(2G/3G)比起PC寬帶差不少,而且部分手機配置比較低,面對一個超過100個請求的網頁,加載的速度會緩慢不少。因而,優化的方向又從新回到合併頁面元素,減小請求數量:
(1)合併HTML展現內容。將CSS和JS直接嵌入到HTML頁面內,不經過鏈接的方式引入。
(2)Ajax動態內容合併請求。對於動態內容,將10次Ajax請求合併爲1次的批量信息查詢。
(3)小圖片合併,經過CSS的偏移量技術Sprites,將不少小圖片合併爲一張。這個優化方式,在PC端的Web優化中,也很是常見。
合併請求,減小了傳輸數據的次數,也就是至關於將它們從一個一個地請求,變爲一次的「批量」請求。上述優化方法,到達「減輕」Web服務器壓力的目的,減小了須要創建的鏈接。
3、 節約Web服務端的內存
前端的優化完成,咱們就須要着眼於Web服務端自己。內存是Web服務器很是重要的資源,更多的內存一般意味着能夠同時放入更多的工做任務。就Web服務佔用內存而言,能夠粗略劃分:
(1)用來維持鏈接的基本內存,進程初始化時,會載入一些基礎模塊到內存。
(2)被傳輸的數據內容載入到各個緩衝區,佔據的內存。
(3)程序執行過程當中,申請和使用的內存。
若是維持一個鏈接,可以儘量少佔用內存,那麼咱們就能夠維持更多的併發鏈接,從而讓Web服務器支持更多的併發鏈接數。
Apache(httpd)是一個成熟而且古老的Web服務,而Apache的發展和演變,一直在追求作到這一點,它試圖不斷減小服務佔據的內存,以支持更大的併發量。以Apache的工做模式的演變爲視角,咱們一塊兒來看看,它們是如何優化內存的問題的。
1. prefork MPM,多進程工做模式
prefork是Apache最成熟和穩定的工做模式,即便是如今,仍然被普遍使用。主進程生成後,它先完成基礎的初始化工做,而後,經過fork 預先產生一批的子進程(子進程會複製父進程的內存空間,不須要再作基礎的初始化工做)。而後等待服務,之因此預先生成,是爲了減小頻繁建立和銷燬進程的開 銷。多進程的好處,是進程之間的內存數據不會相互干擾,同時,某個進程異常終止也不會影響其餘進程。可是,就內存而言,每一個httpd子進程佔用了不少的 內存,由於子進程的內存數據是複製父進程的。咱們能夠粗略認爲,這裏存在大量的「重複數據」被放在內存中。最終,致使咱們可以生成的子進程最大數量是頗有 限。在面對高併發時,由於有很多Keep-alive的長鏈接,將這些子進程「霸佔」住,極可能致使可用子進程耗盡。所以,prefork並不太適合高並 發場景。
優勢:成熟穩定,兼容全部新老模塊。同時,不須要擔憂線程安全的問題。(例如,咱們經常使用的mod_php,將PHP編譯爲Apache的子模塊,就不須要支持線程安全)
缺點:一個服務進程佔用不少內存。
2. worker MPM,多進程和多線程的混合模式
worker模式比起prefork,是使用了多進程和多線程的混合模式。它也預先fork了幾個子進程(數量不多),而後每一個子進程建立一些線程 (其中包括一個監聽線程)。每一個請求過來,會被分配到1個線程來服務。線程比起進程會更輕量,由於線程一般會共享父進程的內存空間,所以,內存的佔用會減 少一些。在高併發的場景下,由於比起prefork更省內存,所以會有更多的可用線程。
可是,它並無解決Keep-alive的長鏈接「霸佔」線程的問題,只是對象變成了比較輕量的線程。
有些人會以爲奇怪,那麼這裏爲何不徹底使用多線程呢,還要引入多進程?由於還須要考慮穩定性,若是一個線程掛了,會致使同一個進程下其餘正常的子 線程都掛了。若是所有采用多線程,某個線程掛掉,就致使整個Apache服務「全軍覆沒」。而目前的工做模式,受影響的只是Apache的一部分服務,而 不是整個服務。
線程共享父進程的內存空間,減小了內存的佔用,卻又引發了新的問題。就是「線程安全」,多個線程修改共享資源致使的「競爭行爲」,又強迫咱們所使用 的模塊必須支持「線程安全」。所以,它有必定程度上增長Web服務的不穩定性。例如,mod_php所使用的PHP拓展,也一樣須要支持「線程安全」,否 則,不能在該模式下使用。
優勢:佔據更少的內存,高併發下表現更優秀。
缺點:必須考慮線程安全的問題,同時鎖的引入又增長了CPU的開銷。
3. event MPM,多進程和多線程的混合模式,引入Epoll
這個是Apache中比較新的模式,在如今的版本(Apache 2.4.10)已是穩定可用的模式。它和worker模式很像,最大的區別在 於,它解決了keep-alive場景下,長期被佔用的線程的資源浪費問題。event MPM中,會有一個專門的線程來管理這些keep-alive類 型的線程,當有真實請求過來的時候,將請求傳遞給服務線程,執行完畢後,又容許它釋放。它減小了「佔據」鏈接而又不使用的資源浪費,加強了高併發場景下的 請求處理能力。由於減小了「閒等」的線程,線程的數量減小,同等場景下,內存佔用會降低一些。
event MPM在遇到某些不兼容的模塊時,會失效,將會回退到worker模式,一個工做線程處理一個請求。新版Apache官方自帶的模塊, 所有是支持event MPM的。注意一點,event MPM須要Linux系統(Linux 2.6+)對EPoll的支持,才能啓用。Apache 的三種模式中在真實應用場景中,event MPM是最節約內存的。
4. 使用比較輕量的Nginx做爲Web服務器
雖然Apache的不斷優化,減小了內存佔用,從而增長了處理高併發的能力。可是,正如前面所說,Apache是一個古老而成熟的Web服務,同 時,集成不少穩定的模塊,是一個比較重的Web服務。Nginx是個比較輕量的Web服務,佔據的內存自然就少於Apache。並且,Nginx經過一個 進程來服務於N個鏈接。所使用的方式,並非Apache的增長進程/線程來支持更多的鏈接。對於Nginx來講,它少建立了大量的進程/線程,減小了很 多內存的開銷。
靜態文件的QPS性能壓測結果,Nginx性能大概3倍於Apache對靜態文件的處理。PHP等動態文件的QPS,Nginx的作法一般是經過 FastCGI的方式和PHP-FPM通訊的方式完成,PHP做爲一個與之無關的外部服務存在。而Apache一般將PHP編譯爲本身的字模塊(新版的 Apache也支持FastCGI)。PHP動態文件,Nginx的表現略遜於Apache。
5. sendfile節約內存
Apache、Nginx等很多Web服務,都帶有sendfile支持的。sendfile能夠減小數據到「用戶態內存空間」(用戶緩衝區)的拷 貝,進而減小內存的佔用。固然,不少同窗第一個反應固然是問Why?爲了儘量清楚講述這個原理,咱們就先回Linux內核態和用戶態的存儲空間的交互。
通常狀況下,用戶態(也就是咱們的程序所在的內存空間)是不會直接讀寫或者操做各類設備(磁盤、網絡、終端等),中間一般用內核做爲「中間人」,來完成對設備的操做或者讀寫。
以最簡單的磁盤讀寫例子,從磁盤中讀取A文件,寫入到B文件。A文件數據是從磁盤開始,而後載入到「內核緩衝區」,而後再拷貝到「用戶緩衝區」,咱們才能夠對數據進行處理。寫入的時候,也同理,從「用戶態緩衝區」載入到「內核緩衝區」,最後寫入到磁盤B文件。
這樣寫文件很累吧,因而有人以爲這裏能夠跳過「用戶緩衝區」的拷貝。其實,這就是MMP(Memory-Mapping,內存映射)的實現,創建一 個磁盤空間和內存的直接映射,數據再也不復制到「用戶態緩衝區」,而是返回一個指向內存空間的指針。因而,咱們以前的讀寫文件例子,就會變成,A文件數據從 磁盤載入到「內核緩衝區」,而後從「內核緩衝區」複製到B文件的「內核緩衝區」,B文件再從」內核緩衝區「寫回到磁盤中。這個過程,減小了一次內存拷貝, 同時也少內存佔用。
好了,回到sendfile的話題上來,簡單的說,sendfile的作法和MMP相似,就是減小數據從」內核態緩衝區「到」用戶態緩衝區「的內存拷貝。
默認的磁盤文件讀取,到傳輸給socket,流程(不使用sendfile)是:
使用sendfile以後:
這種方式,不只節省了內存,並且還有CPU的開銷。
4、節約Web服務器的CPU
對Web服務器而言,CPU是另外一個很是核心的系統資源。雖然通常狀況下,咱們認爲業務程序的執行消耗了咱們主要CPU。可是,就Web服務程序而 言,多線程/多進程的上下文切換,也是比較消耗CPU資源的。一個進程/線程一般不能長期佔有CPU,當發生阻塞或者時間片用完,就沒法繼續佔用CPU, 這個時候,就會發生上下文切換,CPU時間片從老進程/線程切換到新的。除此以外,在併發鏈接數目很高的場景下,對這些用戶創建的鏈接(socket文件 描述符)狀態的輪詢和檢測,也是比較消耗CPU的。
而Apache和Nginx的發展和演變,也在努力減小CPU開銷。
1. Select/Poll(Apache早期版本的I/O多路複用)
一般,Web服務都要維護不少個和用戶通訊的socket文件描述符,I/O多路複用,其實就是爲了方便對這些文件描述符的管理和檢測。 Apache早期版本,是使用select的模式,簡單的說,就是將這些咱們關注的socket文件描述符交給內核,讓內核告訴咱們,那些描述符可操做。 Poll與select原理基本相同,所以放在一塊兒,它們之間的區別,就不贅敘了哈。
select/poll返回的是一個咱們以前提交的文件描述符集合(內核將其中可讀、可寫或者異常狀態的socket文件描述符的標識位修改了), 咱們須要經過輪詢檢查才能得到咱們能夠操做的文件描述符。在這個過程當中,不斷重複執行。在實際應用場景中,大部分被咱們監控的socket文件描述符,都 是」空閒的「,也就是說,不能操做。咱們對整個集合輪詢,就是爲了找了少部分咱們能夠操做的socket文件描述符。因而,當咱們監控的socket文件 描述符越多(用戶併發鏈接數愈來愈多),這個輪詢工做,也就愈來愈沉重,進而致使增大了CPU的開銷。
若是咱們監控的socket文件描述符,幾乎都是」活躍的「,反而使用這種模式更合適一點。
2. Epoll(新版的Apache的event MPM,Nginx等支持)
Epoll是Linux2.6開始正式支持的I/O多路複用,咱們能夠理解爲它是對select/poll的改進。首先,咱們一樣將咱們關注的 socket文件描述符集合告訴給內核,同時,給它們註冊」回調函數「,若是某個socket文件準備好了,就經過回調函數通知咱們。因而,咱們就不須要 專門去輪詢整個全量的socket文件描述符集合,直接能夠獲得已經可操做的socket文件描述符。那麼,那些大部分」空閒「的描述符,咱們就不遍歷 了。即便咱們監控的socket文件描述愈來愈多,咱們輪詢的也只是」活躍可操做「的socket文件描述符。
其實,有一種極端點的場景,就是咱們所有文件描述符幾乎都是」活躍「的,這樣反而致使了大量回調函數的執行,又增長了CPU的開銷。可是,就Web服務的真實場景,絕大部分時候,都是鏈接集合中都存在不少」空閒「鏈接。
3. 線程/進程的建立銷燬和上下文切換
一般,Apache某一個時間內,是一個進程/線程服務於一個鏈接。因而,Apache就有不少的進程/線程,服務於不少的鏈接。Web服務在高峯 期,會創建不少的進程/線程,也就帶來不少的上下文切換開銷。而Nginx,它一般只有1個master主進程和幾個worker子進程,而後,1個 worker進程服務不少個鏈接,進而節省了CPU的上下文切換開銷。
兩種模式雖然不一樣,但實際上不能直接出分好壞,綜合來講,各有各自的優點,就不妄議了哈。
4. 多線程下的鎖對CPU的開銷
Apache中的worker和event模式,都有采用多線程。多線程由於共享父進程的內存空間,在訪問共享數據的時候,就會產生競爭,也就是線 程安全問題。所以一般會引入鎖(Linux下比較經常使用的線程相關的鎖有互斥量metux,讀寫鎖rwlock等),成功獲取鎖的線程能夠繼續執行,獲取失 敗的一般選擇阻塞等待。引入鎖的機制,程序的複雜度每每增長很多,同時還有線程「死鎖」或者「餓死」的風險(多進程在訪問進程間共享資源的時候,也有一樣 的問題)。
死鎖現象(兩個線程彼此鎖住對方想要獲取的資源,相互阻塞等待,永遠沒法達不到知足條件):
餓死現象(某個線程,一直獲取不到它想要鎖資源,永遠沒法執行下一步):
爲了不這些鎖致使的問題,就不得不加大程序的複雜度,解決方案通常有:
(1)對資源的加鎖,根據約定好的順序,你們都先對共享資源X加鎖,加鎖成功以後才能加鎖共享資源Y。
(2)若是線程佔有資源X,卻加鎖資源Y失敗,則放棄加鎖,同時也釋放掉以前佔有的資源X。
在使用PHP的時候,在Apache的worker和event模式下,也必須兼容線程安全。一般,新版本的PHP官方庫是沒有線程安全方面的問 題,須要關注的是第三方擴展。PHP實現線程安全,不是經過鎖的方式實現的。而是爲每一個線程獨立申請一份全局變量的副本,至關於線程的私人內存空間,可是 這樣作相對消耗多一些內存。不過,這樣的好處,是不須要引入複雜的鎖機制實現,也避免了鎖機制對CPU的開銷。
這裏順便提到一下,常常和Nginx搭配工做的PHP-FPM(FastCGI)使用的是多進程,所以不會有線程安全的問題。
5、 小結
可能有些同窗看完以後,會得出結論,Nginx+PHP-FPM的工做方式,彷佛是最節省系統資源的Web系統工做方式。某種程度上說,的確是能夠這麼說的,可是Web系統的搭建,須要從實際業務應用的角度出發,具體問題須要具體分析,尋求最合適的技術方案。
Web服務的不斷演變和發展,努力地追求用盡量少的系統資源,來支撐更多的用戶請求,這是一條波瀾壯闊的前進之路。這些技術方案,匯聚了不少值得學習和借鑑的解決問題的思路。