1. 程式人生 > 其它 >多位專家力推丨大量經典案例分析集錦,帶你深入學習Nginx底層原始碼

多位專家力推丨大量經典案例分析集錦,帶你深入學習Nginx底層原始碼

前言

說到學習不知道大家有沒有一種對知識的渴望,對技術的極致追求。不知道大家有沒有,“或許”我有。當然學習是為了獲取知識、總結學習結果,便於對未來工作應用中得心應手。隨著大家的熱情一觸即發,想幫助更多和筆者們一樣的人共同學習,筆者們就產生了寫書的想法。然後大家不謀而合,把對知識的渴望和熱情撰寫成一本書。

書中作者來自各“網校”與“基礎服務中臺”的多位專家,在本書創造之前,組織過對Nginx底層原始碼的閱讀與除錯。經歷過數個月的學習與實踐,通過積累沉澱了本書。在本書中RTMP模組的解析部分,也應用到多次直播高峰,經歷過數百萬線上直播驗證。

預備知識

在學習Nginx原始碼之前可以對一些知識進行初步瞭解,掌握這些知識後,這樣便於對知識學習與理解。

  • C/C++基礎:首先要掌握C/C++語言基礎,在閱讀原始碼過程中,能否理解語意、語法、以及相關的業務邏輯。便於學習Nginx的相關知識。
  • GDB除錯:在本書中一些除錯程式碼片段是採用GDB進行除錯,瞭解GDB除錯工具也便於對Nginx的程序和一些邏輯進行除錯。
  • Nginx基礎使用:學習的版本為Nginx1.16版本,如果您在閱讀過程中,對Nginx的一些使用已經瞭解。在閱讀的過程中,可以起到一些幫助。
  • HTTP協議與網路程式設計基礎知識。

聯合作者

  • 網校團隊: 聶鬆鬆

好未來學而思網校學習研發直播系統後端負責人,負責網校核心直播系統開發和架構工作。畢業於東北大學電腦科學與技術專業,9年以上音視訊及流媒體相關工作經驗,精通Nginx、ffmpeg相關技術棧。

  • 基礎服務中臺: 趙禹

好未來後端資深開發,曾參與自主創業。目前負責未來雲容器平臺kubernetes元件開發,隸屬於容器iaas團隊。熟悉PHP、Ngnix、Redis、Mysql等原始碼實現,樂於鑽研技術。

  • 網校團隊: 施洪寶

好未來後端開發專家,東南大學碩士,對Redis、Nginx、Mysql等開源軟體有較深的理解,熟悉C/C++、Golang開發,樂於鑽研技術,合著有<<Redis5設計與原始碼分析>>。

  • 網校團隊: 景羅

開源愛好者,高階技術專家,曾任搜狐集團大資料高階研發工程師、新浪微博研發工程師,7年後端架構經驗,熟悉PHP、Nginx、Redis、MySQL等原始碼實現,擅長高併發處理及大型網站架構,“打造學習型團隊”的踐行者。

  • 網校團隊: 黃桃

高階技術專家,8年後端工作經驗,擅長高效能網站服務架構與穩定性建設,著有《PHP7底層設計與原始碼實現》等書籍。

  • 網校團隊: 李樂

好未來學而思網校PHP開發專家,西安電子科技大學碩士,樂於鑽研技術與原始碼研究,對Redis和Nginx有較深理解。合著有《Redis5設計與原始碼分析》。

  • 基礎服務中臺: 張報

好未來集團接入層閘道器方向負責人,對Nginx,Tengine,Openresty等高效能web伺服器有深入理解,精通大型站點架構與流量排程系統的設計與實現。

  • 網校團隊:閆昌

好未來後端開發專家,深耕資訊保安領域多年,對Linux下服務端開發有較深見解,擅長高併發業務的實現。

  • 網校團隊:田峰

學而思網校學服研發部負責人。13年多的網際網路從業經驗,先後主要在搜狗、百度、360、好未來公司從事研發和技術團隊管理工作,在高效能服務架構設計及複雜業務系統開發方面擁有豐富經驗。

學習引導

在學習的過程中,可能大部分人更關注的是收益問題。一方面是技術的硬技能收益,另一方面高階提升。

說到學習,那麼可以整理一些學習路徑。可以通過一張圖來看看學習Nginx原始碼都需要學習些什麼內容。並且可以怎麼樣去學習,如圖1-1所示。

