合并http配置项

  • 微信公众号:郑尔多斯
  • 关注「郑尔多斯」公众号 ,回复「领取资源」,获取IT资源500G干货。
    升职加薪、当上总经理、出任CEO、迎娶白富美、走上人生巅峰!想想还有点小激动
  • 关注可了解更多的Nginx知识。任何问题或建议,请公众号留言;
    关注公众号,有趣有内涵的文章第一时间送达!

前言

前面的文章已经从源码级别将配置的解析过程说的很清楚了,本文我们学习一下nginxhttp配置的merge过程。我们知道nginxhttp配置结构体是一个非常复杂的东东,同样的指令可能出现在不同的位置,比如root指令,既可以出现在httpmain级别,也可以出现在serverlocationif 等上下文中,那么当一个请求到来的时候,nginx会使用哪一个上下文中的配置结果呢?这就牵涉到了http配置的merge过程。

###背景
首先我们了解下merge 的背景:

  • 所谓merge 操作,就是合并内外层的配置。大体原则是:如果内层没有配置,那么以外层为准,如果都没有配置,那么就用默认值;
  • NGX_CORE_MODULE 模块的ctx(ngx_core_module_t)是没有 merge 操作的,所以像 http 块这一层的配置是不需要和上一层去merge 的,想想也明白为什么,http哪来的上一层呢?
  • NGX_HTTP_MODULE模块的 ctx(ngx_http_module_t)是有 merge 操作的,但是仅仅有 merge_srv_confmerge_loc_conf,同理对 main 层不需要 merge
    -merge 操作发生的时机是在 ngx_http_block函数中(即 http 块解析函数),在递归调用 ngx_conf_parse 之后。这是为了让http 块之内所有的指令都解析结束,然后再去做 merge 操作;
  • 不同层级块的逻辑关系,基本上都是放在 ngx_http_core_module 这个模块的不同级别的 conf 中,在 merge中会频繁用到。

源码分析

http配置的merge过程是在ngx_http_block()函数中实现,如下:

// 哈哈,扯淡的东西  cmcf = core main conf  我猜的。 
// cscf = core server conf, clcf = core location conf
    cmcf = ctx->main_conf[ngx_http_core_module.ctx_index];

    for (m = 0; ngx_modules[m]; m++) {
// 只对http module才存在merge操作
        if (ngx_modules[m]->type != NGX_HTTP_MODULE) {
            continue;
        }
        module = ngx_modules[m]->ctx;
// mi === module index 模块索引的意思,表示当前module在该类型中的索引
        mi = ngx_modules[m]->ctx_index;
 
        /* init http{} main_conf's */
        if (module->init_main_conf) {
            rv = module->init_main_conf(cf, ctx->main_conf[mi]);
            if (rv != NGX_CONF_OK) {
                goto failed;
            }
        }
 
        rv = ngx_http_merge_servers(cf, cmcf, module, mi);
        if (rv != NGX_CONF_OK) {
            goto failed;
        }
    }

当执行这部分代码的时候,nginx已经解析完了http的所有配置项,所以才能够实现merge过程。
从代码中可以看出来,会先调用每个HTTP模块的 init_main_conf 函数,下面的是 ngx_http_core_module 模块的钩子函数,如下:

static char *
ngx_http_core_init_main_conf(ngx_conf_t *cf, void *conf)
{
    ngx_http_core_main_conf_t *cmcf = conf;
 
    if (cmcf->server_names_hash_max_size == NGX_CONF_UNSET_UINT) {
        cmcf->server_names_hash_max_size = 512;
    }
 
    if (cmcf->server_names_hash_bucket_size == NGX_CONF_UNSET_UINT) {
        cmcf->server_names_hash_bucket_size = ngx_cacheline_size;
    }
 
    cmcf->server_names_hash_bucket_size =
            ngx_align(cmcf->server_names_hash_bucket_size, ngx_cacheline_size);
 
    if (cmcf->variables_hash_max_size == NGX_CONF_UNSET_UINT) {
        cmcf->variables_hash_max_size = 512;
    }
 
    if (cmcf->variables_hash_bucket_size == NGX_CONF_UNSET_UINT) {
        cmcf->variables_hash_bucket_size = 64;
    }
 
    cmcf->variables_hash_bucket_size =
               ngx_align(cmcf->variables_hash_bucket_size, ngx_cacheline_size);
 
    if (cmcf->ncaptures) {
        cmcf->ncaptures = (cmcf->ncaptures + 1) * 3;
    }
    return NGX_CONF_OK;
}

