Emiller’s Guide To Nginx Module Development

原文链接:https://www.evanmiller.org/nginx-modules-guide.html

要充分理解网络服务器 Nginx,有助于理解漫画人物蝙蝠侠。 蝙蝠侠很快。 Nginx 很快。蝙蝠侠打击犯罪。 Nginx 与浪费的 CPU 周期和内存泄漏作斗争。蝙蝠侠在压力下表现出色。就 Nginx 而言,它在服务器负载很重的情况下表现出色。 但如果没有蝙蝠侠实用腰带,蝙蝠侠几乎什么都不是。

在任何时候,蝙蝠侠的实用腰带都可能包含一个开锁器、几个蝙蝠镖、蝙蝠袖口、一个蝙蝠示踪剂、蝙蝠飞镖、夜视镜、铝热剂手榴弹、烟雾弹、一个手电筒、一个氪石环、一个乙炔手电筒, 或 Apple iPhone。当蝙蝠侠需要镇静、失明、震耳欲聋、昏迷、追踪、停止、熄灭敌人或给敌人发短信时,你最好相信他正在伸手去拿他的蝙蝠腰带。腰带对蝙蝠侠的行动至关重要,如果蝙蝠侠必须在穿裤子和系实用腰带之间做出选择,他肯定会选择腰带。事实上,他确实选择了实用腰带,这就是蝙蝠侠穿橡胶紧身裤而不是裤子的原因。

Nginx 没有实用工具带,而是有一个module chain 模块链。当 Nginx 需要对响应进行 gzip 或块编码时,它会抽出一个模块来完成工作。当 Nginx 根据 IP 地址或 HTTP 身份验证凭据阻止对资源的访问时,模块会进行偏转。当 Nginx 与 Memcache 或 FastCGI 服务器通信时,一个模块就是对讲机。

蝙蝠侠的实用腰带上挂着很多打冰球,但蝙蝠侠偶尔需要一个新工具。也许有一个新的敌人,蝙蝠袖口和蝙蝠镖对它无效。或者蝙蝠侠需要一种新的能力,比如能够在水下呼吸。就在那时,蝙蝠侠打电话给卢修斯·福克斯来设计合适的蝙蝠小工具。

本指南的目的是教你 Nginx 模块链的细节,让你像 Lucius Fox 一样。当你完成本指南后,你将能够设计和生产高质量的模块,使 Nginx 能够做它以前做不到的事情。 Nginx 的模块系统有很多细微差别和细节,所以你可能想经常回顾这篇文档。我试图让概念尽可能清晰,但我会直言不讳,编写 Nginx 模块仍然是一项艰巨的工作。
但是谁说制作蝙蝠工具很容易呢?

准备

您应该熟悉 C。不仅仅是“C 语法”;您应该了解结构的使用方式,不要被指针和函数引用吓跑,并了解预处理器。如果您需要复习,没有什么能比得上 K&R The_C_Programming_Language

https://en.wikipedia.org/wiki/The_C_Programming_Language

对 HTTP 的基本了解很有用。毕竟,您将在 Web 服务器上工作。

您还应该熟悉 Nginx 的配置文件。如果你不是,这里是它的要点:有四种上下文(称为 main、server、upstream 和 location),它们可以包含带有一个或多个参数的指令。main上下文中的指令适用于所有内容;server上下文中的指令适用于特定的主机/端口;upstream上下文中的指令指的是一组后端服务器;location上下文中的和指令仅适用于匹配的 Web 位置(例如,“/”、“/images”等)。location上下文继承自上层的server上下文,server上下文继承自main上下文。upstream上下文既不继承也不赋予它的属性;它有自己的特殊指令,这些指令并不真正适用于其他地方。我会多次提到这四种情况,所以不要忘记它们。

location继承server中配置的内容, server继承main配置中的内容。upstream相对独立。

让我们开始吧。

Nginx 模块委托的高级概述

Nginx 模块具有我们将介绍的三个角色:

  • handlers 处理程序处理请求并产生输出
  • filters 过滤器操纵处理程序产生的输出
  • load-balancers 当多个后端服务器符合条件时,负载均衡器选择一个后端服务器来发送请求

模块完成您可能与 Web 服务器关联的所有“实际工作”:每当 Nginx 提供文件或将请求代理到另一台服务器时,都会有一个处理程序模块执行工作;当 Nginx 压缩输出或者使用增加一些数据时,它使用过滤器模块。 Nginx 的“核心”只负责处理所有网络和应用层协议,并可以针对请求设置一系列模块序列。分散式架构使您可以制作一个漂亮的独立单元来做您想要的事情。

注意:与 Apache 中的模块不同,Nginx 模块不是动态链接的。 (换句话说,它们被直接编译到 Nginx 二进制文件中。)

如何调用模块?通常,在服务器启动时,每个处理程序都有机会将自己附加到配置中定义的特定location;如果多个处理程序附加到特定location,则只有一个会“获胜”(但好的配置编写器不会让冲突发生)。

处理程序可以通过三种方式返回:正常,错误,放弃处理请求并由默认处理程序处理(通常是处理静态文件的时候)。

如果处理程序恰好是一组后端服务器的反向代理,则可以使用另一种类型的模块:负载平衡器。负载均衡器接受一个请求和一组后端服务器,并决定哪个服务器将获得该请求。 Nginx 附带两个负载平衡模块::轮询算法 round-robin(处理请求就像打扑克时发牌那样)和IP hash(它确保特定客户端将在多个请求中访问同一个后端服务器)。

如果处理程序没有产生错误,则可以调用过滤器。多个过滤器可以挂接到每个location,以便(例如)可以压缩响应然后分块。它们的执行顺序在编译时确定。过滤器具有经典的“责任链”设计模式:一个过滤器被调用,完成它的工作,然后调用下一个过滤器,直到最后一个过滤器被调用,Nginx 完成响应。

在收到请求后的过滤器:request body filters处理,收到响应后的过滤器 response body filters

过滤器链最酷的部分是每个过滤器都不会等待前一个过滤器完成;它可以在生成前一个过滤器的输出时对其进行处理,有点像 Unix 管道。过滤器在缓冲区上运行,通常是页面大小 (4K),尽管您可以在 nginx.conf 中更改它。这意味着,例如,模块可以开始压缩来自后端服务器的响应,并在模块收到来自后端的整个响应之前将其流式传输到客户端。好的!

因此,为了总结以上描述,典型的处理周期如下:

客户端发送http请求 → Nginx根据请求对应location选择合适的处理程序 → 如果是反向代理模式负载均衡选择一个后端服务器 → 处理程序接收完数据后将输入缓冲发送到第一个过滤器 → 第一个过滤器处理完成后将结果发送到第二个过滤器 → 第二个给第三个 → 第三个给第四个 → 以此类推 → 最终返回给客户端

我说“通常”是因为 Nginx 的模块调用是高度可定制的。它给模块编写者带来了很大的负担来准确定义模块应该如何以及何时运行(我碰巧认为负担太大)。调用其实是通过一系列的回调来进行的,而且有很多。在下面这些情况下,您可以执行定制函数:

  • server读取配置文件之前
  • 读取location和server的每一条配置指令
  • 当Nginx初始化main配置时
  • 当Nginx初始化server配置时(例如:host/port)
  • 当Nginx合并server配置和main配置时
  • 当Nginx初始化location配置时
  • 当Nginx合并location配置和它的父server配置时
  • 当Nginx的主进程启动时
  • 当一个新的worker进程启动时
  • 当一个worker进程退出时
  • 当主进程退出时
  • 处理一个请求
  • 过滤响应头
  • 过滤响应体
  • 选择一个后端服务器
  • 初始化一个将发往后端服务器的请求
  • 重新-初始化一个将发往后端服务器的请求
  • 处理来自后端服务器的响应
  • 完成与后端服务器的交互