<center>圖1-1 Nginx學習大綱</center>
通過上圖,可以清晰的瞭解到學習Nginx原始碼都需要學習些什麼內容。
在學習初期,可以先了解Nginx原始碼與編譯安裝和Nginx架構基礎與設計念想,從Nginx的優勢、原始碼結構、程序模型等幾個方面瞭解Nginx。然後在學習Nginx的記憶體管理、從記憶體池、共享記憶體展開對Nginx記憶體管理與使用。緊接著可以展開對Nginx的資料結構學習,分別對字串、陣列、連結串列、佇列、雜湊、紅黑樹、基礎樹的資料結構和演算法使用。

學習完資料結構後,可以對Nginx的配置解析、通過main配置塊、events配置塊與http配置塊進行學習,然後學習Nginx配置解析的全部過程。接下來可以學習程序機制,通過程序模式、master程序、worker程序,以及程序建通訊機制完整了解Nginx程序的管理。然後在學習HTTP模組,通過模組初始化流程、請求解析、HTTP的11個階段處理,以及HTTP請求響應,掌握HTTP模組的處理過程。

學習完HTTP模組後,再來學習Upsteam機制,對Upstream初始化、上下游建立、長連線、FastCGI模組做一定理解。

然後可以瞭解一些模組,比如Nginx時間模組實現,Nginx事件模型的檔案事件、時間事件、程序池、連線池等事件處理流程。其次是Nginx的負載均衡、限流、日誌等模組實現。

如果要跨平臺使用Nginx,可以瞭解跨平臺實現,對Nginx的configure編譯檔案,跨平臺原子操作鎖進行一定了解。

對直播比較感興趣,還可以學習Nginx直播模組RTMP實現,通過RTMP協議,模組處理流程,進一步瞭解RTMP模組實現。

基礎架構與設計理念

從誕生以來,Nginx一直以高效能、高可靠、易擴充套件聞名於世,這得益於它諸多優秀的設計理念,本章就站在巨集觀的角度來欣賞Nginx的架構設計之美。

Nginx程序模型

如今大多數系統都需要應對海量的使用者流量,人們也越來越關注系統的高可用、高吞吐、低延時、低消耗等特性,此時小巧且高效的Nginx走進了大家的視野,並很快受到了人們的青睞。Nginx的全新程序模型與事件驅動設計使其能天生輕鬆應對C10K甚至C100K高併發場景。

Nginx使用了Master管理程序(管理程序Master)和Worker工作程序(工作程序Worker)的設計,如圖2-1所示。

<center>圖2-1 Master-Worker程序模型</center>

Master程序負責管理各個Worker,通過訊號或管道的方式來控制Worker的動作。當某個Worker異常退出時,Master程序一般會啟動一個新的Worker程序替代它。Worker是真正處理使用者請求的程序,各Worker程序是平等的,它們通過共享記憶體、原子操作等一些程序間通訊機制來實現負載均衡。多程序模型的設計充分利用了SMP(Symmetrical Multi-Processing)多核架構的併發處理能力,保障了服務的健壯性。

同樣是基於多程序模型,為什麼Nginx能具備如此強的效能與超高的穩定性,其原因有以下幾點。

  • 非同步非阻塞:

Nginx的Worker程序全程工作在非同步非阻塞模式下,從TCP連線的建立到讀取核心緩衝區裡的請求資料,再到各HTTP模組處理請求,或者是反向代理時將請求轉發給上游伺服器,最後再將響應資料傳送給使用者,Worker程序幾乎不會阻塞,當某個系統呼叫發生阻塞時(例如進行I/O操作,但是作業系統還沒將資料準備好),Worker程序會立即處理下一個請求,當條件滿足時作業系統會通知Worker程序繼續完成這次操作,一個請求可能需要多個階段才能完成,但是整體上看每個Worker一直處於高效的工作狀態,因此Nginx只需要很少數Worker程序就能處理大量的併發請求。當然,這些都得益於Nginx的全非同步非阻塞事件驅動框架,尤其是在Linux2.5.45之後作業系統的I/O多路複用模型中新增了epoll這款神器,讓Nginx換上了全新的發動機一路狂飆到效能之巔。

  • CPU繫結

通常在生產環境中配置Nginx的Worker數量等於CPU核心數,同時會通過worker_cpu_affinity將Worker繫結到固定的核上,讓每個Worker獨享一個CPU核心,這樣既能有效地避免頻繁的CPU上下文切換,也能大幅提高CPU快取命中率。

  • 負載均衡

