Nginx的基礎(chǔ)架構(gòu)解析(下)
1. Nginx模塊
1.1 Nginx中的模塊化設(shè)計(jì)
Nginx 的內(nèi)部結(jié)構(gòu)是由核心部分和一系列的功能模塊所組成。這樣劃分是為了使得每個(gè)模塊的功能相對(duì)簡(jiǎn)單,便于開(kāi)發(fā),同時(shí)也便于對(duì)系統(tǒng)進(jìn)行功能擴(kuò)展。Nginx 將各功能模塊組織成一條鏈,當(dāng)有請(qǐng)求到達(dá)的時(shí)候,請(qǐng)求依次經(jīng)過(guò)這條鏈上的部分或者全部模塊,進(jìn)行處理。例如前面講到的 http 請(qǐng)求,會(huì)有11個(gè)處理階段,而每個(gè)階段有對(duì)應(yīng)著許多在此階段生效的模塊對(duì)該 http 請(qǐng)求進(jìn)行處理。同時(shí),Nginx 開(kāi)放了第三方模塊編寫功能,用戶可以自定義模塊,控制 http 請(qǐng)求的處理與響應(yīng),這種高度可定制化催生了 Nginx 的大量第三方模塊,也使得 Nginx 定制化開(kāi)發(fā)在各大互聯(lián)網(wǎng)公司十分流行。
1.2 Nginx中的模塊分類
關(guān)于 Nginx 模塊的分類有很多種方式,目前網(wǎng)上博客中寫的較多的是按照功能進(jìn)行分類,有如下幾大類:
-
event 模塊: 搭建 獨(dú)立于操作系統(tǒng)的事件處理機(jī)制的框架,以及 提供各種具體事件的處理。代表性的模塊有:ngx_events_module, ngx_event_core_module, ngx_epoll_module;
-
handler 模塊: 主要負(fù)責(zé)處理客戶端請(qǐng)求并產(chǎn)生待響應(yīng)的內(nèi)容,比如 ngx_http_static_module 模塊,負(fù)責(zé)客戶端的靜態(tài)頁(yè)面請(qǐng)求處理并將對(duì)應(yīng)的磁盤 文件準(zhǔn)備為響應(yīng)內(nèi)容輸出;
-
filter 模塊: 主要 負(fù)責(zé)處理輸出的內(nèi)容,包括修改輸出內(nèi)容。代表性的模塊有: ngx_http_sub_module;
-
upstream 模塊: 該類模塊都是用于實(shí)現(xiàn)反向代理功能,將真正的請(qǐng)求轉(zhuǎn)發(fā)到后端服務(wù)器上,并從后端服務(wù)器上讀取響應(yīng),發(fā)回給客戶端。比如前面介紹到轉(zhuǎn)發(fā) http、websocket、grpc、rtmp等協(xié)議的模塊都可以劃分為這一類;
-
負(fù)載均衡模塊: 負(fù)載均衡的模塊,實(shí)現(xiàn)相應(yīng)算法。這類模塊都是用于實(shí)現(xiàn) Nginx 的負(fù)載均衡功能。
-
extend 模塊: 又稱第三方模塊,非 Nginx 官方提供,由各大企業(yè)的開(kāi)發(fā)人員結(jié)合自身業(yè)務(wù)開(kāi)發(fā)而成。Nginx 提供了非常好的模塊編寫機(jī)制,遵循相關(guān)的標(biāo)準(zhǔn)可以很快定制出符合我們業(yè)務(wù)場(chǎng)景的模塊,而且內(nèi)部調(diào)用 Nginx 內(nèi)部提供的方法進(jìn)行處理,使得第三方模塊往往都具備很好的性能
對(duì)于官方提供的模塊,我們可以直接在官網(wǎng)文檔上學(xué)習(xí),學(xué)習(xí)的方式和學(xué)習(xí)其他互聯(lián)網(wǎng)組件的方式一致,首先學(xué)習(xí)如何使用,在用至熟練后可以深入分析其源碼了解功能實(shí)現(xiàn)背后的原理。
我們以前面介紹到的 Nginx 的限速模塊(limit_req模塊)進(jìn)行說(shuō)明。首先是掌握該模塊的用法,在該模塊的官方地址中,有關(guān)于該模塊的詳細(xì)介紹,包括該模塊提供的所有指令以及所有變量說(shuō)明。此外,還有比較豐富的指令用例。在多次使用該指令并自認(rèn)為掌握了該模塊的用法之后,想了解限速背后的原理以及相關(guān)算法時(shí),就可以深入到源碼學(xué)習(xí)了。
進(jìn)入 Nginx 的源碼目錄,使用ls
查看源碼文件,限速模塊是在 http 目錄中的。
[root@server nginx-1.17.6]# cd src/
[root@server src]# ls
core event http mail misc os stream
[root@server src]# ls http/modules/ngx_http_limit_*.c
http/modules/ngx_http_limit_conn_module.c
http/modules/ngx_http_limit_req_module.c
找到 Nginx 模塊對(duì)應(yīng)的代碼文件后,我們就可以閱讀里面的代碼進(jìn)行學(xué)習(xí)。往往源碼的閱讀是枯燥無(wú)味的,我們可以借助海量的網(wǎng)絡(luò)資源輔助我們學(xué)習(xí)。這里就有一篇文章,作者深入分析了 Nginx 的限流模塊的源碼以及相應(yīng)限流算法,最后進(jìn)行了相關(guān)的實(shí)驗(yàn)測(cè)試。通過(guò)這樣一個(gè)個(gè)模塊深入學(xué)習(xí),最后在使用每一個(gè) Nginx 指令時(shí),也會(huì)非常熟練,最后成為 Nginx 高手。
1.3 如何學(xué)習(xí)和使用第三方模塊
這里我們演示在 Nginx 中使用第三方模塊。 Openresty 社區(qū)提供了一款 Nginx 中的 Echo 模塊,即echo-nginx-module。在 Nginx 中添加了該模塊后,我們?cè)谂渲梦募锌梢允褂迷撃K提供的 echo 指令返回用戶響應(yīng),簡(jiǎn)單方便。該模塊的源碼在 github 上,并且有良好的文檔和使用示例,非常方便開(kāi)發(fā)者使用。
現(xiàn)在我們?cè)?Nginx 的源碼編譯階段加入該第三方模塊,具體操作如下:
[root@server shencong]# pwd
/root/shencong
[root@server shencong]# mkdir nginx-echo
# 下載 nginx 源碼包和第三方模塊的源碼包
[root@server shencong]# wget http://nginx.org/download/nginx-1.17.6.tar.gz
[root@server shencong]# wget https://github.com/openresty/echo-nginx-module/archive/v0.62rc1.tar.gz
# 解壓
[root@server shencong]# tar -xzf nginx-1.17.6.tar.gz
[root@server shencong]# tar -xzf v0.62rc1.tar.gz
[root@server shencong]# ls
echo-nginx-module-0.62rc1 nginx-1.17.6 nginx-1.17.6.tar.gz nginx-echo v0.62rc1.tar.gz
[root@server shencong]# cd nginx-1.17.6
# 使用--add-module添加第三方模塊,參數(shù)為第三方模塊源碼
[root@server shencong]# ./configure --prefix=/root/shencong/nginx-echo --add-module=/root/shencong/echo-nginx-module-0.62rc1
編譯完成后,我們就可以去nginx-echo
目錄中的 nginx.conf
文件中添加echo
指令 。準(zhǔn)備如下的配置(可以參參考社區(qū)提供的示例):
...
http {
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root html;
index index.html index.htm;
}
# 新增測(cè)試 echo 指令配置
location /timed_hello {
default_type text/plain;
echo_reset_timer;
echo hello world;
echo "'hello world' takes about $echo_timer_elapsed sec.";
echo hiya igor;
echo "'hiya igor' takes about $echo_timer_elapsed sec.";
}
location /echo_with_sleep {
default_type text/plain;
echo hello world;
echo_flush; # ensure the client can see previous output immediately
echo_sleep 2.5; # in sec
echo "'hello' takes about $echo_timer_elapsed sec.";
}
}
}
...
啟動(dòng) Nginx 后,我們就可以在瀏覽器上請(qǐng)求者兩個(gè) URI 地址,看到相應(yīng) echo 返回的信息了。第二個(gè)配置是使用了 echo_sleep 指令,會(huì)使得請(qǐng)求在休眠 2.5s 后才返回。
1.4 如何編寫自己的模塊
想要編寫 Nginx 模塊,首先需要對(duì) Nginx 模塊中的源碼以及相關(guān)的數(shù)據(jù)結(jié)構(gòu)有所了解,還要知曉 Nginx HTTP 模塊的調(diào)用流程。假設(shè)我要實(shí)現(xiàn)前面第三方模塊Echo的最簡(jiǎn)單形式,即只輸出相應(yīng)的字符串即可。假定模塊支持的指令名稱還是 echo, 這個(gè) echo 指令需要跟一個(gè)參數(shù),即輸出的字符串。我們需要做如下幾步:
- 確定模塊名稱,以及模塊中的指令以及參數(shù),還有運(yùn)行的環(huán)境。這里涉及的結(jié)構(gòu)是 ngx_command_t,它定義了模塊里的所有指令格式。下面的代碼表示該模塊中只有一個(gè) echo 指令,它出現(xiàn)的上下文環(huán)境為 location,且有一個(gè)參數(shù)(NGX_CONF_TAKE1)。當(dāng)某個(gè)配置塊中出現(xiàn)
echo
指令時(shí),Nginx 將調(diào)用ngx_http_echo
方法。然后在該方法中,會(huì)設(shè)置處理請(qǐng)求的 handler,這個(gè) handler 就是處理請(qǐng)求的方法。
static ngx_command_t ngx_http_echo_commands[] = {
{ ngx_string("echo"), /* 指令名稱,利用ngx_string宏定義 */
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, /* 用在 location 指令塊內(nèi),且有1個(gè)參數(shù) */
ngx_http_echo, /* 處理回調(diào)函數(shù) */
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_echo_loc_conf_t, ed), /* 指定參數(shù)讀取位置 */
NULL },
ngx_null_command
};
- 完成請(qǐng)求處理的 handler 函數(shù),最重要的部分就在這里;
static char *
ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_core_loc_conf_t *clcf;
/* 找到指令所屬的配置塊,這里我們限定echo指令的上下文環(huán)境只有l(wèi)ocation */
clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
/* 指定處理的handler */
clcf->handler = ngx_http_echo_handler;
...
return NGX_CONF_OK;
}
static ngx_int_t
ngx_http_echo_handler(ngx_http_request_t *r)
{
...
/* 向用戶發(fā)送相應(yīng)包 */
return ngx_http_output_filter(r, &out);
}
- 一些收尾工作,比如配置模塊介入 http 請(qǐng)求的哪些階段等。
/* Http context of the module */
static ngx_http_module_t ngx_http_echo_module_ctx = {
NULL, /* preconfiguration */
NULL, /* postconfiguration */
NULL, /* create main configuration */
NULL, /* init main configuration */
NULL, /* create server configuration */
NULL, /* merge server configuration */
ngx_http_echo_create_loc_conf, /* create location configration */
ngx_http_echo_merge_loc_conf /* merge location configration */
};
/* Module */
ngx_module_t ngx_http_echo_module = {
NGX_MODULE_V1,
&ngx_http_echo_module_ctx, /* module context */
ngx_http_echo_commands, /* module directives */
NGX_HTTP_MODULE, /* module type */
NULL, /* init master */
NULL, /* init module */
NULL, /* init process */
NULL, /* init thread */
NULL, /* exit thread */
NULL, /* exit process */
NULL, /* exit master */
NGX_MODULE_V1_PADDING
};
完成以上幾步,一個(gè)簡(jiǎn)易的自定義模塊就算大功告成了。接下來(lái)我們動(dòng)手完成第一個(gè)自定義的模塊,將其編譯進(jìn)Nginx 二進(jìn)制文件中并進(jìn)行測(cè)試。
2. 案例
我們來(lái)完成一個(gè)簡(jiǎn)單的自定義 http 模塊,來(lái)實(shí)現(xiàn)前面Echo模塊的最簡(jiǎn)單形式,即使用指令輸出 “hello, world” 字符串。首先新建一個(gè)目錄echo-nginx-module
,然后在目錄下新建兩個(gè)文件config
和ngx_http_echo_module.c
[root@server echo-nginx-module]# pwd
/root/shencong/echo-nginx-module
[root@server echo-nginx-module]# ls
config ngx_http_echo_module.c
兩個(gè)文件內(nèi)容分別如下:
[root@server echo-nginx-module]# cat config
ngx_addon_name=ngx_http_echo_module
# 指定模塊名稱
HTTP_MODULES="$HTTP_MODULES ngx_http_echo_module"
# 指定模塊源碼路徑
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_echo_module.c"
[root@server echo-nginx-module]# cat ngx_http_echo_module.c
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
/* Module config */
typedef struct {
ngx_str_t ed;
} ngx_http_echo_loc_conf_t;
static char *ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
static void *ngx_http_echo_create_loc_conf(ngx_conf_t *cf);
static char *ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child);
/* 定義指令 */
static ngx_command_t ngx_http_echo_commands[] = {
{ ngx_string("echo"), /* 指令名稱,利用ngx_string宏定義 */
NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, /* 用在 location 指令塊內(nèi),且有1個(gè)參數(shù) */
ngx_http_echo, /* 處理回調(diào)函數(shù) */
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_echo_loc_conf_t, ed), /* 指定參數(shù)讀取位置 */
NULL },
ngx_null_command
};
/* Http context of the module */
static ngx_http_module_t ngx_http_echo_module_ctx = {
NULL, /* preconfiguration */
NULL, /* postconfiguration */
NULL, /* create main configuration */
NULL, /* init main configuration */
NULL, /* create server configuration */
NULL, /* merge server configuration */
ngx_http_echo_create_loc_conf, /* create location configration */
ngx_http_echo_merge_loc_conf /* merge location configration */
};
/* Module */
ngx_module_t ngx_http_echo_module = {
NGX_MODULE_V1,
&ngx_http_echo_module_ctx, /* module context */
ngx_http_echo_commands, /* module directives */
NGX_HTTP_MODULE, /* module type */
NULL, /* init master */
NULL, /* init module */
NULL, /* init process */
NULL, /* init thread */
NULL, /* exit thread */
NULL, /* exit process */
NULL, /* exit master */
NGX_MODULE_V1_PADDING
};
/* Handler function */
static ngx_int_t
ngx_http_echo_handler(ngx_http_request_t *r)
{
ngx_int_t rc;
ngx_buf_t *b;
ngx_chain_t out;
ngx_http_echo_loc_conf_t *elcf;
/* 獲取指令的參數(shù) */
elcf = ngx_http_get_module_loc_conf(r, ngx_http_echo_module);
if(!(r->method & (NGX_HTTP_HEAD|NGX_HTTP_GET|NGX_HTTP_POST)))
{
/* 如果不是 HEAD/GET/PUT 請(qǐng)求,則返回405 Not Allowed錯(cuò)誤 */
return NGX_HTTP_NOT_ALLOWED;
}
r->headers_out.content_type.len = sizeof("text/html") - 1;
r->headers_out.content_type.data = (u_char *) "text/html";
r->headers_out.status = NGX_HTTP_OK;
r->headers_out.content_length_n = elcf->ed.len;
if(r->method == NGX_HTTP_HEAD)
{
rc = ngx_http_send_header(r);
if(rc != NGX_OK)
{
return rc;
}
}
b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
if(b == NULL)
{
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Failed to allocate response buffer.");
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
out.buf = b;
out.next = NULL;
b->pos = elcf->ed.data;
b->last = elcf->ed.data + (elcf->ed.len);
b->memory = 1;
b->last_buf = 1;
rc = ngx_http_send_header(r);
if(rc != NGX_OK)
{
return rc;
}
/* 向用戶發(fā)送相應(yīng)包 */
return ngx_http_output_filter(r, &out);
}
static char *
ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_core_loc_conf_t *clcf;
clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
/* 指定處理的handler */
clcf->handler = ngx_http_echo_handler;
ngx_conf_set_str_slot(cf,cmd,conf);
return NGX_CONF_OK;
}
static void *
ngx_http_echo_create_loc_conf(ngx_conf_t *cf)
{
ngx_http_echo_loc_conf_t *conf;
conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_echo_loc_conf_t));
if (conf == NULL) {
return NGX_CONF_ERROR;
}
conf->ed.len = 0;
conf->ed.data = NULL;
return conf;
}
static char *
ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
ngx_http_echo_loc_conf_t *prev = parent;
ngx_http_echo_loc_conf_t *conf = child;
ngx_conf_merge_str_value(conf->ed, prev->ed, "");
return NGX_CONF_OK;
}
這樣一個(gè)第三方模塊包就完成了,接下來(lái)我們要向之前使用第三方模塊一樣,將它編譯進(jìn) Nginx,具體操作如下。
[root@server shencong]# cd nginx-1.17.6/
[root@server nginx-1.17.6]# ./configure --prefix=/root/shencong/nginx-echo --add-module=/root/shencong/echo-nginx-module
...
[root@server nginx-1.17.6] # make && make install
...
[root@server nginx-1.17.6]# cd ../nginx-echo/sbin/
[root@server sbin]# ./nginx -V
nginx version: nginx/1.17.6
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC)
configure arguments: --prefix=/root/shencong/nginx-echo --add-module=/root/shencong/echo-nginx-module
接下來(lái),我們只要在 nginx.conf 中加入我們的指令,并給一個(gè)參數(shù),就能看到我們自定義的輸出了。
...
http {
...
server {
listen 80;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
location /test {
echo hello,world;
}
...
}
}
...
最后我們請(qǐng)求主機(jī)的80端口,URI=/test,瀏覽器輸出"hello, world",說(shuō)明我們的自定義模塊成功了!
3. 小結(jié)
在 Nginx 基礎(chǔ)架構(gòu)介紹的最后,主要是介紹了 Nginx 的模塊設(shè)計(jì)以及相應(yīng)的模塊用法。最后,我們簡(jiǎn)單給出了一個(gè)簡(jiǎn)單編寫自己模塊的案例作為本章的實(shí)戰(zhàn)案例。希望這一節(jié)之后,大家能對(duì) Nginx 的模塊化設(shè)計(jì)有所了解,并有興趣在模塊的源碼方向深入學(xué)習(xí)。