专栏名称: 亿级流量网站架构
开涛技术点滴
目录
相关文章推荐
OSC开源社区  ·  谷歌安卓系统“假开源、真垄断”? ·  2 天前  
程序员小灰  ·  DeepSeek让我的朋友一夜暴富! ·  2 天前  
OSC开源社区  ·  继V3之后,沐曦GPU再完成DeepSeek ... ·  3 天前  
OSC开源社区  ·  Gitee邀您参与SBOM行业调研:共建可信 ... ·  4 天前  
码农翻身  ·  为何 Linus ... ·  4 天前  
51好读  ›  专栏  ›  亿级流量网站架构

nginx精讲—变量实现原理(下)

亿级流量网站架构  · 公众号  · 程序员  · 2019-01-07 09:13

正文

承接

顺风详解Nginx系列—Ngx中的变量

顺风详解Nginx系列—nginx变量实现原理(上)


1 初始化变量

尽管是同一个变量,但在定义和索引的时候nginx会创建两个ngx_http_variable_t结构体,然后分别存在于两个不同的容器中。一般情况下定义变量的时候该变量携带的信息更全,而索引变量时则相对少一些。


初始化变量的过程其实就是两个容器融合的过程,这个过程在nginx中对应ngx_http_variables_init_vars()方法。因为最后cmcf->variables_keys容器是要被销毁的,所以融合的一个主要目的是把变量定义时是携带的信息(比如get_handler方法)迁移到cmcf->variables容器中的变量上。另外一个目的是检查cmcf->variables容器的变量是否被定义过,如果存在未定义的变量,并且该变量也不是动态变量,则直接返回错误,并且后台打印一条错误日志:“unknown xxx variable”。


动态变量的检查和设置发生在某个变量不存在于cmcf->variables_keys容器中时,检测的方式非常简单,就是匹配前缀。下面来看一个变量检测的例子:

location / {

return 200 “cookie is:$http_cookie  name is:$arg_name”;

}


首先在配置文件解析阶段,当解析到return指令后,该指令对应的指令方法会把变量“http_cookie”和“arg_name”方法到cmcf->variables容器中,以便获取这两个变量的索引值。此后在初始化变量阶段,nginx先从cmcf->variables_keys容器中查找是否存在“http_cookie”变量,因为这个变量正好是一个内置变量,所以它肯定是存在于cmcf->variables_keys容器中的,找到后把配置信息迁移一下,那么该变量的融合工作就算完成了。变量“arg_name”的融合过程跟“http_cookie”基本相似,不一样的是“arg_name”是一个动态变量,只要你没有为它做过定义(比如set $arg_name “name”),那么它肯定是不存在于cmcf->variables_keys容器中的,所以它会触发动态变量的融合逻辑。


关于动态变量的融合逻辑,感兴趣的同学可以读一下ngx_http_variables_init_vars()方法,很简单,这里就不再叙述了。


看完上面的例子后我们需要对在4.2说过的一句话“该容器基本上算是cmcf->variables_keys容器的一个子集”做一个修正,从例子中可以看到,动态变量是不存在与cmcf->variables_keys容器中的,但被使用后的动态变量确是存在于cmcf->variables容器中的,所以这句话后面再加上一个动态变量的限制就完整了。


该方法的最后一块逻辑是使用cmcf->variables_keys容器中的变量生成一个hash容器,我们前面提到的ngx_http_get_variable()方法就是从这个hash容器中查找变量的,所有这些事请做完之后cmcf->variables_keys容器就会被销毁,所以最终变量就只会存在于两个容器中:hash容器和cmcf->variables容器。



2 如何开发变量

在nginx内部创建和使用变量所需要的基本功能主要就是我们上面提到的知识,但就目前来说这些还只是一个一个的单独的知识点,对于刚接触nginx开发或者对nginx开发不太熟悉的同学来说仍然无法在脑中形成一个比较完整的变量开发轮廓,所以我们本小节会尽可能的把现有的知识点串起来,以便让读者在脑中有一个基本的轮廓。


2.1如何开发自定义变量

在nginx中开发一个支持自定变量的功能大概需要做如下准备:

  1. 我们需要定义一个指令,就像“set”、“geo”等指令那样,比如我们定义自己的指令为“myset”。

  2. 这条指令应该是率属于某个模块的,就像“set”属于http_rewirte模块那样,所有我们还需要创建一个模块。

  3. 需要设计一个结构体,用来存放解析到的指令语句信息,比如下面的指令的信息我们需要把他们解析出来并放到一个地方(比如结构体): myset $a “a”;    myset $b “b”;   myset $c “$a+$b”;

  4. 如果要支持变量插入,那么我们还需要准备一个能够识别字符串中变量的方法,比如指令 myset $c “$a+$b” ;


如果支持变量插入,那么变量c的最终结果应该是“a+b”,如果不支持则应该是其字面意思“$a+$b”。


以上是编写自定义变量指令时需要的一些基本数据,但是就目前介绍的知识还无法优雅的支撑我们写一个完整的变量指令功能,为了避免读者陷入毫无准备的细节中,我们举一个简单例子来描述一下其中的脉络,假设我们有如下配置文件:

20  location /myset {

21       myset $a “a”;

22       myset $b “$a+b”;

23       return 200 “$a”;

24  }


那么我们开发的功能应该是这样工作的:

  1. nginx解析到21行的“myset”指令后会调用其对应的指令方法(这个指令要做的动作),我们用my_handler()代替。

  2. 在my_handler()方法中我们需要调用ngx_http_add_variable()方法,把变量“$a”放入到cmcf->variables_key容器中,然后把对应的值“a”存放到我们事先准备好的结构体中。

  3. 然后设置变量“$a”对应结构体(ngx_http_variable_t)的一些信息,比如get_handler方法、flags标记等。

  4. 解析到22行的“myset”指令后跟21行的指令解析情况基本相似,不同的是22行的指令值中有一个变量“$a”,这时候就不是在定义变量了,而是在使用变量,所以我们先把这个变量解析出来。

  5. 然后调用ngx_http_get_variable_index()方法来获取变量“$a”的索引值。

  6. 最后把这个索引值和后面解析到的字符“+b”放到我们准备好的结构体中,到此我们的自定义指令就算解析完毕了。

  7. 剩下nginx自带的return指令做的工作跟第5步差不多,只不过它在解析过程中用到了脚本的概念,关于脚本我们在后续的文章中会做详细介绍,这里记住有这么个概念就行了。


以上是一个自定义变量指令的大概解析过程。


最后执行的时候会从return指令解析好的信息中获取变量“$a”的索引值,然后通过ngx_http_get_flushed_variable()方法获取变量对应的值,最终输出到客户端,至此我们上面介绍的知识点基本就都串起来了。


2.2开发内置变量

笔者曾经在使用nginx的lua模块的时候,为了监控性能需要拿到当前系统的毫秒级时间,而nginx和lua本身携带的方式都无法满足,它们虽然都可以拿到一个毫秒时间,但是精度都不能保证是1毫秒。当时的解决办法是用c扩展一个lua模块,但是因为需要重新编译,所以后来又引入了luajit中的ffi,通过ffi把获取毫秒时间的代码嵌入到了lua代码中,这样就可以利用luajit,避免我们手动编译c代码,这次我们使用另外一种方式:使用nginx内置变量来实时获取当前系统时间。


在nginx中目前有这样一个内置变量“$msec”,通过阅读官方文档可以看到它其实就一个可以表示毫秒时间的变量,但是nginx为了减少系统调用,把nginx中的时间做了一个缓存,如此一来在某些情况下它就没办法把时间精确到一毫秒。现在我们就仿效这个变量来编写一个不会缓存的时间变量,取名为“$mymsec”并且我们把它的时间精度设置成微秒。


nginx核心内置变量都放在下面的数组中:

ngx_http_variables.c#ngx_http_core_variables[]


这是一个ngx_http_variable_t类型的数组,在前面我们提到创建变量就是要创建该结构体,并且有两种方式:一种是自定义,一种是内置,我们这里就是使用内置方式。现在我们再次把表示变量名的结构体贴出来,看看都需要设置哪些字段:

typedef struct ngx_http_variable_s  ngx_http_variable_t;

struct ngx_http_variable_s {

ngx_str_t                name;

ngx_http_set_variable_pt set_handler;

ngx_http_get_variable_pt get_handler;

uintptr_t                data;

ngx_uint_t             flags;

ngx_uint_t             index;

}


首先分析以下我们要创建的内置变量“$mymsec”的特性:

  1. 变量需要一个名字,就是“mymsec”。

  2. 要求不允许缓存,所以要打上NGX_HTTP_VAR_NOCACHEABLE标记

  3. 一个调用系统函数获取时间的get_handler()方法,我们取名为“ngx_http_variable_mytime”。

  4. 其它目前不需要,我们给一个默认值就可以


最后我们创建的结构体应该是这样:

{ ngx_string("mytime"),

NULL,

ngx_http_variable_mytime,

0,

NGX_HTTP_VAR_NOCACHEABLE,

0 }


我们把他放到ngx_http_core_variables[]数组中,然后再把对应的方法做一个原型声明:

static ngx_int_t ngx_http_variable_mytime(ngx_http_request_t *r,ngx_http_variable_value_t *v, uintptr_t data);


并放到ngx_http_variables.c文件中,这样前期准备就差不多了,剩下就是如何实现这个方法了,我们把具体代码贴一下:

static ngx_int_t ngx_http_variable_mytime(ngx_http_request_t * ngx_http_variable_value_t *v, uintptr_t data)

{

/* 用来存放生成的时间数据 */

u_char      *p;

/* 一个时间结构体,其中tv.tv_sec表示秒,tv.tv_usec表示微秒 */

struct timeval tv;

/* 用来存放生成的秒级时间 */

time_t           sec;

/* 用来存放生成的微秒时间 */

ngx_uint_t       msec;

/* 分配内存空间 */

p = ngx_pnalloc(r->pool, NGX_TIME_T_LEN + 6);

if (p == NULL) {

return NGX_ERROR;

}

/* 调用系统函数获取当前系统时间,结果会放到tv中 */

ngx_gettimeofday(&tv);

/* 秒乘以1000*1000变微秒 */

sec = tv.tv_sec * 1000 * 1000;

msec = sec + tv.tv_usec; // 微秒

/* 获取的时间数据转换成字符后的长度 */

v->len = ngx_sprintf(p, "%M", msec) - p;

v->valid = 1;

v->no_cacheable = 1; // 不允许缓存变量结果

v->not_found = 0;

v->data = p; // 时间数据

return NGX_OK;

}