當客戶端試圖與Nginx伺服器建立連線時,作業系統核心將socket對應的fd返回給Nginx,如果每個Worker都爭搶著去接受(accept)連線就會造成著名的“驚群”問題,也就是最終只會有一個Worker成功接受連線,其他Worker都白白地被作系統喚醒,這勢必會降低系統的整體效能。另外,如果有的Worker運氣不好,一直接受失敗,而有的Worker本身已經很忙碌卻接受成功,就會造成Worker之間負載的不均衡,也會降低Nginx伺服器的處理能力與吞吐量。Nginx通過一把全域性的accept_mutex鎖與一套簡單的負載均衡演算法就很好的解決了這兩個問題。首先每個Worker在監聽之前都會通過ngx_trylock_accept_mutex無阻塞的獲取accept_mutex鎖,只有成功搶到鎖的Worker才會真正監聽埠並accept新的連線,而搶鎖失敗的Worker只能繼續處理已接受連線上的事件。其次,Nginx為每個Worker設計了一個全域性變數ngx_accept_disabled,並通過如下方式對該值進行初始化:

ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n

其中connection_n表示每個Worker一共可同時接受的連線數,free_connnection_n表示空閒連線數,Worker程序啟動時,空閒連線數與可接受連線數相等,也就是ngx_accept_disabled初始值為-7/8 * connection_n。當ngx_accept_disabled為正數時,表明空閒連線數已經不足總數的1/8了,此時說明該Worker程序十分繁忙,於是它本次事件迴圈放棄爭搶accept_mutex鎖,專注於處理已有的連線,同時會將自己的ngx_accept_disabled減一,下次事件迴圈時繼續判斷是否進入搶鎖環節。下面的程式碼摘要展示了上述演算法邏輯:

if (ngx_use_accept_mutex) {
        if (ngx_accept_disabled > 0) {
            ngx_accept_disabled--;
        } else {
            if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
                return;
            }
 ……
    }

總體上來看這種設計略顯粗糙,但它勝在簡單實用,一定程度上維護了各Worker程序的負載均衡,避免了單個Worker耗盡資源而拒絕服務,提升了Nginx伺服器的高效能與健壯性。

另外,Nginx也支援單程序工作模式,但是這種模式不能發揮CPU多核的處理能力,通常只適用於本地除錯。

Nginx模組化設計

Nginx主框架中只提供了少量的核心程式碼,大量強大的功能是在各模組中實現的。模組設計完全遵循了高內聚、低耦合的原則,每個模組只處理自己職責之內的配置項,專注完成某項特定的功能,各型別的模組實現了統一的介面規範,這大大增強了Nginx的靈活性與可擴充套件性。

模組分類

Nginx官方將眾多模組按功能分為5類,如圖2-2所示。

<center>圖2-2 Nginx模組分類圖</center>

1)核心模組: Nginx中最重要的一類模組,包含了ngx_core_module、ngx_http_module、ngx_events_module、ngx_mail_module、ngx_openssl_module、ngx_errlog_module這6個具體的模組,每個核心模組定義了同一種風格型別的模組。

2)HTTP模組:與處理HTTP請求密切相關的一類模組,HTTP模組包含的模組數量遠多於其他型別的模組,Nginx大量豐富的功能基本都是通過HTTP模組實現的。

3)Event模組: 定義了一系列可以執行在不同作業系統,不同核心版本的事件驅動模組,Nginx的事件處理框架完美的支援了各類作業系統提供的事件驅動模型,包括epoll,poll,select,kqueue,eventport等。

4)Mail模組: 與郵件服務相關的模組,Mail模組使Nginx具備了代理IMAP、POP3、SMTP等協議的能力。

5)配置模組: 此類模組只有ngx_conf_module一個成員,但是它是其他模組的基礎,因為其他模組在生效前都需要依賴配置模組處理配置指令並完成各自的準備工作,配置模組指導了所有模組按照配置檔案提供功能,它是Nginx可配置性、可定製化、可擴充套件的基礎。

模組介面

雖然Nginx模組數量眾多,功能複雜多樣,但並沒有給開發人員帶來多少困擾,因為所有的模組都遵循了同一個ngx_module_t介面設計規範,定義如下:

struct ngx_module_s {
    ngx_uint_t            ctx_index;
    ngx_uint_t            index;
    char                   *name;
    ngx_uint_t            spare0;
    ngx_uint_t            spare1;
    ngx_uint_t            version;
    const char           *signature;
    void                   *ctx;
    ngx_command_t        *commands;
    ngx_uint_t            type;

    ngx_int_t           (*init_master)(ngx_log_t *log);
    ngx_int_t           (*init_module)(ngx_cycle_t *cycle);
    ngx_int_t           (*init_process)(ngx_cycle_t *cycle);
    ngx_int_t           (*init_thread)(ngx_cycle_t *cycle);
    void                (*exit_thread)(ngx_cycle_t *cycle);
    void                (*exit_process)(ngx_cycle_t *cycle);
    void                (*exit_master)(ngx_cycle_t *cycle);