上面的函数没有什么复杂的地方,就是对 ngx_http_core_,main_conf_t 结构体的一些字段进行初始化。

下面就是merge的过程了,我们对代码精简一下,如下:

    cmcf = ctx->main_conf[ngx_http_core_module.ctx_index];

    for (m = 0; ngx_modules[m]; m++) {
        if (ngx_modules[m]->type != NGX_HTTP_MODULE) {
            continue;
        }
        module = ngx_modules[m]->ctx;
// mi === module index 模块索引的意思,表示当前module在该类型中的索引
        mi = ngx_modules[m]->ctx_index;
 
        rv = ngx_http_merge_servers(cf, cmcf, module, mi);
        if (rv != NGX_CONF_OK) {
            goto failed;
        }
    }

这段代码的整体逻辑还是比较简单的,遍历所有的HTTP模块,然后对每个module都调用 ngx_http_merge_servers()函数,所以真正的merge逻辑是在这个函数中的。
合并http配置项

/*
① cf 是代入的参数,但是我们真正关心的还是 cf->ctx,这个时候它其实就是 http 级别的三元组(在ngx_http_block函数中赋值)
② cmcf 这个是 http 块的 ngx_http_core_module 的 main_conf 结构,该结构是全局唯一的,因为无论server级别的ctx还是location级别的ctx,他们的main_conf都指向了http全局的main_conf
③ module 是个循环获取的,代表当前遍历到的HTTP module的ctx
④ mi 就是当前模块在 NGX_HTTP_MODULE 模块中的 index

这个函数就实现了当前被遍历到的http module的server以及location的merge操作
*/
static char *
ngx_http_merge_servers(ngx_conf_t *cf, ngx_http_core_main_conf_t *cmcf,
    ngx_http_module_t *module, ngx_uint_t ctx_index)
{
    char                        *rv;
    ngx_uint_t                   s;
    ngx_http_conf_ctx_t         *ctx, saved;
    ngx_http_core_loc_conf_t    *clcf;
    ngx_http_core_srv_conf_t   **cscfp;
 
// cscfp: core server conf pointer 指向保存所有server数组的指针
    cscfp = cmcf->servers.elts;
    ctx = (ngx_http_conf_ctx_t *) cf->ctx;
/* 
这里做了一个保存的操作,因为在下面的代码中要改变 ctx 中的值,
并且同时使用原始的 ctx。在最后又通过saved变量复原了ctx的值
*/
    saved = *ctx;
    rv = NGX_CONF_OK;
/*
这是第二层循环。对于每一个HTTP module,都会遍历所有的server模块。
为什么要再循环一次呢?我是这么理解的:
http {
            instruction_A   value_main_A;
            server {
                # server_1
                instruction_A  value_srv_1;
            }

            server {
                   #server_2
                  instruction_A  value_srv_2
            }
}
上述的instruction既出现在了http  main 级别,又出现在了server级别。并且http配置中有多个server,所以要遍历所有的server,将每个server的配置都和main的配置进行合并。
*/
    for (s = 0; s < cmcf->servers.nelts; s++) {
        ctx->srv_conf = cscfp[s]->ctx->srv_conf;
        if (module->merge_srv_conf) {
            rv = module->merge_srv_conf(cf, saved.srv_conf[ctx_index],
                                        cscfp[s]->ctx->srv_conf[ctx_index]);
            if (rv != NGX_CONF_OK) {
                goto failed;
            }
        }

        if (module->merge_loc_conf) {
            /* merge the server{}'s loc_conf */
            ctx->loc_conf = cscfp[s]->ctx->loc_conf;
            rv = module->merge_loc_conf(cf, saved.loc_conf[ctx_index],
                                        cscfp[s]->ctx->loc_conf[ctx_index]);
            if (rv != NGX_CONF_OK) {
                goto failed;
            }
            /* merge the locations{}' loc_conf's */
            clcf = cscfp[s]->ctx->loc_conf[ngx_http_core_module.ctx_index];
            rv = ngx_http_merge_locations(cf, clcf->locations,
                                          cscfp[s]->ctx->loc_conf,
                                          module, ctx_index);
            if (rv != NGX_CONF_OK) {
                goto failed;
            }
        }
    }
failed:
    *ctx = saved;
    return rv;
}

