ngx_http_slice_module分析

本文详细介绍了Nginx中的slice模块工作原理及其实现细节,包括如何通过配置slice来实现大文件的高效传输,确保文件完整返回,以及模块内部的变量处理流程等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

slice的作用:

CDN系统中,向父层回源时,如果文件过大,使用slice可以让用户快速得到响应。


问题:

对于一个用户的请求,使用slice对文件进行分片,如何保证可以完整的把整个文件返回给用户?



通常配置:

location / {
    slice             1m;
    proxy_cache       cache;
    proxy_cache_key   $uri$is_args$args$slice_range;
    proxy_set_header  Range $slice_range;
    proxy_cache_valid 200 206 1h;
    proxy_pass        http://localhost:8000;
}


该模块流程以及实现

slice是一个filter,按照nginx的phase划分,是处在content phase。

slice命令可以设置每个分片的大小,会设置slice命令所在的当前位置的loc_conf中size


在回源时,会把后端的请求添加一个Range头,值为$slice_range
根据nginx执行顺序,可以知道需要回源时,slice_range的get函数会首先被调用。


static ngx_int_t
ngx_http_slice_add_variables(ngx_conf_t *cf)
{
    ngx_http_variable_t  *var;
    var = ngx_http_add_variable(cf, &ngx_http_slice_range_name, 0);
    if (var == NULL) {
        return NGX_ERROR;
    }
    var->get_handler = ngx_http_slice_range_variable;                 //slice的处理函数
    return NGX_OK;
}


static ngx_int_t
ngx_http_slice_range_variable(ngx_http_request_t *r,
    ngx_http_variable_value_t *v, uintptr_t data)
{
    u_char                     *p;
    ngx_http_slice_ctx_t       *ctx;
    ngx_http_slice_loc_conf_t  *slcf;


    ctx = ngx_http_get_module_ctx(r, ngx_http_slice_filter_module);

//如果没有绑定上下文,则进行分配
    if (ctx == NULL) {
        if (r != r->main || r->headers_out.status) {
            v->not_found = 1;
            return NGX_OK;
        }

//获取slice命令分配的分片大小
        slcf = ngx_http_get_module_loc_conf(r, ngx_http_slice_filter_module);


        if (slcf->size == 0) {
            v->not_found = 1;
            return NGX_OK;
        }


        ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_slice_ctx_t));
        if (ctx == NULL) {
            return NGX_ERROR;
        }


        ngx_http_set_ctx(r, ctx, ngx_http_slice_filter_module);


        p = ngx_pnalloc(r->pool, sizeof("bytes=-") - 1 + 2 * NGX_OFF_T_LEN);
        if (p == NULL) {
            return NGX_ERROR;
        }

        //获取range_start大小,考虑到用户本身携带了range
        ctx->start = slcf->size * (ngx_http_slice_get_start(r) / slcf->size);


        ctx->range.data = p;
        ctx->range.len = ngx_sprintf(p, "bytes=%O-%O", ctx->start,
                                     ctx->start + (off_t) slcf->size - 1)
                         - p;
    }

  封装slice_range变量
    v->data = ctx->range.data;
    v->valid = 1;
    v->not_found = 0;
    v->no_cacheable = 1;
    v->len = ctx->range.len;


    return NGX_OK;
}



static ngx_int_t
ngx_http_slice_init(ngx_conf_t *cf)
{
    ngx_http_next_header_filter = ngx_http_top_header_filter;

//header_filter的处理函数为:ngx_http_slice_header_filter
    ngx_http_top_header_filter = ngx_http_slice_header_filter;


    ngx_http_next_body_filter = ngx_http_top_body_filter;

    //body_filter的处理函数为:ngx_http_slice_body_filter
    ngx_http_top_body_filter = ngx_http_slice_body_filter;


    return NGX_OK;
}



ngx_http_slice_header_filter处理流程:

