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;
}
相关推荐
Leinwin6 小时前
OpenClaw 多 Agent 协作框架的并发限制与企业化规避方案痛点直击
java·运维·数据库
2401_865382506 小时前
信息化项目运维与运营的区别
运维·运营·信息化项目·政务信息化
漠北的哈士奇6 小时前
VMware Workstation导入ova文件时出现闪退但是没有报错信息
运维·vmware·虚拟机·闪退·ova
如意.7597 小时前
【Linux开发工具实战】Git、GDB与CGDB从入门到精通
linux·运维·git
运维小欣7 小时前
智能体选型实战指南
运维·人工智能
yy55277 小时前
Nginx 性能优化与监控
运维·nginx·性能优化
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ8 小时前
Linux 查询某进程文件所在路径 命令
linux·运维·服务器
05大叔10 小时前
网络基础知识 域名,JSON格式,AI基础
运维·服务器·网络
安当加密10 小时前
无需改 PAM!轻量级 RADIUS + ASP身份认证系统 实现 Linux 登录双因子认证
linux·运维·服务器
dashizhi201510 小时前
服务器共享禁止保存到本地磁盘、共享文件禁止另存为本地磁盘、移动硬盘等
运维·网络·stm32·安全·电脑