合并http配置项

这个函数可以分为三部分来分析,第一部分,将mainserver级别的配置合并起来。第二部分将serverlocation级别的配置合并起来。第三部分就是location和内嵌location的合并。
1) mainserver级别级别的merge
这里的ctx_indexngx_http_core_moduleHTTP模块中的index

 for (s = 0; s < cmcf->servers.nelts; s++) {
        /* merge the server{}s' srv_conf's */
/* 改变 cf->ctx 的 srv_conf,换成当前被遍历到的 server 块对应的 srv_conf。*/
        ctx->srv_conf = cscfp[s]->ctx->srv_conf;
        if (module->merge_srv_conf) {
/* 这里就很明朗了,saved 就是http main级别 的cf->ctx 的内容,那么它就是 http main级别块的ctx三元组了,第二个参数也就是当前被遍历到的HTTP module在 http main级别的ctx->srv_conf数组中对应的 srv_conf,也即上图中的http_srv_A结构体,也即parent

cscfp[s] 代表对应的当前遍历的server,而它的 ctx 也就是在解析那个 server 块的时候创建的三元组。所以第三个参数就是上图中的server_srv_A,也即child

http{
      // main级别
       root /data0/w3;
       server {
          // server_A
       }

       server {
            // server_B
      }
}
我们以上面的配置为例来说明:这一部分代码会遍历所有的server块,也就是会逐个遍历server_A和server_B。
为什么要遍历所有的server块呢?因为要把main级别的配置同步到所有的server块中。
通过这两个参数就可以将main级别的配置项和server级别的配置项合并了.
综上所述:main和server的merge其实就是http main级别ctx->srv_conf下的结构体和server级别的ctx->srv_conf下的结构体的合并。
和loc_conf下的结构体没有任何关系。
*/
            rv = module->merge_srv_conf(cf, saved.srv_conf[ctx_index],
                                        cscfp[s]->ctx->srv_conf[ctx_index]);
            if (rv != NGX_CONF_OK) {
                goto failed;
            }
        }
}
  1. server级别和location级别配置项的合并
    server级别和location级别配置项合并的原理和上面的原理基本相同。都是逐个遍历的。
// 这是第二层循环。对于每一个HTTP module,都会遍历所有的server模块
    for (s = 0; s < cmcf->servers.nelts; s++) {
        ctx->srv_conf = cscfp[s]->ctx->srv_conf;
        if (module->merge_loc_conf) {
            /* merge the server{}'s loc_conf */
            ctx->loc_conf = cscfp[s]->ctx->loc_conf;
/*
以下图为例
        http{
               // main级别
                root /data0/w3;
                server {
                     // server_A
                     location balabala{
                           // location_C
                     }

                   location cilili {
                           // location_D
                    }
               }

               server {
                  // server_B
              }
           }

merge_loc_conf()的第二个参数是当前遍历到的HTTP module在 main 级别的 ctx->loc_conf数组中的配置,即上图中的http_loc_A, 是parent级别的location配置。
第三个参数就是 server_A 级别的 ctx->loc_conf[ngx_http_core_module.ctx_index],即上图中的server_loc_A, 是child级别的location配置。
*/
            rv = module->merge_loc_conf(cf, saved.loc_conf[ctx_index],
                                        cscfp[s]->ctx->loc_conf[ctx_index]);
            if (rv != NGX_CONF_OK) {
                goto failed;
            }
        }
    }

3) location和内嵌location的合并