    uintptr_t             spare_hook0;
    uintptr_t             spare_hook1;
    uintptr_t             spare_hook2;
    uintptr_t             spare_hook3;
    uintptr_t             spare_hook4;
    uintptr_t             spare_hook5;
    uintptr_t             spare_hook6;
    uintptr_t             spare_hook7;
};

這是Nginx原始碼中非常重要的一個結構體,它包含了一個模組的基本資訊:包括模組名稱、模組型別、模組指令、模組順序等。注意,其中的init_master、init_module、init_process等7個鉤子函式,讓每個模組能夠在Master程序啟動與退出、模組初始化、Worker程序啟動與退出等階段嵌入各自的邏輯,這大大提高了模組實現的靈活性。

前面我們提到,Nginx對所有模組都進行了分類,每類模組都有自己的特性,實現了自己的特有的方法,那怎樣能將各類模組都能和ngx_module_t這唯一的結構體關聯起來呢?細心的讀者可能已經注意到,ngx_module_t中有一個型別為void的ctx成員,它定義了該模組的公共介面,它是ngx_module_t和各類模組的關係紐帶。何謂“公共介面”? 簡單點講就是每類模組都有各自家族特有的協議規範,通過一個void型別的ctx變數進行抽象,同類型的模組只需要遵循這一套規範即可。這裡拿核心模組和HTTP模組舉例說明:
對於核心模組,ctx指向的是名為ngx_core_module_t的結構體,這個結構體很簡單,除了一個name成員就只有create_conf和init_conf兩個方法,所有的核心模組都會去實現這兩個方法,如果有一天Nginx又創造了新的核心模組,那它也一定是按照ngx_core_module_t這個公共介面來實現。

typedef struct {
    ngx_str_t          name;
    void               *(*create_conf)(ngx_cycle_t *cycle);
    char               *(*init_conf)(ngx_cycle_t *cycle, void *conf);
} ngx_core_module_t;

而對於HTTP模組,ctx指向的是名為ngx_http_module_t的結構體,這個結構體裡定義了8個通用的方法,分別是http模組在解析配置檔案前後,以及建立與合併http段、server段、location段配置時所呼叫的方法,如下面程式碼所示:

typedef struct {
    ngx_int_t   (*preconfiguration)(ngx_conf_t *cf);
    ngx_int_t   (*postconfiguration)(ngx_conf_t *cf);

    void       *(*create_main_conf)(ngx_conf_t *cf);
    char       *(*init_main_conf)(ngx_conf_t *cf, void *conf);

    void       *(*create_srv_conf)(ngx_conf_t *cf);
    char       *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);

    void       *(*create_loc_conf)(ngx_conf_t *cf);
    char       *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
} ngx_http_module_t;

Nginx在啟動的時候,就可以根據當前的執行上下文來依次呼叫所有HTTP模組裡ctx所指定的方法。更重要的是,對於一個開發者來說,只需要按照ngx_http_module_t裡的介面規範實現自己想要的邏輯,這樣不僅降低了開發成本,也增加了Nginx模組的可擴充套件性和可維護性。
從全域性的角度來看,Nginx的模組介面設計兼顧了統一化與差異化的思想,以最簡單實用的方式實現了模組的多型性。

模組分工

既然Nginx對模組進行了分類,每個模組都實現了某種特定的功能。那這麼多模組是如何有效的組織起來的呢? Nginx啟動過程中各模組都需要完成哪些準備工作呢?處理請求的過程中各模組又是如何相互協作完成使命的呢?本章先讓我們有一個大體的認識,後面章節裡會詳細闡述。

事實上,Nginx主框架只關心6個核心模組的實現,每個核心模組分別“代言”了一種型別的模組。例如對於HTTP模組,統一由ngx_http_module管理,什麼時候建立各HTTP模組儲存配置項的結構體,什麼時候執行各模組的初始化操作,完全由ngx_http_module核心模組掌控。就好像一家大型公司的管理團隊,每個高階管理者負責了一個大部門,部門內每個員工專注於完成各自的使命。最高層領導只用關注各部門管理者,各部門管理者只需管理各自的下屬。這種分層的思想使得Nginx的原始碼也具有了高內聚低耦合的特點。

