社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  NGINX

深度好文:Nginx 是如何启动并处理 http 请求的?

高效运维 • 5 年前 • 536 次点击  

很早之前就有看nginx的冲动,但是一直被一些事耽搁着,最近在繁忙之中,抽出点时间,看了下Nginx代码,发现整体上并不是很难看懂,而且刚好想学习nginx+lua开发。

nginx 在互联网公司使用很广,最重要的功能当属反向代理和负载均衡了吧,当然还有缓存。所以有必要对 nginx 熟悉使用和深入了解。

记得我之前在很多文章有提到,后台组件框架主要有三种:redis单进程单线程,memcache单进程多线程,nginx多进程;等看了nginx之后,我也算集齐了。

nginx以模块化方式开发,比如核心模块,event模块,http模块,然后为了支持多平台,event模块下又有对各大平台的封装支持,例如linux平台epoll,mac平台kqueue等等;然后http模块也被拆分成了很多子模块。

这篇文章算是我自己做的笔记吧,把之前研究的东西记录下。

也许是之前看过redis 和 golang 以及 python的 http 框架,nginx整体框架比较容易就看懂了,当然很多细节还需后面慢慢看。

这篇文章主要介绍 nginx 是如何开启,以及请求是怎么执行的,所以这篇文章主要就是以下两点:

  1. nginx开启流程;

  2. 重要回调函数设置;

  3. nginx处理http请求;

  4. 总结

1. nginx开启流程

nginx体量很大,想要在较短时间内看完所有代码很难,而且我看得时间也不是很多,所以,这里主要站在宏观角度,对nginx做个整体剖析。

其实如果直接从main函数直接开始看,其实也是可以看懂大部分,但是 nginx 回调函数太多了,看着看着,突然跑出一个回调函数,经常就懵逼了。

因此,就需要用gdb来定点调试;

要使用gdb,首先需要在gcc编译时,加入-g选项,可以如下操作:

  1. 打开nginx目录/auto/cc/conf文件,然后更改ngx_compile_opt=”-c”选项,添加-g,即为ngx_compile_opt=”-c -g”;

  2. 然后运行./configure和make即可编译生成可执行文件,在文件objs目录下;

生成可执行文件nginx之后,直接在终端运行即可,nginx会加载默认配置文件,以daemon形式运行;

nginx运行之后,即可通过gdb来调试; 

按如下命令开启gdb

然后,通过pidof命令获取nginx进程号,即可attach,如下:

nginx默认开启一个master进程和一个worker进程,因此上述命令会返回两个进程号,在我主机上8125和8126,较小是master进程,较大的是worker进程;接下来,先看下master进程,

这样就可以直接调试nginx的worker进程,用命令bt可以查看master进程的函数栈

nginx开启之后,首先启动的就是master进程,从main函数开始,

  1. main函数主要是做一些初始化操作,初始化启动参数,开启daemon,新建pid文件等等,然后调用ngx_master_process_cycle函数;

  2. 在ngx_master_process_cycle函数中最重要就是开启子进程,然后调用sigsuspend函数,master进程则阻塞在在信号中;

因此,master进程任务就是开启子进程,然后管理子进程;怎么管理了?

信号,对,就是信号;当master进程收到一个信号之后,就把这个信号传递给worker进程,worker进程进而根据不同信号分别处理。

那么问题又来了,master进程是如何把信号传递给worker进程的?

管道,对,就是管道。原理和memcache的master线程和worker线程通信机制一样,即每个worker进程有两个文件描述符fd[0]和fd[1],一个读端,一个写端;

worker进程将读端加入epoll事件监听,当master进程收到一个信号后,在每个worker进程写端写入一个flag,然后worker进程触发读事件,读取flag,并根据flag做相应操作。

因此nginx接收客户端请求以及处理客户端请求,主要是在worker进程,我们来看下,worker进程函数栈

因为 worker 进程是由 master 进程 fork 出来,因此 worker 进程包含 master 进程的函数栈;我们直接从#5函数开始看,

  1. ngx_start_worker_processes 函数调用ngx_spawn_process开启子进程,并且设置master进程和worker进程通信的管道;

  2. ngx_spawn_process函数主要是设置master进程和worker进程间通信管道,例如非阻塞等等,然后通过fork函数正式开启子进程;

    子进程调用通过参数传递进来的回调函数ngx_worker_process_cycle正式切入子进程部分,父进程则接着设置worker进程相关属性;

  3. ngx_worker_process_cycle 一开始调用 ngx_worker_process_init 函数对worker 进程做些初始化设置,包括设置进程优先级,worker进程允许打开的最大文件描述符,对阻塞信号的设置,初始化所有模块,将master进程和worker进程间通信管道添加到监听可读事件等等;

    然后在一个无限循环中,函数ngx_worker_process_cycle接着调用ngx_process_events_and_timers,开启事件监听循环;

  4. 在ngx_process_events_and_timers 函数中,先是获取锁,如果获取到锁,listenfd 即可接收客户端,否则 listenfd 不可接收客户端事件;

    然后调用ngx_process_events函数,这个函数也就是ngx_epoll_process_events函数,开启开启事件监听;