// 这是第二层循环。对于每一个HTTP module,都会遍历所有的server模块
    for (s = 0; s < cmcf->servers.nelts; s++) {
        ctx->srv_conf = cscfp[s]->ctx->srv_conf;
        if (module->merge_loc_conf) {
            /* merge the server{}'s loc_conf */
            ctx->loc_conf = cscfp[s]->ctx->loc_conf;

            /* merge the locations{}' loc_conf's */
// clcf 是 server_A的 ctx->loc_conf[ngx_http_core_module.ctx_index]
            clcf = cscfp[s]->ctx->loc_conf[ngx_http_core_module.ctx_index];
// 第二个参数: clcf->locations 在server_A这个server块下面的所有location组成的queue.
// 第三个参数: server_A级别的 ctx->loc_conf 数组。
// 第四个参数:  因为此时是合并ngx_http_core_module的配置项,所以module参数指的是ngx_http_core_module的module_ctx模块上下文。
// 第五个参数: 因为此时是合并ngx_http_core_module的配置项,所以ctx_index是ngx_http_core_module在所有HTTP module中的ctx_index
            rv = ngx_http_merge_locations(cf, clcf->locations,
                                          cscfp[s]->ctx->loc_conf,
                                          module, ctx_index);
            if (rv != NGX_CONF_OK) {
                goto failed;
            }
        }
    }

这里牵涉到了另一个函数ngx_http_merge_locations(),如下:

// 第二个参数: locations : clcf->locations 在server_A这个server块下面的所有location组成的queue.
// 第三个参数: loc_conf : server_A级别的 ctx->loc_conf 数组。
// 第四个参数:  module : 因为此时是合并ngx_http_core_module的配置项,所以module参数指的是ngx_http_core_module的module_ctx模块上下文。
// 第五个参数: ctx_index : 因为此时是合并ngx_http_core_module的配置项,所以ctx_index是ngx_http_core_module在所有HTTP module中的ctx_index
static char *
ngx_http_merge_locations(ngx_conf_t *cf, ngx_queue_t *locations,
    void **loc_conf, ngx_http_module_t *module, ngx_uint_t ctx_index)
{
    char                       *rv;
    ngx_queue_t                *q;
    ngx_http_conf_ctx_t        *ctx, saved;
    ngx_http_core_loc_conf_t   *clcf;
    ngx_http_location_queue_t  *lq;
 // 如果没有内嵌的location,则该函数直接返回
    if (locations == NULL) {
        return NGX_CONF_OK;
    }
 /* 这里代码看似和 ngx_http_merge_servers 类似,但是差别在于,这个时候 cf->ctx 内
 * 相对应的 srv_conf 和 loc_conf 内容已经被改变为外层的对应 conf。在
 * ngx_http_merge_servers 中的相关代码我们已经分析过了,在这个函数中同样有类似的代码。
*/
    ctx = (ngx_http_conf_ctx_t *) cf->ctx;
    saved = *ctx;
 
    for (q = ngx_queue_head(locations);
         q != ngx_queue_sentinel(locations);
         q = ngx_queue_next(q))
    {
/* 遍历当前server下面的所有 location,逐个 merge。*/
        lq = (ngx_http_location_queue_t *) q;
        clcf = lq->exact ? lq->exact : lq->inclusive;
/* 改变 cf->ctx 的 loc_conf,换成当前 server 块对应的 loc_conf。*/
        ctx->loc_conf = clcf->loc_conf;
/*
http{
     server {
          // server_A
           location B {
               // location_B
            }

          location C{
                 // location_C
           }

         location D{
              // location_D
          }
     }
}
   下面的 merge_loc_conf() 的参数上文已经分析过了。这不过这里是location和内嵌location的合并,我们再分析一下:
第二个参数: loc_conf[ctx_index] = server_A级别的 ctx->loc_conf[ngx_http_core_module.ctx_index],也就是parent级别的数据。
第三个参数:clcf->loc_conf[ctx_index] = location_B级别的 ctx->loc_conf[ngx_http_core_module.ctx_index],也就是child级别的数据。
*/
        rv = module->merge_loc_conf(cf, loc_conf[ctx_index],
                                    clcf->loc_conf[ctx_index]);
/* server 的时候,它是对应 server 块的 loc_conf 数组。在下面的代码中,递归调用
 * ngx_http_merge_locations,代入的 loc_conf 就是这一层块的 loc_conf 数组,所以这里的
 * loc_conf 其实就代表外层块的 loc_conf 数组。而 clcf 是很明显的,是由 locations 队列
 * 遍历产生的,也就是代表当前的 location 块。所以这里,第二个参数是 parent,第三个参
 * 数是 child。
 */
        if (rv != NGX_CONF_OK) {
            return rv;
        }
 // 嵌套的location
/* 这里递归调用了 ngx_http_merge_locations,嵌套 location 的 merge 操作也可以成功解决
 * 了,唯一值得注意的就是那些代入的参数,因为进入一层,所以对应的 locations 和
 * loc_conf 也更进了一层。
*/
        rv = ngx_http_merge_locations(cf, clcf->locations, clcf->loc_conf,
                                      module, ctx_index);
        if (rv != NGX_CONF_OK) {
            return rv;
        } 
    }
    *ctx = saved;
    return NGX_CONF_OK;
}