这有点让人不知所措。您拥有大量的权力供您使用,但您仍然可以仅使用其中的几个钩子和几个相应的函数来做一些有用的事情。是时候深入了解一些模块了。

Nginx 模块的组件

正如我所说,在制作 Nginx 模块时你有很大的灵活性。本节将描述几乎总是存在的部分。它旨在作为理解模块的指南,以及当您认为自己准备好开始编写模块时的参考。

 

Module配置结构

模块最多可以定义三个配置结构,一个用于主上下文、服务器上下文和位置上下文。大多数模块只需要一个位置配置。这些的命名约定是 ngx_http_<模块名称>_(main|srv|loc)_conf_t。这是一个取自 dav 模块的示例:

typedef struct {
    ngx_uint_t  methods;
    ngx_flag_t  create_full_put_path;
    ngx_uint_t  access;
} ngx_http_dav_loc_conf_t;
注意 Nginx 有特殊的数据类型(ngx_uint_t 和 ngx_flag_t);这些只是您了解和喜爱的原始数据类型的别名(如果您好奇,请参阅 core/ngx_config.h)。
配置结构中的元素由模块指令填充。

模块指令

模块的指令出现在 ngx_command_t 的静态数组中。下面是它们如何声明的示例,取自我编写的一个小模块:

static ngx_command_t  ngx_http_circle_gif_commands[] = {
    { ngx_string("circle_gif"),
      NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS,
      ngx_http_circle_gif,
      NGX_HTTP_LOC_CONF_OFFSET,
      0,
      NULL },

    { ngx_string("circle_gif_min_radius"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_num_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_circle_gif_loc_conf_t, min_radius),
      NULL },
      ...
      ngx_null_command
};

这是 ngx_command_t 的声明(我们正在声明的结构),位于 core/ngx_conf_file.h 中:

struct ngx_command_t {
    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;
};

看起来有点多,但每个元素都有其用途。

name 是指令字符串,没有空格。其类型 是一个 ngx_str_t,通常只用(例如)ngx_str(“proxy_pass”) 实例化。注意:ngx_str_t 是一个结构体,其中包含一个数据元素(字符串)和一个 len 元素(该字符串的长度)。 Nginx 在大多数你期望字符串的地方使用这个数据结构。

type 是一组标志,指示指令在何处合法以及指令采用多少参数。按位或运算的适用标志是:

  • NGX_HTTP_MAIN_CONF: 指令在main配置中有效
  • NGX_HTTP_SRV_CONF: 指令在server配置中有效
  • NGX_HTTP_LOC_CONF: 指令在location配置中有效
  • NGX_HTTP_UPS_CONF: 指令在upstream 配置中有效
  • NGX_CONF_NOARGS: 指令无参数
  • NGX_CONF_TAKE1: 指令有1个参数
  • NGX_CONF_TAKE2: 指令有2个参数
  • NGX_CONF_TAKE7: 指令有7个参数
  • NGX_CONF_FLAG: 指令参数为1个布尔型数据 (“on” or “off”)
  • NGX_CONF_1MORE: 指令至少有1个参数
  • NGX_CONF_2MORE: 指令至少有2个参数
还有一些其他选项,请参阅 core/ngx_conf_file.h。还有一些其他选项,请参阅 core/ngx_conf_file.h。
set 是一个函数的指针;通常这个函数会处理传递给这个指令的参数,并在它的配置结构中保存解析后的值。此设置函数将采用三个参数:
  • 指向结构体 ngx_conf_t 的指针, 这个结构体里包含需要传递给指令的参数
  • 指向结构体 ngx_command_t 的指针
  • 指向模块自定义配置结构体的指针

遇到指令时将调用此设置函数。 Nginx 提供了许多函数,用于在自定义配置结构中设置特定类型的值。这些功能包括:

  • ngx_conf_set_flag_slot: 转换 “on” 、”off” 成 1 、 0
  • ngx_conf_set_str_slot: 字符串保存为 ngx_str_t
  • ngx_conf_set_num_slot: 解析数字并保存为 int
  • ngx_conf_set_size_slot:  ngx_conf_set_size_slot:解析数据大小(“8k”、“1m”等)并将其保存到 size_t变量中
还有其他几个,它们非常方便(请参阅 core/ngx_conf_file.h)。如果内置函数不够好,模块也可以在这里引用它们自己的函数。
ngx_conf_set_str_array_slot:可以解析数组(ngx_array_t),ssl模块中证书与私钥配置使用该方法从而支持多个证书
ngx_conf_set_keyval_slot: 可以解析keyvalue格式,存放到数组中
ngx_conf_set_msec_slot:解析时间,毫秒
ngx_conf_set_sec_slot:解析时间,秒
ngx_conf_set_bufs_slot:两个参数一个参数数字,一个参数空间, 标识多大空间 gzip_buffs 4 8k
ngx_conf_set_enum_slot:一个参数转化为二进制值, SSL protocol设置使用此方式
ngx_conf_set_path_slot:用于设置路径
ngx_conf_set_access_slot: 用于设置目录的读写权限

这些内置函数如何知道将数据保存在何处?这就是 ngx_command_t 的下两个元素出现的地方,conf 和 offset。 conf 告诉 Nginx 这个值是否会保存到模块的主配置、服务器配置或位置配置(使用 NGX_HTTP_MAIN_CONF_OFFSET、NGX_HTTP_SRV_CONF_OFFSET 或 NGX_HTTP_LOC_CONF_OFFSET), offset 然后指定要写入对应配置结构体的哪一成员。
最后,post 只是指向模块在读取配置时可能需要的其他废话的指针。它通常为 NULL。
命令数组以 ngx_null_command 作为最后一个元素终止。

 

模块上下文

这是一个静态的 ngx_http_module_t 结构,它只有一堆函数引用,用于创建三个配置并将它们合并在一起。它的名字是 ngx_http_<module name>_module_ctx.。按顺序,函数引用是:

  • preconfiguration
  • postconfiguration
  • creating the main conf (i.e., do a malloc and set defaults)
  • initializing the main conf (i.e., override the defaults with what’s in nginx.conf)
  • creating the server conf
  • merging it with the main conf
  • creating the location conf
  • merging it with the server conf

这些根据他们在做什么而采取不同的论点。这是结构定义,取自 http/ngx_http_config.h,因此您可以看到回调的不同函数签名:

typedef struct {
    ngx_int_t   (*preconfiguration)(ngx_conf_t *cf);
    ngx_int_t   (*postconfiguration)(ngx_conf_t *cf);

    void       *(*create_main_conf)(ngx_conf_t *cf);
    char       *(*init_main_conf)(ngx_conf_t *cf, void *conf);

    void       *(*create_srv_conf)(ngx_conf_t *cf);
    char       *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);

    void       *(*create_loc_conf)(ngx_conf_t *cf);
    char       *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
} ngx_http_module_t;

你可以将不需要的函数设置为 NULL,Nginx 会解决的。

大多数处理程序只使用最后两个:一个为特定于位置的配置分配内存的函数(称为 ngx_http_<module name>_create_loc_conf)),以及一个设置默认值并将此配置与任何继承配置合并的函数(称为 ngx_http_<module name >_merge_loc_conf) ).如果配置无效,合并功能还负责产生错误;这些错误停止服务器启动。

这是一个示例模块上下文结构:

static ngx_http_module_t  ngx_http_circle_gif_module_ctx = {
    NULL,                          /* preconfiguration */
    NULL,                          /* postconfiguration */

    NULL,                          /* create main configuration */
    NULL,                          /* init main configuration */

    NULL,                          /* create server configuration */
    NULL,                          /* merge server configuration */

    ngx_http_circle_gif_create_loc_conf,  /* create location configuration */
    ngx_http_circle_gif_merge_loc_conf /* merge location configuration */
};

是时候深入挖掘一下了。这些配置回调在所有模块中看起来都非常相似,并且使用 Nginx API 的相同部分,因此它们值得了解。

create_loc_conf

这是一个基本的 create_loc_conf 函数的样子,取自我编写的 circle_gif 模块(参见源代码https://github.com/evanmiller/nginx_circle_gif/blob/master/ngx_http_circle_gif_module.c)。它接受一个指令结构 (ngx_conf_t) 并返回一个新创建的模块配置结构(在本例中为 ngx_http_circle_gif_loc_conf_t)。

static void *
ngx_http_circle_gif_create_loc_conf(ngx_conf_t *cf)
{
    ngx_http_circle_gif_loc_conf_t  *conf;

    conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_circle_gif_loc_conf_t));
    if (conf == NULL) {
        return NGX_CONF_ERROR;
    }
    conf->min_radius = NGX_CONF_UNSET_UINT;
    conf->max_radius = NGX_CONF_UNSET_UINT;
    return conf;
}

首先要注意的是 Nginx 的内存分配;只要模块使用 ngx_palloc(一个 malloc 包装器)或 ngx_pcalloc(一个 calloc 包装器),它就会负责释放。

可能的 UNSET 常量是

  • NGX_CONF_UNSET_UINT
  • NGX_CONF_UNSET_PTR
  • NGX_CONF_UNSET_SIZE
  • NGX_CONF_UNSET_MSEC
  • NGX_CONF_UNSET。

UNSET 告诉合并函数应该覆盖该值。

merge_loc_conf

这是 circle_gif 模块中使用的合并函数:

static char *
ngx_http_circle_gif_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
    ngx_http_circle_gif_loc_conf_t *prev = parent;
    ngx_http_circle_gif_loc_conf_t *conf = child;

    ngx_conf_merge_uint_value(conf->min_radius, prev->min_radius, 10);
    ngx_conf_merge_uint_value(conf->max_radius, prev->max_radius, 20);

    if (conf->min_radius < 1) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, 
            "min_radius must be equal or more than 1");
        return NGX_CONF_ERROR;
    }
    if (conf->max_radius < conf->min_radius) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, 
            "max_radius must be equal or more than min_radius");
        return NGX_CONF_ERROR;
    }

    return NGX_CONF_OK;
}

首先请注意,Nginx 为不同的数据类型提供了很好的合并功能 (ngx_conf_merge_<data type>_value);

  1. location对应的变量设置的值
  2. 如果第一个变量没有设置继承的值,
  3. 如果没有设置也没有继承值,默认值

然后将结果存储在第一个参数中。可用的合并函数包括 ngx_conf_merge_size_value、ngx_conf_merge_msec_value 等。有关完整列表,请参阅 core/ngx_conf_file.h。

小问题:这些函数如何写入第一个参数,因为第一个参数是按值传递的?

答:这些函数由预处理器定义(因此它们在到达编译器之前扩展为一些“if”语句和赋值)

#define ngx_conf_merge_value(conf, prev, default)                            \
    if (conf == NGX_CONF_UNSET) {                                            \
        conf = (prev == NGX_CONF_UNSET) ? default : prev;                    \
    }
还要注意错误是如何产生的;该函数向日志文件写入一些内容,并返回 NGX_CONF_ERROR。该返回代码会停止服务器启动。 (由于消息是在 NGX_LOG_EMERG 级别记录的,消息也将转到标准输出;仅供参考,core/ngx_log.h 有一个日志级别列表。)

模块定义

接下来我们再添加一层间接层,即 ngx_module_t 结构。该变量称为  ngx_http_<module name>_module. 这是对上下文和指令的引用以及其余回调(退出线程、退出进程等)的位置。模块定义有时用作查找与特定模块关联的数据的键。模块定义通常如下所示:

ngx_module_t  ngx_http_<module name>_module = {
    NGX_MODULE_V1,
    &ngx_http_<module name>_module_ctx, /* module context */
    ngx_http_<module name>_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
};

…适当地替换 。模块可以为进程/线程的创建和死亡添加回调,但大多数模块,其值设置未NULL不设置回调函数,让事情变得简单。 (对于传递给每个回调的参数,请参阅 core/ngx_http_config.h。)

模块安装

安装模块的正确方法取决于模块是处理程序、过滤器还是负载平衡器;因此,详细信息保留给相应的部分。

 

处理程序

现在我们将把一些微不足道的模块放在显微镜下看看它们是如何工作的。

处理程序剖析(非代理)

处理程序通常做四件事:

  • 获取location配置
  • 生成适当的响应
  • 发送标头
  • 发送正文

处理程序有一个参数,即请求结构。请求结构有很多关于客户端请求的有用信息,例如请求方法、URI 和标头。我们将逐一介绍这些步骤。

获取location配置

这部分很简单。您需要做的就是调用 ngx_http_get_module_loc_conf 并传入当前请求结构和模块定义。这是我的 circle gif 处理程序的相关部分:

static ngx_int_t
ngx_http_circle_gif_handler(ngx_http_request_t *r)
{
    ngx_http_circle_gif_loc_conf_t  *circle_gif_config;
    circle_gif_config = ngx_http_get_module_loc_conf(r, ngx_http_circle_gif_module);
    ...

现在我可以访问我在合并函数中设置的所有变量。

生成响应

这是模块实际工作的有趣部分。

请求结构在这里会有帮助,尤其是这些元素:

typedef struct {
...
/* the memory pool, used in the ngx_palloc functions */
    ngx_pool_t                       *pool; 
    ngx_str_t                         uri;
    ngx_str_t                         args;
    ngx_http_headers_in_t             headers_in;

...
} ngx_http_request_t;

uri 是请求的路径,例如“/query.cgi”。

args 是问号之后的请求部分(例如“name=john”)。

headers_in 有很多有用的东西,比如 cookies 和浏览器信息,但是很多模块不需要它的任何东西。如果您有兴趣,请参阅 http/ngx_http_request.h。

这应该足以产生一些有用的输出信息。完整的 ngx_http_request_t 结构可以在 http/ngx_http_request.h 中找到。

发送响应头
响应标头位于请求结构引用的名为 headers_out 的结构中。处理程序设置它想要的,然后调用 ngx_http_send_header(r)。 headers_out 的一些有用部分包括:
typedef struct {
...
    ngx_uint_t                        status;
    size_t                            content_type_len;
    ngx_str_t                         content_type;
    ngx_table_elt_t                  *content_encoding;
    off_t                             content_length_n;
    time_t                            date_time;
    time_t                            last_modified_time;
..
} ngx_http_headers_out_t;

剩余部分可以在头文件中寻找  http/ngx_http_request.h

因此,例如,如果一个模块要将 Content-Type 设置为“image/gif”,将 Content-Length 设置为 100,并返回 200 OK 响应,则此代码可以解决问题:
r->headers_out.status = NGX_HTTP_OK;
r->headers_out.content_length_n = 100;
r->headers_out.content_type.len = sizeof("image/gif") - 1;
r->headers_out.content_type.data = (u_char *) "image/gif";
ngx_http_send_header(r);

大多数合法的 HTTP 标头(在某处)都可用,您可以轻松设置。但是,有些标头的设置比您在上面看到的要难一些;例如,content_encoding 的类型为 (ngx_table_elt_t*),因此模块必须为其分配内存。这是通过一个名为 ngx_list_push 的函数完成的,该函数接收一个 ngx_list_t(类似于数组)并返回对列表(类型为 ngx_table_elt_t)的新创建成员的引用。以下代码将 Content-Encoding 设置为“deflate”并发送标头:

r->headers_out.content_encoding = ngx_list_push(&r->headers_out.headers);
if (r->headers_out.content_encoding == NULL) {
    return NGX_ERROR;
}
r->headers_out.content_encoding->hash = 1;
r->headers_out.content_encoding->key.len = sizeof("Content-Encoding") - 1;
r->headers_out.content_encoding->key.data = (u_char *) "Content-Encoding";
r->headers_out.content_encoding->value.len = sizeof("deflate") - 1;
r->headers_out.content_encoding->value.data = (u_char *) "deflate";
ngx_http_send_header(r);

当一个头部可以同时有多个值时,通常使用这种机制;它(理论上)使过滤器模块更容易添加和删除某些值,同时保留其他值,因为它们不必求助于字符串操作。

 

发送响应体

现在模块已经生成了响应并放入内存,接下来需要将响应分配给一个特殊的缓冲区,然后将缓冲区分配给一个链节,然后在链节上调用“发送主体”函数。 链条有什么用? Nginx 允许处理程序模块一次生成(并过滤模块处理)响应一个缓冲区;每个链节都保存一个指向链中下一个链接的指针,如果它是最后一个,则为 NULL。我们将保持简单并假设只有一个缓冲区。 首先,一个模块将声明缓冲区和链链接:

ngx_buf_t    *b;
ngx_chain_t   out;

下一步是分配缓冲区并将我们的响应数据指向它:

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;
}

