馬上就要畢業了,也要開始找工作了,大學寫了這么多代碼了,卻沒有好好總結一下常用的概念很是遺憾額,就通過這篇博客記錄一下我最常用的一些知識好了。
說到Web服務器,有很多文章都介紹的很好,之前看到一篇非常不錯的,對我幫助很大,可惜現在找不到原文了,看到博客園有人轉載,我就在這里也記一下好了,在此非常感謝作者的分析,受益匪淺。
那么在說Web服務器之前,先說說線程、進程、以及並發連接數。
1.進程與線程
進程是具有一定獨立功能的程序,關於某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位。從邏輯角度來看,多線程的意義在於一個應用程序(進程)中,有多個執行部分可以同時執行。但操作系統並沒有將多個線程看做多個獨立的應用來實現,而是作為進程來調度和管理以及資源分配。這就是進程和線程的重要區別,進程和線程的主要差別在於,進程有獨立的地址空間,一個進程崩潰后,在保護模式下不會對其它進程產生影響,而線程只是一個進程中的不同執行路徑。線程有自己的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個線程死掉就等於整個進程死掉,所以多進程的程序要比多線程的程序健壯,但在進程切換時,耗費資源較大,效率要差一些。但對於一些要求同時進行並且又要共享某些變量的並發操作,只能用線程,不能用進程。下面我們來說一說並發連接數。
2.並發連接數
(1).什么是最大並發連接數呢?
最大並發連接數是服務器同一時間能處理最大會話數量。
(2).何為會話?
我們打開一個網站就是一個客戶端瀏覽器與服務端的一個會話,而我們瀏覽網頁是基於http協議。
(3).HTTP協議如何工作?
HTTP支持兩種建立連接的方式:非持久連接和持久連接(HTTP1.1默認的連接方式為持久連接)。
(4).瀏覽器與Web服務器之間將完成下列7個步驟
-
建立TCP連接
-
Web瀏覽器向Web服務器發送請求命令
-
Web瀏覽器發送請求頭信息
-
Web服務器應答
-
Web服務器發送應答頭信息
-
Web服務器向瀏覽器發送數據
-
Web服務器關閉TCP連接
一般情況下,一旦Web服務器向瀏覽器發送了請求數據,它就要關閉TCP連接,但是瀏覽器一般其頭信息加入了這行代碼 Connection:keep-alive,TCP連接在發送后將仍然保持打開狀態,於是,瀏覽器可以繼續通過相同的連接發送請求。保持連接目的,節省了為每 個請求建立新連接所需的時間,還節約了網絡帶寬。
3.並發連接數的計算方法
用戶下載服務器上的文件,則為一個連接,用戶文件下載完畢后這個連接就消失了。有時候用戶用迅雷的多線程方式下載的話,這一個用戶開啟了5個線程的話,就算是5個連接。
用戶打開你的頁面,就算停留在頁面沒有對服務器發出任何請求,那么在用戶打開一面以后的15分鍾內也都要算一個在線。
上面的情況用戶繼續打開同一個網站的其他頁面,那么在線人數按照用戶最后一次點擊(發出請求)以后的15分鍾計算,在這個15分鍾內不管用戶怎么點擊(包括新窗口打開)都還是一人在線。
當用戶打開頁面然后正常關閉瀏覽器,用戶的在線人數也會馬上清除。
二、Web服務器提供服務的方式
Web服務器由於要同時為多個客戶提供服務,就必須使用某種方式來支持這種多任務的服務方式。一般情況下可以有以下三種方式來選擇,多進程方式、多線程方式及異步方式。其中,多進程方式中服務器對一個客戶要使用一個進程來提供服務,由於在操作系統中,生成一個進程需要進程內存復制等額外的開銷,這樣在客戶較多時的性能就會降低。為了克服這種生成進程的額外開銷,可以使用多線程方式或異步方式。在多線程方式中,使用進程中的多個線程提供服務, 由於線程的開銷較小,性能就會提高。事實上,不需要任何額外開銷的方式還是異步方式,它使用非阻塞的方式與每個客戶通信,服務器使用一個進程進行輪詢就行了。
雖然異步方式最為高效,但它也有自己的缺點。因為異步方式下,多個任務之間的調度是由服務器程序自身來完成的,而且一旦一個地方出現問題則整個服務器就會出現問題。因此,向這種服務器增加功能,一方面要遵從該服務器自身特定的任務調度方式,另一方面要確保代碼中沒有錯誤存在,這就限制了服務器的功能,使得異步方式的Web服務器的效率最高,但功能簡單,如Nginx服務器。
由於多線程方式使用線程進行任務調度,這樣服務器的開發由於遵從標准,從而變得簡單並有利於多人協作。然而多個線程位於同一個進程內,可以訪問同樣的內存空間,因此存在線程之間的影響,並且申請的內存必須確保申請和釋放。對於服務器系統來講,由於它要數天、數月甚至數年連續不停的運轉,一點點錯誤就會逐漸積累而最終導致影響服務器的正常運轉,因此很難編寫一個高穩定性的多線程服務器程序。但是,不是不能做到時。Apache的worker模塊就能很好的支持多線程的方式。
多進程方式的優勢就在於穩定性,因為一個進程退出的時候,操作系統會回收其占用的資源,從而使它不會留下任何垃圾。即便程序中出現錯誤,由於進程是相互隔離的,那么這個錯誤不會積累起來,而是隨着這個進程的退出而得到清除。Apache的prefork模塊就是支持多進程的模塊。
三、多進程、多線程、異步模式的對比
Web服務器總的來說提供服務的方式有三種,多進程方式,多線程的方式,異步方式。其中效率最高的是異步的方式,最穩定的是多進程方式,占用資源較少的是多線程的方式。
1.多進程
此種架構方式中,web服務器生成多個進程並行處理多個用戶請求,進程可以按需或事先生成。有的web服務器應用程序為每個用戶請求生成一個單獨的進程來進行響應,不過,一旦並發請求數量達到成千上萬時,多個同時運行的進程將會消耗大量的系統資源。(即每個進程只能響應一個請求或多個進程對應多個請求)
優點:
-
最大的優勢就在於穩定性,一個進程出錯不會影響其它進程。如,服務器同時連接100個請求對就的是100個進程,其中一個進程出錯,只會殺死一個進程,還有99個進程繼續響應用戶請求。每個進程響應一個請求
缺點:
-
進程量大,進程切換次數過多,導致CPU資源使用效率低,每個進程的地址空間是獨立的,很多空間中重復的數據,所以內存使用效率低,進程切換由於內核完成,占用CPU資源。
2.多線程
在多線程方式中,每個線程來響應一下請求,由於線程之間共享進程的數據,所以線程的開銷較小,性能就會提高。
優點:
-
線程間共享進程數據,每個線程響應一個請求,線程切換不可避免(切換量級比較輕量),同一進程的線程可以共享進程的諸多資源,對內存的需求較之進程有很大下降,讀可以共享,寫不可以共享
缺點:
-
線程快速切換時會帶來線程抖動,多線程會導致服務器不穩定
3.異步方式
一個進程或線程響應多個請求,不需要任何額外開銷的,性能最高,占用資源最少。但也有問題一但進程或線程出錯就會導致整個服務器的宕機。
四、Web 服務請求過程
在上面的講解中我們說明,Web服務器的如何提供服務的,有多進程的方式、多線程的方式還有異步方式我們先簡單這么理解,后面我們慢慢說,現在我們不管Web服務器是如何提供服務的,多進程也好、多線程好,異步也罷。下面我們來說一下,一個客戶端的具體請求Web服務的具體過程,從上圖中我們可以看到有11步,下面我們來具體說一下,
-
1.首先我們客戶端發送一個請求到Web服務器,請求首先是到網卡。2.網卡將請求交由內核空間的內核處理,其實就是拆包了,發現請求的是80端口。3.內核便將請求發給了在用戶空間的Web服務器,Web服務器接受到請求發現客戶端請求的index.html頁面。4.Web服務器便進行系統調用將請求發給內核。5.內核發現在請求的是一頁面,便調用磁盤的驅動程序,連接磁盤。6.內核通過驅動調用磁盤取得的頁面文件。7.內核將取得的頁面文件保存在自己的緩存區域中便通知Web進程或線程來取相應的頁面文件。8.Web服務器通過系統調用將內核緩存中的頁面文件復制到進程緩存區域中。9.Web服務器取得頁面文件來響應用戶,再次通過系統調用將頁面文件發給內核。10.內核進程頁面文件的封裝並通過網卡發送出去。11.當報文到達網卡時通過網絡響應給客戶端
簡單來說就是:用戶請求-->送達到用戶空間-->系統調用-->內核空間-->內核到磁盤上讀取網頁資源->返回到用戶空間->響應給用戶。上述簡單的說明了一下,客戶端向Web服務請求過程,在這個過程中,有兩個I/O過程,一個就是客戶端請求的網絡I/O,另一個就是Web服務器請求頁面的磁盤I/O。 下面我們就來說說Linux的I/O模型。
五、Linux I/O 模型
1.I/O模型分類
說明:我們都知道web服務器的進程響應用戶請求,但無法直接操作I/O設備,其必須通過系統調用,請求kernel來協助完成I/O動作,如下圖:
對於數據輸入而言,即等待(wait)數據輸入至buffer需要時間,而從buffer復制(copy)數據至進程也需要時間
根據等待模式不同,I/O動作可分為五種模式。
-
1.阻塞I/O,2.非阻塞I/O,3.I/O復用(select和poll),4.信號(事件)驅動I/O(SIGIO),5.異步I/O(Posix.1的aio_系列函數)
2.I/O模型的相關術語
(1).阻塞和非阻塞:
阻塞和非阻塞指的是執行一個操作是等操作結束再返回,還是馬上返回。比如你去車站接朋友,這是一個操作。可以有兩種執行方式。第一種,你這人特實誠,老早就到了車站一直等到車來了接到朋友為止。第二種,你到了車站,問值班的那趟車來了沒有,“還沒有”,你出去逛一圈,可能過會回來再問。第一種就是阻塞方式,第二種則是非阻塞的。我認為阻塞和非阻塞講得是做事方法,是針對做事的人而言的。
(2).同步和異步:
同步和異步又是另外一個概念,它是事件本身的一個屬性。比如老板讓你去搬一堆石頭,而且只讓你一個人干,你只好自己上陣,最后的結果是搬完了,還是你砸到腳了,只有搬完了你才知道。這就是同步的事件。如果老板還給你個小弟,你就可以讓小弟去搬,搬完了告你一聲。這就變成異步的了。其實異步還可以分為兩種:帶通知的和不帶通知的。前面說的那種屬於帶通知的。有些小弟干活可能主動性不是很夠,不會主動通知你,你就需要時不時的去關注一下狀態。這種就是不帶通知的異步。 對於同步的事件,你只能以阻塞的方式去做。而對於異步的事件,阻塞和非阻塞都是可以的。非阻塞又有兩種方式:主動查詢和被動接收消息。被動不意味着一定不好,在這里它恰恰是效率更高的,因為在主動查詢里絕大部分的查詢是在做無用功。對於帶通知的異步事件,兩者皆可。而對於不帶通知的,則只能用主動查詢。
(3).I/O
回到I/O,不管是I還是O,對外設(磁盤)的訪問都可以分成請求和執行兩個階段。請求就是看外設的狀態信息(比如是否准備好了),執行才是真正的I/O操作。在Linux 2.6之前,只有“請求”是異步事件,2.6之后才引入AIO把“執行”異步化。別看Linux/Unix是用來做服務器的,這點上比Windows落后了好多,IOC(Windows上的AIO)在Win2000上就有了。
(4).總結
Linux上的前四種I/O模型的“執行”階段都是同步的,只有最后一種才做到了真正的全異步。第一種阻塞式是最原始的方法,也是最累的辦法。當然累與不累要看針對誰。應用程序是和內核打交道的。對應用程序來說,這種方式是最累的,但對內核來說這種方式恰恰是最省事的。還拿接人這事為例,你就是應用程序,值班員就是內核,如果你去了一直等着,值班員就省事了。當然現在計算機的設計,包括操作系統,越來越為終端用戶考慮了,為了讓用戶滿意,內核慢慢的承擔起越來越多的工作,IO模型的演化也是如此。非阻塞I/O ,I/O復用,信號驅動式I/O其實都是非阻塞的,當然是針對“請求”這個階段。非阻塞式是主動查詢外設狀態。I/O復用里的select,poll也是主動查詢,不同的是select和poll可以同時查詢多個fd(文件句柄)的狀態,另外select有fd個數的限制。epoll是基於回調函數的。信號驅動式I/O則是基於信號消息的。這兩個應該可以歸到“被動接收消息”那一類中。最后就是偉大的AIO的出現,內核把什么事都干了,對上層應用實現了全異步,性能最好,當然復雜度也最高。
六、Linux I/O 模型具體說明
首先我們先來看一下,基本 Linux I/O 模型的簡單矩陣,從圖中我們可以看到的模型有,同步阻塞I/O(阻塞I/O)、同步非阻塞I/O(非阻塞I/O )、異步阻塞I/O(I/O復用),異步非阻塞I/O(有兩種,信號驅動I/O和異步I/O)。好了現在就來具體說一說吧。
1.阻塞I/O
說明:應用程序調用一個IO函數,導致應用程序阻塞,等待數據准備好。 如果數據沒有准備好,一直等待數據准備好了,從內核拷貝到用戶空間,IO函數返回成功指示。這個不用多解釋吧,阻塞套接字。下圖是它調用過程的圖示:(注,一般網絡I/O都是阻塞I/O,客戶端發出請求,Web服務器進程響應,在進程沒有返回頁面之前,這個請求會處於一直等待狀態)
2.非阻塞I/O
我們把一個套接口設置為非阻塞就是告訴內核,當所請求的I/O操作無法完成時,不要將進程睡眠,而是返回一個錯誤。這樣我們的I/O操作函數將不斷的測試數據是否已經准備好,如果沒有准備好,繼續測試,直到數據准備好為止。在這個不斷測試的過程中,會大量的占用CPU的時間,所有一般Web服務器都不使用這種I/O模型。具體過程如下圖:
3.I/O復用(select和poll)
I/O復用模型會用到select或poll函數或epoll函數(Linux2.6以后的內核開始支持),這兩個函數也會使進程阻塞,但是和阻塞I/O所不同的的,這兩個函數可以同時阻塞多個I/O操作。而且可以同時對多個讀操作,多個寫操作的I/O函數進行檢測,直到有數據可讀或可寫時,才真正調用I/O操作函數。具體過程如下圖:
4.信號驅動I/O(SIGIO)
首先,我們允許套接口進行信號驅動I/O,並安裝一個信號處理函數,進程繼續運行並不阻塞。當數據准備好時,進程會收到一個SIGIO信號,可以在信號處理函數中調用I/O操作函數處理數據。具體過程如下圖:
綜上可以看出,越往后,阻塞越少,理論上效率也是最優。其五種I/O模型中,前三種屬於同步I/O,后兩者屬於異步I/O。
同步I/O:
-
1.阻塞I/O,2.非阻塞I/O,3.I/O復用(select和poll)
異步I/O:
-
1.信號驅動I/O(SIGIO) (半異步),2.異步I/O(Posix.1的aio_系列函數) (真正的異步)
異步 I/O 和 信號驅動I/O的區別:
-
信號驅動 I/O 模式下,內核可以復制的時候通知給我們的應用程序發送SIGIO 消息。異步 I/O 模式下,內核在所有的操作都已經被內核操作結束之后才會通知我們的應用程序。
七、Linux I/O模型的具體實現
1.主要實現方式有以下幾種:
-
select,poll,epoll,kqueue,/dev/poll,iocp
其中iocp是Windows實現的,select、poll、epoll是Linux實現的,kqueue是FreeBSD實現的,/dev/poll是SUN的Solaris實現的。select、poll對應第3種(I/O復用)模型,iocp對應第5種(異步I/O)模型,那么epoll、kqueue、/dev/poll呢?其實也同select屬於同一種模型,只是更高級一些,可以看作有了第4種(信號驅動I/O)模型的某些特性,如callback機制。
2.為什么epoll、kqueue、/dev/poll比select高級?
答案是,他們無輪詢。因為他們用callback取代了。想想看,當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。如果能給套接字注冊某個回調函數,當他們活躍時,自動完成相關操作,那就避免了輪詢,這正是epoll、kqueue、/dev/poll做的。這樣子說可能不好理解,那么我說一個現實中的例子,假設你在大學讀書,住的宿舍樓有很多間房間,你的朋友要來找你。select版宿管大媽就會帶着你的朋友挨個房間去找,直到找到你為止。而epoll版宿管大媽會先記下每位同學的房間號,你的朋友來時,只需告訴你的朋友你住在哪個房間即可,不用親自帶着你的朋友滿大樓找人。如果來了10000個人,都要找自己住這棟樓的同學時,select版和epoll版宿管大媽,誰的效率更高,不言自明。同理,在高並發服務器中,輪詢I/O是最耗時間的操作之一,select、epoll、/dev/poll的性能誰的性能更高,同樣十分明了。
3.Windows or *nix (IOCP or kqueue、epoll、/dev/poll)?
誠然,Windows的IOCP非常出色,目前很少有支持asynchronous I/O的系統,但是由於其系統本身的局限性,大型服務器還是在UNIX下。而且正如上面所述,kqueue、epoll、/dev/poll 與 IOCP相比,就是多了一層從內核copy數據到應用層的阻塞,從而不能算作asynchronous I/O類。但是,這層小小的阻塞無足輕重,kqueue、epoll、/dev/poll 已經做得很優秀了。
4.總結一些重點
只有IOCP(windows實現)是asynchronous I/O,其他機制或多或少都會有一點阻塞。select(Linux實現)低效是因為每次它都需要輪詢。但低效也是相對的,視情況而定,也可通過良好的設計改善epoll(Linux實現)、kqueue(FreeBSD實現)、/dev/poll(Solaris實現)是Reacor模式,IOCP是Proactor模式。Apache 2.2.9之前只支持select模型,2.2.9之后支持epoll模型,Nginx 支持epoll模型,Java nio包是select模型
-
(完) by JonnyF