Nginx從入門到精通閱讀筆記1

Nginx架構初窺

Nginx的進程模型:

以前版本的Nginx重啓的時候是發送信號來實現,比如 kill -HUP pid。現在Nginx改成了一系列的命令行參數,比如./nginx -s reload,這樣我們就啓動了一個新的nginx進程,新的進程在解析到reload參數後,就知道我們是重新加載配置文件,它向master發送信號,然後master會先重新加載配置文件,然後啓動新的進程,並向所有的老進程發送信號,不再接收新的請求,老進程處理完當前的連接後退出。master啓動的新進程就開始接收新的請求了。

每個worker進程都是從master進程fork來的,在fork之前master是先建立好listen的socket了,這樣fork出來的多個work進程都可以去accept請求(注意每個進程當然是不同的socket,但是監聽的ip地址和端口是同一個,這個在網絡協議裏面是允許的),當一個請求進來後,所有的socket都會得到通知,但是隻有一個進程可以成功accept,這就是所謂的驚羣現象。Nginx提供了一個accept_mutex來避免驚羣現象,Nginx默認是打開的,我們可以通過配置項來修改關閉。

Nginx的網絡事件模型:

有人認爲Nginx採用多個worker來處理請求,每個worker只有一個主線程,因爲處理併發數有限啊,因爲一般worker的數目和cpu的數目一致。其實不是這樣,nginx採用了異步非阻塞的方式來處理請求。這裏拿apache來對比,apache常用的工作方式是每個請求獨佔一個工作線程,當併發數上到幾千時,同時有幾千的線程處理請求。因爲要線程要切換,這對操作系統來說是個不小的挑戰,並且帶來的內存佔用會非常大,線程的上下文切換帶來的CPU開銷很大,性能自然就上不去了。

分析一下nginx採用的異步非阻塞方式,先看一個請求的完整過程:首先,請求過來,建立連接,接收數據,發送數據。在操作系統層面就是讀寫事件,當讀寫事件沒有準備好時,必然不可操作,不用非阻塞方式來調用,那事件沒有準備好就只能阻塞等待,這樣cpu或者等待或者會切換給別人用,如果等待的話肯定不能高併發,浪費了cpu資源,如果是切換給別人用,那就成了類似apache的模式。所以在Nginx裏面最忌諱的就是阻塞的系統調用了。所以,如果事件沒有準備好,就返回EAGAIN,等一會再來檢查,知道好了爲止。但這樣的輪詢也是很浪費資源。於是在操作系統提供這樣一種機制,同時監控多個事件,仍然是阻塞的調用,我們可以設置超時時間,超時時間之內,有事件準備好了就返回,只有所有事件都沒有準備好時,纔會阻塞。這樣,我們就可以併發處理大量的併發了,當然,這裏的併發請求,是指未處理完的請求,線程只有一個,所以同時能處理的請求當然只有一個了,只是在請求間進行不斷的切換而已,切換也是因爲異步事件未準備好,而主動讓出的。這裏的切換是沒有任何代價的,可以理解爲循環處理多個準備好的事件。與多線程相比,這種事件處理方式有很大優勢,不需要創建線程,每個請求佔用的內存很少,沒有上下文切換,事件處理非常輕量級。併發數再多也不會導致無謂的資源浪費(上下文切換)。這裏我們應該很容易理解爲什麼要把worker的個數設置爲CPU的核數。並且,Nginx提供了更好的多核特性,我們可以將某個進程綁定到某一個核上,這樣就不會因爲進程的切換帶來cache的失效。(Nginx類似的優化很多,比如nginx在比較4個字節的字符串時,轉換成int比較,這樣可以減少CPU指令數)


Nginx的信號事件模型:

對Nginx來說,有一些特定的信號,代表着特定的意義。信號會中斷程序當前的運行,改變狀態後繼續執行。如果是系統調用,則可能會導致系統調用的失敗,需要重入。(感覺介紹的不詳細,個人理解就是中斷當前程序,調用信號處理函數)

Nginx的定時器模型:

epoll_wait等函數在調用的時候可以設置一個超時時間,Nginx藉助這個超時時間來實現定時器。nginx裏面的定時器事件放在一個最小堆裏面,每次進入epoll_wait前,先從最小堆裏面拿到所有定時器的最小時間,在計算出epoll_wait的超時時間後進入epoll_wait。所以,當沒有事件產生,也沒有中斷信號時,epoll_wait會超時,也就是說,定時器事件到了。這時,nginx會檢查所有的超時事件,將他們的狀態設置爲超時,然後再去處理網絡事件,因爲在寫Nginx代碼時,處理網絡事件的回調函數時,第一件事就是判斷超時,然後再去處理網絡事件。

Nginx connection:

某一個子進程accept成功後,會創建與nginx對連接的封裝,即ngx_connection_t結構體。接着,設置讀寫事件處理函數並添加讀寫事件來與客戶端進行數據的交換。最後,nginx或客戶端來主動關掉連接。

nginx也可以作爲客戶端來請求其它server的數據(如upstream),此時,與其它server創建的連接也是封裝在ngx_connection_t中。作爲客戶端,ngxin先獲取一個ngx_connection_t結構體,然後創建socket,並設置socket的屬性。然後通過添加讀寫事件,調用connect/read/write來調用連接,最後關掉連接,釋放ngx_connection_t。

在ngxin中,每個進程有一個最大連接上限,這個和操作系統的上限(fd值,通過ulimit -n獲取)沒關係,各定義各的。ngxin通過設置worker_connections來設置每個進程可使用的連接最大值。nginx在實現時,是通過一個連接池來管理的,每個worker進程都有一個獨立的連接池,連接池的大小是worker_connections。它裏面並不是真實的連接,只是一個worker_connections大小的ngx_connection_t結構數組。並且,nginx會通過一個鏈表free_connections來保存所有的空閒ngx_connection_t,每次獲取一個連接時,就從空閒連接鏈中取一個,用完後,再放回空閒連接鏈表裏面。

因爲worker_connections,表示的是每個進程的連接數,因爲nginx所能建立連接的最大值就是worker_connections*worker_processes。這是作爲http server的連接數,如果是作爲反向代理,則除以二worker_connections*worker_processes/2,因爲反向代理每個請求會佔用兩個連接。


負載均衡:

如果某個進程一直得到accept,但是他的連接數有限,可能導致某些請求得不到服務。這時需要用到accept_mutex選項,只有獲得了accept_mutex的進程纔會去添加accept事件。Ngxin通過一個變量ngx_accept_disabled來控制是否去競爭accept_mutex鎖。這個值是空閒連接數跟總連接數的八分之一比較,當小於八分之一時,就不會去嘗試獲取鎖,這樣就給了其它進程獲取鎖的機會。


發佈了81 篇原創文章 · 獲贊 7 · 訪問量 23萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章