nginx命名location跳转的模块上下文继承

目录

  • [1. 缘起](#1. 缘起)
  • [2. 解决方案](#2. 解决方案)
    • [2.1 保留指定模块的上下文信息](#2.1 保留指定模块的上下文信息)
    • [2.2 获取指定模块的上下文信息](#2.2 获取指定模块的上下文信息)
    • [2.3 设置指定模块的上下文信息](#2.3 设置指定模块的上下文信息)
    • [2.4 设置模块上下文是否需要继承标记](#2.4 设置模块上下文是否需要继承标记)
    • [2.5 对openrety lua代码的支持](#2.5 对openrety lua代码的支持)

1. 缘起

nginx提供了非常棒的功能,命名location,如文章nginx的location匹配规则中描述,有时候我们可以通过lua脚本(在openresty中)或者自研nginx插件模块,根据相应的业务规则将某些请求转发到特定的命名location中执行相应的业务逻辑。

假设我们的location配置如下:

Bash 复制代码
	location / {
		content_by_lua_block { 
			local uri = ngx.var.uri 
			if string.match(uri, "%.mp4$") then 
				ngx.exec("@mp4") 
			elseif string.match(uri, "%.flv$") then
			    ngx.exec("@flv")
			else
				ngx.exit(ngx.HTTP_NOT_FOUND)
			end
		}
	}

	location @mp4 {
	    internal;
		mp4;           # 开启mp4流媒体功能
		root ./html;
	}

	location @flv {
	    internal;
		flv;           # 开启flv流媒体功能
		root ./html;
	}

那么nginx会在匹配到以.mp4为后缀的uri时候将请求转发到@mp4的location,当匹配到.flv为后缀的uri时候将请求转发到@flv的location,否则响应404。当然,大家可能认为好像没有必要那么复杂,直接用[[nginx的location匹配规则]]中说的那样直接用location匹配也可以达到以上目的。本案例只是一个简化的情况,如果是在一个提供多租户服务的CDN系统中,一个边缘cache(cache前端可以用nginx来提供)需要配置成千上万的域名,每个域名都会有不同的location规则,如果每个域名都配置一个server,那么会给nginx带来比较大的配置加载的负担,我们一般的实现是只有一个server,一个location匹配所有客户的域名和location,然后,通过lua程序将用户的请求根据动态配置信息转发到几个预先设置好的location中提供不同的服务,譬如MP4流媒体location,flv流媒体location,大文件下载location等等。

这种情况下,不一定每个客户的mp4文件都是以mp4为后缀的,flv文件是以flv为后缀的,而是需要根据客户的在线配置需求动态配置的,所以可以通过lua程序根据来源域名匹配到相应的规则然后将请求动态转发到对应的命名location中。

然而,在实践中,我们发现nginx的命名转发功能,会把http模块的上下文信息清空,导致在命名location中ngx_http_reqeust_t对象获取不到转发前的模块上下文,从而使转发前和转发后的上下文信息无法传递,带来一些困扰。nginx的原生代码如下:

c 复制代码
ngx_int_t
ngx_http_named_location(ngx_http_request_t *r, ngx_str_t *name)
{
    ngx_http_core_srv_conf_t    *cscf;
    ngx_http_core_loc_conf_t   **clcfp;
    ngx_http_core_main_conf_t   *cmcf;

    r->main->count++;
    r->uri_changes--;

    if (r->uri_changes == 0) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "rewrite or internal redirection cycle "
                      "while redirect to named location \"%V\"", name);

        ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
        return NGX_DONE;
    }

    if (r->uri.len == 0) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "empty URI in redirect to named location \"%V\"", name);

        ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
        return NGX_DONE;
    }

    cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);

    if (cscf->named_locations) {

        for (clcfp = cscf->named_locations; *clcfp; clcfp++) {

            ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                           "test location: \"%V\"", &(*clcfp)->name);

            if (name->len != (*clcfp)->name.len
                || ngx_strncmp(name->data, (*clcfp)->name.data, name->len) != 0)
            {
                continue;
            }

            ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                           "using location: %V \"%V?%V\"",
                           name, &r->uri, &r->args);

            r->internal = 1;
            r->content_handler = NULL;
            r->uri_changed = 0;
            r->loc_conf = (*clcfp)->loc_conf;

            /* clear the modules contexts */
            /* 清理本request的所有模块的上下文 */
            ngx_memzero(r->ctx, sizeof(void *) * ngx_http_max_module);

            ngx_http_update_location_config(r);

            cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);

            r->phase_handler = cmcf->phase_engine.location_rewrite_index;

            r->write_event_handler = ngx_http_core_run_phases;
            ngx_http_core_run_phases(r);

            return NGX_DONE;
        }
    }

    ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                  "could not find named location \"%V\"", name);

    ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);

    return NGX_DONE;
}

上述代码中,有一行代码:

c 复制代码
    ngx_memzero(r->ctx, sizeof(void *) * ngx_http_max_module);

它负责清理当前http request的所有模块的上下文信息,从而导致转发前设置的上下文信息,在转发后再去获取的时候就变成了NULL。

2. 解决方案

2.1 保留指定模块的上下文信息

将函数ngx_http_named_location中的这行代码

c 复制代码
ngx_memzero(r->ctx, sizeof(void *) * ngx_http_max_module);

改成:

c 复制代码
/* clear only the modules contexts which are not derivable */

