nginx架构剖析


一、综述

nginx在启动后,在unix系统中会以daemon的方式在后台运行,后台进程包含一个master进程和多个worker进程。nginx是以多进程的方式来工作,当然nginx也是支持多线程方式的,只是多进程是nginx的默认方式,也是应用主流方式。


1、master进程

master进程主要用来管理worker进程,包含:
1)接收来自外界的信号。
2)向各个worker进程发送信号。
3)监控worker进程运行状态,当worker进程退出后(异常退出),会自动重新启动新的worker进程。


2、worker进程

1)worker进程处理基本的网络事件。
2)多个worker间进程相互独立,是对等的,同等竞争来自客户端的请求。
3)一个请求只可能在一个worker进程处理,一个worker进程,不可能处理其他进程的请求。
4)worker的进程数可以通过配置文件进行设置,个数设置规则一般时与机器CPU核数一致。


3、nginx进程模型
在这里插入图片描述

进程模型的好处

1)独立的进程,不需要加锁,省掉不必要的锁开支。
2)相互不会影响,一个进程退出后,其他进程还在提供服务,master进程很快启动新的worker进程。
3)查找、分析问题比较方便。


重启nginx

可以通过向master进程发送信号:kill -HUP pid;也可以通过命令参数:./nginx -s reload实现nginx的从容重启(nginx服务不会中断)。

重启nginx内部处理过程如下:
1)首先加载配置文件。
2)启动新的worker进程,并向所有老的worker进程发送光荣退休信号。
3)新的worker进程启动后,接收新的请求。
4)老的worker进程处理完当前进程所有未处理完请求后,再退出。


一个连接请求过来,每个进程都有可能处理这个连接,怎么做到的?

  • 1)首先,每个worker进程都是从master进程fork过来,在master进程里,先建立好需要listen的socket(listenfd),然后再fork多个进程。
  • 2)所有worker进程会在新连接到来时变得可读。
  • 3)为保证只有一个进程处理该连接,所有worker进程在注册listenfd读事件前抢accept_mutex,抢到互斥锁的进程注册listenfd读事件,在读事件里调用accept接受该连接。
  • 4)当一个worker进程accept这个连接后,开始读取请求,解析请求,处理请求…再返回给客户端,最后才断开连接。


每个worker只有一个主线程,何来的高并发?

nginx采用异步非阻塞的方式来处理请求,具体到系统调用就是select/poll/epoll/kqueue这样的系统调用。它们提供一种机制:让我们可以同时监控多个事件,调用它们是阻塞的,但可以设置超时时间,在超时时间内,如果有事件准备好,就返回。

以epoll为例:当事件没准备好时,放到epoll里,事件准备好后我们就去读写,当读写返回EAGAIN时,我们将它再次加入到epoll中。这样我们就可以并发处理大量的并发了(这里的并发请求指的是未处理完的请求)。线程只有一个,因此同时能处理请求当然也只有一个,只是在请求间进行不断的切换而已,切换也是因为异步事件未准备好,而主动让出的。(这里的切换可以理解未循环处理多个准备好的事件,没有任何代价)。

与apache的多线程方式相比,优势明显:不需要创建线程,每个请求占用的内存也很少,没有上下文切换,事件处理非常轻量。并发数再多也不会导致无谓的资源浪费(上下文切换),只是会占用更多的内存而已。


定时器

nginx借助epoll_wait函数的超时时间来实现定时器。nginx的定时器事件存放在一棵维护定时器的红黑树里,每次在进入epoll_wait前,先从红黑树拿到所有定时器事件里的最小时间,在计算出epoll_wait的超时时间后进入epoll_wait。

因此,当没有事件产生,也没有中断信号时,epoll_wait会超时,即定时器事件到了。这个时候,nginx会检查所有的超时事件,将它们的状态设置为超时,然后再去处理网络事件。由此,当我们在写nginx代码时,在处理网络事件的回调函数时,通常做的第一件事就是判断超时,然后处理网络事件。



二、nginx基础概念


1、connection

在 nginx 中,每个进程会有一个连接数的最大上限,这个上限与系统对 fd 的限制不一样。在操作系统中,通过 ulimit -n,我们可以得到一个进程所能够打开的 fd 的最大数,即nofile,因为每个 socket 连接会占用掉一个 fd。

nginx 通过设置 worker_connectons 来设置每个进程支持的最大连接数。如果该值大于nofile,那么实际的最大连接数是 nofile, nginx 会有警告。