b->pos = some_bytes; /* first position in memory of the data */
b->last = some_bytes + some_bytes_length; /* last position */

b->memory = 1; /* content is in read-only memory */
/* (i.e., filters should copy it rather than rewrite in place) */

b->last_buf = 1; /* there will be no more buffers in the request */
现在模块将其附加到链节:链
out.buf = b;
out.next = NULL;

最后,我们发送主体,并一次性返回输出过滤器链的状态代码:

return ngx_http_output_filter(r, &out);

缓冲区链是 Nginx IO 模型的关键部分,因此您应该熟悉它们的工作方式

小问题:当我们可以通过检查“next”是否为 NULL 来判断我们在链的末尾时, 为什么缓冲区有 last_buf 变量?

答案:链可能不完整,即有多个缓冲区,但不是此请求或响应中的所有缓冲区。所以有些缓冲区位于链的末尾,但不是请求的末尾。这给我们引入了接下来的内容……

last_buf 表示响应body的最后一个缓冲区,而 next == NULL 只是当前链中传递给过滤器的最后一个缓冲区,它不一定包含整个主体。

https://stackoverflow.com/questions/20119127/nginx-buffer-chain

chain用户链接多个buffer。chain只是管理存放数据的buffer, buffer的last_buffer 标识数据body是否结束。

Upstream(又称 Proxy) Handler

我已经帮你了解了如何让你的handler来产生响应。有时您只需使用一大块 C 代码即可获得该响应,但通常您会希望与另一台服务器通信(例如,如果您正在编写一个模块来实现另一个网络协议)。您可以自己完成所有网络编程,但是如果您收到部分响应会怎样?在等待响应的其余部分时,您不想用自己的事件循环阻塞主要事件循环。你会扼杀 Nginx 的性能。幸运的是,Nginx 允许您直接连接到它自己的处理后端服务器(称为“上游”)的机制,因此您的模块可以与另一台服务器通信而不会妨碍其他请求。本节描述模块如何与上游通信,例如 Memcached、FastCGI 或其他 HTTP 服务器。

Upstream 回调函数概要
与其他模块的处理函数不同,上游模块的处理函数几乎不做“实际工作”。它不调用 ngx_http_output_filter。它只是设置当上游服务器准备好写入和读取时将调​​用的回调。实际上有 6 个可用的钩子:
create_request 生成一个请求缓冲区(或它们的链)发送到上游
reinit_request 如果与后端的连接被重置(就在第二次调用 create_request 之前),则调用reinit_request
process_header 处理上游响应的第一个bit,通常保存一个指向上游“payload”的指针
abort_request 如果客户端中止请求,则调用 abort_request
finalize_request 当 Nginx 从上游完成读取时调用 input_filter 是一个主体过滤器,可以在响应主体上调用(例如,删除尾部)
这些是如何连接的?一个例子是为了。这是代理模块处理程序的简化版本:
static ngx_int_t
ngx_http_proxy_handler(ngx_http_request_t *r)
{
    ngx_int_t                   rc;
    ngx_http_upstream_t        *u;
    ngx_http_proxy_loc_conf_t  *plcf;

    plcf = ngx_http_get_module_loc_conf(r, ngx_http_proxy_module);

/* set up our upstream struct */
    u = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_t));
    if (u == NULL) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    u->peer.log = r->connection->log;
    u->peer.log_error = NGX_ERROR_ERR;

    u->output.tag = (ngx_buf_tag_t) &ngx_http_proxy_module;

    u->conf = &plcf->upstream;

/* attach the callback functions */
    u->create_request = ngx_http_proxy_create_request;
    u->reinit_request = ngx_http_proxy_reinit_request;
    u->process_header = ngx_http_proxy_process_status_line;
    u->abort_request = ngx_http_proxy_abort_request;
    u->finalize_request = ngx_http_proxy_finalize_request;

    r->upstream = u;

    rc = ngx_http_read_client_request_body(r, ngx_http_upstream_init);

    if (rc >= NGX_HTTP_SPECIAL_RESPONSE) {
        return rc;
    }

    return NGX_DONE;
}
它做了一些常规处理,但重要的部分是回调。还要注意有关 ngx_http_read_client_request_body 的部分。这是在 Nginx 完成从客户端读取时设置另一个回调。
这些回调会做什么?通常,reinit_request、abort_request 和 finalize_request 将设置或重置某种内部状态并且只有几行长。真正的主力是 create_requestprocess_header
create_request 

为了简单起见,假设我有一个上游服务器,它读取一个字符并打印出两个字符。我的功能会是什么样子?

create_request 需要为单字符请求分配一个缓冲区,为该缓冲区分配一个链链接,然后将上游结构指向该链链接。它看起来像这样:

static ngx_int_t
ngx_http_character_server_create_request(ngx_http_request_t *r)
{
/* make a buffer and chain */
    ngx_buf_t *b;
    ngx_chain_t *cl;

    b = ngx_create_temp_buf(r->pool, sizeof("a") - 1);
    if (b == NULL)
        return NGX_ERROR;

    cl = ngx_alloc_chain_link(r->pool);
    if (cl == NULL)
        return NGX_ERROR;

/* hook the buffer to the chain */
    cl->buf = b;
/* chain to the upstream */
    r->upstream->request_bufs = cl;

/* now write to the buffer */
    b->pos = "a";
    b->last = b->pos + sizeof("a") - 1;

    return NGX_OK;
}

那还不错,不是吗?当然,实际上您可能希望以某种有意义的方式使用请求 URI。它在 r->uri 中作为 ngx_str_t 可用,GET 参数在 r->args 中,不要忘记您还可以访问request headers 和 cookie数据。

process_header 

现在是 process_header的时候了。正如 create_request添加指向请求主体的指针一样, process_header 将响应指针转移到客户端将接收的部分。它还从上游读取标头并相应地设置客户端响应标头。

这是个小示例,阅读该两个字符的响应。假设第一个字符是标识状态的字符。如果是问号,我们想返回一个 404 File Not Found 给客户端并忽略其他字符。如果它是一个空格,那么我们想要将空格后的另一个字符连同 200 OK 响应一起返回给客户端。好吧,这不是最有用的协议,但它是一个很好的演示。我们将如何编写这个 process_header  函数?