static ngx_int_t  ngx_http_slice_header_filter(ngx_http_request_t *r)
{
    off_t                            end;
    ngx_int_t                        rc;
    ngx_table_elt_t                 *h;
    ngx_http_slice_ctx_t            *ctx;
    ngx_http_slice_loc_conf_t       *slcf;
    ngx_http_slice_content_range_t   cr;

//没有配置slice的,直接走到下一个header_filter
    ctx = ngx_http_get_module_ctx(r, ngx_http_slice_filter_module);
    if (ctx == NULL) {
        return ngx_http_next_header_filter(r);
    }

//如果配置了slice,但是返回又不是206
    if (r->headers_out.status != NGX_HTTP_PARTIAL_CONTENT) {
        if (r == r->main) {//对于主请求则放过,后面如何处理尚不清楚
            ngx_http_set_ctx(r, NULL, ngx_http_slice_filter_module);
            return ngx_http_next_header_filter(r);
        }


        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "unexpected status code %ui in slice response",
                      r->headers_out.status);
        return NGX_ERROR;
    }

    h = r->headers_out.etag;

检查etag

当没有的时候把头携带的etag赋值给ctx->etag

之后的怎么检查?
    if (ctx->etag.len) {
        if (h == NULL
            || h->value.len != ctx->etag.len
            || ngx_strncmp(h->value.data, ctx->etag.data, ctx->etag.len)
               != 0)
        {
            ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                          "etag mismatch in slice response");
            return NGX_ERROR;
        }
    }


    if (h) {
        ctx->etag = h->value;
    }


把content_range里面的数据解析出来
    if (ngx_http_slice_parse_content_range(r, &cr) != NGX_OK) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "invalid range in slice response");
        return NGX_ERROR;
    }

complete_length 代表的是整个文件的长度
    if (cr.complete_length == -1) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "no complete length in slice response");
        return NGX_ERROR;
    }

ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "http slice response range: %O-%O/%O",
                   cr.start, cr.end, cr.complete_length);


    slcf = ngx_http_get_module_loc_conf(r, ngx_http_slice_filter_module);


    end = ngx_min(cr.start + (off_t) slcf->size, cr.complete_length);


    if (cr.start != ctx->start || cr.end != end) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "unexpected range in slice response: %O-%O",
                      cr.start, cr.end);
        return NGX_ERROR;
    }

更新start,赋值为当前header的content_range

   ctx->start = end;

//把状态码设置为200
    r->headers_out.status = NGX_HTTP_OK;
    r->headers_out.status_line.len = 0;

content_length_n 设置为文件的长度
    r->headers_out.content_length_n = cr.complete_length;
    r->headers_out.content_offset = cr.start;
    r->headers_out.content_range->hash = 0;
    r->headers_out.content_range = NULL;

//修改range相关
    r->allow_ranges = 1;
    r->subrequest_ranges = 1;
    r->single_range = 1;


    rc = ngx_http_next_header_filter(r);

//如果不是子请求,则直接返回

   if (r != r->main) {
        return rc;
    }


//后面应该还有对r->headers_out.status的赋值?在哪里?

//主要是设置ctx->start ctx->end
    if (r->headers_out.status == NGX_HTTP_PARTIAL_CONTENT) {
        if (ctx->start + (off_t) slcf->size <= r->headers_out.content_offset) {
            ctx->start = slcf->size
                         * (r->headers_out.content_offset / slcf->size);
        }


        ctx->end = r->headers_out.content_offset
                   + r->headers_out.content_length_n;


    } else {
        ctx->end = cr.complete_length;
    }


    return rc;

}



static ngx_int_t
ngx_http_slice_parse_content_range(ngx_http_request_t *r, ngx_http_slice_content_range_t *cr)

{
    off_t             start, end, complete_length, cutoff, cutlim;
    u_char           *p;
    ngx_table_elt_t  *h;


    h = r->headers_out.content_range;

错误检查
    if (h == NULL
        || h->value.len < 7
        || ngx_strncmp(h->value.data, "bytes ", 6) != 0)
    {
        return NGX_ERROR;
    }

//接下来就是分析30-200/500,这种格式

分别把30 200 500取出来
    p = h->value.data + 6;


    cutoff = NGX_MAX_OFF_T_VALUE / 10;
    cutlim = NGX_MAX_OFF_T_VALUE % 10;


    start = 0;
    end = 0;
    complete_length = 0;

    while (*p == ' ') { p++; }


    if (*p < '0' || *p > '9') {
        return NGX_ERROR;
    }


    while (*p >= '0' && *p <= '9') {
        if (start >= cutoff && (start > cutoff || *p - '0' > cutlim)) {
            return NGX_ERROR;
        }


        start = start * 10 + *p++ - '0';
    }


    while (*p == ' ') { p++; }


    if (*p++ != '-') {
        return NGX_ERROR;
    }


    while (*p == ' ') { p++; }


    if (*p < '0' || *p > '9') {
        return NGX_ERROR;
    }

   while (*p >= '0' && *p <= '9') {
        if (end >= cutoff && (end > cutoff || *p - '0' > cutlim)) {
            return NGX_ERROR;
        }


        end = end * 10 + *p++ - '0';
    }


    end++;


    while (*p == ' ') { p++; }


    if (*p++ != '/') {
        return NGX_ERROR;
    }


    while (*p == ' ') { p++; }


    if (*p != '*') {
        if (*p < '0' || *p > '9') {
            return NGX_ERROR;
        }


        while (*p >= '0' && *p <= '9') {
            if (complete_length >= cutoff
                && (complete_length > cutoff || *p - '0' > cutlim))
            {
                return NGX_ERROR;
            }


            complete_length = complete_length * 10 + *p++ - '0';
        }


    } else {
        complete_length = -1;
        p++;
    }


    while (*p == ' ') { p++; }


    if (*p != '\0') {
        return NGX_ERROR;
    }


    cr->start = start;
    cr->end = end;
    cr->complete_length = complete_length;


    return NGX_OK;

}


