Nginx由内核和模块组成,从官方文档http://nginx.org/en/docs/下的Modules reference可看到些较重要模块,一般分为核心、基础模块及第三方模块。
不过,大多跟协议相关的功能和某应用特有的功能都是由nginx的模块实现。这些功能模块大致分为:事件模块、阶段性处理器、输出过滤器、变量处理器、协议、upstream和负载均衡几个类别,这些共同组成 nginx 的 http 功能。事件模块主要用于提供OS独立的(不同操作系统的事件机制有所不同)事件通知机制如 kqueue、epoll 等。协议模块则负责实现 nginx 通过 http、tls/ssl、smtp、pop3 及 imap 与对应的客户端建立会话。在 Nginx 内部,进程间的通信是通过模块的pipeline或chain实现的;换句话说,每一个功能或操作都由一个模块来实现。例如,压缩、通过FastCGI或uwsgi协议与upstream服务器通信,以及与memcached建立会话等。
Nginx之所以为广大码农喜爱,除其高性能外,还有其优雅的系统架构。与Memcached的经典多线程模型相比,Nginx是经典的多进程模型。Nginx 启动后以 daemon 方式在后台运行,后台进程包含一个 master 进程和多个worker进程,如图:
图1 Nginx
对于每个worker进程来说,独立的进程无需加锁,省掉了锁带来的开销,同时在编程及问题查找时,也方便很多。其次,独立的进程可让相互间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master进程则很快启动新的worker进程,这也是Nginx高效的另个原因。
对 Nginx 进程的控制主要是通过 master 进程来做到的,主要有两种方式:
从图1可看出,master接收信号以管理众woker进程,那么,可通过 kill 向 master 进程发信号,如 kill -HUP pid 用以通知 Nginx 从容重启。从容重启就是不中断服务:master进程在接收到信号后,会先重新加载配置,然后再启动新进程开始接收新请求,并向所有老进程发送信号告知不再接收新请求并在处理完所有未处理完的请求后自动退出。
可通过带命令行参数启动新进程来发信号给master进程,如 ./nginx -s reload 启动个新Nginx进程,而新进程在解析到 reload 参数后会向 master 进程发信号(新进程会把手动发送信号中的动作自动完成)。也可以./nginx -s stop来停止Nginx。
nginx 启动后,在 unix 系统中会以 daemon 方式在后台运行,后台进程包含个master进程和多个 worker 进程。当然 nginx 也是支持多线程的方式的,只是主流方式还是多进程方式,也是nginx默认方式。
Nginx(多进程)采用异步非阻塞方式来处理网络事件,类似于Libevent(单进程单线程),过程如图:
图2 Nginx网络事件
master进程先建好需 listen 的 socket 后,然后再 fork 出多个 woker 进程,这样每个 work 进程都可以 accept 这个 socket 。当一个client连接到来时,所有 accept 的 work 进程都会收到通知,但只有一个进程可 accept 成功,其它的则 accept 失败。Nginx提供一把共享锁accept_mutex 来保证同一时刻只有一个 work 进程在 accept 连接,从而解决惊群问题。当一个worker 进程 accept 这个连接后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,这样一个完成的请求就结束了。
对于基本的web服务器,事件通常有三种类型,网络事件、信号、定时器。从上面讲解中知道,网络事件通过异步非阻塞可以很好的解决掉。如何处理信号与定时器?
惊群简单说就是多个进程或线程在等待同一个事件,当事件发生时,所有线程和进程都会被内核唤醒。唤醒后只有一个进程获得该事件并处理,其他进程发现获取事件失败后又继续进入等待状态,在一定程度上降低了系统性能。惊群通常发生在服务器的监听等待调用上,服务器创建监听 socket 后 fork 多个进程,在每个进程中调用 accept 或 epoll_wait 等待终端的连接。
每个 worker 进程都是从 master 进程 fork 的。在 master 进程里,先建立好需 listen 的 socket后,然后再 fork 出多个 worker 进程,这样每个 worker 进程都可以 accept 这个 socket(当然不是同一个 socket,只是每个进程的这个 socket 会监控在同一个 ip地址与端口,这个在网络协议里面是允许的)。一般,当一个连接进来后,所有在accept在这个socket上的进程,都会收到通知,而只有一个进程可 accept 这个连接,其它的 accept 失败。
内核解决 epoll 的惊群效应比较晚,因此nginx自身解决了该问题(准确说是避免)。其具体思路:不让多个进程在同一时间监听接受连接的socket,而是让每个进程轮流监听,这样当有连接过来时,就只有一个进程在监听那就没有惊群问题。具体做法:利用一把进程间锁,每个进程中都尝试获得这把锁,如获取成功将监听socket加入wait集合中,并设超时等待连接到来,没获得所的进程则将监听 socket 从 wait 集合去除。这里简单讨论 nginx 在处理惊群问题基本做法,实际其代码还处理很多细节问题,如简单的连接的负载均衡、定时事件处理等。
核心的代码如下
void ngx_process_events_and_timers(ngx_cycle_t *cycle){
...
//这里面会对监听socket处理
//1、获得锁则加入wait集合,没有获得则去除
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
...
//设置网络读写事件延迟处理标志,即在释放锁后处理
if (ngx_accept_mutex_held) {
flags |= NGX_POST_EVENTS;
}
...
//这里面epollwait等待网络事件
//网络连接事件,放入ngx_posted_accept_events队列
//网络读写事件,放入ngx_posted_events队列
(void) ngx_process_events(cycle, timer, flags);
...
//先处理网络连接事件,只有获取到锁,这里才会有连接事件
ngx_event_process_posted(cycle, &ngx_posted_accept_events);
//释放锁,让其他进程也能够拿到
if (ngx_accept_mutex_held) {
ngx_shmtx_unlock(&ngx_accept_mutex);
}
//处理网络读写事件
ngx_event_process_posted(cycle, &ngx_posted_events);
}
多线程在多并发情况下,线程的内存占用大,线程上下文切换造成CPU大量开销。想想apache常用工作方式(apache 也有异步非阻塞版本,但因其与自带某些模块冲突,所以不常用),每个请求会独占一个工作线程,当并发数上到几千时,就同时有几千的线程在处理请求。这对于操作系统是个不小的挑战,线程带来的内存占用非常大,线程的上下文切换带来的cpu开销很大,自然性能就上不去了,而这些开销完全是没有意义的。
异步的概念和同步相对的,即不是事件之间不是同时发生的。
非阻塞的概念是和阻塞对应的,阻塞是事件按顺序执行,每一事件都要等待上一事件的完成,而非阻塞是如果事件没有准备好,这个事件可直接返回,过一段时间再进行处理询问,这期间可做其他事情。但多次询问也会带来额外的开销。
淘宝 tengine 团队说测试结果是“24G内存机器上,处理并发请求可达200万”。
Libevent友情知识:https://github.com/libevent/libevent
支持 Libevent 运转的就是个大循环,这个主循环体现在event_base_loop(Event.c/1533)函数里,该函数执行流程如下:
图1 event_base_loop主循环
上图的简单描述就是:
(1)校正系统当前时间。
(2)将当前时间与存放时间的最小堆中的时间依次进行比较,将所有时间小于当前时间的定时器事件从堆中取出来加入到活动事件队列中。
(3)调用I/O封装(比如:Epoll)的事件分发函数dispatch函数,以当前时间与时间堆中的最小值之间的差值(最小堆取最小值复杂度为O(1))作为Epoll/epoll_wait(Epoll.c/dispatch/407)的timeout值,在其中将触发的I/O和信号事件加入到活动事件队列中。
(4)调用函数event_process_active(Event.c/1406)遍历活动事件队列,依次调用注册的回调函数处理相应事件。
附上event_base_loop源码:
int event_base_loop(struct event_base *base, int flags)
{
const struct eventop *evsel = base->evsel;
struct timeval tv;
struct timeval *tv_p;
int res, done, retval = 0;
/* Grab the lock. We will release it inside evsel.dispatch, and again
* as we invoke user callbacks. */
EVBASE_ACQUIRE_LOCK(base, th_base_lock);
if (base->running_loop) {
event_warnx("%s: reentrant invocation. Only one event_base_loop"
" can run on each event_base at once.", __func__);
EVBASE_RELEASE_LOCK(base, th_base_lock);
return -1;
}
base->running_loop = 1;
clear_time_cache(base);
if (base->sig.ev_signal_added && base->sig.ev_n_signals_added)
evsig_set_base(base);
done = 0;
#ifndef _EVENT_DISABLE_THREAD_SUPPORT
base->th_owner_id = EVTHREAD_GET_ID();
#endif
base->event_gotterm = base->event_break = 0;
while (!done) {
base->event_continue = 0;
/* Terminate the loop if we have been asked to */
if (base->event_gotterm) {
break;
}
if (base->event_break) {
break;
}
timeout_correct(base, &tv);
tv_p = &tv;
if (!N_ACTIVE_CALLBACKS(base) && !(flags & EVLOOP_NONBLOCK)) {
timeout_next(base, &tv_p);
} else {
/*
* if we have active events, we just poll new events
* without waiting.
*/
evutil_timerclear(&tv);
}
/* If we have no events, we just exit */
if (!event_haveevents(base) && !N_ACTIVE_CALLBACKS(base)) {
event_debug(("%s: no events registered.", __func__));
retval = 1;
goto done;
}
/* update last old time */
gettime(base, &base->event_tv);
clear_time_cache(base);
res = evsel->dispatch(base, tv_p);
if (res == -1) {
event_debug(("%s: dispatch returned unsuccessfully.",
__func__));
retval = -1;
goto done;
}
update_time_cache(base);
timeout_process(base);
if (N_ACTIVE_CALLBACKS(base)) {
int n = event_process_active(base);
if ((flags & EVLOOP_ONCE)
&& N_ACTIVE_CALLBACKS(base) == 0
&& n != 0)
done = 1;
} else if (flags & EVLOOP_NONBLOCK)
done = 1;
}
event_debug(("%s: asked to terminate loop.", __func__));
done:
clear_time_cache(base);
base->running_loop = 0;
EVBASE_RELEASE_LOCK(base, th_base_lock);
return (retval);
}
十一、好书推荐
《Nginx完全开发指南:使用C、C++和OpenResty》
《深入理解Nginx:模块开发与架构解析(第2版)》