nginx 在实现时,是通过一个连接池来管理的,每个 worker进程都有一个独立的连接池,连接池的大小是worker_connections。(连接池保存的不是真实的连接,它只是一个 worker_connections 大小的一个ngx_connection_t 结构的数组)。同时,nginx会通过一个链表free_connections 来保存所有的空闲 ngx_connection_t,每次获取一个连接时,就从空闲连接链表中获取一个,用完后,再放回空闲连接链表里面。

nginx能建立的最大连接数: worker_connections* worker_processes。
对于 HTTP 请求本地资源来说,能够支持的最大并发数量是worker_connections*worker_processes,而如果是 HTTP 作为反向代理来说,最大并发数量应该是 worker_connections * worker_processes/2。因为作为反向代理服务器,每个并发会建立与客户端的连接和与后端服务的连接,会占用两个连接。


一个客户端连接过来,空闲的进程会竞争这个连接,如何避免某个进程accept的机会比较多、空闲连接用完导致新的tcp连接无法得到空闲连接并无法将该连接转交给其他进程,最终导致此连接得不到处理。nginx如何处理这样的问题?

1)nginx先打开accept_mutex选项,还有获得accept_mutex的进程才会添加accept事件。
2)nginx使用ngx_accept_disabled 的变量来控制是否去竞争 accept_mutex 锁。

ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n;
if (ngx_accept_disabled > 0) {
	ngx_accept_disabled--;
} 
else {
	if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
		return;
	}

	if (ngx_accept_mutex_held) {
		flags |= NGX_POST_EVENTS;
	} 
	else {
		if (timer == NGX_TIMER_INFINITE
				|| timer > ngx_accept_mutex_delay)
		{
			timer = ngx_accept_mutex_delay;
		}
	}
}

ngx_accept_disabled:
当大于0时, 不会尝试获取accept_mutex锁,让出获取连接的机会,并减1。
当空余连接越来越少,ngx_accept_disabled越大,让出的机会越多,其他进程获取到锁的机会也就越大。


2、request

在 nginx 中我们指的是 http 请求,具体到 nginx 中的数据结是 ngx_http_request_t。 ngx_http_request_t 是对一个 http 请求的封装。 nginx 通过 ngx_http_request_t 来保存解析请求与输出响应相关的数据。

对于 nginx 来说,一个请求是从 ngx_http_init_request 开 始 的 , 在 这 个 函 数 中 , 会 设 置 读 事 件 为ngx_http_process_request_line , 也 就 是 说 , 接 下 来 的 网 络 事 件 , 会 由ngx_http_process_request_line 来执行。

处理流程:
在这里插入图片描述



三、基本数据结构


1、ngx_str_t

在 nginx 源码目录的 src/core 下面的 ngx_string.h|c 里面,包含了字符串的封装以及字符串相关操作的 api。 nginx 提供了一个带长度的字符串结构 ngx_str_t,它的原型如下:

typedef struct {
	size_t len;
	u_char *data;
} ngx_str_t;

参数说明:
-> data:指向字符串数据的第一个字符,字符串的结束用长度来表示,而不是由’\0’来表示结束
-> 在编写nginx模块的时候,尽量使用nginx提供的API。

该格式定义的意义:
1)通过长度表示字符串长度,减少计算字符串次数。
2)nginx可以重复引用一段字符串内存,data可以指向任意内存,长度表示结束,不用copy一份自己的字符串(因为如果要以‘\0’结束,而不能修改原字符串,势必要copy一段字符串)。

因此,在nginx中修改字符串时,必须要谨慎,需要考虑:是否可以修改、修改后会不会对其他的引用造成影响等。


2、ngx_pool_t

ngx_pool_t 是一个非常重要的数据结构,在很多重要的场合都有使用,很多重要的数据结构也都在使用它。
它提供了一种机制,帮助管理一系列的资源(如内存,文件等),使得对这些资源的使用和释放统一进行,免除了使用过程中考虑到对各种各样资源的什么时候释放,是否遗漏了释放的担心。

ngx_pool_t 相关结构及操作被定义在文件 src/core/ngx_palloc.h|c 中:

typedef struct ngx_pool_s ngx_pool_t;
struct ngx_pool_s {
	ngx_pool_data_t d;
	size_t max;
	ngx_pool_t *current;
	ngx_chain_t *chain;
	ngx_pool_large_t *large;
	ngx_pool_cleanup_t *cleanup;
	ngx_log_t *log;
};


1)nginx中典型使用ngx_pool_t场景

