Nginx架構賞析

小瑾守護執行緒發表於2021-01-24

淘寶的某位大佬曾經做過測試,在一臺24G記憶體的機器上,Nginx的最大併發連線數達到了200萬。同學們聽到這個結論後,是不是被Nginx的超高效能深深折服了,它內部的架構設計究竟是怎麼樣的呢?這篇文章就帶同學們來認識一下Nginx的架構設計吧。

本文主要參考了淘寶技術團隊寫的Nginx文章,將會從以下個方面去進行分享:

  • Nginx程式模型
  • Nginx事件模型

Nginx程式模型

Nginx預設以多程式的方式啟動執行,當然Nginx也是支援多執行緒的方式的,只是我們主流的方式還是多程式的方式,也是Nginx的預設方式。Nginx採用多程式的方式有很多的好處,這一點與我們大部分同學們的認知發生衝突了。大部分同學會說程式比執行緒重,程式更耗費資源等等,總之可以說出一大堆執行緒優於程式的證明。如果有同學恰恰是這樣想的,那就得擴充下認知啦。比如我們大名鼎鼎的Redis,Oracle裡邊都是有多程式概念的 ,而這些軟體無一例外都具有超高效能。所以,我們今天就主要來學習下Nginx的多程式模式。

Nginx的程式模型長個什麼樣子,廢話不多說,直接上圖。

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

Nginx的多程式模型給它帶來了一大優點: 優雅重啟,服務不間斷。具體它是怎麼做到呢?從上面我們已經得知,master管理worker程式,所以我們只需要與master程式通訊就行了。master程式會接收來自作業系統發來的訊號,再根據訊號做不同的事情。所以我們要控制Nginx,只需要通過作業系統提供的kill命令向master程式傳送訊號就行了。比如使用kill -HUP pid重啟Nginx,master程式在接收到HUP訊號後是怎麼做的呢?首先master程式在接到訊號後,會先重新載入配置檔案,然後再啟動新的worker程式,並向所有老的worker程式傳送訊號,告訴他們可以光榮退休了。新的worker在啟動後,就開始接收新的請求,而老的worker在收到來自master的訊號後,就不再接收新的請求,並且在當前程式中的所有未處理完的請求處理完成後,再退出。

我們知道了在操作Nginx的時候,Nginx內部做了些什麼事情,那麼,worker程式又是如何客戶端處理請求的呢?我們前面有提到,worker程式之間是平等的,每個程式,處理請求的機會也是一樣的。當我們提供80埠的http服務時,一個連線請求過來,每個程式都有可能處理這個連線,怎麼做到的呢?首先,每個worker程式都是從master程式fork過來,在master程式裡面,先建立好ServerSocket,開啟監聽,然後再fork出多個worker程式。所有worker程式都將和master程式持有同一份socket控制程式碼,並且控制程式碼會在新連線到來時變得可讀,為保證只有一個程式處理該連線,所有worker程式在註冊控制程式碼讀事件前搶accept_mutex,搶到互斥鎖的那個程式註冊控制程式碼讀事件,在讀事件裡呼叫accept接受該連線。當一個worker程式在accept這個連線之後,就生成了一個新的socket,通過這個socket開始讀取請求,解析請求,處理請求,產生資料後,再返回給客戶端,最後才斷開連線,一個完整的請求就是這樣玩完了。我們可以看到,一個請求,完全由worker程式來處理,而且只在一個worker程式中處理。

那麼,Nginx採用這種程式模型有什麼好處呢?實在是太多了,重點說上幾條。首先,對於每個worker程式來說,獨立的程式,不需要加鎖,所以省掉了鎖帶來的開銷,同時在程式設計以及問題查詢時,也會方便很多。其次,採用獨立的程式,可以讓互相之間不會影響,一個程式退出後,其它程式還在工作,服務不會中斷,master程式則很快啟動新的worker程式。當然,worker程式的異常退出,肯定是程式有bug了,異常退出,會導致當前worker上的所有請求失敗,不過不會影響到所有請求,所以降低了風險。當然,好處還有很多,同學們可以慢慢體會。

Nginx事件模型

Nginx採用了NIO的方式來處理請求,這是Nginx可以同時處理成千上萬個請求的根本原因。想想低版本Tomcat的IO模型(高版本已支援NIO),每個請求會獨佔一個工作執行緒,當併發數上到幾千時,就同時有幾千的執行緒在處理請求了。這對作業系統來說,是個不小的挑戰,執行緒帶來的記憶體佔用非常大,執行緒的上下文切換帶來的cpu開銷很大,自然效能就上不去了,而這些開銷完全是沒有意義的。

Nginx為什麼要使用NIO呢?NIO到底是怎麼回事呢?IO的本質就是讀寫事件,而當讀寫事件沒有準備好時,必然不可操作,如果不用非阻塞的方式來呼叫,那就得阻塞呼叫了,事件沒有準備好,那就只能等了,等事件準備好了,工作執行緒再繼續吧。阻塞呼叫會進入核心等待,cpu就會讓出去給別人用了,工作執行緒只能傻傻的睡覺等待,對單執行緒的worker來說,顯然不合適,當網路事件越多時,大家都在等待呢,cpu空閒下來沒人用,cpu利用率自然上不去了,更別談高併發了。所以,為了高效能,必須使用NIO。關於NIO,我們這裡就是一筆帶過,想詳細學習的同學可以去看我的NIO原始碼分析文章。

接下來,我們使用一段虛擬碼來總結一下Nginx的事件處理模型吧。

Date now;	// 表示當前時間
while (true) {
    // 處理任務佇列裡的所有任務
    for task in tasks:
        task.handler();

    flushTime(now);	// 重新整理當前時間
    timeout = initValue; // 超時時間
    
    // waitTasks可以理解為註冊到epoll裡的所有有效任務
    for task in waitTasks: 
    	// 列表裡的第一個task的超時時間是最短的,如果它已經超時了,就呼叫超時處理函式
        if (task.time <= now) {	
            task.timeoutHandler();
        } else {
            // 更新超時時間
            timeout = t.time - now;
            break;
        }
    
    // 通過epoll拿到已經就緒的事件
    nevents = epoll(events, timeout);
    // 挨個遍歷處理事件
    for i in nevents:
        Task task;
    	// 讀事件
        if (events[i].type == READ) {
            task.handler = readHandler;
        } else { // 寫事件
            task.handler = writeHandler;
        }
    	// 防到一個專門的執行佇列裡
        tasks(task);
}

好的,文章到這裡就結束啦。希望Nginx的程式模型和事件模型的設計思想,能對朋友們以後的系統設計有所幫助。最後,喜歡我的文章的同學們,歡迎關注我的公眾號“小瑾守護執行緒”,不錯過任何有價值的乾貨。

相關文章