static ngx_int_t
ngx_http_slice_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
    ngx_int_t                   rc;
    ngx_chain_t                *cl;
    ngx_http_request_t         *sr;
    ngx_http_slice_ctx_t       *ctx;
    ngx_http_slice_loc_conf_t  *slcf;


    ctx = ngx_http_get_module_ctx(r, ngx_http_slice_filter_module);

子请求直接走下一个filter
    if (ctx == NULL || r != r->main) {
        return ngx_http_next_body_filter(r, in);
    }


如果是下一个buf,则做标记
    for (cl = in; cl; cl = cl->next) {
        if (cl->buf->last_buf) {
            cl->buf->last_buf = 0;
            cl->buf->last_in_chain = 1;
            cl->buf->sync = 1;
            ctx->last = 1;
        }
    }


    rc = ngx_http_next_body_filter(r, in);


    if (rc == NGX_ERROR || !ctx->last) {
        return rc;
    }


    if (ctx->start >= ctx->end) {
        ngx_http_set_ctx(r, NULL, ngx_http_slice_filter_module);
        ngx_http_send_special(r, NGX_HTTP_LAST);
        return rc;
    }


    if (r->buffered) {
        return rc;
    }

如果是当前请求的最后一个buf,则创建子请求
    if (ngx_http_subrequest(r, &r->uri, &r->args, &sr, NULL, 0) != NGX_OK) {
        return NGX_ERROR;
    }


    ngx_http_set_ctx(sr, ctx, ngx_http_slice_filter_module);


    slcf = ngx_http_get_module_loc_conf(r, ngx_http_slice_filter_module);


    ctx->range.len = ngx_sprintf(ctx->range.data, "bytes=%O-%O", ctx->start,
                                 ctx->start + (off_t) slcf->size - 1)
                     - ctx->range.data;


    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "http slice subrequest: \"%V\"", &ctx->range);


    return rc;

}



执行点:

1,派生多个子请求,头部只能发一次,是通过header_filter来控制的,r里面保存了header_sent,如果已经把头发送过了则把对应的标志置为1

2,派生子请求的流程

     proxy解析变量slice_range时会根据设置的slice命令来组装请求头,在向客户端发送请求结果时,如果是当前的最后一个buffer,则会创建子请求,

     创建的子请求是挂载到主请求的post队列里面的,而且可以看到在slice_body处理函数里面,只有主请求才能再次创建子请求进行下一部分内容的

     读取,那主请求是如何再次唤醒?在子请求结束时,子请求会把主请求自己加入到主请求自己的post队列里面,这样在执行

 ngx_http_run_posted_requests函数时,就会再次执行自己的slice_filter模块,

具体代码为:  if (ngx_http_post_request(pr, NULL) != NGX_OK) {
            r->main->count++;
            ngx_http_terminate_request(r, 0);
            return;
        }

 3,如果主请求长时间不发包或者收包,没有被断开的原因暂时还没有找到。

4,后续再接着补充