对于 nginx 处理的每个 http request, nginx 会生成一个 ngx_pool_t 对象与这个 http request 关联,所有处理过程中需要申请的资源都从这个 ngx_pool_t 对象中获取,当这个 http request 处理完成以后,所有在处理过程中申请的资源,都将随着这个关联的 ngx_pool_t 对象的销毁而释放。

从 ngx_pool_t 的一般使用者的角度来说,可不用关注 ngx_pool_t 结构中各字段作用。


2)ngx_pool_t 相关操作函数

//创建一个初始节点大小为 size 的 pool, log 为后续在该 pool 上进行操作时输出日志的对象。
ngx_pool_t *ngx_create_pool(size_t size, ngx_log_t *log);  

//从这个 pool 中分配一块为 size 大小的内存。注意,此函数分配的内存的起始地址按照NGX_ALIGNMENT 进行了对齐。
void *ngx_palloc(ngx_pool_t *pool, size_t size); 

//从这个 pool 中分配一块为 size 大小的内存。但是此函数分配的内存并没有像上面的函数那样进行过对齐。
void *ngx_pnalloc(ngx_pool_t *pool, size_t size);

//该函数也是分配 size 大小的内存,并且对分配的内存块进行了清零。内部实际上是转调用ngx_palloc 实现的。
void *ngx_pcalloc(ngx_pool_t *pool, size_t size);

//按照指定对齐大小 alignment 来申请一块大小为 size 的内存。此处获取的内存不管大小都将被置于大内存块链中管理。
void *ngx_pmemalign(ngx_pool_t *pool, size_t size, size_t alignment);

//对于被置于大块内存链,也就是被 large 字段管理的一列内存中的某块进行释放
ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);

//ngx_pool_t 中的 cleanup 字段管理着一个特殊的链表
ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size);

//释放 pool 中持有的所有内存
void ngx_destroy_pool(ngx_pool_t *pool);

//释放 pool 中所有大块内存链表上的内存
void ngx_reset_pool(ngx_pool_t *pool);


3、ngx_array_t

ngx_array_t 是 nginx 内部使用的数组结构。 ngx_array_t 的定义位于 src/core/ngx_array.c|h 里面。

typedef struct ngx_array_s ngx_array_t;
struct ngx_array_s {
	void *elts;
	ngx_uint_t nelts;
	size_t size;
	ngx_uint_t nalloc;
	ngx_pool_t *pool;
};

参数说明:

  • elts: 指向实际的数据存储区域。
  • nelts: 数组实际元素个数。
  • size: 数组单个元素的大小,单位是字节。
  • nalloc:数组的容量。表示该数组在不引发扩容的前提下,可以最多存储的元素的个数。当 nelts 增长到达 nalloc 时,如果再往此数组中存储元素,则会引发数组的扩容。数组的容量将会扩展到原有容量的 2 倍大小。实际上是分配新的一块内存,新的一块内存的大小是原有内存大小的 2 倍。原有的数据会被拷贝到新的一块内存中。
  • pool:该数组用来分配内存的内存池。


ngx_array_t 相关操作函数

//创建一个新的数组对象,并返回这个对象
ngx_array_t *ngx_array_create(ngx_pool_t *p, ngx_uint_t n,size_t size);

//销毁该数组对象,并释放其分配的内存回内存池
void ngx_array_destroy(ngx_array_t *a);

//在数组 a 上新追加一个元素,并返回指向新元素的指针
void *ngx_array_push(ngx_array_t *a);

//在数组 a 上追加 n 个元素,并返回指向这些追加元素的首个元素的位置的指针
void *ngx_array_push_n(ngx_array_t *a, ngx_uint_t n);

//如果一个数组对象是被分配在堆上的,那么当调用 ngx_array_destroy 销毁以后,如果想
//再次使用,就可以调用此函数。如果一个数组对象是被分配在栈上的,那么就需要调用此函
//数,进行初始化的工作以后,才可以使用
static ngx_inline ngx_int_t ngx_array_init(ngx_array_t *array,ngx_pool_t *pool, ngx_uint_t n, size_t size);


注意
由于使用 ngx_palloc 分配内存,数组在扩容时,旧的内存不会被释放,会造成内存的浪费。因此,最好能提前规划好数组的容量,在创建或者初始化的时候一次搞定,避免多次扩容,造成内存浪费


4、ngx_hash_t

ngx_hash_t 是 nginx 自己的 hash 表的实现。定义和实现位于 src/core/ngx_hash.h|c 中。
对于常用的解决冲突的方法有线性探测,二次探测和开链法等。
ngx_hash_t 使用的是最常用的一种,也就是开链法,这也是 STL 中的 hash 表使用的方法。


