版權聲明:本文由韓偉原創文章,轉載請註明出處:
文章原文連接:https://www.qcloud.com/community/article/165nginx
來源:騰雲閣 https://www.qcloud.com/community程序員
任何的服務器的性能都是有極限的,面對海量的互聯網訪問需求,是不可能單靠一臺服務器或者一個CPU來承擔的。因此咱們通常都會在運行時架構設計之初,就考慮如何能利用多個CPU、多臺服務器來分擔負載,這就是所謂分佈的策略。分佈式的服務器概念很簡單,可是實現起來卻比較複雜。由於咱們寫的程序,每每都是以一個CPU,一塊內存爲基礎來設計的,因此要讓多個程序同時運行,而且協調運做,這須要更多的底層工做。算法
首先出現能支持分佈式概念的技術是多進程。在DOS時代,計算機在一個時間內只能運行一個程序,若是你想一邊寫程序,同時一邊聽mp3,都是不可能的。可是,在WIN95操做系統下,你就能夠同時開多個窗口,背後就是同時在運行多個程序。在Unix和後來的Linux操做系統裏面,都廣泛支持了多進程的技術。所謂的多進程,就是操做系統能夠同時運行咱們編寫的多個程序,每一個程序運行的時候,都好像本身獨佔着CPU和內存同樣。在計算機只有一個CPU的時候,實際上計算機會分時複用的運行多個進程,CPU在多個進程之間切換。可是若是這個計算機有多個CPU或者多個CPU核,則會真正的有幾個進程同時運行。因此進程就好像一個操做系統提供的運行時「程序盒子」,能夠用來在運行時,容納任何咱們想運行的程序。當咱們掌握了操做系統的多進程技術後,咱們就能夠把服務器上的運行任務,分爲多個部分,而後分別寫到不一樣的程序裏,利用上多CPU或者多核,甚至是多個服務器的CPU一塊兒來承擔負載。數據庫
多進程利用多CPUapache
這種劃分多個進程的架構,通常會有兩種策略:一種是按功能來劃分,好比負責網絡處理的一個進程,負責數據庫處理的一個進程,負責計算某個業務邏輯的一個進程。另一種策略是每一個進程都是一樣的功能,只是分擔不一樣的運算任務而已。使用第一種策略的系統,運行的時候,直接根據操做系統提供的診斷工具,就能直觀的監測到每一個功能模塊的性能消耗,由於操做系統提供進程盒子的同時,也能提供對進程的全方位的監測,好比CPU佔用、內存消耗、磁盤和網絡I/O等等。可是這種策略的運維部署會稍微複雜一點,由於任何一個進程沒有啓動,或者和其餘進程的通訊地址沒配置好,均可能致使整個系統沒法運做;而第二種分佈策略,因爲每一個進程都是同樣的,這樣的安裝部署就很是簡單,性能不夠就多找幾個機器,多啓動幾個進程就完成了,這就是所謂的平行擴展。編程
如今比較複雜的分佈式系統,會結合這兩種策略,也就是說系統既按一些功能劃分出不一樣的具體功能進程,而這些進程又是能夠平行擴展的。固然這樣的系統在開發和運維上的複雜度,都是比單獨使用「按功能劃分」和「平行劃分」要更高的。因爲要管理大量的進程,傳統的依靠配置文件來配置整個集羣的作法,會顯得愈來愈不實用:這些運行中的進程,可能和其餘不少進程產生通訊關係,當其中一個進程變動通訊地址時,勢必影響全部其餘進程的配置。因此咱們須要集中的管理全部進程的通訊地址,當有變化的時候,只須要修改一個地方。在大量進程構建的集羣中,咱們還會碰到容災和擴容的問題:當集羣中某個服務器出現故障,可能會有一些進程消失;而當咱們須要增長集羣的承載能力時,咱們又須要增長新的服務器以及進程。這些工做在長期運行的服務器系統中,會是比較常見的任務,若是整個分佈系統有一個運行中的中心進程,能自動化的監測全部的進程狀態,一旦有進程加入或者退出集羣,都能即時的修改全部其餘進程的配置,這就造成了一套動態的多進程管理系統。開源的ZooKeeper給咱們提供了一個能夠充當這種動態集羣中心的實現方案。因爲ZooKeeper自己是能夠平行擴展的,因此它本身也是具有必定容災能力的。如今愈來愈多的分佈式系統都開始使用以ZooKeeper爲集羣中心的動態進程管理策略了。緩存
動態進程集羣安全
在調用多進程服務的策略上,咱們也會有必定的策略選擇,其中最著名的策略有三個:一個是動態負載均衡策略;一個是讀寫分離策略;一個是一致性哈希策略。動態負載均衡策略,通常會蒐集多個進程的服務狀態,而後挑選一個負載最輕的進程來分發服務,這種策略對於比較同質化的進程是比較合適的。讀寫分離策略則是關注對持久化數據的性能,好比對數據庫的操做,咱們會提供一批進程專門用於提供讀數據的服務,而另一個(或多個)進程用於寫數據的服務,這些寫數據的進程都會每次寫多份拷貝到「讀服務進程」的數據區(可能就是單獨的數據庫),這樣在對外提供服務的時候,就能夠提供更多的硬件資源。一致性哈希策略是針對任何一個任務,看看這個任務所涉及讀寫的數據,是屬於哪一片的,是否有某種能夠緩存的特徵,而後按這個數據的ID或者特徵值,進行「一致性哈希」的計算,分擔給對應的處理進程。這種進程調用策略,能很是的利用上進程內的緩存(若是存在),好比咱們的一個在線遊戲,由100個進程承擔服務,那麼咱們就能夠把遊戲玩家的ID,做爲一致性哈希的數據ID,做爲進程調用的KEY,若是目標服務進程有緩存遊戲玩家的數據,那麼全部這個玩家的操做請求,都會被轉到這個目標服務進程上,緩存的命中率大大提升。而使用「一致性哈希」,而不是其餘哈希算法,或者取模算法,主要是考慮到,若是服務進程有一部分因故障消失,剩下的服務進程的緩存依然能夠有效,而不會整個集羣全部進程的緩存都失效。具體有興趣的讀者能夠搜索「一致性哈希」一探究竟。服務器
以多進程利用大量的服務器,以及服務器上的多個CPU核心,是一個很是有效的手段。可是使用多進程帶來的額外的編程複雜度的問題。通常來講咱們認爲最好是每一個CPU核心一個進程,這樣能最好的利用硬件。若是同時運行的進程過多,操做系統會消耗不少CPU時間在不一樣進程的切換過程上。可是,咱們早期所得到的不少API都是阻塞的,好比文件I/O,網絡讀寫,數據庫操做等。若是咱們只用有限的進程來執行帶這些阻塞操做的程序,那麼CPU會大量被浪費,由於阻塞的API會讓有限的這些進程停着等待結果。那麼,若是咱們但願能處理更多的任務,就必需要啓動更多的進程,以便充分利用那些阻塞的時間,可是因爲進程是操做系統提供的「盒子」,這個盒子比較大,切換耗費的時間也比較多,因此大量並行的進程反而會無謂的消耗服務器資源。加上進程之間的內存通常是隔離的,進程間若是要交換一些數據,每每須要使用一些操做系統提供的工具,好比網絡socket,這些都會額外消耗服務器性能。所以,咱們須要一種切換代價更少,通訊方式更便捷,編程方法更簡單的並行技術,這個時候,多線程技術出現了。網絡
在進程盒子裏面的線程盒子
多線程的特色是切換代價少,能夠同時訪問內存。咱們能夠在編程的時候,任意讓某個函數放入新的線程去執行,這個函數的參數能夠是任何的變量或指針。若是咱們但願和這些運行時的線程通訊,只要讀、寫這些指針指向的變量便可。在須要大量阻塞操做的時候,咱們能夠啓動大量的線程,這樣就能較好的利用CPU的空閒時間;線程的切換代價比進程低得多,因此咱們能利用的CPU也會多不少。線程是一個比進程更小的「程序盒子」,他能夠放入某一個函數調用,而不是一個完整的程序。通常來講,若是多個線程只是在一個進程裏面運行,那實際上是沒有利用到多核CPU的並行好處的,僅僅是利用了單個空閒的CPU核心。可是,在JAVA和C#這類帶虛擬機的語言中,多線程的實現底層,會根據具體的操做系統的任務調度單位(好比進程),儘可能讓線程也成爲操做系統能夠調度的單位,從而利用上多個CPU核心。好比Linux2.6以後,提供了NPTL的內核線程模型,JVM就提供了JAVA線程到NPTL內核線程的映射,從而利用上多核CPU。而Windows系統中,聽說自己線程就是系統的最小調度單位,因此多線程也是利用上多核CPU的。因此咱們在使用JAVA\C#編程的時候,多線程每每已經同時具有了多進程利用多核CPU、以及切換開銷低的兩個好處。
早期的一些網絡聊天室服務,結合了多線程和多進程使用的例子。一開始程序會啓動多個廣播聊天的進程,每一個進程都表明一個房間;每一個用戶鏈接到聊天室,就爲他啓動一個線程,這個線程會阻塞的讀取用戶的輸入流。這種模型在使用阻塞API的環境下,很是簡單,但也很是有效。
當咱們在普遍使用多線程的時候,咱們發現,儘管多線程有不少優勢,可是依然會有明顯的兩個缺點:一個內存佔用比較大且不太可控;第二個是多個線程對於用一個數據使用時,須要考慮複雜的「鎖」問題。因爲多線程是基於對一個函數調用的並行運行,這個函數裏面可能會調用不少個子函數,每調用一層子函數,就會要在棧上佔用新的內存,大量線程同時在運行的時候,就會同時存在大量的棧,這些棧加在一塊兒,可能會造成很大的內存佔用。而且,咱們編寫服務器端程序,每每但願資源佔用儘可能可控,而不是動態變化太大,由於你不知道何時會由於內存用完而當機,在多線程的程序中,因爲程序運行的內容致使棧的伸縮幅度可能很大,有可能超出咱們預期的內存佔用,致使服務的故障。而對於內存的「鎖」問題,一直是多線程中複雜的課題,不少多線程工具庫,都推出了大量的「無鎖」容器,或者「線程安全」的容器,而且還大量設計了不少協調線程運做的類庫。可是這些複雜的工具,無疑都是證實了多線程對於內存使用上的問題。
同時排多條隊就是並行
因爲多線程仍是有必定的缺點,因此不少程序員想到了一個釜底抽薪的方法:使用多線程每每是由於阻塞式API的存在,好比一個read()操做會一直中止當前線程,那麼咱們能不能讓這些操做變成不阻塞呢?——selector/epoll就是Linux退出的非阻塞式API。若是咱們使用了非阻塞的操做函數,那麼咱們也無需用多線程來併發的等待阻塞結果。咱們只須要用一個線程,循環的檢查操做的狀態,若是有結果就處理,無結果就繼續循環。這種程序的結果每每會有一個大的死循環,稱爲主循環。在主循環體內,程序員能夠安排每一個操做事件、每一個邏輯狀態的處理邏輯。這樣CPU既無需在多線程間切換,也無需處理複雜的並行數據鎖的問題——由於只有一個線程在運行。這種就是被稱爲「併發」的方案。
服務員兼了點菜、上菜就是併發
實際上計算機底層早就有使用併發的策略,咱們知道計算機對於外部設備(好比磁盤、網卡、顯卡、聲卡、鍵盤、鼠標),都使用了一種叫「中斷」的技術,早期的電腦使用者可能還被要求配置IRQ號。這個中斷技術的特色,就是CPU不會阻塞的一直停在等待外部設備數據的狀態,而是外部數據準備好後,給CPU發一個「中斷信號」,讓CPU轉去處理這些數據。非阻塞的編程實際上也是相似這種行爲,CPU不會一直阻塞的等待某些I/O的API調用,而是先處理其餘邏輯,而後每次主循環去主動檢查一下這些I/O操做的狀態。
多線程和異步的例子,最著名就是Web服務器領域的Apache和Nginx的模型。Apache是多進程/多線程模型的,它會在啓動的時候啓動一批進程,做爲進程池,當用戶請求到來的時候,從進程池中分配處理進程給具體的用戶請求,這樣能夠節省多進程/線程的建立和銷燬開銷,可是若是同時有大量的請求過來,仍是須要消耗比較高的進程/線程切換。而Nginx則是採用epoll技術,這種非阻塞的作法,可讓一個進程同時處理大量的併發請求,而無需反覆切換。對於大量的用戶訪問場景下,apache會存在大量的進程,而nginx則能夠僅用有限的進程(好比按CPU核心數來啓動),這樣就會比apache節省了很多「進程切換」的消耗,因此其併發性能會更好。
Nginx的固定多進程,一個進程異步處理多個客戶端
Apache的多態多進程,一個進程處理一個客戶
在現代服務器端軟件中,nginx這種模型的運維管理會更簡單,性能消耗也會稍微更小一點,因此成爲最流行的進程架構。可是這種好處,會付出一些另外的代價:非阻塞代碼在編程的複雜度變大。