### 配置 Nginx 使用 `ngx_stream_geoip2_module` 模块 #### 安装依赖项 为了成功编译和运行 `ngx_stream_geoip2_module`,需要先安装必要的开发工具和库文件。这通常包括 MaxMind 的数据库支持库。 ```bash yum install libmaxminddb-devel -y ``` #### 下载并准备模块源码 获取最新的 `ngx_http_geoip2_module` 和 `ngx_stream_geoip2_module` 源代码: ```bash cd /usr/local/src/ git clone https://github.com/leev/ngx_http_geoip2_module.git ``` 注意:虽然这里提到的是 `ngx_http_geoip2_module`,但是 GitHub 上该项目也包含了流模式的支持即 `ngx_stream_geoip2_module`. #### 编译安装 NGINX 并添加自定义模块 在编译 NGINX 之前,确保已经下载了官方发布的稳定版本,并将其解压缩到合适的位置。接着,在配置阶段通过命令行参数指定要加入的新功能模块路径。 ```bash tar -zxvf nginx-1.20.1.tar.gz cd nginx-1.20.1/ ./configure \ --with-stream \ --add-dynamic-module=../ngx_http_geoip2_module \ --prefix=/etc/nginx \ --sbin-path=/usr/sbin/nginx \ --modules-path=/usr/lib/nginx/modules \ --conf-path=/etc/nginx/nginx.conf \ --error-log-path=/var/log/nginx/error.log \ --http-log-path=/var/log/nginx/access.log \ --pid-path=/var/run/nginx.pid \ --lock-path=/var/run/nginx.lock \ --http-client-body-temp-path=/var/cache/nginx/client_temp \ --http-proxy-temp-path=/var/cache/nginx/proxy_temp \ --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \ --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \ --http-scgi-temp-path=/var/cache/nginx/scgi_temp \ --user=nginx \ --group=nginx \ --with-compat \ --with-file-aio \ --with-http_ssl_module \ --with-http_v2_module \ --with-http_realip_module \ --with-http_addition_module \ --with-http_xslt_module=dynamic \ --with-http_image_filter_module=dynamic \ --with-http_geoip_module=dynamic \ --with-http_sub_module \ --with-http_dav_module \ --with-http_flv_module \ --with-http_mp4_module \ --with-http_gunzip_module \ --with-http_gzip_static_module \ --with-http_random_index_module \ --with-http_secure_link_module \ --with-http_degradation_module \ --with-http_slice_module \ --with-http_stub_status_module \ --with-mail=static \ --with-mail_ssl_module \ --with-pcre \ --with-pcre-jit \ --with-stream=dynamic \ --with-stream_ssl_module \ --with-stream_realip_module \ --with-stream_geoip_module=dynamic \ --with-stream_ssl_preread_module \ --with-debug make && make install ``` 上述过程会将 `ngx_stream_geoip2_module` 动态加载至 NGINX 中[^3]。 #### 获取 IP 地理位置数据集 (MaxMind DB 文件) 从官方网站或其他可信资源处获得最新版的 GeoLite2 数据包,并按照说明放置于适当位置供 NGINX 访问。 ```bash mkdir -p /etc/nginx/geoip cd /tmp wget https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=YOUR_LICENSE_KEY&suffix=tar.gz tar xf geoip_download\?edition_id\=GeoLite2-City* -C /etc/nginx/geoip --strip-components=1 ``` 请注意替换 URL 参数中的 `YOUR_LICENSE_KEY` 为你自己的许可证密钥[^2]。 #### 修改 NGINX 配置以启用 Stream GeoIP 支持 编辑 `/etc/nginx/nginx.conf` 或者相应的虚拟主机配置文件来引入新的指令用于处理 TCP/UDP 请求的同时解析客户端的真实地理位置信息。 ```nginx stream { upstream backend { server 192.168.1.1:80; } map $remote_addr $country_code { default "unknown"; include /etc/nginx/geoip/country.map; # 自定义映射表可选 } log_format stream_combined '$remote_addr [$time_local] ' '"$protocol $status" $bytes_sent "$upstream_addr"' ' country=$country_code'; access_log /var/log/nginx/stream-access.log stream_combined; server { listen 12345; proxy_pass backend; geoip2 /etc/nginx/geoip/GeoLite2-City.mmdb { auto_load on; $city city.names.en; $country_code country.iso_code; } set $allowed_countries "CN US CA"; # 允许访问的国家列表 if ($country_code !~* ^($allowed_countries)$ ) { return 503; } } } ``` 此段配置实现了基于地理区域限制服务可用性的基本逻辑,仅当请求来自特定几个允许的国家时才转发流量给后端服务器;否则返回 HTTP 503 错误响应表示暂时不可用[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值