如何提高伺服器併發處理能力
說明
以下內容為入門級介紹,意在對老技術作較全的總結而不是較深的研究。主要參考《構建高效能Web站點》一書。
什麼是伺服器併發處理能力
一臺伺服器在單位時間裡能處理的請求越多,伺服器的能力越高,也就是伺服器併發處理能力越
有什麼方法衡量伺服器併發處理能力
1. 吞吐率
吞吐率,單位時間裡伺服器處理的最大請求數,單位req/s。
從伺服器角度,實際併發使用者數的可以理解為伺服器當前維護的代表不同使用者的檔案描述符總數,也就是併發連線數。伺服器一般會限制同時服務的最多使用者數,比如apache的MaxClents引數。
這裡再深入一下,對於伺服器來說,伺服器希望支援高吞吐率,對於使用者來說,使用者只希望等待最少的時間,顯然,雙方不能滿足,所以雙方利益的平衡點,就是我們希望的最大併發使用者數。
2. 壓力測試
有一個原理一定要先搞清楚,假如100個使用者同時向伺服器分別進行10個請求,與1個使用者向伺服器連續進行1000次請求,對伺服器的壓力是一樣嗎?實際上是不一樣的,因對每一個使用者,連續傳送請求實際上是指傳送一個請求並接收到響應資料後再發送下一個請求。這樣對於1個使用者向伺服器連續進行1000次請求, 任何時刻伺服器的網絡卡接收緩衝區中只有1個請求,而對於100個使用者同時向伺服器分別進行10個請求,伺服器的網絡卡接收緩衝區最多有100個等待處理的請求,顯然這時的伺服器壓力更大。
壓力測試前提考慮的條件
-
併發使用者數: 指在某一時刻同時向伺服器傳送請求的使用者總數(HttpWatch)
-
總請求數
-
請求資源描述
-
請求等待時間(使用者等待時間)
-
使用者平均請求的等待時間
-
伺服器平均請求處理的時間
-
硬體環境
壓力測試中關心的時間又細分以下2種:
-
使用者平均請求等待時間(這裡暫不把資料在網路的傳輸時間,還有使用者PC本地的計算時間計算入內)
-
伺服器平均請求處理時間
使用者平均請求等待時間主要用於衡量伺服器在一定併發使用者數下,單個使用者的服務質量;而伺服器平均請求處理時間就是吞吐率的倒數,一般來說,使用者平均請求等待時間 = 伺服器平均請求處理時間 * 併發使用者數
怎麼提高伺服器的併發處理能力
1. 提高CPU併發計算能力
伺服器之所以可以同時處理多個請求,在於作業系統通過多執行流體系設計使得多個任務可以輪流使用系統資源,這些資源包括CPU,記憶體以及I/O. 這裡的I/O主要指磁碟I/O, 和網路I/O。
多程序 & 多執行緒
多執行流的一般實現便是程序,多程序的好處可以對CPU時間的輪流使用,對CPU計算和IO操作重疊利用。這裡的IO主要是指磁碟IO和網路IO,相對CPU而言,它們慢的可憐。
而實際上,大多數程序的時間主要消耗在I/O操作上。現代計算機的DMA技術可以讓CPU不參與I/O操作的全過程,比如程序通過系統呼叫,使得CPU向網絡卡或者磁碟等I/O裝置發出指令,然後程序被掛起,釋放出CPU資源,等待I/O裝置完成工作後通過中斷來通知程序重新就緒。對於單任務而言,CPU大部分時間空閒,這時候多程序的作用尤為重要。
多程序不僅能夠提高CPU的併發度。其優越性還體現在獨立的記憶體地址空間和生命週期所帶來的穩定性和健壯性,其中一個程序崩潰不會影響到另一個程序。
但是程序也有如下缺點:
-
fork()系統呼叫開銷很大: prefork
-
程序間排程和上下文切換成本: 減少程序數量
-
龐大的記憶體重複:共享記憶體
-
IPC程式設計相對比較麻煩
減少程序切換
當硬體上下文頻繁裝入和移出時,所消耗的時間是非常可觀的。可用Nmon工具監視伺服器每秒的上下文切換次數。為了儘量減少上下文切換次數,最簡單的做法就是減少程序數,儘量使用執行緒並配合其它I/O模型來設計併發策略。
還可以考慮使用程序繫結CPU技術,增加CPU快取的命中率。若程序不斷在各CPU上切換,這樣舊的CPU快取就會失效。
減少使用不必要的鎖
伺服器處理大量併發請求時,多個請求處理任務時存在一些資源搶佔競爭,這時一般採用“鎖”機制來控制資源的佔用,當一個任務佔用資源時,我們鎖住資源,這時其它任務都在等待鎖的釋放,這個現象稱為鎖競爭。
通過鎖競爭的本質,我們要意識到儘量減少併發請求對於共享資源的競爭。比如在允許情況下關閉伺服器訪問日誌,這可以大大減少在鎖等待時的延遲時間。要最大程度減少無辜的等待時間。
這裡說下無鎖程式設計,就是由核心完成這個鎖機制,主要是使用原子操作替代鎖來實現對共享資源的訪問保護 ,使用原子操作時,在進行實際的寫操作時,使用了lock指令,這樣就可以阻止其他任務寫這塊記憶體,避免出現數據競爭現象。原子操作速度比鎖快,一般要快一倍以上。
例如fwrite(), fopen(),其是使用append方式寫檔案,其原理就是使用了無鎖程式設計,無鎖程式設計的複雜度高,但是效率快,而且發生死鎖概率低。
考慮程序優先順序
程序排程器會動態調整執行佇列中程序的優先順序,通過top觀察程序的PR值
考慮系統負載
可在任何時刻檢視/proc/loadavg, top中的load average也可看出
考慮CPU使用率
除了使用者空間和核心空間的CPU使用率以外,還要關注I/O wait,它是指CPU空閒並且等待I/O操作完成的時間比例(top中檢視wa的值)。
2. 考慮減少記憶體分配和釋放
伺服器的工作過程中,需要大量的記憶體,使得記憶體的分配和釋放工作尤為重要。可以通過改善資料結構和演算法複製度來適當減少中間臨時變數的記憶體分配及資料複製時間,而伺服器本身也使用了各自的策略來提高效率。
例如Apache,在執行開始時一次申請大片的記憶體作為記憶體池,若隨後需要時就在記憶體池中直接獲取,不需要再次分配,避免了頻繁的記憶體分配和釋放引起的記憶體整理時間。
再如Nginx使用多執行緒來處理請求,使得多個執行緒之間可以共享記憶體資源,從而令它的記憶體總體使用量大大減少,另外,nginx分階段的記憶體分配策略,按需分配,及時釋放,使得記憶體使用量保持在很小的數量範圍。
另外,還可以考慮共享記憶體。共享記憶體指在多處理器的計算機系統中,可以被不同中央處理器(CPU)訪問的大容量記憶體,也可以由不同程序共享,是非常快的程序通訊方式。
但是使用共享記憶體也有不好的地方,就是對於多機器時資料不好統一。
shell命令ipcs可用來顯示系統下共享記憶體的狀態,函式shmget可以建立或開啟一塊共享記憶體區,函式shmat將一個存在的共享記憶體段連線到本程序空間, 函式shmctl可以對共享記憶體段進行多種操作,函式shmdt函式分離該共享記憶體。
3. 考慮使用持久連線
持久連線也為長連線,它本身是TCP通訊的一種普通方式,即在一次TCP連線中持續傳送多分資料而不斷開連線,與它相反的方式稱為短連線,也就是建立連線後傳送一份資料就斷開,然後再次建立連線傳送下一份資料, 周而復始。是否採用持久連線,完全取決於應用特點。
從效能角度看,建立TCP連線的操作本身是一項不小的開銷,在允許的情況下,連線次數越少,越有利於效能的提升; 尤其對於密集型的圖片或網頁等小資料請求處理有明顯的加速所用。
HTTP長連線需要瀏覽器和web伺服器的共同協作,目前瀏覽器普遍支援長連線,表現在其發出的HTTP請求資料頭中包含關於長連線的宣告,如下: Connection: Keep-Alive,主流的web伺服器都支援長連線,比如apache中,可以用KeepAlive off關閉長連線。
對於長連線的有效使用,還有關鍵一點在於長連線超時時間的設定,即長連線在什麼時候關閉嗎? Apache的預設設定為5s, 若這個時間設定過長,則可能導致資源無效佔有,維持大量空閒程序,影響伺服器效能。
4. 改進I/O 模型
I/O操作根據裝置的不同分為很多型別,比如記憶體I/O, 網路I/O, 磁碟I/O. 對於網路I/O和磁碟I/O, 它們的速度要慢很多,儘管使用RAID磁碟陣列可通過並行磁碟磁碟來加快磁碟I/O速度,購買大連獨享網路頻寬以及使用高頻寬網路介面卡可以提高網路i/O的速度。
但這些I/O操作需要核心系統呼叫來完成,這些需要CPU來排程,這使得CPU不得不浪費寶貴的時間來等待慢速I/O操作。我們希望讓CPU足夠少的時間在i/O操作的排程上,如何讓高速的CPU和慢速的I/O裝置更好地協調工作,是現代計算機一直探討的話題。各種I/O模型的本質區別在於CPU的參與方式。
1. DMA技術
I/O裝置和記憶體之間的資料傳輸方式由DMA控制器完成。在DMA模式下,CPU只需向DMA下達命令,讓DMA控制器來處理資料的傳送,這樣可以大大節省系統資源。
2. 非同步I/O
非同步I/O指主動請求資料後便可以繼續處理其它任務,隨後等待I/O操作的通知,這樣程序在資料讀寫時不發生阻塞。
非同步I/O是非阻塞的,當函式返回時,真正的I/O傳輸已經完成,這讓CPU處理和I/O操作達到很好的重疊。
3. I/O多路複用
epoll伺服器同時處理大量的檔案描述符是必不可少的,若採用同步非阻塞I/O模型,若同時接收TCP連線的資料,就必須輪流對每個socket呼叫接收資料的方法,不管這些socket有沒有可接收的資料,都要詢問一次。
假如大部分socket並沒有資料可以接收,那麼程序便會浪費很多CPU時間用於檢查這些socket有沒有可以接收的資料。多路I/O就緒通知的出現,提供了對大量檔案描述符就緒檢查的高效能方案,它允許程序通過一種方法同時監視所有檔案描述符,並可以快速獲得所有就緒的檔案描述符,然後只針對這些檔案描述符進行資料訪問。
epoll可以同時支援水平觸發和邊緣觸發,理論上邊緣觸發效能更高,但是程式碼實現複雜,因為任何意外的丟失事件都會造成請求處理錯誤。
epoll主要有2大改進:
epoll只告知就緒的檔案描述符,而且當呼叫epoll_wait()獲得檔案描述符時,返回並不是實際的描述符,而是一個代表就緒描述符數量的值,然後只需去epoll指定的一個數組中依次取得相應數量的檔案描述符即可,這裡使用了記憶體對映(mmap)技術,這樣徹底省掉了這些檔案描述符在系統呼叫時複製的開銷。
epoll採用基於事件的就緒通知方式。其事先通過epoll_ctrl()註冊每一個檔案描述符,一旦某個檔案描述符就緒時,核心會採用類似callback的回撥機制,當程序呼叫epoll_wait()時得到通知
關於IO模型,可以參考前面寫的相關文章Java NIO.2; 關於epoll,可以參考前面寫的文章select、poll和epoll簡介。
4. Sendfile
大多數時候,我們都向伺服器請求靜態檔案,比如圖片,樣式表等,在處理這些請求時,磁碟檔案的資料先經過核心緩衝區,然後到使用者記憶體空間,不需經過任何處理,其又被送到網絡卡對應的核心緩衝區,接著再被送入網絡卡進行傳送。
Linux提供sendfile()系統呼叫,可以講磁碟檔案的特定部分直接傳送到代表客戶端的socket描述符,加快了靜態檔案的請求速度,同時減少CPU和記憶體的開銷。
適用場景: 對於請求較小的靜態檔案,sendfile發揮的作用不那麼明顯,因傳送資料的環節在整個過程中所佔時間的比例相比於大檔案請求時小很多。
5. 記憶體對映
Linux核心提供一種訪問磁碟檔案的特殊方式,它可以將記憶體中某塊地址空間和我們指定的磁碟檔案相關聯,從而對這塊記憶體的訪問轉換為對磁碟檔案的訪問。這種技術稱為記憶體對映。
多數情況下,記憶體對映可以提高磁碟I/O的效能,無須使用read()或write()等系統呼叫來訪問檔案,而是通過mmap()系統呼叫來建立記憶體和磁碟檔案的關聯,然後像訪問記憶體一樣自由訪問檔案。
缺點:在處理較大檔案時,記憶體對映會導致較大的記憶體開銷,得不償失。
6. 直接I/O
在linux 2.6中,記憶體對映和直接訪問檔案沒有本質差異,因為資料需要經過2次複製,即在磁碟與核心緩衝區之間以及在核心緩衝區與使用者態記憶體空間。
引入核心緩衝區的目的在於提高磁碟檔案的訪問效能,然而對於一些複雜的應用,比如資料庫伺服器,它們為了進一步提高效能,希望繞過核心緩衝區,由自己在使用者態空間實現並管理I/O緩衝區,比如資料庫可根據更加合理的策略來提高查詢快取命中率。另一方面,繞過核心緩衝區也可以減少系統記憶體的開銷,因核心緩衝區本身就在使用系統記憶體。
Linux在open()系統呼叫中增加引數選項O_DIRECT,即可繞過核心緩衝區直接訪問檔案,實現直接I/O。
在Mysql中,對於Innodb儲存引擎,自身進行資料和索引的快取管理,可在my.cnf配置中分配raw分割槽跳過核心緩衝區,實現直接I/O。
改進伺服器併發策略
伺服器併發策略的目的,是讓I/O操作和CPU計算儘量重疊進行,一方面讓CPU在I/O等待時不要空閒,另一方面讓CPU在I/O排程上儘量花最少的時間。
一個程序處理一個連線,非阻塞I/O
這樣會存在多個併發請求同時到達時,伺服器必然要準備多個程序來處理請求。其程序的開銷限制了它的併發連線數。但從穩定性和相容性的角度,則其相對安全,任何一個子程序的崩潰不會影響伺服器本身,父程序可以建立新的子程序;這種策略典型的例子就是Apache的fork和prefork模式。對於併發數不高(如150以內)的站點同時依賴Apache其它功能時的應用選擇Apache還是可以的。
一個執行緒處理一個連線,非阻塞IO
這種方式允許在一個程序中通過多個執行緒來處理多個連線,一個執行緒處理一個連線。Apache的worker模式就是這種典型例子,使其可支援更多的併發連線。不過這種模式的總體效能還不如prefork,所以一般不選用worker模式。
一個程序處理多個連線,非同步I/O
一個執行緒同時處理多個連線,潛在的前提條件就是使用IO多路複用就緒通知。這種情況下,將處理多個連線的程序叫做worker程序或服務程序。worker的數量可以配置,如Nginx中的worker_processes 4。
一個執行緒處理多個連線,非同步IO
即使有高效能的IO多路複用就緒通知,但磁碟IO的等待還是無法避免的。更加高效的方法是對磁碟檔案使用非同步IO,目前很少有Web伺服器真正意義上支援這種非同步IO。
6. 改進硬體環境
還有一點要提及的是硬體環境,伺服器的硬體配置對應用程式的效能提升往往是最直接,也是最簡單的方式,這就是所謂的scale up。這裡不做論述。
1歡迎工作一到十年的Java工程師朋友們加入Java進階高階架構:828545509
2本群提供免費的學習指導 架構資料 以及免費的解答
3不懂得問題都可以在本群提出來 之後還會有職業生涯規劃以及面試指導