nginx架构介绍(syl)

mac2024-10-26  18

nginx架构介绍

Nginx 的代码是由一个 核心 和一系列的 模块 组成。

核心

核心的功能如下:

主要用于提供 WebServer 的基本功能; 实现 Web 和 Mail 反向代理的功能; 还用于启用网络协议; 创建必要的运行时环境以及确保不同的模块之间平滑地进行交互。

模块

大多跟协议相关的功能和应用特有的功能都是由 Nginx 的模块实现的。

这些功能模块大致可以分为:事件模块、阶段性处理器、输出过滤器、变量处理器、协议、upstream 和负载均衡几个类别,这些功能模块共同组成了 Nginx 的 http 功能。

其中:

事件模块主要用于提供 OS 独立的(不同操作系统的事件机制有所不同)事件通知机制,如 kqueue 或 epoll 等。协议模块则负责实现 Nginx 通过 HTTP、TLS/SSL、SMTP、POP3 以及 IMAP 与对应的客户端建立会话。

在 Nginx 内部,进程间的通信是通过模块的 pipeline 或 chain 实现的。

nginx 进程介绍

首先我们要知道,Nginx 是以多进程的方式来工作的,当然 Nginx 也是支持多线程的方式的。

Nginx 启动后,在 unix 系统中会以 daemon (守护进程)的方式在后台运行,后台进程包含一个 master 进程和多个 worker 进程(你可以理解为工人和管理员)。

这里就主要讲解 Nginx 的多进程模式。

Nginx 处理连接的过程

Nginx 不会为每个连接派生进程或线程,而是由 worker 进程通过监听共享套接字接收新请求,并且使用高效的循环来处理数千个连接。

Nginx 不使用仲裁器或分发器来分发连接,这个工作由操作系统内核机制完成。

监听套接字在启动时就完成初始化,worker 进程通过这些套接字接收、读取请求和输出响应。

master 进程

当 Nginx 在启动后,会有一个 master 进程和多个worker 进程。

master 进程主要用来管理 worker 进程,master 要做的就是:接收来自外界的信号,向各 worker 进程发送信号,监控 worker 进程的运行状态,当 worker 进程退出后(异常情况下),会自动重新启动新的 worker 进程。

master 进程主要完成如下工作:

读取并验证配置信息;创建、绑定及关闭套接字;启动、终止 worker 进程及维护 worker 进程的个数;无须中止服务而重新配置工作;控制非中断式程序升级,启用新的二进制程序并在需要时回滚至老版本;重新打开日志文件;编译嵌入式 Perl 脚本。

worker 进程

对于基本的网络事件,Nginx 则是放在 worker 进程中来处理。多个 worker 进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的。(会发生惊群<内核小于2.6.18,Nginx中处理epoll时惊群问题的思路很简单,多个子进程有一个锁,谁拿到锁,谁才将accept的fd加入到epoll队列中,其他的子进程拿不到锁,也就不会将fd加入到epoll中,连接到来也就不会导致所有子进程的epoll被唤醒返回)

一个请求,只可能在一个 worker 进程中处理,一个 worker 进程,不可能处理其它进程的请求(一对一)。

然而 Nginx 没有专门地仲裁或连接分布的 worker,这项工作是由操作系统内核机制完成的。在启动时,创建一组初始的监听套接字,HTTP 请求和响应之时,worker 连续接收、读取和写入套接字。

worker 进程主要完成如下工作:

接收、传入并处理来自客户端的连接提供反向代理及过滤功能nginx 任何能完成的其它任务

Nginx 的进程模型

上图是 Nginx 的进程模型,下面我们会简单讲解图中不同部分的含义:

既然 worker 进程之间是平等的,每个进程,处理请求的机会也是一样的。当我们提供 80 端口的 http 服务时,一个连接请求过来,每个进程都有可能处理这个连接。那么问题来了,到底最后怎样处理,是由什么决定的呢?我们来看一看一个完整的请求是怎样通过互相的协作来实现的:

(1)首先,每个 worker 进程都是从 master 进程 fork 过来,在 master 进程里面,先建立好需要 listen 的 **socket(listenfd)**之后,然后再 fork 出多个 worker 进程。

(2)所有 worker 进程的 listenfd 会在新连接到来时变得可读,为保证只有一个进程处理该连接,所有 worker 进程会在注册 listenfd 读事件前抢 accept_mutex,抢到互斥锁的那个进程注册 listenfd 读事件,然后在读事件里调用 accept 接受该连接。(解决惊群)

(3)当一个 worker 进程在 accept 这个连接之后,就开始读取请求、解析请求、处理请求。产生数据后,再返回给客户端,最后才断开连接,这样一个完整的请求就是这样的了。

我们可以看到:一个请求,完全由 worker 进程来处理,而且只在一个 worker 进程中处理。

也许你还有个疑问,那就是 Nginx 采用多 worker 的方式来处理请求,每个 worker 里面只有一个主线程,那能够处理的并发数很有限啊,多少个 worker 就能处理多少个并发,何来高并发呢?这就是 Nginx 的高明之处,Nginx 采用了 异步非阻塞 的方式来处理请求,也就是说,Nginx 是可以同时处理成千上万个请求的。