static ngx_int_t
ngx_http_character_server_process_header(ngx_http_request_t *r)
{
    ngx_http_upstream_t       *u;
    u = r->upstream;

    /* read the first character */
    switch(u->buffer.pos[0]) {
        case '?':
            r->header_only; /* suppress this buffer from the client */
            u->headers_in.status_n = 404;
            break;
        case ' ':
            u->buffer.pos++; /* move the buffer to point to the next character */
            u->headers_in.status_n = 200;
            break;
    }

    return NGX_OK;
}

 

就是这样。操作表头,改变指针,大功告成。请注意, headers_in实际上是我们之前看到的响应标头结构(参见  http/ngx_http_request.h),但它可以使用上游的标头填充。一个真正的代理模块会做更多的http header处理,更不用说错误处理了,但你明白了主要思想。

但是……如果我们在一个缓冲区中没有来自上游的整个header怎么办?

 

Keeping state

好吧,还记得我说过 abort_requestreinit_request, 和 finalize_request 可以用于重置内部状态吗?那是因为许多上游模块都有内部状态。该模块将需要定义一个自定义上下文结构来跟踪到目前为止它从上游读取的内容。这与上面提到的“模块上下文”不同。这是预定义类型,而自定义上下文可以包含您需要的任何元素和数据(这是您的结构)。这个上下文结构应该在  create_request  函数中实例化,可能像这样:

ngx_http_character_server_ctx_t   *p;   /* my custom context struct */

p = ngx_pcalloc(r->pool, sizeof(ngx_http_character_server_ctx_t));
if (p == NULL) {
    return NGX_HTTP_INTERNAL_SERVER_ERROR;
}

ngx_http_set_ctx(r, p, ngx_http_character_server_module);

最后一行实质上是使用特定请求和模块名称注册自定义上下文结构,以便以后轻松检索。每当您需要此上下文结构时(可能在所有回调函数中),只需执行以下操作:

ngx_http_proxy_ctx_t  *p;
p = ngx_http_get_module_ctx(r, ngx_http_proxy_module);

p 将具有当前状态。设置它,重置它,递增,递减,将任意数据存入其中,随心所欲。这是在从上游读取以块形式返回数据时使用持久状态机的好方法,同样不会阻塞主事件循环。好的!

 

Handler Installation

 

通过将代码添加到使用模块的指令的回调函数位置, 来安装处理程序。例如,我的circle gif模块 ngx_command_t看起来像这样:

{ ngx_string("circle_gif"),
  NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS,
  ngx_http_circle_gif,
  0,
  0,
  NULL }

回调函数是第三个元素,在本例中为 ngx_http_circle_gif. 回想一下,这个回调的参数是指令结构(ngx_conf_t,它保存用户的参数)、相关的ngx_command_t  结构和指向模块自定义配置结构的指针。对于我的 circle gif 模块,函数如下所示:

static char *
ngx_http_circle_gif(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_circle_gif_handler;

    return NGX_CONF_OK;
}

这里有两个步骤:首先,获取该位置的“core”结构,然后为其分配一个处理程序。很简单,是吧?

我已经说了所有关于处理程序模块的知识。是时候进入过滤器模块了,输出过滤器链中的组件。

 

 

Filters

过滤器操纵处理程序生成的响应。标头过滤器操纵 HTTP 标头,主体过滤器操纵响应内容。

Header Filter剖析

Header Filter由三个步骤组成:

  1. 决定何时操作响应
  2. 操作响应
  3. 调用下一个filter

举个例子,这是“not modified”header过滤器的简化版本,如果客户端的 If-Modified-Since header与响应的 Last-Modified 标头匹配,它将状态设置为 304 Not Modified。请注意,header过滤器将ngx_http_request_t结构作为唯一参数,这使我们可以通过这个结构访问客户端header和即将发送的响应标头。

static
ngx_int_t ngx_http_not_modified_header_filter(ngx_http_request_t *r)
{
    time_t  if_modified_since;

    if_modified_since = ngx_http_parse_time(
                              r->headers_in.if_modified_since->value.data,
                              r->headers_in.if_modified_since->value.len);

/* step 1: decide whether to operate */
    if (if_modified_since != NGX_ERROR && 
        if_modified_since == r->headers_out.last_modified_time) {

/* step 2: operate on the header */
        r->headers_out.status = NGX_HTTP_NOT_MODIFIED;
        r->headers_out.content_type.len = 0;
        ngx_http_clear_content_length(r);
        ngx_http_clear_accept_ranges(r);
    }

/* step 3: call the next filter */
    return ngx_http_next_header_filter(r);
}

headers_out 结构与我们在有关处理程序的部分中看到的一样(cf. http/ngx_http_request.h),并且可以无休止地进行操作。

 

Body Filter剖析

 

因为主体过滤器一次只能在一个buffer(chain linke)上操作, buffer chain 使得编写主体过滤器变得有点棘手。模块必须决定是否覆盖输入buffer,用新分配的buffer替换buffer,或者在当前使用的缓冲区之前或之后插入一个新缓冲区。使事情复杂化的是,有时一个模块会接收多个缓冲区,因此它必须在一个不完整的缓冲区链上进行操作。不幸的是,Nginx 不提供用于操作缓冲区链的高级 API,因此主体过滤器可能难以理解(和编写)。但是,这里有一些您可能会在实际操作中看到的操作。

一个body filter原型大概是这个样子(例子代码从Nginx源代码的“chunked” filter中取得)

static ngx_int_t ngx_http_chunked_body_filter(ngx_http_request_t *r, ngx_chain_t *in);

第一个参数是我们的老朋友-请求结构(ngx_http_request_t)。第二个参数是指向当前部分链(可能包含 0、1 或更多缓冲区)头部的指针。

让我们举一个简单的例子。假设我们想在每个请求的末尾插入文本“<l!– served=”” by=”” nginx=”” –=””>”。首先,我们需要弄清楚响应的最终缓冲区是否包含在给定的in缓冲区链中。就像我说的,没有花哨的 API,所以我们将滚动我们自己的 for 循环:

ngx_chain_t *chain_link;
int chain_contains_last_buffer = 0;

chain_link = in;
for ( ; ; ) {
    if (chain_link->buf->last_buf)
        chain_contains_last_buffer = 1;
    if (chain_link->next == NULL)
        break;
    chain_link = chain_link->next;
}

因为是末尾插入文本, 如果没有最后的缓冲区就返回:

if (!chain_contains_last_buffer)
    return ngx_http_next_body_filter(r, in);

很好,现在最后一个缓冲区已经存在链表中了。接下来我们分配一个新缓冲区:

ngx_buf_t    *b;
    b = ngx_calloc_buf(r->pool);
    if (b == NULL) {
        return NGX_ERROR;
    }

把之前约定的数据进去:

b->pos = (u_char *) "<!-- Served by Nginx -->";
b->last = b->pos + sizeof("<!-- Served by Nginx -->") - 1;

将这个buffer挂载到新的链表上:

ngx_chain_t   *added_link;

added_link = ngx_alloc_chain_link(r->pool);
if (added_link == NULL)
    return NGX_ERROR;

added_link->buf = b;
added_link->next = NULL;

最后,把这个新链表挂在先前链表的末尾:

chain_link->next = added_link;

设置last_buf变量,应该设置到新加入的链表的buffer上。

chain_link->buf->last_buf = 0;
added_link->buf->last_buf = 1;

并将修改后的链传递给下一个输出过滤器:

return ngx_http_next_body_filter(r, in);

