nginx原理

這裏寫圖片描述

nginx在啓動後,會有一個master進程和多個worker進程。master進程主要用來管理worker進程,包含:接收來自外界的信號,向各worker進程發送信號,監控worker進程的運行狀態,當worker進程退出後(異常情況下),會自動重新啓動新的worker進程。而基本的網絡事件,則是放在worker進程中來處理了。多個worker進程之間是對等的,他們同等競爭來自客戶端的請求,各進程互相之間是獨立的。一個請求,只可能在一個worker進程中處理,一個worker進程,不可能處理其它進程的請求。worker進程的個數是可以設置的,一般我們會設置與機器cpu核數一致,這裏面的原因與nginx的進程模型以及事件處理模型是分不開的。

一般使用kill -HUP pid命令才重啓nginx。0.8版本後引入了一系列命令行參數來方便管理,./nginx -s reload,就是來重啓nginx,./nginx -s stop,就是來停止nginx的運行。
worker進程之間是平等的,每個進程,處理請求的機會也是一樣的。當我們提供80端口的http服務時,一個連接請求過來,每個進程都有可能處理這個連接,怎麼做到的呢?首先,每個worker進程都是從master進程fork過來,在master進程裏面,先建立好需要listen的socket(listenfd)之後,然後再fork出多個worker進程。所有worker進程的listenfd會在新連接到來時變得可讀,爲保證只有一個進程處理該連接,所有worker進程在註冊listenfd讀事件前搶accept_mutex,搶到互斥鎖的那個進程註冊listenfd讀事件,在讀事件裏調用accept接受該連接。當一個worker進程在accept這個連接之後,就開始讀取請求,解析請求,處理請求,產生數據後,再返回給客戶端,最後才斷開連接

對於每個worker進程來說,獨立的進程,不需要加鎖,所以省掉了鎖帶來的開銷,同時在編程以及問題查找時,也會方便很多。其次,採用獨立的進程,可以讓互相之間不會影響,一個進程退出後,其它進程還在工作,服務不會中斷,master進程則很快啓動新的worker進程。當然,worker進程的異常退出,肯定是程序有bug了,異常退出,會導致當前worker上的所有請求失敗,不過不會影響到所有請求,所以降低了風險。當然,好處還有很多,大家可以慢慢體會。
請求過來,要建立連接,然後再接收數據,接收數據後,再發送數據。具體到系統底層,就是讀寫事件,而當讀寫事件沒有準備好時,必然不可操作,如果不用非阻塞的方式來調用,那就得阻塞調用了,事件沒有準備好,那就只能等了,等事件準備好了,你再繼續吧。阻塞調用會進入內核等待,cpu就會讓出去給別人用了,對單線程的worker來說,顯然不合適,當網絡事件越多時,大家都在等待呢,cpu空閒下來沒人用,cpu利用率自然上不去了,更別談高併發了。好吧,你說加進程數,這跟apache的線程模型有什麼區別,注意,別增加無謂的上下文切換。所以,在nginx裏面,最忌諱阻塞的系統調用了。不要阻塞,那就非阻塞嘍。非阻塞就是,事件沒有準備好,馬上返回EAGAIN,告訴你,事件還沒準備好呢,你慌什麼,過會再來吧。好吧,你過一會,再來檢查一下事件,直到事件準備好了爲止,在這期間,你就可以先去做其它事情,然後再來看看事件好了沒。雖然不阻塞了,但你得不時地過來檢查一下事件的狀態,你可以做更多的事情了,但帶來的開銷也是不小的。所以,纔會有了異步非阻塞的事件處理機制,具體到系統調用就是像select/poll/epoll/kqueue這樣的系統調用。它們提供了一種機制,讓你可以同時監控多個事件,調用他們是阻塞的,但可以設置超時時間,在超時時間之內,如果有事件準備好了,就返回。這種機制正好解決了我們上面的兩個問題,拿epoll爲例(在後面的例子中,我們多以epoll爲例子,以代表這一類函數),當事件沒準備好時,放到epoll裏面,事件準備好了,我們就去讀寫,當讀寫返回EAGAIN時,我們將它再次加入到epoll裏面。這樣,只要有事件準備好了,我們就去處理它,只有當所有事件都沒準備好時,纔在epoll裏面等着。這樣,我們就可以併發處理大量的併發了,當然,這裏的併發請求,是指未處理完的請求,線程只有一個,所以同時能處理的請求當然只有一個了,只是在請求間進行不斷地切換而已,切換也是因爲異步事件未準備好,而主動讓出的。這裏的切換是沒有任何代價,你可以理解爲循環處理多個準備好的事件,事實上就是這樣的。與多線程相比,這種事件處理方式是有很大的優勢的,不需要創建線程,每個請求佔用的內存也很少,沒有上下文切換,事件處理非常的輕量級。併發數再多也不會導致無謂的資源浪費(上下文切換)。更多的併發數,只是會佔用更多的內存而已。 我之前有對連接數進行過測試,在24G內存的機器上,處理的併發請求數達到過200萬。現在的網絡服務器基本都採用這種方式,這也是nginx性能高效的主要原因。

首先,nginx在啓動時,會解析配置文件,得到需要監聽的端口與ip地址,然後在nginx的master進程裏面,先初始化好這個監控的socket(創建socket,設置addrreuse等選項,綁定到指定的ip地址端口,再listen),然後再fork出多個子進程出來,然後子進程會競爭accept新的連接。此時,客戶端就可以向nginx發起連接了。當客戶端與服務端通過三次握手建立好一個連接後,nginx的某一個子進程會accept成功,得到這個建立好的連接的socket,然後創建nginx對連接的封裝,即ngx_connection_t結構體。接着,設置讀寫事件處理函數並添加讀寫事件來與客戶端進行數據的交換。最後,nginx或客戶端來主動關掉連接,到此,一個連接就壽終正寢了。
每個進程會有一個連接數的最大上限,這個值就通過ulimit –n可以查看,就是系統的最大句柄數,nginx的最大併發數就是最大句柄數*work數。
nginx會將整個請求頭都放在一個buffer裏面,這個buffer的大小通過配置項client_header_buffer_size來設置,如果用戶的請求頭太大,這個buffer裝不下,那nginx就會重新分配一個新的更大的buffer來裝請求頭,這個大buffer可以通過large_client_header_buffers來設置,這個large_buffer這一組buffer,比如配置4 8k,就是表示有四個8k大小的buffer可以用。注意,爲了保存請求行或請求頭的完整性,一個完整的請求行或請求頭,需要放在一個連續的內存裏面,所以,一個完整的請求行或請求頭,只會保存在一個buffer裏面。這樣,如果請求行大於一個buffer的大小,就會返回414錯誤,如果一個請求頭大小大於一個buffer大小,就會返回400錯誤。
這裏寫圖片描述
以上這些,就是nginx中一個http請求的生命週期了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章