ngx_hash_t特点:

  • ngx_hash_t 不像其他的 hash 表的实现,可以插入删除元素,它只能一次初始化,就构建起整个 hash 表以后,既不能再删除,也不能在插入元素。
  • ngx_hash_t 的开链并不是真的开了一个链表,实际上是开了一段连续的存储空间,几乎可以看做是一个数组。

从特点分析,这个值越大,越造成内存的浪费。


ngx_hash_t 相关操作函数

typedef struct {
	ngx_hash_t *hash;
	ngx_hash_key_pt key;
	ngx_uint_t max_size;
	ngx_uint_t bucket_size;
	char *name;
	ngx_pool_t *pool;
	ngx_pool_t *temp_pool;
} ngx_hash_init_t;

typedef struct {
	ngx_str_t key;
	ngx_uint_t key_hash;
	void *value;
} ngx_hash_key_t;

//ngx_hash_t 的初始化
ngx_int_t ngx_hash_init(ngx_hash_init_t *hinit,ngx_hash_key_t *names,ngx_uint_t nelts);

//查找 key 对应的 value
void *ngx_hash_find(ngx_hash_t *hash, ngx_uint_t key, u_cha *name, size_t len);


5、ngx_hash_wildcard_t

nginx 为了处理带有通配符的域名的匹配问题,实现了 ngx_hash_wildcard_t 这样的 hash 表。

可以支持两种类型的带有通配符的域名。

1)一种是通配符在前的,例如: “ * .abc.com”,也可以省略掉星号,直接写成”.abc.com”。这样的 key,可以匹配 www.abc.com, qqq.www.abc.com之类的。

2)另外一种是通配符在末尾的,例如: “mail.xxx.*”,请特别注意通配符在末尾的不像位于开始的通配符可以被省略掉。这样的通配符,可以匹配 mail.xxx.com、 mail.xxx.com.cn、mail.xxx.net 之类的域名。

有一点必须说明,就是一个 ngx_hash_wildcard_t 类型的 hash 表只能包含通配符在前的 key或者是通配符在后的 key。不能同时包含两种类型的通配符的 key。


ngx_hash_wildcard_t 相关操作函数

//构建一个可以包含通配符 key 的 hash 表
ngx_int_t ngx_hash_wildcard_init(ngx_hash_init_t *hinit,ngx_hash_key_t *names,ngx_uint_t nelts);

//询包含通配符在前的 key 的 hash 表
void *ngx_hash_find_wc_head(ngx_hash_wildcard_t *hwc, u_char *name, size_t len);

//查询包含通配符在末尾的 key 的 hash 表
void *ngx_hash_find_wc_tail(ngx_hash_wildcard_t *hwc, u_char *name, size_t len);



四、nginx的配置系统


nginx 的配置系统由一个主配置文件和其他一些辅助的配置文件构成。
这些配置文件均是纯文本文件,全部位于 nginx 安装目录下的 conf 目录下。

在 主配置文件nginx.conf 中,包含若干配置项。每个配置项由配置指令和指令参数 2 个部分构成。


1、指令参数

指令的参数使用一个或者多个空格或者 TAB 字符与指令分开。指令的参数有一个或者多个TOKEN 串组成。 TOKEN 串之间由空格或者 TAB 键分隔。

TOKEN 串分为简单字符串或者是复合配置块。复合配置块即是由大括号括起来的一堆内容。
一个复合配置块中可能包含若干其他的配置指令。


简单配置项示例:

error_page 500 502 503 504 /50x.html;


复杂配置项示例:

location / {
	root /home/jizhao/nginx-book/build/html;
	index index.html index.htm;
}


2、指令上下文

nginx.conf 中的配置信息,根据其逻辑上的意义,对它们进行了分类,也就是分成了多个作用域,或者称之为配置指令上下文。不同的作用域含有一个或者多个配置项。

nginx支持的几个指令上下文:
在这里插入图片描述
指令上下文,可能有包含的情况出现,示例:

user nobody;
worker_processes 1;
error_log logs/error.log info;

events {
	worker_connections 1024;
}

http {
	server {
		listen 80;
		server_name www.linuxidc.com;
		access_log logs/linuxidc.access.log main;
		location / {
				index index.html;
				root /var/www/linuxidc.com/htdocs;
			}
	}
	server {
		listen 80;
		server_name www.Androidj.com;
		access_log logs/androidj.access.log main;
		location / {
			index index.html;
			root /var/www/androidj.com/htdocs;
		}
	}
}