生成的函数比你用 mod_perl ($response->body =~ s/$/<!-- Served by mod_perl -->/) 做的要花费更多的精力,但是缓冲链是一个非常强大的构造,允许程序员增量处理数据,以便客户端尽快得到一些东西。然而,在我看来,缓冲链迫切需要一个更简洁的接口,这样程序员就不能让链处于不一致的状态。现在,操作它需要您自担风险

 

过滤器安装

 

过滤器挂载到post-configuration阶段。我们将header过滤器和body过滤器安装在同一个地方。

让我们来看下chunked filter module的一个简单例子, 他们模块上下文 module_context 如下:

static ngx_http_module_t  ngx_http_chunked_filter_module_ctx = {
    NULL,                                  /* preconfiguration */
    ngx_http_chunked_filter_init,          /* postconfiguration */
  ...
};

ngx_http_chunked_filter_init:

static ngx_int_t
ngx_http_chunked_filter_init(ngx_conf_t *cf)
{
    ngx_http_next_header_filter = ngx_http_top_header_filter;
    ngx_http_top_header_filter = ngx_http_chunked_header_filter;

    ngx_http_next_body_filter = ngx_http_top_body_filter;
    ngx_http_top_body_filter = ngx_http_chunked_body_filter;

    return NGX_OK;
}

发生了什么呢?好吧,如果你还记得,过滤模块组成了一条 ”CHAIN OF RESPONSIBILITY“。当handler生成一个响应后,调用2个函数:ngx_http_output_filter它调用全局函数ngx_http_top_body_filter;以及ngx_http_send_header 它调用全局函数ngx_top_header_filter

ngx_http_top_body_filter 和 ngx_http_top_header_filter是body和header各自的filter链的”链表头“。链表上的每一个“链”都保存着链表中下一个连接的函数引用(分别是 ngx_http_next_body_filter 和 ngx_http_next_header_filter)。当一个filter完成工作之后,它只需要调用下一个filter,直到一个特殊的被定义成 write filter被调用,这个 write filter的作用是包装最终的HTTP响应。你在这个filter_init函数中看到的就是,模块把自己添加到filter链表中;它先把旧的header filter当做是自己的 next,然后再声明”它自己是 top filter。(因此,最后一个被添加的filter会第一个被执行。)

链表挂载的过程, 每个filter将自己放到链表头部, 并将原有的filter, 保存到自己的next节点中。

边注: 这到底是怎么工作的?

每个filter要么返回一个错误码,要么用return ngx_http_next_body_filter();来作为返回语句

因此,如果filter顺利链执行到了链尾(那个特别定义的的 write filter),将返回一个”OK”响应,但如果执行过程中遇到了错误,链将被砍断,同时Nginx将给出一个错误的信息。这是一个单向的,错误快速返回的,只使用函数引用实现的链表。帅啊!

 

Load-Balancers

load-balancer只是决定哪个后端服务器将接收特定请求的一种方式;存在用于以 round-robin方式分发请求或散列有关请求的某些信息的实现。本节将使用 upstream_hash 模块(full source) 作为示例来描述负载均衡器的安装和调用。 upstream_hash 通过散列 nginx.conf 中指定的变量来选择后端。

一个负load-balancer 模块有六个部分:

  1. 启用配置指令(例如,hash)将调用注册函数。
  2. 注册函数将定义合法的服务器选项(例如,weight=)并注册一个上游初始化函数
  3. 在验证配置后立即调用上游初始化函数,它
    • 解析 server 名称为特定的IP地址
    • 为每个sokcet连接分配空间
    • 设置peer初始化函数的回调入口
  4. 每个请求调用一次peer初始化函数, 设置load-balancing 函数将访问和操作的数据结构;
  5. load-balancing功能决定将请求路由到哪里;每个客户端请求至少调用一次(更多,如果后端请求失败)。这就是主要功能的在这里实现。
  6. 最后,peer释放函数可以在与对应的后端服务器结束通信之后更新统计信息 (成功或失败)。

下面我来逐一讲讲上述6个过程:

 

启用指令

指令声明,既确定了他们在哪里生效又确定了一旦流程遇到指令将要调用什么函数。load-balancer的指令需要置NGX_HTTP_UPS_CONF标志位,以便让Nginx知道这个指令只会在upstream块中有效。同时它需要提供一个指向注册函数的指针。下面列出的是upstream_hash模块的 hash 指令声明:

{ ngx_string("hash"),
      NGX_HTTP_UPS_CONF|NGX_CONF_NOARGS,
      ngx_http_upstream_hash,
      0,
      0,
      NULL },

其他都很熟悉了。

注册函数

上面的回调 ngx_http_upstream_hash注册函数,之所以这样命名(由我命名)是因为它相关的 upstream 配置注册了一个上游初始化函数。此外,注册函数定义了特定的 upstream 块中的 server 指令的哪些选项(例如,weight=fail_timeout=)。这是 upstream_hash 模块的注册函数:

ngx_http_upstream_hash(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 {
    ngx_http_upstream_srv_conf_t  *uscf;
    ngx_http_script_compile_t      sc;
    ngx_str_t                     *value;
    ngx_array_t                   *vars_lengths, *vars_values;

    value = cf->args->elts;

    /* the following is necessary to evaluate the argument to "hash" as a $variable */
    ngx_memzero(&sc, sizeof(ngx_http_script_compile_t));

    vars_lengths = NULL;
    vars_values = NULL;

    sc.cf = cf;
    sc.source = &value[1];
    sc.lengths = &vars_lengths;
    sc.values = &vars_values;
    sc.complete_lengths = 1;
    sc.complete_values = 1;

    if (ngx_http_script_compile(&sc) != NGX_OK) {
        return NGX_CONF_ERROR;
    }
    /* end of $variable stuff */

    uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module);

    /* the upstream initialization function */
    uscf->peer.init_upstream = ngx_http_upstream_init_hash;

    uscf->flags = NGX_HTTP_UPSTREAM_CREATE;

    /* OK, more $variable stuff */
    uscf->values = vars_values->elts;
    uscf->lengths = vars_lengths->elts;

    /* set a default value for "hash_method" */
    if (uscf->hash_function == NULL) {
        uscf->hash_function = ngx_hash_key;
    }

    return NGX_CONF_OK;
 }
static char *
ngx_http_upstream_hash(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_upstream_hash_srv_conf_t  *hcf = conf;

    ngx_str_t                         *value;
    ngx_http_upstream_srv_conf_t      *uscf;
    ngx_http_compile_complex_value_t   ccv;

    value = cf->args->elts;

    ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t));

    ccv.cf = cf;
    ccv.value = &value[1];
    ccv.complex_value = &hcf->key;

    if (ngx_http_compile_complex_value(&ccv) != NGX_OK) {
        return NGX_CONF_ERROR;
    }

    uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module);

    if (uscf->peer.init_upstream) {
        ngx_conf_log_error(NGX_LOG_WARN, cf, 0,
                           "load balancing method redefined");
    }

    uscf->flags = NGX_HTTP_UPSTREAM_CREATE
                  |NGX_HTTP_UPSTREAM_WEIGHT
                  |NGX_HTTP_UPSTREAM_MAX_CONNS
                  |NGX_HTTP_UPSTREAM_MAX_FAILS
                  |NGX_HTTP_UPSTREAM_FAIL_TIMEOUT
                  |NGX_HTTP_UPSTREAM_DOWN;

    if (cf->args->nelts == 2) {
        uscf->peer.init_upstream = ngx_http_upstream_init_hash;

    } else if (ngx_strcmp(value[2].data, "consistent") == 0) {
        uscf->peer.init_upstream = ngx_http_upstream_init_chash;

    } else {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                           "invalid parameter \"%V\"", &value[2]);
        return NGX_CONF_ERROR;
    }

    return NGX_CONF_OK;
}