Nginx啟動時需要完成配置檔案的解析,這部分工作完全是以Nginx配置模組與解析引擎為基礎完成的,對於每一項配置指令,除了需要精準無誤的讀取識別,更重要的是儲存與解析。首先Nginx會找到對該指令感興趣的模組並呼叫該模組預先設定好的處理函式,多數情況下這裡會將引數儲存到該模組儲存配置項的結構體裡並進行初始化操作。而核心模組在啟動過程中不僅會建立用於儲存該“家族”所有儲存配置結構體的容器,而且會按順序將各結構體組織起來,這樣眾多的模組的配置資訊統一由其所屬家族的“老大”管理起來,Nginx也能按照序號從這些全域性的容器裡迅速獲取到某個模組的配置項。另外,對於事件模組在啟動過程中需要完成最重要的工作,就是根據使用者配置以及作業系統選擇一款事件驅動模型,在Linux系統中,Nginx會預設選擇epoll模型,在Worker程序被fork出來並進入初始化階段時,事件模組會建立各自的epoll物件,並通過epoll_ctl系統呼叫將監聽埠的fd新增到epoll中。

對於使用者請求的處理則主要是各HTTP模組負責,為了讓處理流程更加靈活,各模組耦合度更低,Nginx有意將處理HTTP請求的過程劃分為了11個階段,每個階段理論上都允許多個模組執行相應的邏輯。在啟動階段解析完配置檔案之後,各HTTP模組會將各自的handler函式以hook的形式掛載到某個階段中。Nginx的事件模組會根據各種事件排程HTTP模組依次執行各階段的handler處理方法,並通過返回值來判定是繼續向下執行還是結束當前請求,這種流水線式的請求處理流程使各HTTP模組完全解耦,給Nginx模組的設計帶來了極大的便捷,開發者在完成模組核心處理邏輯之後,只需要考慮將handler函式註冊到哪個階段即可。

Nginx自開源以來,社群湧現了大量優良的第三方模組,極大的擴充套件了原生Nginx的核心功能,這些都得益於Nginx優秀的模組化設計思想。

Nginx事件驅動

Nginx全非同步事件驅動框架是保障其高效能的重要基石。事件驅動並不是Nginx首創的,這一概念很早就出現在了計算機領域,它指的是在持續的事物管理過程中進行決策的一種策略,即跟隨當前時間點上出現的事件,調動可用資源,執行相關任務,使不斷出現的問題得以解決,防止事務堆積。通常事件驅動架構核心由三部分組成:事件收集器、事件發生器、事件處理器。顧名思義,事件收集器專門負責收集所有的事件,作為一款Web伺服器,Nginx主要處理的事件來自於網路和磁碟,包括TCP連線的建立與斷開,接收和傳送網路資料包,磁碟檔案的I/O操作等,每種型別都對應了一個讀事件和寫事件。事件分發器則負責將收集到的事件分發到目標物件中,Nginx通過event模組實現了讀寫事件的管理和分發;而事件處理器作為消費者,負責接收分發過來的各種事件並處理,通常Nginx中每個模組都有可能成為事件消費者,模組處理完業務邏輯之後立刻將控制權交還給事件模組,進行下一個事件的排程分發。由於消費事件的主體是各HTTP模組,事件處理函式是在一個程序中完成,只要各HTTP模組不讓程序進入休眠狀態,那麼整個請求的處理過程是非常迅速的,這是Nginx保持超高網路吞吐量的關鍵。當然,這種設計會增加了一定的程式設計難度,開發者需要通過一定的手段(例如非同步回撥的方式)解決阻塞問題。
不同作業系統提供了不同事件驅動模型,例如Linux 2.6系統同時支援epoll、poll、select模型,FreeBSD系統支援kqueue模型,Solaris 10上支援eventport模型。為了保證其跨平臺特性,Nginx的事件框架完美的支援了各類作業系統的事件驅動模型,針對每一種模型Nginx都設計了一個event模組,包括了ngx_epoll_module、ngx_poll_module、ngx_select_module、ngx_kqueue_module等。事件框架會在模組初始化時選取其中一個作為Nginx程序的事件驅動模組,對於大多數生產環境中Liunx系統的Web伺服器,Nginx預設選取最強大的事件管理epoll模型,這部分知識我們將在第7章進行詳細講解。

總結

更多內容掌握可以購買《Nginx底層設計與原始碼分析》進行學習。


如果對Nginx底層原始碼比較感興趣,可以購買本書紙質版本進行閱讀。或者加入作者團隊,一起學習共勉。作者所在團隊分別為“網校團隊”與“基礎服務中臺團隊”。
在“網校團隊”可以與大佬們手牽手一起學習底層喲,在“基礎服務中臺團隊”也可以和老師們學習Nginx底層與k8s底層。