mail {
	auth_http 127.0.0.1:80/auth.php;
	pop3_capabilities "TOP" "USER";
	imap_capabilities "IMAP4rev1" "UIDPLUS";
	server {
		listen 110;
		protocol pop3;
		proxy on;
	}
	server {
		listen 25;
		protocol smtp;
		proxy on;
		smtp_auth login plain;
		xclient off;
	}
}

1)存在于 main 上下文中的配置指令如下

  • user
  • worker_processes
  • error_log
  • events
  • http
  • mail

2)存在于 http 上下文中的指令如下

  • server

3)存在于 mail 上下文中的指令如下

  • server
  • auth_http
  • imap_capabilities



五、nginx模块化体系结构


nginx 的内部结构是由核心部分和一系列的功能模块所组成。
这种划分方式,使得每个模块的功能相对简单,便于开发,同时也便于对系统进行功能扩展。

nginx 提供了 web 服务器的基础功能,同时提供了 web 服务反向代理, email 服务反向代理功能。
nginx core 实现了底层的通讯协议,为其他模块和 nginx 进程构建了基本的运行时环境,并且构建了其他各模块的协作基础。

根据功能,nginx模块可以分为下面几种类型:
在这里插入图片描述



六、nginx的请求处理


worker 进程中有一个函数,执行无限循环,不断处理收到的来自客户端的请求,并进行处理,直到整个 nginx 服务被停止。 worker 进程中, ngx_worker_process_cycle()函数就是这个无限循环的处理函数。
在这个函数中,一个请求的简单处理流程如下:

1)操作系统提供的机制(例如 epoll, kqueue 等)产生相关的事件。
2)接收和处理这些事件,如是接受到数据,则产生更高层的 request 对象。
3)处理 request 的 header 和 body。
4)产生响应,并发送回客户端。
5)完成 request 的处理。
6)重新初始化定时器及其他事件。


1、请求的处理流程

这里以 HTTP Request 为例进行说明。一个 HTTP Request 的处理过程涉及到以下几个阶段:

1)初始化 HTTP Request(读取来自客户端的数据,生成 HTTP Request 对象,该对象含有该请求所有的信息)。
2)处理请求头。
3)处理请求体。
4)如果有的话,调用与此请求( URL 或者 Location)关联的 handler。
5)依次调用各 phase handler 进行处理。


通常情况下,一个 phase handler 对这个 request 进行处理,并产生一些输出。通常 phase handler 是与定义在配置文件中的某个 location 相关联的。一个 phase handler 通常执行以下几项任务:
1)获取location 配置。
2)产生适当的响应。
3)发送 response header。
4)发送 response body。

当 nginx 读取到一个 HTTP Request 的 header 的时候, nginx 首先查找与这个请求关联的虚拟主机的配置。如果找到了这个虚拟主机的配置,那么通常情况下,这个 HTTP Request 将会经过以下几个阶段的处理( phase handlers):
在这里插入图片描述
在内容产生阶段,为了给一个 request 产生正确的响应, nginx 必须把这个 request 交给一个合适的 content handler 去处理。

如果这个 request 对应的 location 在配置文件中被明确指定了一个 content handler,那么 nginx 就可以通过对 location 的匹配,直接找到这个对应的 handler,并把这个 request 交给这个 content handler 去处理。这样的配置指令包括像,perl, flv, proxy_pass, mp4 等。

如果一个 request 对应的 location 并没有直接有配置的 content handler,那么 nginx 依次尝试:

  • 如果一个 location 里面有配置 random_index on,那么随机选择一个文件,发送给客户端。
  • 如果一个 location 里面有配置 index 指令,那么发送 index 指令指明的文件,给客户端。
  • 如果一个 location 里面有配置 autoindex on,那么就发送请求地址对应的服务端路径下的文件列表给客户端。
  • 如果这个 request 对应的 location 上有设置 gzip_static on,那么就查找是否有对应的.gz文件存在,有的话,就发送这个给客户端(客户端支持 gzip 的情况下)。
  • 请求的 URI 如果对应一个静态文件, static module 就发送静态文件的内容到客户端。

内容产生阶段完成以后,生成的输出会被传递到 filter 模块去进行处理。 filter 模块也是与 location 相关的。所有的 fiter 模块都被组织成一条链。输出会依次穿越所有的 filter,直到有一个 filter 模块的返回值表明已经处理完成。

常见的 filter 模块有:server-side includes、XSLT filtering、图像缩放之类的、gzip 压缩。

在所有的 filter 中,有几个 filter 模块需要关注一下。按照调用的顺序依次说明如下:
在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值