关于nginx:深度好文Nginx-是如何启动并处理-http-请求的

0次阅读

共计 6280 个字符,预计需要花费 16 分钟才能阅读完成。

很早之前就有看 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/ng…

正文完
 0