searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

负载均衡中L7XGW会话保持模块梳理及功能验证

2023-11-03 10:06:00
26
0

首次请求

      首先,客户端第一次发来http请求,在PREACCESS阶段会执行ngx_http_session_sticky_header_handler,从而调用ngx_http_session_sticky_get_cookie获取cookie。

    cookies = (ngx_table_elt_t **) r->headers_in.cookies.elts;
    for (i = 0; i < r->headers_in.cookies.nelts; i++) {
        cookie = &cookies[i]->value;
        p = ngx_strnstr(cookie->data, (char *) sscf->cookie.data, cookie->len);
        if (p == NULL) {
            continue;
        }

        if (*(p + sscf->cookie.len) == ' ' || *(p + sscf->cookie.len) == '=') {
            break;
        }
    }

    if (i >= r->headers_in.cookies.nelts) {
        goto not_found;
    }

      该函数会通过获取请求头中的cookie字段来获取cookie,由于第一次请求并没有cookie,会直接进入not_found。not_found中会对ctx进行配置。

    ctx->frist = 1;
    ctx->sid.len = 0;
    ctx->sid.data = NULL;
    ctx->firstseen = now;
    ctx->lastseen = now;

    ngx_http_session_sticky_tmtoa(r, &ctx->s_lastseen, ctx->lastseen);
    ngx_http_session_sticky_tmtoa(r, &ctx->s_firstseen, ctx->firstseen);

    if (ctx->s_lastseen.data == NULL || ctx->s_firstseen.data == NULL) {
        return NGX_ERROR;
    }

    return NGX_OK;

       同时,还会走到upstream模块中,去向获取上游服务器。在get_peer中如果是第一次访问,也会走到failed中。

static ngx_int_t
ngx_http_upstream_session_sticky_get_peer(ngx_peer_connection_t *pc, void *data)
{
    ngx_int_t                          rc;
    ngx_uint_t                         i, n;
    ngx_http_ss_ctx_t                 *ctx;
    ngx_http_request_t                *r;
    ngx_http_ss_server_t              *server;
    ngx_http_upstream_ss_srv_conf_t   *sscf;
    ngx_http_upstream_ss_peer_data_t  *sspd = data;

    sscf = sspd->sscf;
    r = sspd->r;
    n = sscf->number;
    server = sscf->server;

    ctx = ngx_http_get_module_ctx(r, ngx_http_upstream_session_sticky_module);

    if (ctx->frist == 1 || ctx->sid.len == 0) {
        goto failed;
    }
  
		...
      
failed:

    rc = sspd->get_rr_peer(pc, &sspd->rrp);
    if (rc != NGX_OK) {
        return rc;
    }

    for (i = 0; i < n; i++) {
        if (server[i].name->len == pc->name->len
            && ngx_strncmp(server[i].name->data, pc->name->data,
                           pc->name->len) == 0)
        {
            ctx->sid.len = server[i].sid.len;
            ctx->sid.data = server[i].sid.data;
            break;
        }
    }
    ctx->frist = 1;

    return rc;
}

如果是第一次访问,即ctx中first为1时,会将ctx->sid设为对应的服务器的sid。然后根据服务器返回的响应头中的Set-cookie字段,按照设定模式的不同去执行不同的cookie设定方法。

三种不同的模式

insert mode

      insert模式会通过set-cookie头直接插入响应中并返回给客户端,即使后端服务器没有返回cookie。这点不同于rewrite模式,在rewrite中如果后端服务器返回响应没有携带cookie时是不会进行sesstion sticky的。在后端服务器第一次返回响应的时候,lb会找到服务器返回响应头的set-cookie字段。

static ngx_int_t
ngx_http_session_sticky_insert(ngx_http_request_t *r)
{
    u_char             *p;
    ngx_uint_t          i;
    ngx_list_part_t    *part;
    ngx_table_elt_t    *set_cookie, *table;
    ngx_http_ss_ctx_t  *ctx;

    ctx = ngx_http_get_module_ctx(r, ngx_http_upstream_session_sticky_module);
    if (ctx->frist != 1 && ctx->sscf->maxidle == NGX_CONF_UNSET) {
        return NGX_OK;
    }

    set_cookie = NULL;
    if (ctx->sscf->flag & NGX_HTTP_SESSION_STICKY_INDIRECT) {
        //.....indirect模式
    }
		//创建set_cookie字段,如果与后端服务器重名直接替换,否则会添加
    if (set_cookie == NULL) {
        set_cookie = ngx_list_push(&r->headers_out.headers);
        if (set_cookie == NULL) {
            return NGX_ERROR;
        }

        set_cookie->hash = 1;
        ngx_str_set(&set_cookie->key, "Set-Cookie");
    }
		//设置set-cookie的长度
    set_cookie->value.len = ctx->sscf->cookie.len
                          + sizeof("=") - 1
                          + ctx->sid.len
                          + sizeof("; Domain=") - 1
                          + ctx->sscf->domain.len
                          + sizeof("; Path=") - 1
                          + ctx->sscf->path.len;
		//如果还设置了maxidle字段,则返回给客户端的cookie类似于																																										test1=a4e4d973977a99fbbdbe39f610943f46|1694572446|1694572446
    if (ctx->sscf->maxidle != NGX_CONF_UNSET) {
        set_cookie->value.len = set_cookie->value.len
                              + ctx->s_lastseen.len
                              + ctx->s_firstseen.len
                              + 2; /* '|' and '|' */
    } else {
        set_cookie->value.len = set_cookie->value.len
                              + sizeof("; Max-Age=") - 1
                              + ctx->sscf->maxage.len
                              + sizeof("; Expires=") - 1
                              + sizeof("Xxx, 00-Xxx-00 00:00:00 GMT") - 1;
    }

    p = ngx_pnalloc(r->pool, set_cookie->value.len);
    if (p == NULL) {
        return NGX_ERROR;
    }

    set_cookie->value.data = p;
		//设置cookie名称
    p = ngx_cpymem(p, ctx->sscf->cookie.data, ctx->sscf->cookie.len);
    *p++ = '=';
  	//cookie value字段
    p = ngx_cpymem(p, ctx->sid.data, ctx->sid.len);
    if (ctx->sscf->maxidle != NGX_CONF_UNSET) {
        *(p++) = NGX_HTTP_SESSION_STICKY_DELIMITER; //分隔符"|"
        p = ngx_cpymem(p, ctx->s_lastseen.data, ctx->s_lastseen.len);
        *(p++) = NGX_HTTP_SESSION_STICKY_DELIMITER;
        p = ngx_cpymem(p, ctx->s_firstseen.data, ctx->s_firstseen.len);
    }
  	//添加上Set-cookie的其他字段
    if (ctx->sscf->domain.len) {
        p = ngx_cpymem(p, "; Domain=", sizeof("; Domain=") - 1);
        p = ngx_cpymem(p, ctx->sscf->domain.data, ctx->sscf->domain.len);
    }
    if (ctx->sscf->path.len) {
        p = ngx_cpymem(p, "; Path=", sizeof("; Path=") - 1);
        p = ngx_cpymem(p, ctx->sscf->path.data, ctx->sscf->path.len);
    }
    if (ctx->sscf->maxidle == NGX_CONF_UNSET && ctx->sscf->maxage.len) {
        p = ngx_cpymem(p, "; Max-Age=", sizeof("; Max-Age=") - 1);
        p = ngx_cpymem(p, ctx->sscf->maxage.data, ctx->sscf->maxage.len);
        p = ngx_cpymem(p, "; Expires=", sizeof("; Expires=") - 1);
        ngx_uint_t maxage = ngx_atoi(ctx->sscf->maxage.data,
                                      ctx->sscf->maxage.len);
        p = ngx_http_cookie_time(p, ngx_time() + maxage);
    }

    set_cookie->value.len = p - set_cookie->value.data;

    return NGX_OK;
}

      ngx_http_session_sticky_insert函数首先会往headers_out.headers中添加set_cookie,并根据配置项设置好长度,如果存在maxidle字段,则会在cookie追加fisrtseen和lastseen,并用分隔符"|"进行分割。

      当客户端携带着cookie发出请求时,ss模块会对insert模式发来的请求进行解析:

static ngx_int_t
ngx_http_session_sticky_get_cookie(ngx_http_request_t *r)
{
    time_t                           now;
    u_char                          *p, *v, *vv, *st, *last, *end;
    ngx_int_t                        diff, delimiter, legal;
    ngx_str_t                       *cookie;
    ngx_uint_t                       i;
    ngx_table_elt_t                **cookies;
    ngx_http_ss_ctx_t               *ctx;
    ngx_http_upstream_ss_srv_conf_t *sscf;
    enum {
        pre_key = 0,
        key,
        pre_equal,
        pre_value,
        value
    } state;

    legal = 1;
    ctx = ngx_http_get_module_ctx(r, ngx_http_upstream_session_sticky_module);
    sscf = ctx->sscf;
    ctx->tries = 1;

    p = NULL;
    cookie = NULL;
    now = ngx_time();
    cookies = (ngx_table_elt_t **) r->headers_in.cookies.elts;//找到请求头中的cookies
    for (i = 0; i < r->headers_in.cookies.nelts; i++) {
      	//找到与配置项中同名的cookie
        cookie = &cookies[i]->value;
        p = ngx_strnstr(cookie->data, (char *) sscf->cookie.data, cookie->len);
        if (p == NULL) {
            continue;
        }

        if (*(p + sscf->cookie.len) == ' ' || *(p + sscf->cookie.len) == '=') {
            break;
        }
    }
  
    st = p;
    v = p + sscf->cookie.len + 1;
    last = cookie->data + cookie->len;

    state = 0;
    while (p < last) {
        //...负责找到cookie.value的起始地址和结尾
    }

		//...

success:

    if (sscf->flag & NGX_HTTP_SESSION_STICKY_PREFIX) {
				//prefix模式的处理流程
    } else {
        vv = p;
    }

    if ((sscf->flag & NGX_HTTP_SESSION_STICKY_INSERT)//如果是insert模式并且配置了maxidle字段
        && sscf->maxidle != NGX_CONF_UNSET)
    {
        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                       "session_sticky mode [insert]");

        delimiter = 0;
        for (p = v; p < vv; p++) {
            if (*p == NGX_HTTP_SESSION_STICKY_DELIMITER) {
                delimiter++;
                if (delimiter == 1) {//第一个分隔符,分隔出sid,cookie的原始value
                    ctx->sid.len = p - v;
                    ctx->sid.data = ngx_pnalloc(r->pool, ctx->sid.len);
                    if (ctx->sid.data == NULL) {
                        return NGX_ERROR;
                    }
                    ngx_memcpy(ctx->sid.data, v, ctx->sid.len);
                    v = p + 1;

                } else if(delimiter == 2) {//找到lastseen
                    ctx->s_lastseen.len = p - v;
                    ctx->s_lastseen.data = ngx_pnalloc(r->pool,
                                                       ctx->s_lastseen.len);
                    if (ctx->s_lastseen.data == NULL) {
                        return NGX_ERROR;
                    }
                    ngx_memcpy(ctx->s_lastseen.data, v, ctx->s_lastseen.len);
                    v = p + 1;
                    break;

                } else {//两个以上,非法了
                    legal = 0;
                    goto finish;
                }
            }
        }

        if (p >= vv || v >= vv) {
            legal = 0;
            goto finish;

        }

        ctx->s_firstseen.len = vv - v;//firstseen
        ctx->s_firstseen.data = ngx_pnalloc(r->pool, ctx->s_firstseen.len);
        if (ctx->s_firstseen.data == NULL) {
            return NGX_ERROR;
        }
        ngx_memcpy(ctx->s_firstseen.data, v, ctx->s_firstseen.len);

        ctx->firstseen = ngx_atotm(ctx->s_firstseen.data, ctx->s_firstseen.len);
        ctx->lastseen = ngx_atotm(ctx->s_lastseen.data, ctx->s_lastseen.len);

        if (ctx->firstseen == NGX_ERROR || ctx->lastseen == NGX_ERROR) {
            legal = 0;
            goto finish;
        }

        if (ctx->sid.len != 0) {//判断ss是否超时
            diff = (ngx_int_t) (now - ctx->lastseen);
            if (diff > ctx->sscf->maxidle || diff < -86400) {
                legal = 0;
                goto finish;
            }

            diff = (ngx_int_t) (now - ctx->firstseen);
            if (diff > ctx->sscf->maxlife || diff < -86400) {
                legal = 0;
                goto finish;
            }
        }
				//设定新的lastseen
        ngx_http_session_sticky_tmtoa(r, &ctx->s_lastseen, now);

    } else { //否则是rewrite模式或没有配置maxidle的insert模式
        ctx->sid.len = vv - v;
        ctx->sid.data = ngx_pnalloc(r->pool, ctx->sid.len);
        if (ctx->sid.data == NULL) {
            return NGX_ERROR;
        }
        ngx_memcpy(ctx->sid.data, v, ctx->sid.len);
    }

finish:

    if (sscf->flag
        & (NGX_HTTP_SESSION_STICKY_PREFIX | NGX_HTTP_SESSION_STICKY_INDIRECT))
    {
        //处理prefix和indirect option逻辑
    }

    //...
    return NGX_OK;
}

      get_cookie用于解析客户端发来的http请求中的cookie,在获取到和配置同名的cookie后,得到cookie对应的value字段。如果设置了maxidle,则会根据分隔符"|"解析出sid、firstseen和lastseen,并判断是否超时,更新lastseen为当前时间;如果没有设置maxidle,则会和rewrite走相同的路径,仅仅设置sid为cookie的value。

      对添加了maxidle的insert模式进行测试可获得如下信息:

*   Trying 10.1.2.250:8080...
*   Connected to 10.1.2.250 (10.1.2.250) port 8080 (#0)

> GET / HTTP/1.1
> Host: 10.1.2.250:8080
> User-Agent: curl/7.71.1
> Accept: */*

* Mark bundle as not supporting multiuse
  < HTTP/1.1 200 OK
  < Server: CTYun LB
  < Date: Wed, 13 Sep 2023 02:34:06 GMT
  < Content-Type: text/plain
  < Content-Length: 31
  < Connection: keep-alive
  < Expires: Wed, 13 Sep 2023 02:34:11 GMT
  < Cache-Control: max-age=5
* Added cookie test1="aabbccddee1111" for domain 10.1.2.250, path /, expire 0
  < Set-Cookie: test1 = aabbccddee1111
* Added cookie test2="aabbccddee2222" for domain 10.1.2.250, path /, expire 0
  < Set-Cookie: test2 = aabbccddee2222
* Added cookie test3="aabbccddee3333" for domain 10.1.2.250, path /, expire 0
  < Set-Cookie: test3 = aabbccddee3333
  < Cache-Control: 10
* Replaced cookie test1="a4e4d973977a99fbbdbe39f610943f46|1694572446|1694572446" for domain 10.1.2.250, path /, expire 0
  < Set-Cookie: test1=a4e4d973977a99fbbdbe39f610943f46|1694572446|1694572446; Path=/
  < 
  lbtest-vm-aZ3-2-10.1.1.3:51001
* Connection #0 to host 10.1.2.250 left intact
* Trying 10.1.2.250:8080...
* Connected to 10.1.2.250 (10.1.2.250) port 8080 (#0)

> GET / HTTP/1.1
> Host: 10.1.2.250:8080
> User-Agent: curl/7.71.1
> Accept: */*
> Cookie: test1=a4e4d973977a99fbbdbe39f610943f46|1694572446|1694572446; test2=aabbccddee2222; test3=aabbccddee3333

* Mark bundle as not supporting multiuse
  < HTTP/1.1 200 OK
  < Server: CTYun LB
  < Date: Wed, 13 Sep 2023 02:34:06 GMT
  < Content-Type: text/plain
  < Content-Length: 31
  < Connection: keep-alive
  < Expires: Wed, 13 Sep 2023 02:34:11 GMT
  < Cache-Control: max-age=5
* Replaced cookie test1="aabbccddee1111" for domain 10.1.2.250, path /, expire 0
  < Set-Cookie: test1 = aabbccddee1111
* Replaced cookie test2="aabbccddee2222" for domain 10.1.2.250, path /, expire 0
  < Set-Cookie: test2 = aabbccddee2222
* Replaced cookie test3="aabbccddee3333" for domain 10.1.2.250, path /, expire 0
  < Set-Cookie: test3 = aabbccddee3333
  < Cache-Control: 10
* Replaced cookie test1="a4e4d973977a99fbbdbe39f610943f46|1694572446|1694572446" for domain 10.1.2.250, path /, expire 0
  < Set-Cookie: test1=a4e4d973977a99fbbdbe39f610943f46|1694572446|1694572446; Path=/
  < 
  lbtest-vm-aZ3-2-10.1.1.3:51001

      可以看到,客户端接收到的返回信息都是由10.1.1.3:51001生成(lbtest-vm-aZ3-2-10.1.1.3:51001)。第一请求完成后,客户端以后发送的请求报文中都携带了cookie,cookie value中保存了firstseen和lastseen,由于每次请求都会更新lastseen,所以返回响应的时候lb7每次都会更新Set-Cookie当中的cookie,每个请求的cookie都会发生改变。

      下面是没有加maxidle字段执行curl脚本的打印消息:

*   Trying 10.1.2.250:8080...
* Connected to 10.1.2.250 (10.1.2.250) port 8080 (#0)
> GET / HTTP/1.1
> Host: 10.1.2.250:8080
> User-Agent: curl/7.71.1
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: CTYun LB
< Date: Wed, 13 Sep 2023 08:59:15 GMT
< Content-Type: text/plain
< Content-Length: 31
< Connection: keep-alive
< Expires: Wed, 13 Sep 2023 08:59:20 GMT
< Cache-Control: max-age=5
* Added cookie test2="aabbccddee2222" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test2 = aabbccddee2222
* Added cookie test3="aabbccddee3333" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test3 = aabbccddee3333
< Cache-Control: 10
* Added cookie test1="a4e4d973977a99fbbdbe39f610943f46" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test1=a4e4d973977a99fbbdbe39f610943f46; Path=/
< 
lbtest-vm-aZ3-2-10.1.1.3:51001
* Connection #0 to host 10.1.2.250 left intact
*   Trying 10.1.2.250:8080...
* Connected to 10.1.2.250 (10.1.2.250) port 8080 (#0)
> GET / HTTP/1.1
> Host: 10.1.2.250:8080
> User-Agent: curl/7.71.1
> Accept: */*
> Cookie: test2=aabbccddee2222; test3=aabbccddee3333; test1=a4e4d973977a99fbbdbe39f610943f46
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: CTYun LB
< Date: Wed, 13 Sep 2023 08:59:15 GMT
< Content-Type: text/plain
< Content-Length: 31
< Connection: keep-alive
< Expires: Wed, 13 Sep 2023 08:59:20 GMT
< Cache-Control: max-age=5
#第二次及以后直接跳过处理响应报文
* Replaced cookie test2="aabbccddee2222" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test2 = aabbccddee2222
* Replaced cookie test3="aabbccddee3333" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test3 = aabbccddee3333
< Cache-Control: 10
< 
lbtest-vm-aZ3-2-10.1.1.3:51001

      处理insert模式响应报文的时候会先进行判断。

    ctx = ngx_http_get_module_ctx(r, ngx_http_upstream_session_sticky_module);
    if (ctx->frist != 1 && ctx->sscf->maxidle == NGX_CONF_UNSET) {
        return NGX_OK;
    }

      即如果不是第一次(首次请求不携带cookie)且没有设置maxidle的时候,直接返回NGX_OK,不再对cookie进行更新。可以在返回信息中看到,只有在首次请求中返回了test1,就是lb中ss模块设定的cookie name。如果设定了maxidle,那么由于ctx->lastseen每次请求都会改变,每次传给客户端的cookie都会不一样。

rewrite mode

      重写模式使用服务端的标识符来覆盖后端服务器中用于进行会话保持的cookie。不同于insert模式,如果后端服务器在响应头中没有设置对应名称的cookie,insert模式会直接插入cookie,而rewrite模式则会认为当前请求不需要进行session sticky,不进行任何操作。

      在解析后端服务器返回的响应时,会通过headers_out.headers找到set-cookie字段,调用ngx_http_session_sticky_rewrite,对比lb设置好的cookie name,如果找不到匹配的cookie name,直接返回NGX_AGAIN,如果都没有匹配直接调用下一个头过滤模块。找到匹配的cookie name后,拿到对应的cookie value,在headers中删除cookie value并重新设置为ctx->sid。

static ngx_int_t
ngx_http_session_sticky_rewrite(ngx_http_request_t *r, ngx_table_elt_t *table)
{
 		//...
  
  	//如果没有匹配的cookie name,直接返回NGX_AGAIN查找下一个headers
    if (p == NULL) {
        return NGX_AGAIN;
    }
		//找到匹配的cookie name,初始化索引,找到对应的cookie value
    st = p;
    start = table->value.data;
    last = table->value.data + table->value.len;

    state = 0;
    //...
    if (p >= last && (state == value || state == pre_equal)) {
        goto success;
    }

    return NGX_AGAIN;

success:

    en = p; 
  	//删除对应的cookie value,并添加ctx->sid
    table->value.len = table->value.len
                     - (en - st)
                     + ctx->sscf->cookie.len
                     + 1 /* '=' */
                     + ctx->sid.len;
    table->value.data = p;
    p = ngx_cpymem(p, start, st - start);
    p = ngx_cpymem(p, ctx->sscf->cookie.data, ctx->sscf->cookie.len);
    *p++ = '=';
    p = ngx_cpymem(p, ctx->sid.data, ctx->sid.len);
    p = ngx_cpymem(p, en, last - en);

    return NGX_OK;
}

       rewrite模式处理client发来的请求头中的cookie相对insert模式简单的多,只需要在获取到cookie value后添加到ctx->sid当中。

static ngx_int_t
ngx_http_session_sticky_get_cookie(ngx_http_request_t *r)
{
    //find sscf->cookie's value

not_found:
		//...

success:

    if (sscf->flag & NGX_HTTP_SESSION_STICKY_PREFIX) {
				//...
    } else {
        vv = p;
    }

    if ((sscf->flag & NGX_HTTP_SESSION_STICKY_INSERT)
        && sscf->maxidle != NGX_CONF_UNSET)
    {
				//INSERT mode process
    } else {//处理rewrete模式的cookie value
        ctx->sid.len = vv - v;
        ctx->sid.data = ngx_pnalloc(r->pool, ctx->sid.len);
        if (ctx->sid.data == NULL) {
            return NGX_ERROR;
        }
        ngx_memcpy(ctx->sid.data, v, ctx->sid.len);
    }

       从功能验证中可以看到,rewrite模式在第一次接收到Set-cookie响应报文时就改写了对应cookie name的value,和不带maxidle的insert模式不同的是,rewrite模式每次都会修改Set-cookie。

*   Trying 10.1.2.250:8080...
* Connected to 10.1.2.250 (10.1.2.250) port 8080 (#0)
> GET / HTTP/1.1
> Host: 10.1.2.250:8080
> User-Agent: curl/7.71.1
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: CTYun LB
< Date: Fri, 15 Sep 2023 03:26:16 GMT
< Content-Type: application/octet-stream
< Content-Length: 32
< Connection: keep-alive
* Added cookie test1="a4e4d973977a99fbbdbe39f610943f46" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test1=a4e4d973977a99fbbdbe39f610943f46
* Added cookie test2="aaabbbccc222" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test2=aaabbbccc222
* Added cookie test3="aaabbbccc333" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test3=aaabbbccc333
< 
lbtest-vm-az3-2-10.1.1.3:51001
* Connection #0 to host 10.1.2.250 left intact
*   Trying 10.1.2.250:8080...
* Connected to 10.1.2.250 (10.1.2.250) port 8080 (#0)
> GET / HTTP/1.1
> Host: 10.1.2.250:8080
> User-Agent: curl/7.71.1
> Accept: */*
> Cookie: test1=a4e4d973977a99fbbdbe39f610943f46; test2=aaabbbccc222; test3=aaabbbccc333
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: CTYun LB
< Date: Fri, 15 Sep 2023 03:26:16 GMT
< Content-Type: application/octet-stream
< Content-Length: 32
< Connection: keep-alive
#这里可以看到,第二次也会对setcookie进行修改
* Replaced cookie test1="a4e4d973977a99fbbdbe39f610943f46" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test1=a4e4d973977a99fbbdbe39f610943f46
* Replaced cookie test2="aaabbbccc222" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test2=aaabbbccc222
* Replaced cookie test3="aaabbbccc333" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test3=aaabbbccc333
< 
lbtest-vm-az3-2-10.1.1.3:51001

      但是,如果后端服务器并没有配置和upstream中同名的cookie,则认为不需要进行会话保持。

    #这里我们对server的配置文件进行了修改,将test1删除。
    server {
        listen 51001-51004;
        server_name lbtest-vm-aZ3-2-10.1.1.3;
        #add_header Set-Cookie test1=aaabbbccc111;
        add_header Set-Cookie test2=aaabbbccc222;
        add_header Set-Cookie test3=aaabbbccc333;
        location / {
            return 200 $server_name:$server_port\r\n;
        }
    }
    
#shell
*   Trying 10.1.2.250:8080...
* Connected to 10.1.2.250 (10.1.2.250) port 8080 (#0)
> GET / HTTP/1.1
> Host: 10.1.2.250:8080
> User-Agent: curl/7.71.1
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: CTYun LB
< Date: Fri, 15 Sep 2023 03:36:54 GMT
< Content-Type: application/octet-stream
< Content-Length: 32
< Connection: keep-alive
#这里Set-Cookie中没有添加test1,rewrite模式找不到,直接跳过
* Added cookie test2="aaabbbccc222" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test2=aaabbbccc222
* Added cookie test3="aaabbbccc333" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test3=aaabbbccc333
< 
lbtest-vm-az3-2-10.1.1.3:51002
* Connection #0 to host 10.1.2.250 left intact
*   Trying 10.1.2.250:8080...
* Connected to 10.1.2.250 (10.1.2.250) port 8080 (#0)
> GET / HTTP/1.1
> Host: 10.1.2.250:8080
> User-Agent: curl/7.71.1
> Accept: */*
> Cookie: test2=aaabbbccc222; test3=aaabbbccc333
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: CTYun LB
< Date: Fri, 15 Sep 2023 03:36:54 GMT
< Content-Type: application/octet-stream
< Content-Length: 32
< Connection: keep-alive
* Replaced cookie test2="aaabbbccc222" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test2=aaabbbccc222
* Replaced cookie test3="aaabbbccc333" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test3=aaabbbccc333
< #由于没有携带能够进行会话保持的cookie,会话保持没有生效
lbtest-vm-az3-2-10.1.1.3:51003

prefix mode

      prefix模式不会生成新的cookie,而是直接添加sid到cookie的value之前,并用~进行分隔类似于:cookie: name = sid~value。在处理后端服务器返回的响应报文时进行添加,并传给client;在收到client的请求报文时,将前缀删除,整个过程对后端服务器完去哪感知不到cookie前缀的存在,对后端服务器透明。

static ngx_int_t
ngx_http_session_sticky_prefix(ngx_http_request_t *r, ngx_table_elt_t *table)
{
    u_char             *p, *s, *t, *last;
    ngx_http_ss_ctx_t  *ctx;
    enum {
        pre_equal = 0,
        pre_value
    } state;

    ctx = ngx_http_get_module_ctx(r, ngx_http_upstream_session_sticky_module);
  	//找到配置项cookie name对应的value
    p = ngx_strlcasestrn(table->value.data,
                         table->value.data + table->value.len,
                         ctx->sscf->cookie.data,
                         ctx->sscf->cookie.len - 1);
    if (p == NULL) {
        return NGX_AGAIN;
    }

    last = table->value.data + table->value.len;
    state = 0;
    p += ctx->sscf->cookie.len;//p直接跳过了cookie name
    while (p < last) {
        //过滤掉=和空格,将p指向cookie value的首地址
    return NGX_AGAIN;

success:
		//原始的set-cookie长度加上sid的长度和一个~
    table->value.len += ctx->sid.len + 1;
    s = ngx_pnalloc(r->pool, table->value.len);
    if (s == NULL) {
        return NGX_ERROR;
    }

    t = s;
  	//cookie name
    t = ngx_cpymem(t, table->value.data, p - table->value.data);
  	//赋值sid,后端服务器标识符,默认用md5
    t = ngx_cpymem(t, ctx->sid.data, ctx->sid.len);
    *t++ = '~';
  	//添加原始的cookie value
    t = ngx_cpymem(t, p, last - p);

    table->value.data = s;

    return NGX_OK;
}

      获取client请求报文时,PREFIX模式有一个单独的处理逻辑,以~为分隔符设定sid,同时将headers_in当中的cookie前缀删除。

static ngx_int_t
ngx_http_session_sticky_get_cookie(ngx_http_request_t *r)
{	
    //find cookie value...
success:

    if (sscf->flag & NGX_HTTP_SESSION_STICKY_PREFIX) {

        for (vv = v; vv < p; vv++) {//找到~,便于分隔sid和cookie value
            if (*vv == '~') {
                end = vv + 1;
                break;
            }
        }
        if (vv >= p) {
            goto not_found;
        }
        st = v;

    }

    if ((sscf->flag & NGX_HTTP_SESSION_STICKY_INSERT)
        && sscf->maxidle != NGX_CONF_UNSET)
    {
				//insert mode process
    } else {
      	//设置sid为cookie前缀
        ctx->sid.len = vv - v;
        ctx->sid.data = ngx_pnalloc(r->pool, ctx->sid.len);
        if (ctx->sid.data == NULL) {
            return NGX_ERROR;
        }
        ngx_memcpy(ctx->sid.data, v, ctx->sid.len);
    }

finish:

    if (sscf->flag
        & (NGX_HTTP_SESSION_STICKY_PREFIX | NGX_HTTP_SESSION_STICKY_INDIRECT))
    {
      	//假设cookie: name=sid~value
        //  st指向s,end指向v
      	//cookie->len长度减去了value前面的长度
        cookie->len -= (end - st);

        if (cookie->len == 0) {
            cookies[i]->hash = 0;
            return NGX_OK;
        }
				//替换成只有value的cookie
        while (end < last) {
            *st++ = *end++;
        }
    }
		//...
}

      对prefix模式进行功能验证,成功给返回客户端的cookie添加了前缀:

*   Trying 10.1.2.250:8080...
* Connected to 10.1.2.250 (10.1.2.250) port 8080 (#0)
> GET / HTTP/1.1
> Host: 10.1.2.250:8080
> User-Agent: curl/7.71.1
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: CTYun LB
< Date: Fri, 15 Sep 2023 05:50:57 GMT
< Content-Type: application/octet-stream
< Content-Length: 32
< Connection: keep-alive
* Added cookie test1="a4e4d973977a99fbbdbe39f610943f46~aaabbbccc111" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test1=a4e4d973977a99fbbdbe39f610943f46~aaabbbccc111
* Added cookie test2="aaabbbccc222" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test2=aaabbbccc222
* Added cookie test3="aaabbbccc333" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test3=aaabbbccc333
< 
lbtest-vm-az3-2-10.1.1.3:51001
* Connection #0 to host 10.1.2.250 left intact
*   Trying 10.1.2.250:8080...
* Connected to 10.1.2.250 (10.1.2.250) port 8080 (#0)
> GET / HTTP/1.1
> Host: 10.1.2.250:8080
> User-Agent: curl/7.71.1
> Accept: */*
> Cookie: test1=a4e4d973977a99fbbdbe39f610943f46~aaabbbccc111; test2=aaabbbccc222; test3=aaabbbccc333
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: CTYun LB
< Date: Fri, 15 Sep 2023 05:50:57 GMT
< Content-Type: application/octet-stream
< Content-Length: 32
< Connection: keep-alive
* Replaced cookie test1="a4e4d973977a99fbbdbe39f610943f46~aaabbbccc111" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test1=a4e4d973977a99fbbdbe39f610943f46~aaabbbccc111
* Replaced cookie test2="aaabbbccc222" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test2=aaabbbccc222
* Replaced cookie test3="aaabbbccc333" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test3=aaabbbccc333
< 
lbtest-vm-az3-2-10.1.1.3:51001
0条评论
0 / 1000
l****n
1文章数
0粉丝数
l****n
1 文章 | 0 粉丝
l****n
1文章数
0粉丝数
l****n
1 文章 | 0 粉丝
原创

负载均衡中L7XGW会话保持模块梳理及功能验证

2023-11-03 10:06:00
26
0

首次请求

      首先,客户端第一次发来http请求,在PREACCESS阶段会执行ngx_http_session_sticky_header_handler,从而调用ngx_http_session_sticky_get_cookie获取cookie。

    cookies = (ngx_table_elt_t **) r->headers_in.cookies.elts;
    for (i = 0; i < r->headers_in.cookies.nelts; i++) {
        cookie = &cookies[i]->value;
        p = ngx_strnstr(cookie->data, (char *) sscf->cookie.data, cookie->len);
        if (p == NULL) {
            continue;
        }

        if (*(p + sscf->cookie.len) == ' ' || *(p + sscf->cookie.len) == '=') {
            break;
        }
    }

    if (i >= r->headers_in.cookies.nelts) {
        goto not_found;
    }

      该函数会通过获取请求头中的cookie字段来获取cookie,由于第一次请求并没有cookie,会直接进入not_found。not_found中会对ctx进行配置。

    ctx->frist = 1;
    ctx->sid.len = 0;
    ctx->sid.data = NULL;
    ctx->firstseen = now;
    ctx->lastseen = now;

    ngx_http_session_sticky_tmtoa(r, &ctx->s_lastseen, ctx->lastseen);
    ngx_http_session_sticky_tmtoa(r, &ctx->s_firstseen, ctx->firstseen);

    if (ctx->s_lastseen.data == NULL || ctx->s_firstseen.data == NULL) {
        return NGX_ERROR;
    }

    return NGX_OK;

       同时,还会走到upstream模块中,去向获取上游服务器。在get_peer中如果是第一次访问,也会走到failed中。

static ngx_int_t
ngx_http_upstream_session_sticky_get_peer(ngx_peer_connection_t *pc, void *data)
{
    ngx_int_t                          rc;
    ngx_uint_t                         i, n;
    ngx_http_ss_ctx_t                 *ctx;
    ngx_http_request_t                *r;
    ngx_http_ss_server_t              *server;
    ngx_http_upstream_ss_srv_conf_t   *sscf;
    ngx_http_upstream_ss_peer_data_t  *sspd = data;

    sscf = sspd->sscf;
    r = sspd->r;
    n = sscf->number;
    server = sscf->server;

    ctx = ngx_http_get_module_ctx(r, ngx_http_upstream_session_sticky_module);

    if (ctx->frist == 1 || ctx->sid.len == 0) {
        goto failed;
    }
  
		...
      
failed:

    rc = sspd->get_rr_peer(pc, &sspd->rrp);
    if (rc != NGX_OK) {
        return rc;
    }

    for (i = 0; i < n; i++) {
        if (server[i].name->len == pc->name->len
            && ngx_strncmp(server[i].name->data, pc->name->data,
                           pc->name->len) == 0)
        {
            ctx->sid.len = server[i].sid.len;
            ctx->sid.data = server[i].sid.data;
            break;
        }
    }
    ctx->frist = 1;

    return rc;
}

如果是第一次访问,即ctx中first为1时,会将ctx->sid设为对应的服务器的sid。然后根据服务器返回的响应头中的Set-cookie字段,按照设定模式的不同去执行不同的cookie设定方法。

三种不同的模式

insert mode

      insert模式会通过set-cookie头直接插入响应中并返回给客户端,即使后端服务器没有返回cookie。这点不同于rewrite模式,在rewrite中如果后端服务器返回响应没有携带cookie时是不会进行sesstion sticky的。在后端服务器第一次返回响应的时候,lb会找到服务器返回响应头的set-cookie字段。

static ngx_int_t
ngx_http_session_sticky_insert(ngx_http_request_t *r)
{
    u_char             *p;
    ngx_uint_t          i;
    ngx_list_part_t    *part;
    ngx_table_elt_t    *set_cookie, *table;
    ngx_http_ss_ctx_t  *ctx;

    ctx = ngx_http_get_module_ctx(r, ngx_http_upstream_session_sticky_module);
    if (ctx->frist != 1 && ctx->sscf->maxidle == NGX_CONF_UNSET) {
        return NGX_OK;
    }

    set_cookie = NULL;
    if (ctx->sscf->flag & NGX_HTTP_SESSION_STICKY_INDIRECT) {
        //.....indirect模式
    }
		//创建set_cookie字段,如果与后端服务器重名直接替换,否则会添加
    if (set_cookie == NULL) {
        set_cookie = ngx_list_push(&r->headers_out.headers);
        if (set_cookie == NULL) {
            return NGX_ERROR;
        }

        set_cookie->hash = 1;
        ngx_str_set(&set_cookie->key, "Set-Cookie");
    }
		//设置set-cookie的长度
    set_cookie->value.len = ctx->sscf->cookie.len
                          + sizeof("=") - 1
                          + ctx->sid.len
                          + sizeof("; Domain=") - 1
                          + ctx->sscf->domain.len
                          + sizeof("; Path=") - 1
                          + ctx->sscf->path.len;
		//如果还设置了maxidle字段,则返回给客户端的cookie类似于																																										test1=a4e4d973977a99fbbdbe39f610943f46|1694572446|1694572446
    if (ctx->sscf->maxidle != NGX_CONF_UNSET) {
        set_cookie->value.len = set_cookie->value.len
                              + ctx->s_lastseen.len
                              + ctx->s_firstseen.len
                              + 2; /* '|' and '|' */
    } else {
        set_cookie->value.len = set_cookie->value.len
                              + sizeof("; Max-Age=") - 1
                              + ctx->sscf->maxage.len
                              + sizeof("; Expires=") - 1
                              + sizeof("Xxx, 00-Xxx-00 00:00:00 GMT") - 1;
    }

    p = ngx_pnalloc(r->pool, set_cookie->value.len);
    if (p == NULL) {
        return NGX_ERROR;
    }

    set_cookie->value.data = p;
		//设置cookie名称
    p = ngx_cpymem(p, ctx->sscf->cookie.data, ctx->sscf->cookie.len);
    *p++ = '=';
  	//cookie value字段
    p = ngx_cpymem(p, ctx->sid.data, ctx->sid.len);
    if (ctx->sscf->maxidle != NGX_CONF_UNSET) {
        *(p++) = NGX_HTTP_SESSION_STICKY_DELIMITER; //分隔符"|"
        p = ngx_cpymem(p, ctx->s_lastseen.data, ctx->s_lastseen.len);
        *(p++) = NGX_HTTP_SESSION_STICKY_DELIMITER;
        p = ngx_cpymem(p, ctx->s_firstseen.data, ctx->s_firstseen.len);
    }
  	//添加上Set-cookie的其他字段
    if (ctx->sscf->domain.len) {
        p = ngx_cpymem(p, "; Domain=", sizeof("; Domain=") - 1);
        p = ngx_cpymem(p, ctx->sscf->domain.data, ctx->sscf->domain.len);
    }
    if (ctx->sscf->path.len) {
        p = ngx_cpymem(p, "; Path=", sizeof("; Path=") - 1);
        p = ngx_cpymem(p, ctx->sscf->path.data, ctx->sscf->path.len);
    }
    if (ctx->sscf->maxidle == NGX_CONF_UNSET && ctx->sscf->maxage.len) {
        p = ngx_cpymem(p, "; Max-Age=", sizeof("; Max-Age=") - 1);
        p = ngx_cpymem(p, ctx->sscf->maxage.data, ctx->sscf->maxage.len);
        p = ngx_cpymem(p, "; Expires=", sizeof("; Expires=") - 1);
        ngx_uint_t maxage = ngx_atoi(ctx->sscf->maxage.data,
                                      ctx->sscf->maxage.len);
        p = ngx_http_cookie_time(p, ngx_time() + maxage);
    }

    set_cookie->value.len = p - set_cookie->value.data;

    return NGX_OK;
}

      ngx_http_session_sticky_insert函数首先会往headers_out.headers中添加set_cookie,并根据配置项设置好长度,如果存在maxidle字段,则会在cookie追加fisrtseen和lastseen,并用分隔符"|"进行分割。

      当客户端携带着cookie发出请求时,ss模块会对insert模式发来的请求进行解析:

static ngx_int_t
ngx_http_session_sticky_get_cookie(ngx_http_request_t *r)
{
    time_t                           now;
    u_char                          *p, *v, *vv, *st, *last, *end;
    ngx_int_t                        diff, delimiter, legal;
    ngx_str_t                       *cookie;
    ngx_uint_t                       i;
    ngx_table_elt_t                **cookies;
    ngx_http_ss_ctx_t               *ctx;
    ngx_http_upstream_ss_srv_conf_t *sscf;
    enum {
        pre_key = 0,
        key,
        pre_equal,
        pre_value,
        value
    } state;

    legal = 1;
    ctx = ngx_http_get_module_ctx(r, ngx_http_upstream_session_sticky_module);
    sscf = ctx->sscf;
    ctx->tries = 1;

    p = NULL;
    cookie = NULL;
    now = ngx_time();
    cookies = (ngx_table_elt_t **) r->headers_in.cookies.elts;//找到请求头中的cookies
    for (i = 0; i < r->headers_in.cookies.nelts; i++) {
      	//找到与配置项中同名的cookie
        cookie = &cookies[i]->value;
        p = ngx_strnstr(cookie->data, (char *) sscf->cookie.data, cookie->len);
        if (p == NULL) {
            continue;
        }

        if (*(p + sscf->cookie.len) == ' ' || *(p + sscf->cookie.len) == '=') {
            break;
        }
    }
  
    st = p;
    v = p + sscf->cookie.len + 1;
    last = cookie->data + cookie->len;

    state = 0;
    while (p < last) {
        //...负责找到cookie.value的起始地址和结尾
    }

		//...

success:

    if (sscf->flag & NGX_HTTP_SESSION_STICKY_PREFIX) {
				//prefix模式的处理流程
    } else {
        vv = p;
    }

    if ((sscf->flag & NGX_HTTP_SESSION_STICKY_INSERT)//如果是insert模式并且配置了maxidle字段
        && sscf->maxidle != NGX_CONF_UNSET)
    {
        ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                       "session_sticky mode [insert]");

        delimiter = 0;
        for (p = v; p < vv; p++) {
            if (*p == NGX_HTTP_SESSION_STICKY_DELIMITER) {
                delimiter++;
                if (delimiter == 1) {//第一个分隔符,分隔出sid,cookie的原始value
                    ctx->sid.len = p - v;
                    ctx->sid.data = ngx_pnalloc(r->pool, ctx->sid.len);
                    if (ctx->sid.data == NULL) {
                        return NGX_ERROR;
                    }
                    ngx_memcpy(ctx->sid.data, v, ctx->sid.len);
                    v = p + 1;

                } else if(delimiter == 2) {//找到lastseen
                    ctx->s_lastseen.len = p - v;
                    ctx->s_lastseen.data = ngx_pnalloc(r->pool,
                                                       ctx->s_lastseen.len);
                    if (ctx->s_lastseen.data == NULL) {
                        return NGX_ERROR;
                    }
                    ngx_memcpy(ctx->s_lastseen.data, v, ctx->s_lastseen.len);
                    v = p + 1;
                    break;

                } else {//两个以上,非法了
                    legal = 0;
                    goto finish;
                }
            }
        }

        if (p >= vv || v >= vv) {
            legal = 0;
            goto finish;

        }

        ctx->s_firstseen.len = vv - v;//firstseen
        ctx->s_firstseen.data = ngx_pnalloc(r->pool, ctx->s_firstseen.len);
        if (ctx->s_firstseen.data == NULL) {
            return NGX_ERROR;
        }
        ngx_memcpy(ctx->s_firstseen.data, v, ctx->s_firstseen.len);

        ctx->firstseen = ngx_atotm(ctx->s_firstseen.data, ctx->s_firstseen.len);
        ctx->lastseen = ngx_atotm(ctx->s_lastseen.data, ctx->s_lastseen.len);

        if (ctx->firstseen == NGX_ERROR || ctx->lastseen == NGX_ERROR) {
            legal = 0;
            goto finish;
        }

        if (ctx->sid.len != 0) {//判断ss是否超时
            diff = (ngx_int_t) (now - ctx->lastseen);
            if (diff > ctx->sscf->maxidle || diff < -86400) {
                legal = 0;
                goto finish;
            }

            diff = (ngx_int_t) (now - ctx->firstseen);
            if (diff > ctx->sscf->maxlife || diff < -86400) {
                legal = 0;
                goto finish;
            }
        }
				//设定新的lastseen
        ngx_http_session_sticky_tmtoa(r, &ctx->s_lastseen, now);

    } else { //否则是rewrite模式或没有配置maxidle的insert模式
        ctx->sid.len = vv - v;
        ctx->sid.data = ngx_pnalloc(r->pool, ctx->sid.len);
        if (ctx->sid.data == NULL) {
            return NGX_ERROR;
        }
        ngx_memcpy(ctx->sid.data, v, ctx->sid.len);
    }

finish:

    if (sscf->flag
        & (NGX_HTTP_SESSION_STICKY_PREFIX | NGX_HTTP_SESSION_STICKY_INDIRECT))
    {
        //处理prefix和indirect option逻辑
    }

    //...
    return NGX_OK;
}

      get_cookie用于解析客户端发来的http请求中的cookie,在获取到和配置同名的cookie后,得到cookie对应的value字段。如果设置了maxidle,则会根据分隔符"|"解析出sid、firstseen和lastseen,并判断是否超时,更新lastseen为当前时间;如果没有设置maxidle,则会和rewrite走相同的路径,仅仅设置sid为cookie的value。

      对添加了maxidle的insert模式进行测试可获得如下信息:

*   Trying 10.1.2.250:8080...
*   Connected to 10.1.2.250 (10.1.2.250) port 8080 (#0)

> GET / HTTP/1.1
> Host: 10.1.2.250:8080
> User-Agent: curl/7.71.1
> Accept: */*

* Mark bundle as not supporting multiuse
  < HTTP/1.1 200 OK
  < Server: CTYun LB
  < Date: Wed, 13 Sep 2023 02:34:06 GMT
  < Content-Type: text/plain
  < Content-Length: 31
  < Connection: keep-alive
  < Expires: Wed, 13 Sep 2023 02:34:11 GMT
  < Cache-Control: max-age=5
* Added cookie test1="aabbccddee1111" for domain 10.1.2.250, path /, expire 0
  < Set-Cookie: test1 = aabbccddee1111
* Added cookie test2="aabbccddee2222" for domain 10.1.2.250, path /, expire 0
  < Set-Cookie: test2 = aabbccddee2222
* Added cookie test3="aabbccddee3333" for domain 10.1.2.250, path /, expire 0
  < Set-Cookie: test3 = aabbccddee3333
  < Cache-Control: 10
* Replaced cookie test1="a4e4d973977a99fbbdbe39f610943f46|1694572446|1694572446" for domain 10.1.2.250, path /, expire 0
  < Set-Cookie: test1=a4e4d973977a99fbbdbe39f610943f46|1694572446|1694572446; Path=/
  < 
  lbtest-vm-aZ3-2-10.1.1.3:51001
* Connection #0 to host 10.1.2.250 left intact
* Trying 10.1.2.250:8080...
* Connected to 10.1.2.250 (10.1.2.250) port 8080 (#0)

> GET / HTTP/1.1
> Host: 10.1.2.250:8080
> User-Agent: curl/7.71.1
> Accept: */*
> Cookie: test1=a4e4d973977a99fbbdbe39f610943f46|1694572446|1694572446; test2=aabbccddee2222; test3=aabbccddee3333

* Mark bundle as not supporting multiuse
  < HTTP/1.1 200 OK
  < Server: CTYun LB
  < Date: Wed, 13 Sep 2023 02:34:06 GMT
  < Content-Type: text/plain
  < Content-Length: 31
  < Connection: keep-alive
  < Expires: Wed, 13 Sep 2023 02:34:11 GMT
  < Cache-Control: max-age=5
* Replaced cookie test1="aabbccddee1111" for domain 10.1.2.250, path /, expire 0
  < Set-Cookie: test1 = aabbccddee1111
* Replaced cookie test2="aabbccddee2222" for domain 10.1.2.250, path /, expire 0
  < Set-Cookie: test2 = aabbccddee2222
* Replaced cookie test3="aabbccddee3333" for domain 10.1.2.250, path /, expire 0
  < Set-Cookie: test3 = aabbccddee3333
  < Cache-Control: 10
* Replaced cookie test1="a4e4d973977a99fbbdbe39f610943f46|1694572446|1694572446" for domain 10.1.2.250, path /, expire 0
  < Set-Cookie: test1=a4e4d973977a99fbbdbe39f610943f46|1694572446|1694572446; Path=/
  < 
  lbtest-vm-aZ3-2-10.1.1.3:51001

      可以看到,客户端接收到的返回信息都是由10.1.1.3:51001生成(lbtest-vm-aZ3-2-10.1.1.3:51001)。第一请求完成后,客户端以后发送的请求报文中都携带了cookie,cookie value中保存了firstseen和lastseen,由于每次请求都会更新lastseen,所以返回响应的时候lb7每次都会更新Set-Cookie当中的cookie,每个请求的cookie都会发生改变。

      下面是没有加maxidle字段执行curl脚本的打印消息:

*   Trying 10.1.2.250:8080...
* Connected to 10.1.2.250 (10.1.2.250) port 8080 (#0)
> GET / HTTP/1.1
> Host: 10.1.2.250:8080
> User-Agent: curl/7.71.1
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: CTYun LB
< Date: Wed, 13 Sep 2023 08:59:15 GMT
< Content-Type: text/plain
< Content-Length: 31
< Connection: keep-alive
< Expires: Wed, 13 Sep 2023 08:59:20 GMT
< Cache-Control: max-age=5
* Added cookie test2="aabbccddee2222" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test2 = aabbccddee2222
* Added cookie test3="aabbccddee3333" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test3 = aabbccddee3333
< Cache-Control: 10
* Added cookie test1="a4e4d973977a99fbbdbe39f610943f46" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test1=a4e4d973977a99fbbdbe39f610943f46; Path=/
< 
lbtest-vm-aZ3-2-10.1.1.3:51001
* Connection #0 to host 10.1.2.250 left intact
*   Trying 10.1.2.250:8080...
* Connected to 10.1.2.250 (10.1.2.250) port 8080 (#0)
> GET / HTTP/1.1
> Host: 10.1.2.250:8080
> User-Agent: curl/7.71.1
> Accept: */*
> Cookie: test2=aabbccddee2222; test3=aabbccddee3333; test1=a4e4d973977a99fbbdbe39f610943f46
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: CTYun LB
< Date: Wed, 13 Sep 2023 08:59:15 GMT
< Content-Type: text/plain
< Content-Length: 31
< Connection: keep-alive
< Expires: Wed, 13 Sep 2023 08:59:20 GMT
< Cache-Control: max-age=5
#第二次及以后直接跳过处理响应报文
* Replaced cookie test2="aabbccddee2222" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test2 = aabbccddee2222
* Replaced cookie test3="aabbccddee3333" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test3 = aabbccddee3333
< Cache-Control: 10
< 
lbtest-vm-aZ3-2-10.1.1.3:51001

      处理insert模式响应报文的时候会先进行判断。

    ctx = ngx_http_get_module_ctx(r, ngx_http_upstream_session_sticky_module);
    if (ctx->frist != 1 && ctx->sscf->maxidle == NGX_CONF_UNSET) {
        return NGX_OK;
    }

      即如果不是第一次(首次请求不携带cookie)且没有设置maxidle的时候,直接返回NGX_OK,不再对cookie进行更新。可以在返回信息中看到,只有在首次请求中返回了test1,就是lb中ss模块设定的cookie name。如果设定了maxidle,那么由于ctx->lastseen每次请求都会改变,每次传给客户端的cookie都会不一样。

rewrite mode

      重写模式使用服务端的标识符来覆盖后端服务器中用于进行会话保持的cookie。不同于insert模式,如果后端服务器在响应头中没有设置对应名称的cookie,insert模式会直接插入cookie,而rewrite模式则会认为当前请求不需要进行session sticky,不进行任何操作。

      在解析后端服务器返回的响应时,会通过headers_out.headers找到set-cookie字段,调用ngx_http_session_sticky_rewrite,对比lb设置好的cookie name,如果找不到匹配的cookie name,直接返回NGX_AGAIN,如果都没有匹配直接调用下一个头过滤模块。找到匹配的cookie name后,拿到对应的cookie value,在headers中删除cookie value并重新设置为ctx->sid。

static ngx_int_t
ngx_http_session_sticky_rewrite(ngx_http_request_t *r, ngx_table_elt_t *table)
{
 		//...
  
  	//如果没有匹配的cookie name,直接返回NGX_AGAIN查找下一个headers
    if (p == NULL) {
        return NGX_AGAIN;
    }
		//找到匹配的cookie name,初始化索引,找到对应的cookie value
    st = p;
    start = table->value.data;
    last = table->value.data + table->value.len;

    state = 0;
    //...
    if (p >= last && (state == value || state == pre_equal)) {
        goto success;
    }

    return NGX_AGAIN;

success:

    en = p; 
  	//删除对应的cookie value,并添加ctx->sid
    table->value.len = table->value.len
                     - (en - st)
                     + ctx->sscf->cookie.len
                     + 1 /* '=' */
                     + ctx->sid.len;
    table->value.data = p;
    p = ngx_cpymem(p, start, st - start);
    p = ngx_cpymem(p, ctx->sscf->cookie.data, ctx->sscf->cookie.len);
    *p++ = '=';
    p = ngx_cpymem(p, ctx->sid.data, ctx->sid.len);
    p = ngx_cpymem(p, en, last - en);

    return NGX_OK;
}

       rewrite模式处理client发来的请求头中的cookie相对insert模式简单的多,只需要在获取到cookie value后添加到ctx->sid当中。

static ngx_int_t
ngx_http_session_sticky_get_cookie(ngx_http_request_t *r)
{
    //find sscf->cookie's value

not_found:
		//...

success:

    if (sscf->flag & NGX_HTTP_SESSION_STICKY_PREFIX) {
				//...
    } else {
        vv = p;
    }

    if ((sscf->flag & NGX_HTTP_SESSION_STICKY_INSERT)
        && sscf->maxidle != NGX_CONF_UNSET)
    {
				//INSERT mode process
    } else {//处理rewrete模式的cookie value
        ctx->sid.len = vv - v;
        ctx->sid.data = ngx_pnalloc(r->pool, ctx->sid.len);
        if (ctx->sid.data == NULL) {
            return NGX_ERROR;
        }
        ngx_memcpy(ctx->sid.data, v, ctx->sid.len);
    }

       从功能验证中可以看到,rewrite模式在第一次接收到Set-cookie响应报文时就改写了对应cookie name的value,和不带maxidle的insert模式不同的是,rewrite模式每次都会修改Set-cookie。

*   Trying 10.1.2.250:8080...
* Connected to 10.1.2.250 (10.1.2.250) port 8080 (#0)
> GET / HTTP/1.1
> Host: 10.1.2.250:8080
> User-Agent: curl/7.71.1
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: CTYun LB
< Date: Fri, 15 Sep 2023 03:26:16 GMT
< Content-Type: application/octet-stream
< Content-Length: 32
< Connection: keep-alive
* Added cookie test1="a4e4d973977a99fbbdbe39f610943f46" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test1=a4e4d973977a99fbbdbe39f610943f46
* Added cookie test2="aaabbbccc222" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test2=aaabbbccc222
* Added cookie test3="aaabbbccc333" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test3=aaabbbccc333
< 
lbtest-vm-az3-2-10.1.1.3:51001
* Connection #0 to host 10.1.2.250 left intact
*   Trying 10.1.2.250:8080...
* Connected to 10.1.2.250 (10.1.2.250) port 8080 (#0)
> GET / HTTP/1.1
> Host: 10.1.2.250:8080
> User-Agent: curl/7.71.1
> Accept: */*
> Cookie: test1=a4e4d973977a99fbbdbe39f610943f46; test2=aaabbbccc222; test3=aaabbbccc333
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: CTYun LB
< Date: Fri, 15 Sep 2023 03:26:16 GMT
< Content-Type: application/octet-stream
< Content-Length: 32
< Connection: keep-alive
#这里可以看到,第二次也会对setcookie进行修改
* Replaced cookie test1="a4e4d973977a99fbbdbe39f610943f46" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test1=a4e4d973977a99fbbdbe39f610943f46
* Replaced cookie test2="aaabbbccc222" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test2=aaabbbccc222
* Replaced cookie test3="aaabbbccc333" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test3=aaabbbccc333
< 
lbtest-vm-az3-2-10.1.1.3:51001

      但是,如果后端服务器并没有配置和upstream中同名的cookie,则认为不需要进行会话保持。

    #这里我们对server的配置文件进行了修改,将test1删除。
    server {
        listen 51001-51004;
        server_name lbtest-vm-aZ3-2-10.1.1.3;
        #add_header Set-Cookie test1=aaabbbccc111;
        add_header Set-Cookie test2=aaabbbccc222;
        add_header Set-Cookie test3=aaabbbccc333;
        location / {
            return 200 $server_name:$server_port\r\n;
        }
    }
    
#shell
*   Trying 10.1.2.250:8080...
* Connected to 10.1.2.250 (10.1.2.250) port 8080 (#0)
> GET / HTTP/1.1
> Host: 10.1.2.250:8080
> User-Agent: curl/7.71.1
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: CTYun LB
< Date: Fri, 15 Sep 2023 03:36:54 GMT
< Content-Type: application/octet-stream
< Content-Length: 32
< Connection: keep-alive
#这里Set-Cookie中没有添加test1,rewrite模式找不到,直接跳过
* Added cookie test2="aaabbbccc222" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test2=aaabbbccc222
* Added cookie test3="aaabbbccc333" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test3=aaabbbccc333
< 
lbtest-vm-az3-2-10.1.1.3:51002
* Connection #0 to host 10.1.2.250 left intact
*   Trying 10.1.2.250:8080...
* Connected to 10.1.2.250 (10.1.2.250) port 8080 (#0)
> GET / HTTP/1.1
> Host: 10.1.2.250:8080
> User-Agent: curl/7.71.1
> Accept: */*
> Cookie: test2=aaabbbccc222; test3=aaabbbccc333
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: CTYun LB
< Date: Fri, 15 Sep 2023 03:36:54 GMT
< Content-Type: application/octet-stream
< Content-Length: 32
< Connection: keep-alive
* Replaced cookie test2="aaabbbccc222" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test2=aaabbbccc222
* Replaced cookie test3="aaabbbccc333" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test3=aaabbbccc333
< #由于没有携带能够进行会话保持的cookie,会话保持没有生效
lbtest-vm-az3-2-10.1.1.3:51003

prefix mode

      prefix模式不会生成新的cookie,而是直接添加sid到cookie的value之前,并用~进行分隔类似于:cookie: name = sid~value。在处理后端服务器返回的响应报文时进行添加,并传给client;在收到client的请求报文时,将前缀删除,整个过程对后端服务器完去哪感知不到cookie前缀的存在,对后端服务器透明。

static ngx_int_t
ngx_http_session_sticky_prefix(ngx_http_request_t *r, ngx_table_elt_t *table)
{
    u_char             *p, *s, *t, *last;
    ngx_http_ss_ctx_t  *ctx;
    enum {
        pre_equal = 0,
        pre_value
    } state;

    ctx = ngx_http_get_module_ctx(r, ngx_http_upstream_session_sticky_module);
  	//找到配置项cookie name对应的value
    p = ngx_strlcasestrn(table->value.data,
                         table->value.data + table->value.len,
                         ctx->sscf->cookie.data,
                         ctx->sscf->cookie.len - 1);
    if (p == NULL) {
        return NGX_AGAIN;
    }

    last = table->value.data + table->value.len;
    state = 0;
    p += ctx->sscf->cookie.len;//p直接跳过了cookie name
    while (p < last) {
        //过滤掉=和空格,将p指向cookie value的首地址
    return NGX_AGAIN;

success:
		//原始的set-cookie长度加上sid的长度和一个~
    table->value.len += ctx->sid.len + 1;
    s = ngx_pnalloc(r->pool, table->value.len);
    if (s == NULL) {
        return NGX_ERROR;
    }

    t = s;
  	//cookie name
    t = ngx_cpymem(t, table->value.data, p - table->value.data);
  	//赋值sid,后端服务器标识符,默认用md5
    t = ngx_cpymem(t, ctx->sid.data, ctx->sid.len);
    *t++ = '~';
  	//添加原始的cookie value
    t = ngx_cpymem(t, p, last - p);

    table->value.data = s;

    return NGX_OK;
}

      获取client请求报文时,PREFIX模式有一个单独的处理逻辑,以~为分隔符设定sid,同时将headers_in当中的cookie前缀删除。

static ngx_int_t
ngx_http_session_sticky_get_cookie(ngx_http_request_t *r)
{	
    //find cookie value...
success:

    if (sscf->flag & NGX_HTTP_SESSION_STICKY_PREFIX) {

        for (vv = v; vv < p; vv++) {//找到~,便于分隔sid和cookie value
            if (*vv == '~') {
                end = vv + 1;
                break;
            }
        }
        if (vv >= p) {
            goto not_found;
        }
        st = v;

    }

    if ((sscf->flag & NGX_HTTP_SESSION_STICKY_INSERT)
        && sscf->maxidle != NGX_CONF_UNSET)
    {
				//insert mode process
    } else {
      	//设置sid为cookie前缀
        ctx->sid.len = vv - v;
        ctx->sid.data = ngx_pnalloc(r->pool, ctx->sid.len);
        if (ctx->sid.data == NULL) {
            return NGX_ERROR;
        }
        ngx_memcpy(ctx->sid.data, v, ctx->sid.len);
    }

finish:

    if (sscf->flag
        & (NGX_HTTP_SESSION_STICKY_PREFIX | NGX_HTTP_SESSION_STICKY_INDIRECT))
    {
      	//假设cookie: name=sid~value
        //  st指向s,end指向v
      	//cookie->len长度减去了value前面的长度
        cookie->len -= (end - st);

        if (cookie->len == 0) {
            cookies[i]->hash = 0;
            return NGX_OK;
        }
				//替换成只有value的cookie
        while (end < last) {
            *st++ = *end++;
        }
    }
		//...
}

      对prefix模式进行功能验证,成功给返回客户端的cookie添加了前缀:

*   Trying 10.1.2.250:8080...
* Connected to 10.1.2.250 (10.1.2.250) port 8080 (#0)
> GET / HTTP/1.1
> Host: 10.1.2.250:8080
> User-Agent: curl/7.71.1
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: CTYun LB
< Date: Fri, 15 Sep 2023 05:50:57 GMT
< Content-Type: application/octet-stream
< Content-Length: 32
< Connection: keep-alive
* Added cookie test1="a4e4d973977a99fbbdbe39f610943f46~aaabbbccc111" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test1=a4e4d973977a99fbbdbe39f610943f46~aaabbbccc111
* Added cookie test2="aaabbbccc222" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test2=aaabbbccc222
* Added cookie test3="aaabbbccc333" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test3=aaabbbccc333
< 
lbtest-vm-az3-2-10.1.1.3:51001
* Connection #0 to host 10.1.2.250 left intact
*   Trying 10.1.2.250:8080...
* Connected to 10.1.2.250 (10.1.2.250) port 8080 (#0)
> GET / HTTP/1.1
> Host: 10.1.2.250:8080
> User-Agent: curl/7.71.1
> Accept: */*
> Cookie: test1=a4e4d973977a99fbbdbe39f610943f46~aaabbbccc111; test2=aaabbbccc222; test3=aaabbbccc333
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: CTYun LB
< Date: Fri, 15 Sep 2023 05:50:57 GMT
< Content-Type: application/octet-stream
< Content-Length: 32
< Connection: keep-alive
* Replaced cookie test1="a4e4d973977a99fbbdbe39f610943f46~aaabbbccc111" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test1=a4e4d973977a99fbbdbe39f610943f46~aaabbbccc111
* Replaced cookie test2="aaabbbccc222" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test2=aaabbbccc222
* Replaced cookie test3="aaabbbccc333" for domain 10.1.2.250, path /, expire 0
< Set-Cookie: test3=aaabbbccc333
< 
lbtest-vm-az3-2-10.1.1.3:51001
文章来自个人专栏
源码阅读
1 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
0
0