您好,登錄后才能下訂單哦!
本篇內容主要講解“高并發Web服務的演變是什么”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“高并發Web服務的演變是什么”吧!
一、越來越多的并發連接數
現在的Web系統面對的并發連接數在近幾年呈現指數增長,高并發成為了一種常態,給Web系統帶來不小的挑戰。以最簡單粗暴的方式解決,就是增加 Web系統的機器和升級硬件配置。雖然現在的硬件越來越便宜,但是一味地通過增加機器來解決并發量的增長,成本是非常高昂的。結合技術優化方案,才是更有 效的解決方法。
并發連接數為什么呈指數增長?實際上,從這幾年的用戶基數上看,這個數量并沒有出現指數增長,因此它并非主要原因。主要原因,還是web變得更復雜,交互更豐富所導致的。
1. 頁面元素增多,交互復雜
Web頁面元素越來越多,更為豐富。更多的資源元素,意味著更多的下載請求。Web系統的交互越來越復雜,交互場景和次數也大幅增加。以 “www.qq.com”的首頁為例子,刷新一次,大概會有244個請求。并且,在頁面打開完成之后,還會有一些定時的查詢或者上報請求持續運作。
目前的Http請求,為了減少反復的創建和銷毀連接行為,通常都建立長連接(Connection keep-alive)。一經建立,這個連接會 被保持住一段時間,被后續請求復用。然而,它也帶來了另一個新的問題,連接的保持是會占用Web系統服務端資源的,如果不充分使用這個連接,會導致資源浪 費。長連接被創建后,首批資源傳輸完畢,之后幾乎沒有數據交互,一直到超時時間,才會自動釋放長連接占據的系統資源。
除此之外,還有一些Web需求本身就需要長期保持連接的,例如Web socket。
2. 主流的本瀏覽器的連接數在增加
面對越來越豐富的Web資源,主流瀏覽器并發連接數也在增加,同一個域下,早期的瀏覽器一般只有1-2個下載連接,而目前的主流瀏覽器通常在2-6 個。增加瀏覽器并發連接數目,在需要下載資源比較多的場景下,可以加快頁面的加載速度。更多的連接對瀏覽器加載頁面元素是有好處的,在某些連接遭遇“網絡 阻塞”的情況下,其他正常的下載連接可以繼續工作。
這樣自然無形增加了Web系統后端的壓力,更多的下載連接意味著占據了更多的Web服務器的資源。而在用戶訪問高峰期,自熱而然就形成了“高并發” 場景。這些連接和請求,占據了服務器的大量CPU和內存等資源。尤其在資源數目超過100+的網站頁面中,使用更多的下載連接,非常有必要。
二、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服務器壓力的目的,減少了需要建立的連接。
三、 節約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的開銷。
四、節約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)使用的是多進程,因此不會有線程安全的問題。
到此,相信大家對“高并發Web服務的演變是什么”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。