ok,worker 进程此时已就绪,等待客户端连接以及请求数据。

为了避免惊群现象以及实现worker进程负载均衡,每次有客户端连接时,所有worker进程会先争抢锁,如果某个worker进程获取到锁,即可执行接收客户端和客户端请求事件;

如果worker进程没有争抢到锁,只执行客户端请求事件。

2. 重要回调函数设置

当nginx的master进程和worker进程开启之后,客户端即可发送请求;接下来,就看看nginx是如何处理请求的;

当客户端发送请求之后,首先是通过tcp三次握手建立连接;当连接建立成功之后,即执行listenfd的回调函数,但是listenfd的回调函数是哪个了?这对于新手来说,其实是很难发现listenfd回调函数。

下面分析下:

像listenfd的回调函数以及模块间是如何拼凑在一起,这些几乎都是在模块初始化时完成的。

对于listenfd的回调函数即是在event模块初始化时或者调用event模块一些设置函数时设置;

客户端连接上服务器之后,服务器收到请求之后的回调函数也是在http模块初始化时或者调用模http模块一些设置函数时设置的。

在event模块初始化时,调用的是ngx_event_process_init函数,下面列出这个函数最重要的代码:

在for循环中,迭代每个监听套接字,recv为listenfd连接对象的读事件,这里设置listenfd读事件的回调函数为ngx_event_accept函数,然后将每个listenfd添加到事件监听中,并设置为可读事件。

ok,当我们去看ngx_add_conn和ngx_add_event的定义时,如下:

说明 ngx_add_conn 和 ngx_add_event 都是结构体 ngx_event_actions 结构体中设置的函数指针;

其实这个ngx_event_actions就是nginx跨平台的关键,因为不同平台使用的事件监听器是不一样的,导致ngx_event_actions也就不一样。

例如linux使用的是epoll,因此ngx_event_actions结构体就是在epoll模块加载时设置,在上述代码前半部分。我们来看下epoll模块actions.init函数:

从代码可以看出,ngx_event_actions被设置为ngx_epoll_module_ctx.actions,接着看下这个结构体:

因此,当调用ngx_add_conn和ngx_add_event时,分别调用的是ngx_epoll_add_connection和ngx_epoll_add_event;

如此一来,如果此时是mac平台,那么使用的事件监听器是kqueue,那么当调用ngx_add_event时,调用的就是ngx_kqueue_add_event。

如果使用的poll监听器,那么调用将是ngx_poll_add_event等等。

接下来,再分析一个很重要的回调函数,即客户端连上客户端之后,发送请求时的回调函数,先来看下,listenfd回调函数

当客户端连接服务器时,首先listenfd回调函数先是调用accept函数接收客户端请求,然后从对象池中获取一个封装客户端socket连接对象。

如果目前使用的是epoll事件监听器,则调用ngx_add_conn(c)放入事件监听,最后调用ngx_listening_t的回调函数,对客户端连接进一步操作;

ok,这个ls->handler(c)是个啥?我在第一次看代码时,一脸懵逼!!!

还记得之前说的吗?模块之间的衔接,几乎都是在模块初始化或者调用模块一些设置函数时设置的,因此接下来,就来看看http模块初始化时做了什么。

http模块并没有在模块初始化函数中设置 ls->handler(c),而是在当读取到”http”命令时,执行命令函数  ngx_http_block 中设置;

真是藏的够深,经历了四个函数,终于看到ls-handler设置函数了,即为ngx_http_init_connection函数,而这个函数在http模块,为客户端http请求处理的入口函数;

到此为止,我们可以知道服务器在接收到客户端之后,首先将客户端封装成ngx_connection_t结构体,然后交给http模块执行http请求。

3. nginx 处理 http 请求

