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;
}
相关推荐
weifont2 小时前
自动化抓取和上架商品:能不动手,就少动手
运维·前端·自动化
拂拉氏2 小时前
【知识讲解】 Linux之动静态库的了解
linux·运维·服务器·动静态库
Mikko73 小时前
Linux 重启后日志没了?进程被 OOM 杀了去哪看?logrotate 之后日志还往旧文件写?三台真机实测日志与性能排查
linux·运维·centos
光依旧3 小时前
发版就掉请求?Spring Boot优雅停机的四步
运维·spring boot·nginx·高可用·优雅停机·mcp server
yichengerp5 小时前
电子行业常用erp系统有哪些?推荐适合中小电子厂的易呈erp软件,操作简单,易落地
大数据·运维·人工智能·云计算·制造
天国梦5 小时前
国密门禁品牌怎么选?2026年最新3个避坑要点
运维·服务器·数据库·python
信可维6 小时前
自动化操作多平台账号:登录态、双会话与 User Data 的 3 个坑
运维·自动化
龙仔7256 小时前
RustDesk 自建服务器 Key 强校验配置笔记
运维·服务器·笔记
大侠归来6 小时前
Docker 部署 Dolibarr:从零开始的完整指南
运维·docker·容器
KING-WU5126 小时前
Linux GDB 调试实战指南:从编译到断点调试
linux·运维·服务器