这里补充一下异步非阻塞的概念: 异步的概念是和同步相对的,也就是不同事件之间不是同时发生的。 非阻塞的概念是和阻塞对应的,阻塞是事件按顺序执行,每一事件都要等待上一事件的完成,而非阻塞是如果事件没有准备好,这个事件可以直接返回,过一段时间再进行处理询问,这期间可以做其他事情。

nginx模块介绍

Nginx 真正的魅力在于它的模块,整个应用程序建立在一个模块化系统之上,在编译时,可以对每一个模块进行启用或者禁用,需要什么就定制什么。

对 Nginx 模块的基本原理总结一下,基本就是:在特定地方调用函数。(Nginx 本身支持多种模块,如 HTTP 模块、EVENTS 模块和 MAIL 模块,这里只简单讲述 HTTP 的模块及其中的命令)

下面是配置文件结构图:

Nginx 本身做的工作实际很少,当它接到一个 HTTP 请求时,它仅仅是通过查找配置文件将此次请求映射到一个 location block,而此 location 中所配置的各个指令则会启动不同的模块去完成工作,因此模块可以看做 Nginx 真正的劳动工作者。

通常一个 location 中的指令会涉及一个 handler 模块和多个 filter 模块(当然,多个 location 可以复用同一个模块)。 handler 模块负责处理请求,完成响应内容的生成;filter 模块对响应内容进行处理。

因此 Nginx 模块开发分为 handler 开发和 filter 开发(本文不考虑 load-balancer 模块)。

下图展示了一次常规请求和响应的过程。

http index 模块

模块:ngx_http_index_module

语法:

index file ...;

配置范例:

location / { index index.$geo.html index.html; }

默认值:index index.html;

作用域:http, server, location

模块功能及注意:

定义将要被作为默认页的文件。文件的名字可以包含变量。文件以配置中指定的顺序被 Nginx 检查。列表中的最后一个元素可以是一个带有绝对路径的文件。

例子:

index index.$geo.html index.0.html /index.html;

需要注意的是,index 文件会引发内部重定向,请求可能会被其它 location 处理。 比如,下面这个例子:

location = / { index index.html; } location / { ... }

请求 “/” 实际上将会在第二个 location 中作为 “/index.html” 被处理。

http log 模块

模块:ngx_http_log_module

配置范例:

log_format gzip '$remote_addr-$remote_user[$time_local]' '$request$status $bytes_sent' '"$ http _ referer" "$http_user_agent" "$gzip_ratio"'; access_log /spool/logs/nginx-access.log gzip buffer=32k;

access_log 指令 语法:

access_log path [format [buffer=size]]; # or access_log off;

默认值:access_log log/access.log combined

作用域:http, server, location

指令功能及注意:

指令 access_log 指派路径、格式和缓存大小。参数 “off” 将清除当前级别的所有 access_log 指令。如果未指定格式,则使用预置的 “combined” 格式。缓存不能大于能写入磁盘的文件的最大值。在 FreeBSD 3.0-6.0 ,缓存大小无此限制。

log_format 指令 语法:

log_format name format [format ...];

默认值: log_format combined "…"

作用域: http server

Access 模块

模块:ngx_http_access_module

模块功能及注意:

此模块提供了一个简易的基于主机的访问控制(对网络地址有放行和禁止的权利),使 Nginx 可以对特定 IP 客户端进行控制。规则为:顺序匹配,以第一次匹配到的结果为准。

配置范例:

location / { deny 192.168.1.1; allow 192.168.1.0/24; allow 10.1.1.0/16; deny all; }

在上面的例子中,仅允许网段 10.1.1.0/16 和 192.168.1.0/24 中除 192.168.1.1 之外的 ip 访问。

放行语法

allow address | CIDR | all;

在上面的配置范例中, 192.168.1.1 为 address,192.168.1.0/24 为 CIDR,all 对应 all。

作用域:http, server, location, limit_except

指令功能:allow 描述的网络地址有权直接访问

禁止语法

deny address | CIDR | all;

作用域:http, server, location, limit_except

指令功能:deny 描述的网络地址拒绝访问

Rewrite 模块

模块:ngx_http_rewrite_module

模块功能及注意:

执行 URL 重定向,允许你去掉带有恶意的 URL,包含多个参数(修改)利用正则的匹配,分组和引用,达到目的 配置范例: if ($http_user_agent ~ MSIE) { rewrite ^(.*)$ /msie/$1 break; } if ($http_cookie ~* "id=([^;] +)(?:;|$)" ) { set $id $1; } if ($request_method = POST ) { return 405; } if (!-f $request_filename) { break; proxy_pass http://127.0.0.1; } if ($slow) { limit_rate 10k; } if ($invalid_referer) { return 403; }

该模块允许使用正则表达式改变 URL,并且根据变量来转向以及选择配置

if 语句块 语法:

if (condition) { ... }

功能描述:对重定向进行流程控制

作用域:server, location

return 语句 语法:

return code;

作用域:server, location, if

功能描述:这个指令根据规则的执行情况,返回一个状态值给客户端。

可使用值包括:204,400,402 - 406,408,410,411,413,416 及 500 - 504。也可以发送非标准的 444 代码 - 未发送任何头信息,然后结束连接。

rewrite 语句

语法:

rewrite regex replacement flag

regex :用于匹配 url 的正则表达式。使用括号()标记要截取的内容 replacement : 匹配成功后用于替换 url 中被截取内容的字符串 flag : 用来设置 rewrite 对 url 的处理行为

常用的 flag 如下:

last : 表示完成 rewritebreak: 本规则匹配完成后,终止匹配,不再匹配后面的规则redirect : 返回 302 临时重定向,地址栏会显示跳转后的地址permanent: 返回 301 永久重定向,地址栏会显示跳转后的地址 作用域:server, location, if

功能描述:这个指令根据正则表达式或者待替换的字符串来更改 URL。指令根据配置文件中的先后顺序执行生效。

Proxy 模块

模块:ngx_http_proxy_module

功能描述:此模块能代理请求到其它服务器。就是说允许你把客户端的 HTTP 请求转到后端服务器(这部分的指令非常多,但不是全部都会被用到,这里是比较常见的指令简介)。

指令:proxy_pass_header Server;

功能描述:该指令强制一些被忽略的头传递到客户端。

指令:proxy_redirect off; 功能描述:允许改写出现在 HTTP 头却被后端服务器触发重定向的 URL,对响应本身不做任何处理。

指令:proxy_set_header Host $http_host; 功能描述:允许你重新定义代理 header 值再转到后端服务器,目标服务器可以看到客户端的原始主机名。

指令: proxy_set_header X-Real-IP $remote_addr; 功能描述:目标服务器可以看到客户端的真实 IP,而不是转发服务器的 IP。

proxy 更多的指令查询

upstream 模块

模块:ngx_http_upstream_module

语法:

upstream name { ... }

功能简介:该指令使请求被上行信道之间的基于客户端的 IP 地址分布

配置范例:

upstream backend { server backend1.example.com weight=5; server backend2.example.com:8080; server unix:/tmp/backend3; } server { location / { proxy_pass http://backend; } }

upstream 指令 语法:

upstream name { ... }

作用域:http

指令功能及注意:

这个指令描述了一个服务器的集合,该集合可被用于 proxy_pass 和 fastcgi_pass 指令中,作为一个单独的实体。这些服务器可以是监听在不同的端口,另外,并发使用同时监听 TCP 端口和 Unix 套接字的服务器是可能的。这些服务器能被分配不同的权重。如果没有指定,则都为 1 。

示例:

upstream backend { server backend1.example.com weight=5; server 127.0.0.1:8080 max_fails=3 fail_timeout=30s; server unix:/tmp/backend3; }

ip_hash 指令 作用域: upstream

upstream backend { ip_hash; server backend1.example.com; server backend2.example.com; server backend3.example.com; server backend4.example.com; }

指令功能及注意:

指定服务器组的负载均衡方法,请求基于客户端的 IP 地址在服务器间进行分发。IPv4 地址的前三个字节或者 IPv6 的整个地址,会被用来作为一个散列 key。这种方法可以确保从同一个客户端过来的请求,会被传给同一台服务器。除了当服务器被认为不可用的时候,这些客户端的请求会被传给其他服务器,而且很有可能也是同一台服务器。

如果其中一个服务器想暂时移除,应该加上 down 参数。这样可以保留当前客户端 IP 地址散列分布。就像这样:

upstream backend { ip_hash; server backend1.example.com; server backend2.example.com; server backend3.example.com down; server backend4.example.com; }

server 指令 语法:

server address [parameters];

作用域: upstream

指令功能及注意:

定义服务器的地址 address 和其他参数 parameters。地址可以是域名或者 IP 地址,端口是可选的,或者是指定“unix:”前缀的 UNIX 域套接字的路径。如果没有指定端口,就使用 80 端口。如果一个域名解析到多个 IP,本质上是定义了多个 server。 实例: upstream backend { server backend1.example.com weight=5; server 127.0.0.1:8080 max_fails=3 fail_timeout=30s; server unix:/tmp/backend3; }

这一章主要讲述 nginx 的进程,及 http 主要模块。这里有 nginx 全要模块,大家看这里会有更深入的了解,虽然很枯燥,但这是实践的理论依据,下一章会有较简单的模块开发实战,两边对照起来补充是最好的。

nginx 模块开发实验

实验介绍

实验内容

整个实验中我们自己开发一个模块,它的作用就是打印出一个 hello world

实验知识点

模块结构体认识 编译安装 Nginx Nginx 基本配置

适合人群

本节实验开发了一个简单的 Nginx 模块,读者如果自己开发需要阅读很多说明文档和资料。其实模块与模块的很多

实验原理

下面介绍有关实验的原理。

模块的代码可以通过如下方式获得:

# echo模块代码 $ git clone https://github.com/shiyanlou/ngx_http_echo_module

建议完全没有 Nginx 模块编写经验的学习者对照代码和文档进行学习,在了解之后尝试独立撰写模块代码。

# 进入模块代码所在的文件夹 $ cd ngx_http_echo_module # 使用 vim 编辑模块代码 $ vim ngx_http_echo_module.c

Nginx 模块工作原理回顾

我们在上一章的 nginx 模块与进程中讲过:当 Nginx 接到一个 HTTP 请求时,它仅仅是通过查找配置文件将此次请求映射到一个 location block,而此 location 中所配置的各个指令则会启动不同的模块去完成工作,因此模块可以看做 Nginx 真正的劳动工作者。

通常一个 location 中的指令会涉及一个 handler 模块和多个 filter 模块(当然,多个 location 可以复用同一个模块)。handler 模块(这一章实验的重点)负责处理请求,完成响应内容的生成,而 filter 模块对响应内容进行处理。因此 Nginx 模块开发分为 handler 开发和 filter 开发。

在这次实验中,我们需要参照 Nginx 的工作原理,开发一个叫 echo 的 handler 模块,这个模块功能非常简单,它接收 “echo” 指令,指令可指定一个字符串参数,模块会输出这个字符串作为 HTTP 响应,直观来看,要实现这个功能需要三步:

1、读入配置文件中 echo 指令及其参数 2、进行 HTTP 包装(添加 HTTP 头等工作) 3、将结果返回给客户端

模块配置结构

前面我们已经使用 vim 编辑器打开 ngx_http_echo_module.c 了,为了后面实验讲解的方便,我们还需要设置显示行号。方式为:在刚进入 vim 的界面按下 Shift + ;,这时光标会移动到 vim 编辑器的最末行(即进入末行模式),输入 set nu 并回车, 这时 vim 就显示行号了。

首先这里要说一下,这里的模块配置的命令根据 Nginx 模块开发规则(也为了方便阅读),这个结构的命名规则为 ngx_http_[module-name]_[main|srv|loc]_conf_t,中间是模块名称,后面是表示模块运行在哪一层。

// 参考代码从 5 行起 typedef struct { ngx_str_t ed; } ngx_http_echo_loc_conf_t;

第一个结构体用于存储从配置文件中读进来的相关指令参数,即模块配置信息结构。其中字符串 ed 用于存储 echo 指令指定的需要输出的字符串。

// Nginx 源码的 src/core/ngx_string.h 中 typedef struct { size_t len; u_char *data; } ngx_str_t;

上面这两个字段分别表示字符串的长度和数据起始地址。注意在 Nginx 源代码中对数据类型进行了别称定义,如 ngx_int_t 为 intptr_t 的别称,为了保持一致,在开发 Nginx 模块时也应该使用这些 Nginx 源码定义的类型而不要使用 C 原生类型。

除了 ngx_str_t 外,其它三个常用的 nginx type 分别为:

// 在 Nignx 源码的 src/core/ngx_config.h typedef intptr_t ngx_int_t; typedef uintptr_t ngx_uint_t; typedef intptr_t ngx_flag_t;

这些就是最基础的配置文件结构,总的来说:两个结构体,一个类型定义。

模块配置指令

我们要清楚 echo 模块需要接受指令“echo”。

Nginx 模块使用一个 ngx_command_s 数组表示模块所能接受的所有指令,其中每一个元素表示一个条指令(这是在 nginx 中常用的模式)。ngx_command_t 是 ngx_command_s 的一个别称(Nginx 习惯于使用“_s”后缀命名结构体,然后 typedef 一个同名 _t 后缀名称作为此结构体的类型名)

// 在 Nignx 源码的 src/core/ngx_conf_file.h struct ngx_command_s { ngx_str_t name; ngx_uint_t type; char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); ngx_uint_t conf; ngx_uint_t offset; void *post; }; // 留意这一行,接下来要用 #define ngx_null_command { ngx_null_string, 0, NULL, 0, 0, NULL } name: 配置指令的名称。type: 该配置的类型,其实更准确一点说,是该配置指令属性的集合。nginx 提供了很多预定义的属性值(一些宏定义),通过逻辑或运算符可组合在一起,形成对这个配置指令的详细的说明。相关可用 type 定义在 Nginx 源码包中的 src/core/ngx_config_file.h。set: 是一个函数指针,用于指定一个参数转化函数,这个函数一般是将配置文件中相关指令的参数转化成需要的格式并存入配置结构体。Nginx 预定义了一些转换函数,可以方便我们调用,这些函数定义在 core/ngx_confile.h 中,一般以“_slot”结尾,例如 ngx_conf_set_flag_slot 将“on 或 off”转换为“1 或 0”,再如 ngx_conf_set_str_slot 将裸字符串转化为 ngx_str_t。conf: 用于指定 Nginx 相应配置文件内存起始地址,一般可以通过内置常量指定,如 NGX_HTTP_LOC_CONF_OFFSET,offset 指定此条指令的参数的偏移量。

下面是 echo 模块的指令定义:

// 参考代码第 8 行 // 这是参数转化函数的函数原型,接下来会讲解 static char * ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); // 参考代码从 12 行起 static ngx_command_t ngx_http_echo_commands[] = { { ngx_string("echo"), NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_http_echo, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_echo_loc_conf_t, ed), NULL }, ngx_null_command }; NGX_HTTP_LOC_CONF: 可以出现在 http server 块里面的 location 配置指令里。NGX_CONF_TAKE1:配置指令接受 1 个参数。offset: 指定该配置项值的精确存放位置,一般指定为某一个结构体变量的字段偏移。因为对于配置信息的存储,一般我们都是定义一个结构体来存储的。比如我们定义了一个结构体 A,该项配置的值需要存储到该结构体的 b 字段,那么在这里就可以填写为 offsetof(A, b)。对于有些配置项,它的值不需要保存或者是需要保存到更为复杂的结构中时,这里可以设置为 0。ngx_http_hello_commands: 这个数组每 5 个元素为一组,用来描述一个配置项的所有情况。那么如果有多个配置项,只要按照需要再增加 5 个对应的元素对新的配置项进行说明。需要注意的是,ngx_http_[module-name]_commands 这个数组定义的最后,都要加一个 ngx_null_command 作为结尾。

参数转化函数的代码为:

// 参考代码从 91 行起 static char * ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_core_loc_conf_t *clcf; clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); clcf->handler = ngx_http_echo_handler; ngx_conf_set_str_slot(cf,cmd,conf); return NGX_CONF_OK; }

这个函数除了调用 ngx_conf_set_str_slot 转化 echo 指令的参数外,还修改了核心模块配置(也就是这个 location 的配置),将其 handler 替换为我们编写的 handler:ngx_http_echo_handler。

如果不替换的话,他会自动调用默认的 handler 模块,现在就可以使用 ngx_http_echo_handler 产生 HTTP 响应。

定义模块 Context

下一步是定义模块 Context

这里首先需要定义一个 ngx_http_module_t 类型的结构体变量(命名规则为 ngx_http_[module-name]_module_ctx),这个结构主要用于定义各个 Hook 函数。下面是 echo 模块的 context 结构:

// 参考代码从 22 行起 static ngx_http_module_t ngx_http_echo_module_ctx = { NULL, /* preconfiguration */ NULL, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ ngx_http_echo_create_loc_conf,/* create location configration */ ngx_http_echo_merge_loc_conf/* merge location configration */ };

可以看到一共有 8 个 Hook 注入点,分别会在不同时刻被 Nginx 调用,由于我们的模块仅仅用于 location 域,这里将不需要的注入点设为 NULL 即可。

其中 create_loc_conf 用于初始化一个配置结构体,如为配置结构体分配内存等工作;merge_loc_conf 用于将其父 block 的配置信息合并到此结构体中,也就是实现配置的继承。这两个函数会被 Nginx 自动调用。

这里是 echo 模块的两个函数:

// 参考代码从 100 行起 static void * ngx_http_echo_create_loc_conf(ngx_conf_t *cf) { ngx_http_echo_loc_conf_t *conf; conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_echo_loc_conf_t)); if (conf == NULL) { return NGX_CONF_ERROR; } conf->ed.len = 0; conf->ed.data = NULL; return conf; } static char * ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) { ngx_http_echo_loc_conf_t *prev = parent; ngx_http_echo_loc_conf_t *conf = child; ngx_conf_merge_str_value(conf->ed, prev->ed, ""); return NGX_CONF_OK; }

其中 ngx_pcalloc 用于在 Nginx 内存池中分配一块空间,是 pcalloc 的一个包装。使用 ngx_pcalloc 分配的内存空间不必手工 free,Nginx 会自行管理,在适当时释放。

create_loc_conf 新建一个 ngx_http_echo_loc_conf_t,分配内存,并初始化其中的数据,然后返回这个结构的指针;

而 merge_loc_conf 将父 block 域的配置信息合并到 create_loc_conf 新建的配置结构体中。

handler 模块

handler 模块处理的结果通常有三种情况:处理成功,处理失败(处理的时候发生了错误)或者是拒绝处理。在拒绝处理的情况下,这个 location 的处理就会由默认的 handler 模块来进行处理(例如,当请求一个静态文件的时候,如果关联到这个 location 上的一个 handler 模块拒绝处理,就会由默认的 ngx_http_static_module 模块进行处理,该模块是一个典型的 handler 模块。)。

这个模块是核心,前面的都是铺垫,让我们稍微整理一下思路,回顾一下实现一个 handler 的步骤: 读入模块配置,处理功能业务,产生 HTTP header,产生 HTTP body。

// 参考代码从 47 行起 static ngx_int_t ngx_http_echo_handler(ngx_http_request_t *r) { ngx_int_t rc; ngx_buf_t *b; ngx_chain_t out; ngx_http_echo_loc_conf_t *elcf; elcf = ngx_http_get_module_loc_conf(r, ngx_http_echo_module); if(!(r->method & (NGX_HTTP_HEAD|NGX_HTTP_GET|NGX_HTTP_POST))) { return NGX_HTTP_NOT_ALLOWED; } r->headers_out.content_type.len = sizeof("text/html") - 1; r->headers_out.content_type.data = (u_char *) "text/html"; r->headers_out.status = NGX_HTTP_OK; r->headers_out.content_length_n = elcf->ed.len; if(r->method == NGX_HTTP_HEAD) { rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } } b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t)); if(b == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "Failed to allocate response buffer."); return NGX_HTTP_INTERNAL_SERVER_ERROR; } out.buf = b; out.next = NULL; b->pos = elcf->ed.data; b->last = elcf->ed.data + (elcf->ed.len); b->memory = 1; b->last_buf = 1; rc = ngx_http_send_header(r); if(rc != NGX_OK) { return rc; } return ngx_http_output_filter(r, &out); } 获取模块配置信息:这一块只要简单使用 ngx_http_get_module_loc_conf 就可以了。功能逻辑:因为 echo 模块非常简单,只是简单输出一个字符串,所以这里没有功能逻辑代码。设置 response header:Header 内容可以通过填充 headers_out 实现,我们这里只设置了 Content-type 和 Content-length 等基本内容,ngx_http_headers_out_t 定义了所有可以设置的 HTTP Response Header 信息。输出 Response body:首先了解下 Nginx 的 I/O 机制,Nginx 允许 handler 一次产生一组输出,可以产生多次,Nginx 将输出组织成一个单链表结构,链表中的每个节点是一个 chain_t,定义在 core/ngx_buf.h 。

到了这里 我们要做的 就只是将这些小模块集合到一起。

// 参考代码从 32 行起 ngx_module_t ngx_http_echo_module = { NGX_MODULE_V1, &ngx_http_echo_module_ctx, /* module context */ ngx_http_echo_commands, /* module directives */ NGX_HTTP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING };

模块可以提供一些回调函数给 nginx,当 nginx 在创建进程线程或者结束进程线程时进行调用。但大多数模块在这些时刻并不需要做什么,所以都简单赋值为 NULL。这里主要需要填入的信息从上到下依次为:context、指令数组、模块类型以及若干特定事件的回调处理函数(不需要可以置为 NULL)。

注意我们的 echo 是一个 HTTP 模块,所以这里类型是 NGX_HTTP_MODULE,其它可用类型还有 NGX_EVENT_MODULE(事件处理模块)和 NGX_MAIL_MODULE(邮件模块)。

下面是 ngx_module_t 的定义,可与 echo 模块进行对应:

typedef struct ngx_module_s ngx_module_t; struct ngx_module_s { ngx_uint_t ctx_index; ngx_uint_t index; ngx_uint_t spare0; ngx_uint_t spare1; ngx_uint_t abi_compatibility; ngx_uint_t major_version; ngx_uint_t minor_version; void *ctx; ngx_command_t *commands; ngx_uint_t type; ngx_int_t (*init_master)(ngx_log_t *log); ngx_int_t (*init_module)(ngx_cycle_t *cycle); ngx_int_t (*init_process)(ngx_cycle_t *cycle); ngx_int_t (*init_thread)(ngx_cycle_t *cycle); void (*exit_thread)(ngx_cycle_t *cycle); void (*exit_process)(ngx_cycle_t *cycle); void (*exit_master)(ngx_cycle_t *cycle); uintptr_t spare_hook0; uintptr_t spare_hook1; uintptr_t spare_hook2; uintptr_t spare_hook3; uintptr_t spare_hook4; uintptr_t spare_hook5; uintptr_t spare_hook6; uintptr_t spare_hook7; };

以下链接是淘宝的服务运维团队的资料书,有兴趣的同学可以好好研读 http://tengine.taobao.org/book/chapter_03.html

nginx配置实战:流量及并发连接数限制

实验内容

本节实验通过 Nginx 的限制模块对于下载一个 mp4 文件的下载速度进行限制。

实验知识点

流量限制 并发限制

文件获取

在这之前,我们需要先下载一个 mp4 文件,供后面实验测试使用哦。

先建立一个 Documents 目录

$ mkdir Documents && cd Documents 再使用命令:

$ git clone https://github.com/shiyanlou/seven

实现流量限制具体配置

编译 Nginx

首先要清楚,我们的配置是基于 ngx_http_core_module 模块的。要简单完成流量限制,我们必须做一点前期准备。

我们刚才已经在下载测试使用包,现在要做的,就是安装 nginx,上几章的讲解已经很详细了,这里我们选择源码安装。(前面的实验中我们已经编译安装了)

$ cd /home/shiyanlou/ $ wget http://labfile.oss.aliyuncs.com/nginx-1.7.9.tar.gz

接下来就是解压,编译,安装,前两章着重讲过,这里不再赘述。

下面我们就修改配置文件,前几章我们讲过对于 Nginx 来说,最重要的莫过于他自己的配置文件 nginx.conf。文件路径为**/usr/local/nginx/conf/nginx.conf**,我们需要在 http 的 server 中加一个 location,就好比之前我们做的简单的 echo 模块,格式为:

location /刚下好的测试文件夹/ { root /刚才下载测试文件的目录/; ... ... ... }

相关指令

接下来就是调用命令实现流量限制。nginx 的模块中早就写好了对应的命令,我们只需要调用就好。

实现流量限制由两个指令 limit_rate 和 limit_rate_after 共同完成:

(1)limit_rate 指令

语法:

limit_rate rate;

默认值:limit_rate 0;

作用域:http, server, location, if in location

命令概述:

限制向客户端传送响应的速率限制。参数 rate 的单位是字节/秒,设置为 0 将关闭限速。nginx 按连接限速,所以如果某个客户端同时开启了两个连接,那么客户端的整体速率是这条指令设置值的 2 倍。 (2)limit_rate_after 指令

语法:

limit_rate_after size;

默认值:limit_rate_after 0;

作用域:http, server, location, if in location

命令概述:

设置不限速传输的响应大小。当传输量大于此值时,超出部分将限速传送。

要是想了解更多,这里有更加详细的命令信息: http://wiki.nginx.org/HttpCoreModule#limit_rate

具体配置

讲了要使用的指令,我们接下来就实战配置。

这是实验的具体配置(传输量限制为 3m,速率限制为 20k/s):

location /seven/{ root /home/shiyanlou/Documents; limit_rate_after 3m; limit_rate 20k; }

代码截图如下:

配置完以后记得重新启动 nginx(注意这是编译安装的)

$ cd /home/shiyanlou/nginx-1.7.9/objs $ sudo ./nginx

配置之前与配置之后的测试

接下来就是测试

$ cd /home/shiyanlou/ $ wget http://本机地址/seven/seven.mp4

本机地址用 ifconfig 来查看

修改配置之前,速率没有限制:

修改配置之后,可见由于传输量大于 3M,超出部分的传输速率已经被限制在 20k/s:

也许你已经发现配置之后刚开始的一段时间内传输速度很高,因为,传输量大于设定值的部分才会受到限制。这就说明,我们两个命令都发挥了作用,这也就完成了我们第一个实战。

实现并发连接数限制的具体配置

相关指令

这个的配置是基于 ngx_http_limit_zone_module 模块的,要简单完成并发限制,我们要涉及到 limit_conn_zone 和 limit_conn 这两个指令:

(1)limit_conn_zone 指令

语法: limit_conn_zone zone_name $variable the_size

默认值: no

作用域: http

本指令定义了一个数据区,里面记录会话状态信息。 variable 定义判断会话的变量;the_size 定义记录区的总容量。

(2)limit_conn 指令

语法: limit_conn zone_name the_size

默认值: no

作用域: http,server,location

指令概述:

指定一个会话最大的并发连接数。 当超过指定的最发并发连接数时,服务器将返回 “Service unavailable” (503)。

配置示例:

http { limit_conn_zone $binary_remote_addr zone=one:10m; ... server { ... location /seven/ { limit_conn one 1; ..... } } ... } ... limit_conn_zone $binary_remote_addr zone=one:10m;定义一个叫 one 的记录区,总容量为 10M。以变量 $binary_remote_addr 作为会话的判断基准(即一个地址一个会话)。

你可以注意到,在这里使用的是 b i n a r y r e m o t e a d d r ∗ ∗ 而 不 是 ∗ ∗ binary_remote_addr** 而不是 ** binaryremoteaddrremote_addr。 r e m o t e a d d r ∗ ∗ 的 长 度 为 7 至 15 b y t e s , 会 话 信 息 的 长 度 为 32 或 64 b y t e s 。 而 ∗ ∗ remote_addr** 的长度为 7 至 15 bytes,会话信息的长度为 32 或 64 bytes。 而** remoteaddr715bytes3264bytesbinary_remote_addr 的长度为 4 bytes,会话信息的长度为 32 bytes。 当 zone 的大小为 1M 的时候,大约可以记录 32000 个会话信息(一个会话占用 32 bytes)。

limit_conn one 1;限制 /seven/ 目录下,一个会话只能进行一个连接。 简单点,就是限制 /seven/ 目录下,一个 IP 只能发起一个连接,多过一个,一律 503。

由于环境的原因这里没有办法测试只是让大家了解,限制并发连接数 Nginx 的对应模块的配置,如果大家有需要,可以进一步参照 http://wiki.nginx.org/HttpLimitZoneModule 这里有更详细的讲解。

Nginx配置实战:访问控制及DDoS预防

实验介绍

实验内容

本节实验讲解了 Nginx 的访问控制,可以有效的防治 DDoS 工具,并有 ab 命令做压力测试,大家不要觉的难,其实我们要改的东西比较简单,只是前面在进行原理讲解,可以直接看实验步骤,然后回头看指令的用法。

实验知识点

nginx 访问控制配置 ab 命令 log 排查

适合人群

本节实验设计了更多的关于 nginx 的配置内容,更加深入的了解了 nginx 的功能和配置,适合运维人员或者感兴趣的同学学习。

访问控制配置

基于各种原因,我们要进行访问控制。比如说,一般网站的后台都不能让外部访问,所以要添加 IP 限制,通常只允许公司的 IP 访问。访问控制就是指只有符合条件的 IP 才能访问到这个网站的某个区域。

相关指令

涉及模块:ngx_http_access_module

模块概述:允许限制某些 IP 地址的客户端访问。

对应指令:

allow 语法: allow address | CIDR | unix: | all;

默认值: 无

作用域: http, server, location, limit_except

允许某个 IP 或者某个 IP 段访问。如果指定 unix,那将允许 socket 的访问。

注意:unix 在 1.5.1 中新加入的功能,如果你的版本比这个低,请不要使用这个方法。

deny 语法: deny address | CIDR | unix: | all;

默认值: 无

作用域:http, server, location, limit_except

禁止某个 IP 或者一个 IP 段访问。如果指定 unix,那将禁止 socket 的访问。

配置范例:

location / { deny 192.168.1.1; allow 192.168.1.0/24; allow 10.1.1.0/16; allow 2001:0db8::/32; deny all; }

规则按照顺序依次检测,直到匹配到第一条规则。

在这个例子里,IPv4 的网络中只有 10.1.1.0/16 和 192.168.1.0/24 允许访问,但 192.168.1.1 除外;对于 IPv6 的网络,只有 2001:0db8::/32 允许访问。

ngx_http_access_module 配置允许的地址能访问,禁止的地址被拒绝。

这只是很简单的访问控制,而在规则很多的情况下,使用 ngx_http_geo_module 模块变量更合适。 这里的 ngx_http_geo_module 模块大家下来可以了解 : ngx_http_geo_module

DDoS 预防配置

DDoS 的特点是分布式,针对带宽和服务攻击,也就是四层流量攻击和七层应用攻击,相应的防御瓶颈四层在带宽,七层的多在架构的吞吐量。

对于七层的应用攻击,我们还是可以做一些配置来防御的,使用 Nginx 的 http_limit_conn 和 http_limit_req 模块通过限制连接数和请求数能相对有效的防御。

ngx_http_limit_conn_module:可以限制单个 IP 的连接数ngx_http_limit_req_module:可以限制单个 IP 每秒请求数

限制每秒请求数 ngx_http_limit_req_module 模块通过漏桶原理来限制单位时间内的请求数,一旦单位时间内请求数超过限制,就会返回 503 错误。配置需要在两个地方设置:

nginx.conf 的 http 段内定义触发条件,可以有多个条件

在 location 内定义达到触发条件时 nginx 所要执行的动作

http { limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s; ... server { ... location ~ \.php$ { limit_req zone=one burst=5 nodelay; } } }

参数说明

参数描述binary_remote_addr二进制远程地址,这个参数就些这个就好了,不需要改zone=one:10m定义 zone 名字叫 one,并为这个 zone 分配 10M 内存,用来存储会话(二进制远程地址),1m 内存可以保存 16000 会话rate=10r/s限制频率为每秒 10 个请求burst=5允许超过频率限制的请求数不多于 5 个,假设 1、2、3、4 秒请求为每秒 9 个,那么第 5 秒内请求 15 个是允许的,反之,如果第一秒内请求 15 个,会将 5 个请求放到第二秒,第二秒内超过 10 的请求直接 503,类似多秒内平均速率限制。nodelay超过的请求不被延迟处理,设置后 15 个请求在 1 秒内处理。

限制 IP 连接数

上一章讲过,我们就直接写出来

http { limit_conn_zone $binary_remote_addr zone=addr:10m; //触发条件 ... server { ... location /download/ { limit_conn addr 1; // 限制同一时间内1个连接,超出的连接返回503 } } }

白名单设置

http_limit_conn 和 http_limit_req 模块限制了单 IP 单位时间内的连接和请求数,但是如果 Nginx 前面有 lvs 或者 haproxy 之类的负载均衡或者反向代理,nginx 获取的都是来自负载均衡的连接或请求,这时不应该限制负载均衡的连接和请求,就需要 geo 和 map 模块设置白名单:

geo $whiteiplist { default 1; 10.11.15.161 0; } map $whiteiplist $limit { 1 $binary_remote_addr; 0 ""; } limit_req_zone $limit zone=one:10m rate=10r/s; limit_conn_zone $limit zone=addr:10m;

geo 模块定义了一个默认值是 1 的变量 whiteiplist,当在 ip 在白名单中,变量 whiteiplist 的值为 0,反之为 1

下面是设置的逻辑关系解释: 如果在白名单中–> whiteiplist=0 --> $limit="" --> 不会存储到 10m 的会话状态(one 或者 addr)中 --> 不受限制;

反之,不在白名单中 --> whiteiplist=1 --> $limit=二进制远程地址 -->存储进 10m 的会话状态中 --> 受到限制。

最新回复(0)