以上就是全部逻辑,重新编译并安装nginx后就可以使用这个变量了,来看一个例子:

location /start {

return 200 "$msec -- $mymsec";

}


curl http://127.0.0.1/start

1528025269.481-- 1528025269481836


后面的数据就是$mymsec变量打印的微秒数据,但是目前通过nginx自带的模块很难用例子证明我们上面说的“$msec”有缓存而“$mymsec”是实时获取的,这个等后面我们涉及到lua模块的时候再回过头细说这一块,这里就不展开了。


3 变量如何做到请求间隔离

通过上面的内容我们知道,变量的定义最终会放在cmcf->variables容器中,并且只此一份(这里我们不考虑前面提到的hash容器),而各个请求又都要用到这一份定义,但是请求之间又是相互独立的,同一个变量在不同的请求中肯定要展示跟当前请求相关的变量值,做到这一点的简单方式就是每个请求都保存属于自己的变量值,实际上nginx也是这么做的,下面就来看看具体实现细节。


3.1创建数组容器

在nginx中有一个专门用来表示请求的结构体,每当一个请求过来的时候都会创建一个这样的结构体,然后把相关的请求信息封装到该结构体中,所以把保存变量值的容器放到该结构体中也是最合情合理的,该结构体的定义大致如下:

typedef struct ngx_http_request_s  ngx_http_request_t;

struct ngx_http_request_s {

ngx_http_variable_value_t        *variables;

}


为了节省篇幅我们省略了不必要的成员字段,只列出了我们需要的字段,该结构体的定义在/src/http/ngx_http_request.h文件中,需要的读者请自行查看。


数组容器是在/src/http/ngx_http_request.c#ngx_http_create_reqeust()方法中完成创建的,该方法的逻辑不算复杂,我们这里其实只需要关心其中的两个语句,一个是用来为ngx_http_request_t结构体分配内存空间的:

r = ngx_pcalloc(pool, sizeof(ngx_http_request_t));


另一个是为r->variables这个数组容器分配空间的:

r->variables = ngx_pcalloc(r->pool, cmcf->variables.nelts * sizeof(ngx_http_variable_value_t));


其中cmcf->variables.nelts表示容器大小,乘以每个变量值结构体需要的空间就是整个数组容器需要占用的内存空间,这样容器的创建就算完成了。


其中r->variables这个指针变量在这里实际是一个数组,这对于不了解c语法的同学来说会产生一点困惑,这里我们做一个简单的介绍。在c语言中也存在表示数组的语法,正常情况下我们可以这样定义一个数组:

ngx_http_variable_value_t  variables[n];


其中n表示该数组可以容纳的元素个数,这是一个必填值,并且一旦定义完毕后variables这个变量是不允许被修改的,它的结构在内存中大致是这样的:

这个结构和内存空间在程序运行后就已经确定了,其中第一个小方块中的“*”号代表地址,但是由于c语言中数组的特性这个值是不允许被修改的。


另一种使用数组的方式就是我们上面的请求结构体中表示的那样,这种定义方式在程序启动后在内存的结构体是这样的:

可以看到,只是为这个变量分配了一个块内存空间,这个内存空间里面放的是地址(此时是空),并且这个内存空间是可以被修改的,对这样一个指针变量,我们可以把它指向一个变量值结构体(ngx_http_variable_value_t),也是指向多个,比如这样:


这就和上面定义数组的形式就一样了,并且variables这个值是可以被改变的,所以以上两种方法都可以表示一个数组。


3.2 使用数组容器


当请求创建完成后,在当前请求过程中所有变量值都会存在该数组容器中,其中也包括动态变量的取值,下面通过一个例子来看一下变量值是如何围绕该数组容器工作的:

location /var {

set $a “aaa”;

return 200 “$a+$a”;

}


上面这个配置中,内容的输出是由return指令负责的(实际上是有该指令翻译后的脚本负责的),当我们向nginx发送请求后,return指令最终需要做的就是通过变量“$a”的索引值获取变量值,对应的方法就是我们前面提到的ngx_http_get_indexed_variable()方法。针对我们当前的例子,当取第一个变量“$a”的值的时候,因为r->variables容器中还没有对应的值,所以该变量会通过绑定的get_handler()方法来获取变量值,然后把变量值放入到r->variables容器中,我们假设index是变量“$a”的索引值,那么该变量值就是r->variables[index]。当取第二个变量值的时候,因为该变量已经存在容器中了,并且用set指令设置的变量值是允许缓存的,所以这次的变量值直接从容器中获取就可以了。最后因为每个请求都会有自己所属的数组容器,所以不会相互干扰。







请到「今天看啥」查看全文