一直向写关于nginx的博客但是一直没有能够将nginx的内容形成自己的知识体系,所有没有勇气写下去。今天鼓起勇气写下这篇博客,也希望借此形成对nginx的整体认识。
首先看下nginx的进程模型:
nginx一般是通过一个master进程+多个worker进程(和cpu核数一样多)的模式工作的。worker是master进程通过fork出来的,master用来监听连接,然后把连接交给worker进行处理和交互,除此之外,master还会监控worker进程的运行状态,如果有worker异常退出时,master还会重新启动新的worker。
那nginx是采用哪种方式工作的呢?答案是:异步非阻塞,nginx一般启动多进程的方式,进程数和核数一致,而不会使用多线程,这是由于线程之间的切换会消耗cpu和内存,这也是nginx为什么能够使用异步非阻塞的原因,针对一个进程,它的任务就是不断的处理epoll里面的任务,当有任务被唤醒时就会被放到epoll中等待处理,如果epoll为空才会进入阻塞状态。
结合一个tcp连接的生命周期,我们看看nginx是如何处理一个连接的。首先,nginx在启动时,会解析配置文件,得到需要监听的端口与ip地址,然后在nginx的master进程里面,先初始化好这个监控的socket(创建socket,设置addrreuse等选项,绑定到指定的ip地址端口,再listen),然后再fork出多个子进程出来,然后子进程会竞争accept新的连接。此时,客户端就可以向nginx发起连接了。当客户端与服务端通过三次握手建立好一个连接后,nginx的某一个子进程会accept成功,得到这个建立好的连接的socket,然后创建nginx对连接的封装,即ngx_connection_t结构体。接着,设置读写事件处理函数并添加读写事件来与客户端进行数据的交换。最后,nginx或客户端来主动关掉连接,到此,一个连接就寿终正寝了。
下面来看下nginx的处理流程:
对于nginx来说,一个请求是从ngx_http_init_request开始的,在这个函数中,会设置读事件为ngx_http_process_request_line,也就是说,接下来的网络事件,会由ngx_http_process_request_line来执行,而函数中通过ngx_http_read_request_header来读取请求数据。然后调用ngx_http_parse_request_line函数来解析请求行;
请求行处理完成后,下面就是怎样处理我们的请求头了,nginx会设置读事件的handler为ngx_http_process_request_headers,然后后续的请求就在ngx_http_process_request_headers中进行读取与解析。ngx_http_process_request_headers函数用来读取请求头,跟请求行一样,还是调用ngx_http_read_request_header来读取请求头,调用ngx_http_parse_header_line来解析一行请求头,解析到的请求头会保存到ngx_http_request_t的域headers_in中,headers_in是一个链表结构,保存所有的请求头。而HTTP中有些请求是需要特别处理的,这些请求头与请求处理函数存放在一个映射表里面,即ngx_http_headers_in,在初始化时,会生成一个hash表,当每解析到一个请求头后,就会先在这个hash表中查找,如果有找到,则调用相应的处理函数来处理这个请求头。比如:Host头的处理函数是ngx_http_process_host;
请求头处理完后,下面是真正处理请求体了,真正开始处理数据,是在ngx_http_handler这个函数里面,这个函数会设置write_event_handler为ngx_http_core_run_phases,并执行ngx_http_core_run_phases函数。ngx_http_core_run_phases这个函数将执行多阶段请求处理,nginx将一个http请求的处理分为多个阶段,那么这个函数就是执行这些阶段来产生数据。因为ngx_http_core_run_phases最后会产生数据,所以我们就很容易理解,为什么设置写事件的处理函数为ngx_http_core_run_phases了。在这里,我简要说明了一下函数的调用逻辑,我们需要明白最终是调用ngx_http_core_run_phases来处理请求,产生的响应头会放在ngx_http_request_t的headers_out中,这一部分内容,我会放在请求处理流程里面去讲。nginx的各种阶段会对请求进行处理,最后会调用filter来过滤数据,对数据进行加工,如truncked传输、gzip压缩等。这里的filter包括header filter与body filter,即对响应头或响应体进行处理。filter是一个链表结构,分别有header filter与body filter,先执行header filter中的所有filter,然后再执行body filter中的所有filter。在header filter中的最后一个filter,即ngx_http_header_filter,这个filter将会遍历所有的响应头,最后需要输出的响应头在一个连续的内存,然后调用ngx_http_write_filter进行输出。ngx_http_write_filter是body filter中的最后一个,所以nginx首先的body信息,在经过一系列的body filter之后,最后也会调用ngx_http_write_filter来进行输出。
nginx的事件机制epoll:
epoll的使用只有三个函数:epoll_create()、epoll_ctl()以及epoll_wait()
1、epoll_create():
int epoll_create(int size);
参数 size 是告知 epoll 所要处理的大致事件数目。
系统调用 epoll_create() 创建一个 epoll 的句柄,之后 epoll 的使用都将依靠这个句柄来标识
2、epoll_ctl()
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
epoll_ctl 向 epoll 对象中添加、修改或者删除感兴趣的事件,返回 0 表示成功,否则返回 -1,此时需要根据 errno 错误码
判断错误类型。
epfd:是 epoll_create() 返回的句柄;
op:表示动作,可取的值有
EPOLL_CTL_ADD: 添加新的事件到 epoll 中
EPOLL_CTL_MOD: 修改 epoll 中的事件
EPOLL_CTL_DEL:删除 epoll 中的事件
fd: 需要监听的描述符;
3、epoll_wait()
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll_wait 的返回值表示当前发生的事件个数,
epfd:是 epoll_create() 返回的句柄;
events:是分配好的 epoll_event 结构体数组,epoll 将会把发生的事件复制到 events 数组中(events 不可以是空指针,
内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存)
maxevents:表示本次可以返回的最大事件数目,通常 maxevents 参数与预分配的 events 数组的大小是相等的;
timeout:表示在没有检测到事件发生时最多等待的时间(单位为毫秒),如果 timeout 为 0,则表示 epoll_wait 在
rdllist 链表为空时,立刻返回,不会等待。
epoll和以往的poll和select不同的在于:
1、epoll在每次注册(epoll_ctl)新事件时就把fd拷贝到内核,这样就不会向poll和select那样每次查询哪些事件被触发都会复制fd到内核会很耗时间
2、epoll为每一个fd都设置了回调函数,那样的话每当设备就绪时,就会触发回调函数,将fd加到对应的链表里,这样的话,通过epoll_wait调用返回的fd都是就绪的,不会像poll和select那样还需要对返回的fd做判断
3、epoll没有最大文件描述符这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048
最后来看下nginx的配置:
########### 每个指令必须有分号结束。#################
#user administrator administrators; #配置用户或者组,默认为nobody nobody。
#worker_processes 2; #允许生成的进程数,默认为1
#pid /nginx/pid/nginx.pid; #指定nginx进程运行文件存放地址
error_log log/error.log debug; #制定日志路径,级别。这个设置可以放入全局块,http块,server块,级别以此为:debug|info|notice|warn|error|crit|alert|emerg
events {
accept_mutex on; #设置网路连接序列化,防止惊群现象发生,默认为on
multi_accept on; #设置一个进程是否同时接受多个网络连接,默认为off
#use epoll; #事件驱动模型,select|poll|kqueue|epoll|resig|/dev/poll|eventport
worker_connections 1024; #最大连接数,默认为512
}
http {
include mime.types; #文件扩展名与文件类型映射表
default_type application/octet-stream; #默认文件类型,默认为text/plain
#access_log off; #取消服务日志
log_format myFormat '$remote_addr–$remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $http_x_forwarded_for'; #自定义格式
access_log log/access.log myFormat; #combined为日志格式的默认值
sendfile on; #允许sendfile方式传输文件,默认为off,可以在http块,server块,location块。
sendfile_max_chunk 100k; #每个进程每次调用传输数量不能大于设定的值,默认为0,即不设上限。
keepalive_timeout 65; #连接超时时间,默认为75s,可以在http,server,location块。
upstream mysvr {
server 127.0.0.1:7878;
server 192.168.10.121:3333 backup; #热备
}
error_page 404 https://www.baidu.com; #错误页
server {
keepalive_requests 120; #单连接请求上限次数。
listen 4545; #监听端口
server_name 127.0.0.1; #监听地址
location ~*^.+$ { #请求的url过滤,正则匹配,~为区分大小写,~*为不区分大小写。
#root path; #根目录
#index vv.txt; #设置默认页
proxy_pass http://mysvr; #请求转向mysvr 定义的服务器列表
deny 127.0.0.1; #拒绝的ip
allow 172.18.5.54; #允许的ip
}
}
}
Nginx命令:
1、启动nginx:nginx -c
nginx -c /usr/local/nginx/conf/nginx.conf
2、重启nginx:
nginx -s reload
在一般的Python项目中大家会使用Nginx+uWSGI+Django,下面带大家看下整体的交互流程如下:
知识点:
worker_connections:表示每个worker进程所能建立连接的最大值。如果是HTTP作为反向代理来说,最大并发数量应该是worker_connections * worker_processes/2。因为作为反向代理服务器,每个并发会建立与客户端的连接和与后端服务的连接,会占用两个连接
惊群:通常场景一个端口P1只能被一个进程A监听,所以端口P1发的事件都会被该进程A所处理。但是,如果进程A通过系统调用fork(),创建子进程B,那么进程B也能够监听端口P1。这样就可以实现多进程监听同一个端口并且进入阻塞状态。这样就引发了一个问题,当客户端发起TCP连接的时候,那么到底由谁来负责处理Accept事件呢?总不能多个进程同时处理?最终只能有一个进程来处理Accept事件,也就是说当Accept事件来了,操作系统会把所有进程都唤醒(之前是阻塞状态),这么多进程同时去抢占,抢到进程处理后续流程,没有抢到的进程继续阻塞。就是所谓的惊群。解决方案:负载均衡和互斥锁
来源:CSDN
作者:PYTHON探路者
链接:https://blog.csdn.net/zphdgqs/article/details/91038950