注:个人学习的一些知识点笔记
参考:《Nginx开发从入门到精通》,《深入理解Nginx》
1.nginx可以后台运行,也可以前台运行(一般用于调试);
2.nginx支持多线程,只是我们主流的方式还是多进程的方式,也是nginx的默认方式;
3.nginx在启动后,会有一个master进程和多个worker进程。
master进程主要用来管理worker进程,包含:接收来自外界的信号,向各worker进程发送信号,监控worker进程的运行状态,当worker进程退出后(异常情况下),会自动重新启动新的worker进程。
而基本的网络事件,则是放在worker进程中来处理了。
多个worker进程之间是对等的,各进程互相之间是独立的。一个请求,只可能在一个worker进程中处理。
worker进程的个数是可以设置的,一般我们会设置与机器cpu核数一致,这里面的原因与nginx的进程模型以及事件处理模型是分不开的。
nginx的进程模型,可以由下图来表示:
4.当一个http连接请求过来,每个进程都有可能处理这个连接。首先,在master进程里面,先建立好需要listen的socket(listenfd)之后,然后再fork出多个worker进程。所有worker进程的listenfd会在新连接到来时变得可读,为保证只有一个进程处理该连接,所有worker进程在注册listenfd读事件前抢accept_mutex,抢到互斥锁的那个进程注册listenfd读事件,在读事件里调用accept接受该连接。当一个worker进程在accept这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,这样一个完整的请求就是这样的了。
我们可以看到,一个请求,完全由worker进程来处理,而且只在一个worker进程中处理。
5.异步非阻塞:当事件没准备好时,放到epoll里面,事件准备好了,我们就去读写,当读写返回EAGAIN时,我们将它再次加入到epoll里面。这样,只要有事件准备好了,我们就去处理它,只有当所有事件都没准备好时,才在epoll里面等着。这样,我们就可以并发处理大量的并发了,当然,这里的并发请求,是指未处理完的请求,线程只有一个,所以同时能处理的请求当然只有一个了,只是在请求间进行不断地切换而已,切换也是因为异步事件未准备好,而主动让出的。这里的切换是没有任何代价,可以理解为循环处理多个准备好的事件。与多线程相比,这种事件处理方式是有很大的优势的,不需要创建线程,每个请求占用的内存也很少,没有上下文切换,事件处理非常的轻量级。并发数再多也不会导致无谓的资源浪费(上下文切换)。更多的并发数,只是会占用更多的内存而已。现在的网络服务器基本都采用这种方式,这也是nginx性能高效的主要原因。
6.事件通常有三种类型,网络事件、信号、定时器。
网络事件:通过异步非阻塞解决;
信号:如果nginx正在等待事件(epoll_wait时),如果程序收到信号,在信号处理函数处理完后,epoll_wait会返回错误,然后程序可再次进入epoll_wait调用。
定时器:nginx的定时器事件是放在一棵维护定时器的红黑树里面,每次在进入epoll_wait前,先从该红黑树里面拿到所有定时器事件的最小时间,在计算出epoll_wait的超时时间后进入epoll_wait。所以,当没有事件产生,也没有中断信号时,epoll_wait会超时。这时,nginx会检查所有的超时事件,将他们的状态设置为超时,然后再去处理网络事件。由此可以看出,当我们写nginx代码时,在处理网络事件的回调函数时,通常做的第一个事情就是判断超时,然后再去处理网络事件。
nginx的事件处理模型总结:
while (true) {
for t in run_tasks:
t.handler();
update_time(&now);
timeout = ETERNITY;
for t in wait_tasks: /* sorted already */
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 { /* events[i].type == WRITE */
t.handler = write_handler;
}
run_tasks_add(t);
}
7.如上,nginx是一个事件驱动架构的web服务器;它将一个网络事件划分为多个阶段,比如 请求建立TCP连接事件,TCP连接可读事件,TCP连接关闭事件,TCP连接可写事件,异步读磁盘成功事件……这些事件由事件收集分发器(如epoll)进行分发。因此一个worker可以处理多个连接请求,通过异步非阻塞,来压榨性能;
8.划分请求的原则:
先找到请求处理流程中的阻塞方法或阻塞代码段,按照下面方式划分:
a.将阻塞进程的方法按照相关的触发事件分解为两个阶段:阻塞方法改为非阻塞,调用非阻塞方法后将进程归还给事件分发器;第二阶段处理非阻塞方法最终返回的结果;
b.将阻塞方法调用按照事件分解为多个阶段的方法调用:比如要读取10MB内容,且需要到驱动硬盘寻址,那么可以将其划分为每次读取10KB,这个时间是可控的,使得整个系统可以及时处理其他请求。
c.使用定时器划分阶段,避免不必要的标志位轮询;
d.如果阻塞方法完全无法继续划分,则必须使用独立的进程执行这个阻塞方法;
9.
10.
11.