1.23 版本代码

除了做一些额外的事情,以便我们稍后计算 $variable , 剩下的都很简单,就是分配一个回调函数,设置一些标志位。哪些标志位是有效的呢?

  • NGX_HTTP_UPSTREAM_CREATE: 让upstream块中有 server 指令。我实在想不出那种情形会用不到它。
  • NGX_HTTP_UPSTREAM_WEIGHT: 让server指令获取选项 weight=
  • NGX_HTTP_UPSTREAM_MAX_FAILS: 允许选项max_fails=
  • NGX_HTTP_UPSTREAM_FAIL_TIMEOUT: 允许选项fail_timeout=
  • NGX_HTTP_UPSTREAM_DOWN: 允许选项 down
  • NGX_HTTP_UPSTREAM_BACKUP: 允许选项backup

每个模块都可以访问这些配置值。由模块决定如何处理它们。也就是说,不会自动强制执行 max_fails ;所有的失败逻辑都取决于模块作者。稍后会详细介绍。目前,我们还没有完成对回调的追踪。接下来,我们有上游初始化函数(上一个函数中的 init_upstream 回调)。

每个模块都可以访问这些配置值。由模块决定如何处理它们。也就是说,不会自动强制执行 max_fails;所有的失败逻辑都取决于模块作者。稍后会详细介绍。目前,我们还没有完成对回调的追踪。接下来,是上游初始化函数(上一个函数中的 init_upstream 回调)。

 

初始化函数

upstream初始化函数的目的是解析主机名,为套接字分配空间,并分配(又一个)回调。以下是 upstream_hash 的做法:

ngx_int_t
ngx_http_upstream_init_hash(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us)
{
    ngx_uint_t                       i, j, n;
    ngx_http_upstream_server_t      *server;
    ngx_http_upstream_hash_peers_t  *peers;

    /* set the callback */
    us->peer.init = ngx_http_upstream_init_upstream_hash_peer;

    if (!us->servers) {
        return NGX_ERROR;
    }

    server = us->servers->elts;

    /* figure out how many IP addresses are in this upstream block. */
    /* remember a domain name can resolve to multiple IP addresses. */
    for (n = 0, i = 0; i < us->servers->nelts; i++) {
        n += server[i].naddrs;
    }

    /* allocate space for sockets, etc */
    peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_hash_peers_t)
            + sizeof(ngx_peer_addr_t) * (n - 1));

    if (peers == NULL) {
        return NGX_ERROR;
    }

    peers->number = n;

    /* one port/IP address per peer */
    for (n = 0, i = 0; i < us->servers->nelts; i++) {
        for (j = 0; j < server[i].naddrs; j++, n++) {
            peers->peer[n].sockaddr = server[i].addrs[j].sockaddr;
            peers->peer[n].socklen = server[i].addrs[j].socklen;
            peers->peer[n].name = server[i].addrs[j].name;
        }
    }

    /* save a pointer to our peers for later */
    us->peer.data = peers;

    return NGX_OK;
}
static ngx_int_t
ngx_http_upstream_init_hash(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us)
{
    if (ngx_http_upstream_init_round_robin(cf, us) != NGX_OK) {
        return NGX_ERROR;
    }

    us->peer.init = ngx_http_upstream_init_hash_peer;

    return NGX_OK;
}

1.23版本

这个函数包含的东西ms比我们期望的多些。大部分的工作ms都该被抽象出来,但事实却不是,我们只能忍受这一点。倒是有一种简化的策略:调用另一个模块的upstream初始化函数,把这些脏活累活(对端的分配等等)都让它干了,然后再覆盖其us->peer.init这个回调函数。例子可以参见http/modules/ngx_http_upstream_ip_hash_module.c

在我们这个观点中的关键点是设置对端初始化函数的指向,在我们这个例子里是ngx_http_upstream_init_upstream_hash_peer

 

peer初始化

每个请求都会调用一次peer初始化函数。它建立了一个数据结构,该模块将在尝试用他找到合适的后端服务器来为该请求提供服务;这个结构在后端重试中保留相关数据(重试次数),所以它是跟踪连接失败次数或计算的哈希值的方便地方。按照惯例,这个结构称为 ngx_http_upstream_<module name>_peer_data_t

此外,peer初始化函数设置了两个回调:
  • get: load-balancing 函数
  • free: peer 释放函数  (通常在一个连接关闭时更新状态)

似乎还不止这些,它同时还初始化了一个叫做tries的变量。只要tries是正数,Nginx将继续重试当前的load-banlancer。当tries变为0时,Nginx将放弃重试。一切都取决于get 和 free 如何设置合适的tries

下面是upstream_hash中对端初始化函数的例子:

static ngx_int_t
ngx_http_upstream_init_hash_peer(ngx_http_request_t *r,
    ngx_http_upstream_srv_conf_t *us)
{
    ngx_http_upstream_hash_peer_data_t     *uhpd;
    
    ngx_str_t val;

    /* evaluate the argument to "hash" */
    if (ngx_http_script_run(r, &val, us->lengths, 0, us->values) == NULL) {
        return NGX_ERROR;
    }

    /* data persistent through the request */
    uhpd = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_hash_peer_data_t)
        + sizeof(uintptr_t) 
          * ((ngx_http_upstream_hash_peers_t *)us->peer.data)->number 
                  / (8 * sizeof(uintptr_t)));
    if (uhpd == NULL) {
        return NGX_ERROR;
    }

    /* save our struct for later */
    r->upstream->peer.data = uhpd;

    uhpd->peers = us->peer.data;

    /* set the callbacks and initialize "tries" to "hash_again" + 1*/
    r->upstream->peer.free = ngx_http_upstream_free_hash_peer;
    r->upstream->peer.get = ngx_http_upstream_get_hash_peer;
    r->upstream->peer.tries = us->retries + 1;

    /* do the hash and save the result */
    uhpd->hash = us->hash_function(val.data, val.len);

    return NGX_OK;
}
static ngx_int_t
ngx_http_upstream_init_hash_peer(ngx_http_request_t *r,
    ngx_http_upstream_srv_conf_t *us)
{
    ngx_http_upstream_hash_srv_conf_t   *hcf;
    ngx_http_upstream_hash_peer_data_t  *hp;

    hp = ngx_palloc(r->pool, sizeof(ngx_http_upstream_hash_peer_data_t));
    if (hp == NULL) {
        return NGX_ERROR;
    }

    r->upstream->peer.data = &hp->rrp;

    if (ngx_http_upstream_init_round_robin_peer(r, us) != NGX_OK) {
        return NGX_ERROR;
    }

    r->upstream->peer.get = ngx_http_upstream_get_hash_peer;

    hcf = ngx_http_conf_upstream_srv_conf(us, ngx_http_upstream_hash_module);

    if (ngx_http_complex_value(r, &hcf->key, &hp->key) != NGX_OK) {
        return NGX_ERROR;
    }

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "upstream hash key:\"%V\"", &hp->key);

    hp->conf = hcf;
    hp->tries = 0;
    hp->rehash = 0;
    hp->hash = 0;
    hp->get_rr_peer = ngx_http_upstream_get_round_robin_peer;


    return NGX_OK;
}

1.23

看上去不错,我们现在可以来选择一台upstream服务器了。

 

load-balancing 函数

主要部分现在才开始。货真价实的哦。模块就是在这里选择upstream服务器的。load-balancing 函数的原型看上去是这样的:

static ngx_int_t 
ngx_http_upstream_get_<module_name>_peer(ngx_peer_connection_t *pc, void *data);

