Nginx

以下简介摘自百度百科

Nginx是一款轻量级的HTTP服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,在BSD-like 协议下发行。其特点是占有内存少,并发能力强,事实上nginx的并发能力在同类型的网页服务器中表现较好,中国大陆使用nginx网站用户有:百度、京东、新浪、网易、腾讯、淘宝等

初探Nginx架构

Nginx的启动

Nginx在启动后,在Unix系统中会以daemon守护进程)的方式在后台运行,后台进程包含一个master进程和多个worker进程。

当然,我们也可以手动关掉daemon模式,让Nginx在前台运行,这个时候Nginx就是单进程模式。这个方法一般用于调试,其他时候不用。

master进程主要用来管理worker进程。包含:接受外界信号、向各worker进程发送性、监控worker进程的运行状态、当worker异常退出时,重启新的worker
多个worker进程之间是对等的,它们共同竞争来自客户端的请求,各进程互相之间是独立的。一个请求,只可能在一个worker进程中处理,一个worker进程,不可能处理其它进程的请求。

Nginx的操作

刚刚我们说到,master来管理worker进程。所以我们可以忽略worker进程,专注与master进程的通信master进程会接收来自外界发来的信号,再根据信号做不同的处理。所以我们要控制Nginx,只需要通过kill向master进程发送信号就行了。

比如kill-HUP pid,则是告诉Nginx,从容的重启Nginx。因为是从容的重启,因此服务是不中断的。master进程在接收到这个信号的时候,会先重新加载配置文件,然后再启动新的进程。同时向所有的老进程发送信号。老进程在接收到信号后,先执行完当前的任务,然后就可以退休了。
当然,现在一般不给master进程发送信号,而是直接使用命令行参数:比如./nginx -s stop 停止运行、./nginx -s reload 重新加载。

worker进程处理请求

我们提到了,worker进程之间是平等的,每个进程处理请求的机会也是一样的。

当一个请求过来的时候,每个进程都有同等的机会去处理。这是因为,每个worker进程都是从master进程fork过来的,在master进程里面,先建立好需要listen的socket之后,再fork处多个worker进程,这样每个worker进程都可以通过去accept这个socket。

当然,并不是同一个socket,而是监听同一个ip和port的一组socket。

一般来说,当一个连接进来后,所有在accept这个socket上的进程都会收到通知。但是只有一个进程会处理这个请求,其他则accept失败,这就是惊群现象

因此,Nginx采用了acept_mutex这个东西。有了这把共享锁之后,同一时刻,就只会由于一个进程在accept连接,这样就可以解决惊群问题。

Nginx处理高并发的策略

Nginx采用异步非阻塞的方式来处理请求。与apache的常用工作方式相比Nginx采用的方法能够大大解决线程的上下文切换带来的CPU开销。

Apache常用工作方式:
一个请求独占一个工作线程。但是当并发非常大的时候,会有很多线程存在。这对操作系统来说是一个很大的开销。

我们来看一下Nginx为什么要采用异步非阻塞呢?

我们来看一个完整的请求过程:首先,建立连接,然后再接受数据,接受数据后再发送数据。

再具体一点,就是读写事件。如果读写事件没有准备好,必然是不可能操作的。如果不以非阻塞的方式,那么就会一直进行等待,阻塞调用会进入内核等待。一旦网络中的事件变多,大家都在等待。CPU没人用,利用率自然上不去。所以,在Nginx的设计里面,最忌讳的就是阻塞的系统调用

那么我们以非阻塞调用,当事件没好,你可以去干别的事件,然后时不时回来看看事件好没好。但是这个时不时回来看一下就会增加开销。所以我们需要异步。

异步就是当事件准备好了,内核会通知你。不用你去一遍遍检查好没好。

Q:为什么Nginx推荐设置worker数为CPU核心数
异步非阻塞也正是Nginx推荐设置worker为CPU核数的原因。更多的worker数只会导致进程来竞争CPU资源。
而且,Nginx还提供了CPU亲缘绑定选项,可以把某个进程绑定在某个核上。
同时Nginx在做4字节的字符串比较的时候,回答四个字符转换成一个整型,来减少CPU指令数。

Nginx异步处理模型

我们知道:对于一个基本的web服务器来说,事件通常有:网络事件、信号、定时器三种。

刚刚我们通过异步非阻塞可以很好的解决。现在还差信号和定时器。

Q1:Nginx如何处理信号
对于Nginx来首,特定的信号代表着特定的意义。信号会中断掉程序当前运行,在改变状态后继续执行
对于Nginx来说,如果Nginx正在等待事件(epoll_wait)时,如果程序收到信号,在信号处理函数处理完后,epoll_wait会返回错误,然后程序再次进入epoll_wait

Q2:Nginx如何处理定时器
Nginx借助epoll_wait设置的超时时间来实现定时器。Nginx里面的定时器事件是放在一个最小堆里面。每次在进入epoll_wait之前,先从最小堆里面拿到所有定时器事件的最小时间,在计算出epoll_wait的超时时间后进入epoll_wait。
所以,当没有事件产生,也没有中断信号时,epoll_wait会超时。

以下伪代码可以总结以下Nginx的事件处理模型:

while(true)
{
   
//执行处理函数
 for t in run_tasks:
 	t.handler();
 
 //设置超时时间
 update_time(&now);
 time_out = ETERNITY;

 for t in wait_tasks:
 	if(t.time <= now)	//超时
 	{
   
		t.timeout_handler();
	}
	else
	{
   
		timeout = t.time - now;
		break;
	}

	nevents = poll_function(events,timeout);

	for i in nevents:
	{
   	
		task t;
	
		//设置读写回调函数
		if(events[i].type == READ)
		{
   
			t.handler = read_handler;
		}
		else if(events[i].type == WRITE)
		{
   
			t.handler = write_handler;
		}
	}
	run_tasks_add(t);
}

参考文献

[1] Nginx开发从入门到精通