Python探路-Nginx

假装没事ソ 提交于 2019-11-27 03:37:28

一直向写关于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事件来了,操作系统会把所有进程都唤醒(之前是阻塞状态),这么多进程同时去抢占,抢到进程处理后续流程,没有抢到的进程继续阻塞。就是所谓的惊群。解决方案:负载均衡和互斥锁

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!