data是我们存放所关注的客户端连接中有用信息的结构体。pc则是要存放我们将要去连接的server的相关信息。负载均衡函数做的事情就是填写pc->sockaddrpc->socklen, 和 pc->name。如果你懂一点网络编程的话,这些东西应该都比较熟悉了;但实际上他们跟我们手头上的任务来比并不算很重要。我们不关心他们代表什么;我们只想知道从哪里找到合适的值来填写他们。

这个函数必须找到一个可用server的列表,挑一个分配给pc。我们来看看upstream_hash是怎么做的吧:

upstream_hash模块已经通过调用ngx_http_upstream_init_hash,把server列表存放在了ngx_http_upstream_hash_peer_data_t 这一结构中。这个结构就是现在的data:

ngx_http_upstream_hash_peer_data_t *uhpd = data;

后端服务peers的信息现在存储在 uhpd->peers->peer 中。让我们通过将计算的哈希值除以服务器数量来从这个数组中选择一个对等点:

ngx_peer_addr_t *peer = &uhpd->peers->peer[uhpd->hash % uhpd->peers->number];

现在大功告成

pc->sockaddr = peer->sockaddr;
pc->socklen  = peer->socklen;
pc->name     = &peer->name;

return NGX_OK;

就这样!如果负载均衡器返回 NGX_OK,则表示“继续尝试此服务器”。如果返回NGX_BUSY,说明所有后端主机都不可用,Nginx应该重试。

但是……我们如何跟踪不可用的东西?如果我们不想再试一次怎么办?
peer 释放函数

peer释放功能在上游连接发生后运行;它的目的是跟踪故障。下面是它的函数原型:

void 
ngx_http_upstream_free_<module name>_peer(ngx_peer_connection_t *pc, void *data, 
    ngx_uint_t state);

前两个参数与我们在负载均衡器函数中看到的一样。第三个参数是 state 变量,表示连接是否成功。它可能包含按位或运算在一起的两个值: NGX_PEER_FAILED (连接失败)和 NGX_PEER_NEXT(连接失败,或者连接成功但应用程序返回错误)。零表示连接成功。

由模块作者决定如何处理这些失败事件。如果要使用它们,则结果应存储在 data中,即指向自定义每个请求数据结构的指针。

但是 peer release 函数的关键目的是将pc->tries 设置为零,如果你不希望 Nginx 在这个请求期间继续尝试这个load-balancer。最简单的peer 释放功能如下所示:

pc->tries = 0;

这将确保如果后端服务器有错误,将向客户端返回 502 Bad Proxy 错误。

这是一个更复杂的例子,取自 upstream_hash 模块。如果后端连接失败,它会在位向量(称为 tried,类型为 uintptr_t 的数组)中将其标记为失败,然后继续选择一个新的后端,直到找到一个没有失败的后端。

#define ngx_bitvector_index(index) index / (8 * sizeof(uintptr_t))
#define ngx_bitvector_bit(index) (uintptr_t) 1 << index % (8 * sizeof(uintptr_t))

static void
ngx_http_upstream_free_hash_peer(ngx_peer_connection_t *pc, void *data,
    ngx_uint_t state)
{
    ngx_http_upstream_hash_peer_data_t  *uhpd = data;
    ngx_uint_t                           current;

    if (state & NGX_PEER_FAILED
            && --pc->tries)
    {
        /* the backend that failed */
        current = uhpd->hash % uhpd->peers->number;

       /* mark it in the bit-vector */
        uhpd->tried[ngx_bitvector_index(current)] |= ngx_bitvector_bit(current);

        do { /* rehash until we're out of retries or we find one that's untried */
            uhpd->hash = ngx_hash_key((u_char *)&uhpd->hash, sizeof(ngx_uint_t));
            current = uhpd->hash % uhpd->peers->number;
        } while ((uhpd->tried[ngx_bitvector_index(current)] & ngx_bitvector_bit(current)) && --pc->tries);
    }
}

这是有效的,因为负载平衡功能将只查看uhpd->hash的新值。

许多应用程序不需要重试或高可用性逻辑,但可以像您在此处看到的那样只提供几行代码。

 

写一个nginx模块

所以现在,您应该准备好查看 Nginx 模块并尝试了解正在发生的事情(并且您将知道到哪里寻求帮助)。查看 src/http/modules/ 以查看可用模块。选择一个类似于您要完成的模块并浏览它。东西看起来很熟悉?它应该。请参阅本指南和模块源代码以了解正在发生的事情。

但是 Emiller 没有写一份关于阅读 Nginx 模块的指南,绝不是。这是一份毫不含糊的指南。我们不是在阅读。我们正在编写、创造、与全世界分享。

首先,您将需要一个地方来处理您的模块。在硬盘驱动器上的任何位置为您的模块创建一个文件夹,但与 Nginx 源分开(并确保您拥有来自 nginx 的最新副本)。您的新文件夹应包含两个文件:

  • “config”
  • “ngx_http_<your module>_module.c”

config 文件将包含在 ./configure 中,其内容将取决于模块的类型。

filter 模块的config:

ngx_addon_name=ngx_http_<your module>_module
HTTP_AUX_FILTER_MODULES="$HTTP_AUX_FILTER_MODULES ngx_http_<your module>_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_<your module>_module.c"

其他模块的config:

ngx_addon_name=ngx_http_<your module>_module
HTTP_MODULES="$HTTP_MODULES ngx_http_<your module>_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_<your module>_module.c"

有关“config”文件格式及其各种选项的更多信息,请参阅有关  wiki page  页面。

现在为您的 C 文件。我建议复制一个现有的模块,它的功能与您想要的类似,但将其重命名为”ngx_http_<your module>_module.c”。当您改变行为以满足您的需要时,让它成为您的模型,并在您理解和重新设计不同部分时参考本指南。
当你准备好编译时,只需进入 Nginx 目录并输入
./configure --add-module=path/to/your/new/module/directory

然后像往常一样 make 和 make install  。如果一切顺利,您的模块将被正确编译。很好,是吧?无需处理 Nginx 源代码,将您的模块添加到新版本的 Nginx 是轻而易举的事,只需使用相同的./configure 命令即可。顺便说一句,如果您的模块需要任何动态链接库,您可以将其添加到您的“config”文件中

CORE_LIBS="$CORE_LIBS -lfoo"

foo 是您需要的库。如果你制作了一个很酷或有用的模块,请务必向 Nginx 邮件列表发送一条消息并分享你的工作。

 

高级主题

 

本指南涵盖了 Nginx 模块开发的基础知识。有关编写更复杂模块的技巧,Emiller’s Advanced Topics In Nginx Module Development.

 

Appendix A: 代码参考

 

Appendix B: Changelog

  • December 10, 2018: Added link documenting the “config” file format.
  • August 11, 2017: Updated links to Nginx and module source code.
  • January 16, 2013: Corrected code sample in 5.5.
  • December 20, 2011: Corrected code sample in 4.2 (one more time).
  • March 14, 2011: Corrected code sample in 4.2 (again).
  • November 11, 2009: Corrected code sample in 4.2.
  • August 13, 2009: Reorganized, and moved Advanced Topics to a separate article.
  • July 23, 2009: Corrected code sample in 3.5.3.
  • December 24, 2008: Corrected code sample in 3.4.
  • July 14, 2008: Added information about subrequests; slight reorganization
  • July 12, 2008: Added Grzegorz Nosek’s guide to shared memory
  • July 2, 2008: Corrected “config” file for filter modules; rewrote introduction; added TODO section
  • May 28, 2007: Changed the load-balancing example to the simpler upstream_hash module
  • May 19, 2007: Corrected bug in body filter example
  • May 4, 2007: Added information about load-balancers
  • April 28, 2007: Initial draft

参考及引用

图片from Etos Yang

Comments are closed.