四、总结
整个merge的过程可以总结如下:
第一层循环:遍历所有的 NGX_HTTP_MODULE 模块,对所有module进行调用ngx_http_merge_servers()函数,我们以 ngx_http_core_module 处理
第二层循环:该层循环在 ngx_http_merge_servers()函数内部,遍历所有的 server 块,逐个进行处理

  1. 调用 ngx_http_core_modulecreate_srv_conf() 函数对 main 级别的 srv_conf[ctx_index]结构体和各个server级别 srv_conf[ctx_index] 配置结构体合并。
    为什么这样做呢?我们以client_header_timeout指令为例:

Defines a timeout for reading client request header. If a client does not transmit the entire header within this time, the request is terminated with the 408 (Request Time-out) error.

这个指令的作用:该指令决定了nginx接收request header的最长时间。如果服务器在指定的时间内没有接收到完整的request header,那么这个HTTP请求就会返回408错误(该错误表示Request Time-out请求超时)。

    { 
      ngx_string("client_header_timeout"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_msec_slot,
      NGX_HTTP_SRV_CONF_OFFSET,
      offsetof(ngx_http_core_srv_conf_t, client_header_timeout),
      NULL 
    }

client_header_timeout指令,可以放到mainserver上下文中。从源码中可以看出来,client_header_timeout指令的配置数据是存储到所在级别的 ctx->srv_conf[ngx_http_core_module.ctx_index]中的(因为client_header_timeout的配置指令中offsetNGX_HTTP_SRV_CONF_OFFSET)。

   http {
       client_header_timeout 10s;
       server server_A {
        }

       server server_B {
       client_header_timeout 5s;
     }
}

main级别配置了client_header_timeout指令, 但是server_A 并没有配置 client_header_timeout 指令,所以我们要把 main 级别的配置合并到 server 级别。这样 server_A 就可以有自己的 client_header_timeout 配置了。
因为 client_header_timeout 可以出现在任何的 server 模块中,所以要遍历所有的 server,将出现在 main 中的配置项合并到各个server中。
其实将mainserver合并的过程吧:只是针对client_header_timeout这种指令的,因为他们只能出现在main, server级别,并且保存在对应层级的 ctx_srv_conf[ctx_index]中。上面的合并函数传递的参数都是各个层级的 srv_conf[ctx_index].

五、参考
https://blog.csdn.net/weiwangchao_/article/details/46379999


喜欢本文的朋友们,欢迎长按下图关注订阅号郑尔多斯,更多精彩内容第一时间送达 ![郑尔多斯](https://mmbiz.qpic.cn/mmbiz_jpg/rLxDDpWK43nImp1Fs8OzHkyYiaUnvXvGibjiazibNIYPsHOibKib0B1ARQicuZ2X01jdV4YuYUGfyFyeKwxp38MEPsGpg/0?wx_fmt=jpeg)
上一篇:LayUI实现 文件的多个上传以及回显


下一篇:程序员的浪漫-桃心表白