for (i = 0; i < ngx_http_max_module; i++){

	if ( ((uintptr_t)r->ctx[i] & (uintptr_t)0x01u) == 0 ){

		r->ctx[i] = NULL;

}

意思是如果http request的某个ctx元素中保存的指针地址如果最低位是0,才清理上下文,否则保留上下文信息。因为在系统中指针地址至少是按4byte对齐的,所以最低的两位一定是0,我们这里就是用最低位的0来表示是否需要在命名location跳转的时候保留对应模块的保留上下文信息。

2.2 获取指定模块的上下文信息

如上文描述,由于上下文指针地址中的值可能复用的一个标记位,实际值不是对应的真正的上下文内存地址,所以需要对原先获取模块上下文信息的宏定义进行改造,原来为:

c 复制代码
#define ngx_http_get_module_ctx(r, module)  (r)->ctx[module.ctx_index]

改成:

c 复制代码
#define ngx_http_get_module_ctx(r, module) \
     (void*)((uintptr_t)(r)->ctx[module.ctx_index] & ~(uintptr_t)1u)

2.3 设置指定模块的上下文信息

原生的设定上下文代码如下:

c 复制代码
#define ngx_http_set_ctx(r, c, module)      r->ctx[module.ctx_index] = c;

这块沿用原生的代码,不用更改。

2.4 设置模块上下文是否需要继承标记

代码如下:

c 复制代码
#define ngx_http_set_ctx_derivable(r,module) \
     r->ctx[module.ctx_index] = (void*)  \
         ((uintptr_t)ngx_http_get_module_ctx(r,module) | (uintptr_t)1u)

#define ngx_http_unset_ctx_derivable(r,module) \
    r->ctx[module.ctx_index] = (void*) \
          ((uintptr_t)ngx_http_get_module_ctx(r,module) & ~(uintptr_t)1u)

程序逻辑需要设定某个模块可以被named location跳转继承,那么就调用

c 复制代码
ngx_http_set_ctx_derivable(r, module_name)

&emps;  反之,则调用:

c 复制代码
ngx_http_unset_ctx_derivable(r, module_name)

2.5 对openrety lua代码的支持

以上代码修改完成后,已经可以完美支持nginx的c插件模块的上下文的继承设置了,但是对于openresty lua代码,我们还需要对openresty开放相关的接口,或者如果希望强制openresty lua模块每次在named location跳转的时候都需要继承上下文信息,那么可以修改ngx_http_lua_handle_exec代码,如下:

c 复制代码
static ngx_int_t
ngx_http_lua_handle_exec(lua_State *L, ngx_http_request_t *r,
    ngx_http_lua_ctx_t *ctx)
{
    ngx_int_t               rc;

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "lua thread initiated internal redirect to %V",
                   &ctx->exec_uri);

    ngx_http_lua_cleanup_pending_operation(ctx->cur_co_ctx);

    ngx_http_lua_probe_coroutine_done(r, ctx->cur_co_ctx->co, 1);

    ctx->cur_co_ctx->co_status = NGX_HTTP_LUA_CO_DEAD;

    if (r->filter_finalize) {
        ngx_http_set_ctx(r, ctx, ngx_http_lua_module);
    }

    ngx_http_lua_request_cleanup(ctx, 1 /* forcible */);

    if (ctx->exec_uri.data[0] == '@') {
        if (ctx->exec_args.len > 0) {
            ngx_log_error(NGX_LOG_WARN, r->connection->log, 0,
                          "query strings %V ignored when exec'ing "
                          "named location %V",
                          &ctx->exec_args, &ctx->exec_uri);
        }

        r->write_event_handler = ngx_http_request_empty_handler;

#if 1
        if (r->read_event_handler == ngx_http_lua_rd_check_broken_connection) {
            /* resume the read event handler */

            r->read_event_handler = ngx_http_block_reading;
        }
#endif

		/* 设置lua模块的ctx在named_location跳转的时候保持原始的ctx */
		ngx_http_set_ctx_derivable(r, ngx_http_lua_module);

        rc = ngx_http_named_location(r, &ctx->exec_uri);
      
        if (rc == NGX_ERROR || rc >= NGX_HTTP_SPECIAL_RESPONSE) {
            return rc;
        }
......

    return NGX_DONE;
}
相关推荐
Ztiddler5 小时前
【Linux Shell命令-不定期更新】
linux·运维·服务器·ssh
小小不董5 小时前
Oracle OCP认证考试考点详解082系列16
linux·运维·服务器·数据库·oracle·dba
IPdodo全球网络5 小时前
如何利用静态住宅IP优化Facebook商城的网络稳定性与运营效率
运维·服务器
运维&陈同学5 小时前
【模块一】kubernetes容器编排进阶实战之k8s基础概念
运维·docker·云原生·容器·kubernetes·云计算
40kuai6 小时前
Ingress nginx 公开TCP服务
nginx·ingress
m0_519523106 小时前
Linux——简单认识vim、gcc以及make/Makefile
linux·运维·vim
mit6.8246 小时前
[Docker#4] 镜像仓库 | 部分常用命令
linux·运维·docker·容器·架构
zyp2468107 小时前
Linux之DNS服务器
linux·运维·服务器
wdxylb7 小时前
将C++搭建的简单HTTP服务器升级为 HTTPS 服务器
运维·服务器·https
Denzel张7 小时前
ubuntu_dpkg_dep_list
linux·运维·ubuntu