內(nèi)容提綱:
- WebServe 分類
- WebServe 執(zhí)行流程
- WebServe 如何返回?cái)?shù)據(jù)
- 什么是NIO
- Nginx和Tomcat是如何使用NIO的?
- 為什么通常認(rèn)為Nginx會(huì)更快呢?
- 為什么Nginx最擅長(zhǎng)反向代理
一、WebServe 分類
按照我個(gè)人對(duì)web服務(wù)器的理解,我將其分為兩類:
1、一類是以nginx、Apache Http Server為代表的通用的Web Server
2、一類是基于語言的web Server,比如C++的Piatache、Java的Tomcat、Python的gunicorn等
區(qū)分的依據(jù)主要是,是否可以執(zhí)行計(jì)算任務(wù)!
二、WebServe 執(zhí)行流程
詳細(xì)的說,一個(gè)web服務(wù)器的工作是什么呢?其實(shí)就是:
1、監(jiān)聽用戶的請(qǐng)求
2、收到請(qǐng)求后對(duì)HTTP請(qǐng)求進(jìn)行解析
3、根據(jù)解析的內(nèi)容,返回?cái)?shù)據(jù)
三、WebServe 如何返回?cái)?shù)據(jù)
前面我說的兩種服務(wù)器的區(qū)別就在第三步,也就是返回的數(shù)據(jù),一般是一個(gè)HTML的頁面。如果是是一個(gè)靜態(tài)頁面,換句話說,就是返回一個(gè)文本文件,那么就很容易了,直接調(diào)用send()系統(tǒng)調(diào)用就可以了,這是Nginx等通用WebServer可以搞定的!但是如果需要?jiǎng)討B(tài)生成HTML呢?比如需要先查詢數(shù)據(jù)庫,然后執(zhí)行一些操作,最終再生成數(shù)據(jù)然后返回呢?這個(gè)時(shí)候Nginx就做不到了,或者說沒辦法直接做到了!因?yàn)槲业奶幚矸绞绞莏ava或php寫的,這個(gè)時(shí)候nginx顯然是沒辦法直接拿到結(jié)果的!
怎么辦呢?
1、使用tomcat,因?yàn)樗旧砭褪荍ava寫的,上述webserver的1、2步驟都是由Java實(shí)現(xiàn),當(dāng)需要第三步動(dòng)態(tài)生成數(shù)據(jù)的時(shí)候他只需要啟動(dòng)一個(gè)線程來處理一下就行了
2、php的解決方案,nginx會(huì)將這個(gè)請(qǐng)求發(fā)給PHP-FPM,后者會(huì)啟動(dòng)php解析器調(diào)用php腳本,然后將結(jié)果通過標(biāo)準(zhǔn)輸入輸出的方式發(fā)給nginx,nginx再返回解雇,這個(gè)就是FastCGI協(xié)議!
四、什么是NIO
關(guān)于NIO和BIO我在下面的文章中,詳細(xì)的討論過,這是我寫過的最好的總結(jié),不知道為啥閱讀量低的可憐!
我在這里再闡述一下NIO:
我們?cè)敿?xì)的展開上述的webserver的監(jiān)聽請(qǐng)求的過程:
其實(shí)就是創(chuàng)建一個(gè)套接字,然后監(jiān)聽,這個(gè)套接字我們將其命名為listen-fd,是的,它是一個(gè)文件描述符!
監(jiān)聽是需要一個(gè)線程的。此時(shí)一個(gè)用戶的一個(gè)HTTP請(qǐng)求到達(dá),這個(gè)時(shí)候我們需要和用戶建立一個(gè)TCP鏈接,然后創(chuàng)建一個(gè)新的套接字與用戶通訊,創(chuàng)建的這個(gè)套接字我們稱之為handle-fd!注意,一個(gè)TCP請(qǐng)求,可以傳輸多個(gè)HTTP請(qǐng)求!也就是長(zhǎng)連接!
那么BIO和NIO區(qū)別就在如何在處理這個(gè)handle-fd:
- 前者直接啟動(dòng)一個(gè)新的線程,來接受并處理用戶發(fā)來的請(qǐng)求,也就是每個(gè)handle-fd對(duì)應(yīng)一個(gè)線程,但是由于HTTP通常是長(zhǎng)連接的,那么在沒有HTTP請(qǐng)求的時(shí)候,此線程就被阻塞了!而且我們需要為每個(gè)用戶的TCP連接創(chuàng)建一個(gè)線程,這會(huì)使得服務(wù)器不敢重負(fù)!
- 而NIO使用epoll,他會(huì)監(jiān)控handle-fd,當(dāng)handle-fd可讀時(shí)(即一個(gè)HTTP請(qǐng)求到達(dá)),觸發(fā)一個(gè)事件,我們的線程只需要循環(huán)處理事件就可以了!這樣的話一個(gè)線程可以同時(shí)處理多個(gè)TCP連接,當(dāng)其中一個(gè)發(fā)來了HTTP請(qǐng)求,epoll就會(huì)產(chǎn)生一個(gè)事件,我們的線程會(huì)循環(huán)的檢查有沒有事件產(chǎn)生從而處理他!
這就是NIO的優(yōu)勢(shì)!
五、Nginx和Tomcat是如何使用NIO的?
Nginx
對(duì)于Nginx,會(huì)啟動(dòng)多個(gè)worker進(jìn)程,所有的進(jìn)程都會(huì)監(jiān)聽listen-fd,當(dāng)TCP連接到達(dá)時(shí),理論上所有的worker都會(huì)收到請(qǐng)求,也就是群驚,但是通過一定的方法可以保證只有一個(gè)人監(jiān)聽到了TCP的到達(dá)!
每個(gè)worker進(jìn)程維護(hù)了一個(gè)epoll,epoll監(jiān)聽了listen-fd,當(dāng)他知道TCP到達(dá)后就會(huì)產(chǎn)生對(duì)應(yīng)的事件,然后會(huì)喚醒正在等待事件的線程,為了好理解,我們認(rèn)為一個(gè)worker進(jìn)程中就一個(gè)線程!然后處理這個(gè)TCP請(qǐng)請(qǐng)求,也就是調(diào)用accept系統(tǒng)調(diào)用創(chuàng)建一個(gè)handle-fd,然后將其掛到epoll中,當(dāng)handle-fd接收到HTTP請(qǐng)求,同樣epoll會(huì)產(chǎn)生事件,線程再處理這個(gè)事件!
可以看到線程其實(shí)是串行的處理每一個(gè)HTTP請(qǐng)求!多個(gè)worker實(shí)現(xiàn)了并行!這樣線程就會(huì)一直在工作不會(huì)因?yàn)閔ttp請(qǐng)求沒有到達(dá)而被阻塞,同時(shí)有確保了線程數(shù)目不會(huì)過多!可以認(rèn)為其比較充分的利用了CPU資源!
Tomcat
Tomcat我沒研究過,我用與之類似的Pistache來說明,我的主頁有對(duì)于Pistache代碼的全面解析,有興趣的可以去看看!
和Nginx的Master-Worker模式略有不同,但是本質(zhì)上沒區(qū)別,有個(gè)名字叫Reactor 多線程模型!
首先會(huì)啟動(dòng)一個(gè)名為Reactor 的線程和多個(gè)handler線程,這個(gè)Reactor線程的任務(wù)就是監(jiān)聽listen-fd,當(dāng)有TCP鏈接時(shí),選擇其中一個(gè)handler線程,然后handler線程創(chuàng)建handle-fd,然后監(jiān)聽handle-fd,也是通過epoll機(jī)制,本質(zhì)上沒有任何區(qū)別。
六、為什么通常認(rèn)為Nginx會(huì)更快呢?
終于到了問題了!從上面的分析看Niginx,在設(shè)計(jì)上,并不存在任何優(yōu)勢(shì),而且Nginx僅僅能處理靜態(tài)的資源!
我認(rèn)為問題就恰恰出在了這里,那就是如何將靜態(tài)資源返回給用戶。
其實(shí)我是不知道Nginx怎么實(shí)現(xiàn)的,我可以猜,我覺得是這樣的:
直接通過sendfile()系統(tǒng)調(diào)用把靜態(tài)文件的數(shù)據(jù)發(fā)從出去,這過程用C語言是很容易的。
而我們用Java,特別是使用高級(jí)的web開發(fā)框架,他的過程一般是這樣的:
1、read()讀文件到內(nèi)存
2、修改其中的部分內(nèi)容,對(duì)于靜態(tài)文件則不需要改
3、調(diào)用send()發(fā)送出去
看上去好像差不多,因?yàn)槎家劝褦?shù)據(jù)讀到內(nèi)存再發(fā)送。
區(qū)別在于底層的實(shí)現(xiàn),這需要一定的OS的知識(shí),sendfile的過程,是先把讀文件,OS只要你讀文件,會(huì)首先把文件讀pagecache中,也就是頁面緩存,然后數(shù)據(jù)直接從頁面緩存復(fù)制到TCP的發(fā)送緩沖中,整個(gè)過程不需要在用戶空間執(zhí)行任何代碼!
然而你用read()的話,數(shù)據(jù)先到pagecache,然后被copy到用戶空間的緩沖區(qū),然后再賦值給TCP緩沖區(qū),這一上一下就會(huì)浪費(fèi)很多的時(shí)間!
當(dāng)然你可能會(huì)疑問,這個(gè)開銷和從磁盤加載文件比,豈不是小巫見大巫?的確是的,但是pagecache被加載一次之后,除非內(nèi)存告急被回收,否則會(huì)一直存在,這樣這個(gè)部分的時(shí)間開銷兩者就都沒有了!
其實(shí)Nginx采用的其實(shí)就是所謂的”零拷貝“方式!甚至,可以繞過TCP緩沖區(qū),直接從pagecache的中讀數(shù)據(jù)!
至此,我必須作一個(gè)免責(zé)聲明,以上都是我之前研究并發(fā)的時(shí)候?qū)σ幌盗械膯栴}的調(diào)研,以及我對(duì)相關(guān)問題的理解,我沒有翻閱過Nginx的源碼,也沒深入的了解過零拷貝,我只是從理論的角度分析,他的優(yōu)勢(shì)!但是對(duì)于Pistache、epoll以及pagecache我還是比較有把握的。
七、為什么Nginx最擅長(zhǎng)反向代理
作為問題的結(jié)果,我想解釋這個(gè)問題,其實(shí)這和NIO、Epoll是息息相關(guān)的!畢竟Nginx的最廣泛應(yīng)用就是反向代理。
所謂反向代理就是,請(qǐng)求發(fā)給Nginx,然后再把請(qǐng)求轉(zhuǎn)發(fā)出去,等待結(jié)果的,然后再將結(jié)果返回!
Epoll即是實(shí)現(xiàn)NIO的核心,但是我們需要知道,Epoll最擅長(zhǎng)的是監(jiān)聽socket和eventfd!也即是最擅長(zhǎng)監(jiān)聽網(wǎng)絡(luò)文件描述符,對(duì)于本地的文件,Epoll是沒辦法的,因此要想實(shí)現(xiàn)對(duì)文件的異步讀寫,還是得需要多線程,這也是為什么nodejs的libuv使用了兩種方法實(shí)現(xiàn)異步,一種是Epoll,一種是多線程!
而實(shí)現(xiàn)反向代理,恰好就是訪問網(wǎng)絡(luò)IO,也就是Nginx創(chuàng)建一個(gè)client-fd,與要轉(zhuǎn)發(fā)的服務(wù)器進(jìn)行通訊,當(dāng)目標(biāo)服務(wù)器把數(shù)據(jù)寫回的時(shí)候,被Nginx的epoll監(jiān)聽到,然后再處理這個(gè)事件!
是不是也是在NIO呢?
當(dāng)然了和php通訊也是類似的,只不過并沒有用網(wǎng)絡(luò)套接字,而是用標(biāo)準(zhǔn)輸入輸出,這也是一個(gè)特殊的fd,只要php-fpm將準(zhǔn)備好的數(shù)據(jù)寫道標(biāo)準(zhǔn)輸出,然后被Nginx感知到!也是異步IO。