nginx处理http的请求是nginx最重要的职能,也是最复杂的一部分。可以大概说下执行流程:

  1. 读取解析请求行;

  2. 读取解析请求头;

  3. 开始最重要的部分,即多阶段处理;

    nginx把请求处理划分成了11个阶段,也就是说当nginx读取了请求行和请求头之后,将请求封装了结构体ngx_http_request_t,然后每个阶段的handler都会根据这个ngx_http_request_t,对请求进行处理,例如重写uri,权限控制,路径查找,生成内容以及记录日志等等;

  4. 将结果返回给客户端;

多阶段处理是nginx模块最重要的部分,因为第三方模块也是注册在这;例如有人写了一个利用nginx和memcache做页面缓存的第三方模块,也可以把memcache换成redis集群等等;

而且nginx多阶段处理有点类似python和golang web框架的中间件,后者主要是用装饰器模式,对handler一层一层封装,而nginx是用数组(链表)形式组合多阶段handler,然后按handler链表执行即可;

因为多阶段这块内容还没完全看懂,所以跟着网上教程,写了个最简单的第三方模块,用于设置定点调试,观察http阶段函数执行过程,步骤如下:

  1. 在nginx目录下新建一个目录thm(third mudole),在新建一个foo目录(foo模块),然后在foo目录下新建ngx_http_foo_module.c

然后同样是在foo目录下新建一个配置文件config

这样,一个最简单的第三方模块就编写完成。

上述两个函数很好理解,一个是初始化函数,将这个模块的 handler 注册到某个阶段中。

这个例子是在阶段NGX_HTTP_CONTENT_PHASE,然后当程序执行到上述阶段时,即可执行foo模块;最后重新编译生成可执行文件即可。

接下来,利用gdb来看下http执行过程,把定点设置在

简要说明下上述函数,我阅读的版本和运行版本不一样,因此上述仅供参考:

  1. 当有客户端发送tcp连接请求时,ngx_epoll_process_events返回listenfd可读事件,调用ngx_event_accept函数接收客户端请求,然后将请求封装成ngx_connection_t结构体,最后调用ngx_http_init_connection函数进入http处理;

  2. 在新版nginx中,并没有看到ngx_http_wait_request_handler,而是改成了ngx_http_init_connection(ngx_connection_t *c)函数,然后在这个函数内部调用ngx_http_init_request函数初始化请求结构体ngx_http_request_t以及调用ngx_http_process_request_line函数;

  3. ngx_http_process_request_line函数内部先是调用ngx_http_read_request_header函数将请求行读取到缓存中,然后调用ngx_http_parse_request_line函数解析出请求行信息,最后调用ngx_http_process_request_header处理请求头;

  4. 在函数 ngx_http_process_request_header 内部先是调用函数ngx_http_read_request_header 读取请求头,然后调用 ngx_http_parse_header_line 函数解析出请求头,接着调用 ngx_http_process_request_header 函数对请求头进行必要的验证,最后调用ngx_http_process_request 函数处理请求;

  5. 在ngx_http_process_request 函数内部调用 ngx_http_handler(ngx_http_request_t r) 函数,而在ngx_http_handler(ngx_http_request_t r) 函数内部调用 函数ngx_http_core_run_phases进行多阶段处理;

  6. 我们来看下多阶段处理函数ngx_http_core_run_phases  

  7. http 多阶段处理,每个阶段可能对应一个 handler,也可能对应多个 handler,而每个阶段对应同一个checker。

因此上述while循环中,迭代所有http模块handler,然后在handler函数中根据请求结构体ngx_http_request_t做出相应的处理;

上述gdb调试结果,可以看出NGX_HTTP_CONTENT_PHASE 阶段的 checker函数为 ngx_http_core_content_phase,然后再在这个 checker 函数内部执行foo 模块的 handler(ngx_http_foo_handler)。

等到多阶段处理结束之后,最后再将 response 返回给客户端。

4. 总结

这篇文章主要就是宏观分析下nginx整体运行流程,因为第一次看nginx时,有很多看不懂的地方,所以这篇文章也算是做笔记吧。后续还需认真看多阶段处理,因为第三方开发模块也是注册在多阶段过程,以及熟悉ngx+lua模块开发。

本文链接:http://luodw.cc/2017/03/17/nginx01/

如何玩转 Nginx ? 百万并发下的 Nginx 如何优化?GOPS 2018 上海站,《深入理解 Nginx》作者陶辉教你百万并发下Nginx的优化之道!更多作者信息,下方扫码开启传送门~

戳这里▼

GOPS 2018 · 上海站精彩视频独家曝光


GOPS 2018 · 上海站更多精彩,点击阅读原文开启🔛


今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/xh5KihJMh9
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/22394
 
536 次点击