关注点:HTTP Cookie 的格式;负载均衡 (loadbalancer) 类型模块的编写;Haproxy 是如 何实现类似的功能的;

此模块通过给客户端请求添加 Cookie 并通过检查这个 Cookie 的值判断请求应该发往哪 个后端应用服务器。它的功能和 ip_hash 模块类似,但是有些场景下 ip_hash 不太 合适:

Using a persistance by IP (with the ip_hash upstream module) is maybe not a good idea because there could be situations where a lot of different browsers are coming with the same IP address (behind proxies) and the load balancing system won't be fair.

Using a cookie to track the upstream server makes each browser unique.

sticky 模块只适合浏览器支持 Cookie 的场景。当 sticky 模块碰到不适用的情况 时,会切换到 Nginx 默认的 Round-Robin 模式 或者 返回 "Bad Gateway" (no_fallback 为 1)。

Cookie's Structure

Set-Cookie: name=value;expiry;path;domain;secure;http-only # response
Cookie: name=value; name2=value2                           # request
  • Besides the name-value pair, servers can also set those cookie attributes: a cookie domain, a path, expiration time or maximum age, secure flag and httponly flag.

  • Browsers will not send cookie attribute back to the server. They will only send the cookie's name-value pare.

  • Cookie attributes are used by browsers to determine when to delete a cookie, block a cookie or whether to send a cookie to the servers.

模块用法

upstream {
    sticky;
    server 127.0.0.1:9000;
    server 127.0.0.1:9001;
    server 127.0.0.1:9002;
}

sticky 指令支持的选项:

sticky [name=route] [domain=] [path=] [expires=1h] [hash=index|md5|sha1] \
    [hmac hmac_key=private_key] [no_fallback];
  • HMAC is used to simultaneously verify both the data integrity and the authentication of a message. 基本相当于加了 salt 的 hash 算法。

数据结构

  • ngx_http_upstream_main_conf_t - 存储配置文件所有 upstream {} 配置块。

    typedef struct {
        ...
        ngx_array_t     upstreams; /* of ngx_http_upstream_srv_conf_t */
    } ngx_http_upstream_main_conf_t;
    
  • ngx_http_upstream_srv_conf_t - 对应 upstream {} 配置块。

    struct ngx_http_upstream_srv_conf_s {
        ngx_http_upstream_peer_t    peer;     /* 此 upstream {} peer 相关操作 */
        ...
        ngx_array_t                 *servers; /* of ngx_http_upstream_server_t */
        ...
    };
    
  • ngx_http_upstream_peer_t - 所属 upstream {} peer 的初始化和选择逻辑。

    typedef struct {
        ngx_http_upstream_init_pt       init_upstream;
        ngx_http_upstream_init_peer_pt  init; /* 请求相关的后端服务器使用
                                                 状态初始化函数 */
        void                            *data; /* 负载均衡相关的运行时数据 */
    } ngx_http_upstream_peer_t;
    
    • init_upstream 在配置解析时由负载均衡模块设置,并在配置解析完成后 在 init_main 阶段由 ngx_http_upstream_init_main_conf 函数调用,它会将后 端服务器信息构造成负载均衡模块运行时需要使用的数据结构 (hash表、有序数组等)。

    • init_upstream 还将设置 init 函数和 datainit 函数用于构造请求相 关的后端服务器使用状态结构体,并设置用于操作 (选择、释放) 后端服务器的回调 函数。

  • ngx_http_upstream_server_t - 对应 upstream {} 作用域里的一个 server 配 置行信息,其中的后端服务器信息被整理后转存成 ngx_http_upstream_rr_peer_t 类型 变量,其中每个此类型变量对应一个 ngx_http_upstream_server_t 中的 ngx_addr_t

    typedef struct {
        ngx_addr_t      *addrs;
        ngx_uint_t      naddrs;
        ngx_uint_t      weight;
        ngx_uint_t      max_fails;
        time_t          fail_timeout;
        unsigned        down:1;
        unsigned        backup:1;
    } ngx_http_upstream_server_t;
    
  • ngx_http_upstream_rr_peer_t - 代表 RR 模式下的一个后端应用服务器信息 (IP, weight, fail等)

    typedef struct {
        struct sockaddr         *sockaddr;
        socklen_t               socklen;
        ngx_str_t               name;        /* 来自配置参数 */
    
        ngx_int_t               current_weight; /* 初始值为 0 */
        ngx_int_t               effective_weight; /* 初始值和 weight 相等 */
        ngx_int_t               weight;       /* 来自配置参数 */
    
        ngx_uint_t              fails;        /* 此 peer 的失败次数 */
        time_t                  accessed;
        time_t                  checked;
    
        ngx_uint_t              max_fails;    /* 来自配置参数 */
        time_t                  fail_timeout; /* 来自配置参数 */
    
        ngx_uint_t              down;         /* 来自配置参数 */
    } ngx_http_upstream_rr_peer_t;
    
  • ngx_http_upstream_rr_peers_t - RR 模式下,所有后端应用服务器信息的存储结构。

    struct ngx_http_upstream_rr_peers_s {
        ngx_uint_t                      number; /* naddrs of primary or backup */
        ngx_uint_t                      total_weight; /* naddrs of primary or backup */
        ...
        /* 指向标识为 backup 类型的后端应用服务器信息 */
        ngx_http_upstream_rr_peers_t    *next; 
        ngx_http_upstream_rr_peer_t     peer[1];
    };
    

负载均衡模块涉及到的相似的数据结构忒多,最后大致绘个结构关系图(sticky 模块 基于Nginx 标配的 Round-Robin 模块,图中把 rr 相关的结构体也一并画出):

ngx_http_upstream_main_conf_t
    + upstreams --> [ ngx_http_upstream_srv_conf_t, ... ]
                         |
                         + servers --> [ ngx_http_upstream_server_t, ... ]
                         |
                         + peer (ngx_http_upstream_peer_t)
                             + init_upstream
                             + init
                             + data (ngx_http_upstream_rr_peers_t)
                                         + peer (ngx_http_upstream_rr_peer_t)
                                         + next (ngx_http_upstream_rr_peers_t)
                                            /
               backup (ngx_http_upstream_rr_peers_t) if any

rr 结构体基础上,sticky 提供了根据摘要值选择后端服务的功能,它在 rr 结 果的基础上,提供了存储摘要信息的结构体:

typedef struct {
    ngx_http_upstream_rr_peer_t *rr_peer; /* 指向实际对象 */
    ngx_str_t                    digest;
} ngx_http_sticky_peer_t;

typedef struct {
    ngx_str_t                       cookie_name;
    ngx_str_t                       cookie_domain;
    ngx_str_t                       cookie_path;
    time_t                          cookie_expires;
    ngx_str_t                       hmac_key;
    ngx_http_sticky_misc_hash_pt    hash;
    ngx_http_sticky_misc_hmac_pt    hmac;
    ngx_uint_t                      no_fallback;
    ngx_http_sticky_peer_t          *peers;
} ngx_http_sticky_srv_conf_t;

初始化

负载均衡模块指令只能定义于 upstream {} 作用域中,upstream {} 目前又只能用 于 http {} 中。虽然这个作用域中不能再定义 server {} 或者 location {} 等子 作用域,为了和 http {} 作用域的其它部分保持一致,Nginx 基本上把 upstream {} 作用域等同于 server {} 看待 (基本一样的解析处理方式)。

static char *
ngx_http_upstream(ngx_conf_t *cf, ngx_command_t *cmd, void *dummy)
{
    ...
    ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_conf_ctx_t));
    ...
    ctx->main->conf = http_ctx->main_conf;

    ctx->srv_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
    ...
    ctx->srv_conf[ngx_http_upstream_module.ctx_index] = uscf;

    uscf->srv_conf = ctx->srv_conf;

    ctx->loc_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
    ...
    /* each module's create_srv_conf and create_loc_conf */

    pcf = *cf;
    cf->ctx = ctx;
    cf->cmd_type = NGX_HTTP_UPS_CONF;

    rv = ngx_conf_parse(cf, NULL);
    *cf = pcf;
    ...
}

sticky 模块 (以及 Nginx 自带的 ip_hash 模块) 在定义配置指令时,并未指明配 置指令所在模块的配置结构体偏移量,这样的做法会使指令解析处理函数的第三个参数不 可用:

/* cf->ctx */
typedef struct {
    void        **main_conf;
    void        **srv_conf;
    void        **loc_conf;
} ngx_http_conf_ctx_t;

{ ngx_string("sticky"),
  NGX_HTTP_UPS_CONF|NGX_CONF_ANY,
  ngx_http_sticky_set,  /* set */
  0,                    /* conf */
  0,                    /* offset */
  NULL },

static ngx_int_t
ngx_conf_handler(ngx_conf_t *cf, ngx_int_t last)
{
    ...
    else if (cf->ctx) {
        confp = *(void **) ((char *) cf->ctx + cmd->conf);

        if (confp) {
            conf = confp[ngx_modules[i]->ctx_index];
        }
    }

    rv = cmd->set(cf, cmd, conf);
    ...
}
  • cmd->conf 为 NULL 的情况下,confp 实际指向了 main_conf。但是此模块并未 创建 HTTP MAIN 级别的配置项,所以 conf 值为 NULL。

  • 实际上,上一条的分析没有任何意义。因为未设置 ngx_command_t::conf 的时候,就 应该把指令解析处理函数的第三个参数当成无效参数看待。

  • sticky 模块在解析处理函数中使用 ngx_http_conf_get_module_srv_conf 函数找 到其 HTTP SRV 级别的配置结构体 ngx_http_sticky_srv_conf_t

ngx_http_sticky_set 是该模块的唯一一个指令处理回调函数,它解析整个指令行,并 设置 ngx_http_sticky_srv_conf_t 结构体;同时,设置上节提到的 init_upstream 函数指针为 ngx_http_init_upstream_sticky

配置文件的 http {} 解析完毕后,ngx_http_block 开始调用各个 HTTP 模块的 init_main_conf 回调函数,对模块按刚刚得到的配置进行初始化操作。这时 upstream 模块的 ngx_http_upstream_init_main_conf 调用 ngx_http_init_upstream_sticky 构造后端应用服务器信息的存储结构:

for (i = 0; i < umcf->upstreams.nelts; i++) {
    init = uscfp[i]->peer.init_upstream ? uscfp[i]->peer.init_upstream:
                                        ngx_http_upstream_init_round_robin;
    if (init(cf, uscfp[i]) != NGX_OK) {
        return NGX_CONF_ERROR;
    }
}

static ngx_int_t
ngx_http_init_upstream_sticky(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us)
{
    ngx_uint_t                      i;
    ngx_http_sticky_srv_conf_t      *conf;
    ngx_http_upstream_rr_peers_t    *rr_peers;
    ...
    ngx_http_upstream_init_round_robin(cf, us);
    ...
    rr_peers = us->peer.data;
    ...
    us->peer.init = ngx_http_init_sticky_peer;

    conf = ngx_http_conf_upstream_srv_conf(us, ngx_http_sticky_module);
    ...
    /* if 'index', no need to alloc and generate digest */
    if (!conf->hash && !conf->hmac) {
        conf->peers = NULL;
        return NGX_OK;
    }

    conf->peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_sticky_peer_t)
                                        * rr_peers->number);
    ...
    for (i = 0; i < rr_peers->number; i++) {
        conf->peers[i].rr_peer = &&rr_peers->peer[i];

        if (conf->hmac) {
            conf->hmac(cf->poo., rr_peers->peer[i].sockaddr,
                       rr_peers->peer[i].socklen, &conf->hmac_key,
                       &conf->peers[i].digest);

        } else {
            conf->hash(cf->pool), rr_peers->peer[i].sockaddr,
                       rr->peers->peer[i].socklen, &conf->peers[i].digest);
        }
    }

    return NGX_OK;
}

到此,sticky 模块初始化过程结束。

获取Peers信息

负载均衡的目的就是将不同的请求按一定的规则 (rr, ip_hash, sticky 等) 分发 到不同的后端应用服务器上,所以,每个独立的请求都需要维护 Nginx 提供给自己的可用 后端地址,以继续后面的操作 (连接、请求、接收响应)。

每个请求和负载均衡相关的数据结构如下:

struct ngx_http_request_s {
    ...
    ngx_http_upstream_t     *upstream;
    ...
};

struct ngx_http_upstream_s {
    ...
    ngx_peer_connection_t   peer;
    ...
};

struct ngx_peer_connection_s {
    ngx_connection_t        *connection;
    ...
    ngx_uint_t              tries;  /* rrp->peers->number */
    ngx_event_get_peer_pt   get;    /* ngx_http_get_sticky_peer */
    ngx_event_free_peer_pt  free;   /* ngx_http_upstream_free_round_robin_peer */
    void                    *data;  /* ngx_http_sticky_peer_data_t */
    ...
};

typedef struct {
    ngx_http_upstream_rr_peers_t    *peers; /* 所有后端应用服务器信息 */
    ngx_uint_t                      current;
    uintptr_t                       *tried; /* bitmap,bit 值表示该请求是否
                                               已经尝试过对应的 peer */
    uintptr_t                       data;
} ngx_http_upstream_rr_peer_data_t;

typedef struct {
    ngx_http_upstream_rr_peer_data_t    rrp;         /* fallback 的实现保障 */
    ngx_event_get_peer_pt               get_rr_peer; /* 备份 RR 的 get 方法 */
    ngx_int_t                           selected_peer;
    ngx_uint_t                          no_fallback;
    ngx_http_sticky_srv_conf_t          *sticky_conf;
    ngx_http_request_t                  *request;
} ngx_http_sticky_peer_data_t;

上面的这几个结构体由 upstream 模块在准备后端连接 (ngx_http_upstream_init) 时创建和初始化,其中 ngx_peer_connection_t 的部分和负载均衡相关的成员变量由 上面提到的各负载均衡模块实现的 init_upstream 完成:

static void
ngx_http_upstream_init_request(ngx_http_request_t *r)
{
    ...
    if (uscf->peer.init(r, uscf) != NGX_OK) {
        ...
    }

    ngx_http_upstream_connect(r, u);
    /* TODO */
}


static ngx_int_t
ngx_http_init_sticky_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us)
{
    ngx_http_sticky_peer_data_t *iphp;
    ...
    iphp = ngx_palloc(r->pool, sizeof(ngx_http_sticky_peer_data_t));
    ...
    r->upstream->peer.data = &iphp->rrp;
    ...
    ngx_http_upstream_init_round_robin_peer(r, us);
    ...

    iphp->get_rr_peer = ngx_http_upstream_get_round_robin_peer;        
    r->upstream->peer.get = ngx_get_sticky_peer;

    iphp->selected_peer = -1;
    iphp->no_fallback = 0;
    ...
    if (ngx_http_parse_multi_header_lines(&r->headers_in.cookies,
                                          &iphp->sticky_conf->cookie_name,
                                          &route) != NGX_DECLINED)
    {
        if (iphp->sticky_conf->hash || iphp->sticky_conf->hmac) {

            for (i = 0; i < iphp->rrp.peers->number; i++) {
                /* bingo */
                iphp->selected_peer = i;

                return NGX_OK;
            }

        } else {
            n = ngx_atoi(route.data, route.len);
            if (n == NGX_ERROR) {
                ...
            } else if (n >= 0 && n < (ngx_int_t) iphp->rrp.peers->number) {
                ...
                iphp->selected_peer = n;
                return NGX_OK;
            }
        }
    }

    return NGX_OK;
}

对上述代码的补充说明:

  • ngx_http_upstream_init_round_robin_peerngx_http_upstream_rr_peer_data_t 进行初始化:peers 指向保存有全部 peer 信息的结构体;tried 初始化为 bitmap,每个 bit 的值表示对应的 peer 是否被使用过。

    ngx_int_t
    ngx_http_upstream_init_round_robin_peer(ngx_http_request_t *r,
        ngx_http_upstream_srv_conf_t *us)
    {
        ...
        rrp = r->upstream->peer.data;
        ...
        rrp->peers = us->peer.data;
        rrp->current = 0;
        ...
        rrp->tried = /* ... */;
    }
    
  • ngx_http_upstream_init_round_robin_peerngx_peer_connection_t 中的回调 函数字段进行赋值。

    ngx_int_t
    ngx_http_upstream_init_round_robin_peer(ngx_http_request_t *r,
        ngx_http_upstream_srv_conf_t *us)
    {
        ...
        r->upstream->peer.get = ngx_http_upstream_get_round_robin_peer;
        r->upstream->peer.free = ngx_http_upstream_free_round_robin_peer;
        r->upstream->peer.tries = rrp->peers->number;
        ...
    }
    
  • 根据 request 的 Cookie 值查找对应的 peer,并将其在 peers 中编号赋给 selected_peer 变量。未能成功匹配到合适的 peer 时,selected_peer 值为 -1。

  • ngx_http_sticky_peer_data_t 类型的 iphp 存储着 sticky 模块的状态,并且 它是在 ngx_http_upstream_rr_peer_data_t 类型上的封装,通过 r->upstream->peer.data 并进行适当的类型转换就可以引用到。

  • r->upstream->peer.get = ngx_get_sticky_peer; 重新指定了 get 函数,并将 RR 模块设置的 get 函数保存到 iphp->get_rr_peer 中。

经过上面的这些处理,请求 r 就和后端 peer 信息关联了起来。

RR模式

本节介绍一下 sticky 模块依赖的 RR 负载均衡模块相关的字段含义和根据权重进行 peer 选择的算法。

  • peer->down - 当前 peer 是否在配置文件中就被标识为不可用状态。在 ip_hash 模式下对下线的服务加上此标识,就不会改变其它 peerhash 值。

  • pc->tries - 该请求还可以再尝试连接 peer 的次数 (也就是剩余可用的 peer 个数)。

  • peer->checked - peer 最近一次检查点 (记录最近一次失败或成功时间点)。

  • peer->accessed - peer 最近一次失败时间点。

  • peer->max_fails - The number of unsuccessful attempts to communicate with the server that should happen in the duration set by the fail_timeout parameter to consider the server unavailable for a duration also set by the fail_timeout parameter.

    • peer 累计失败次数超过 max_fails 并且最近一次失败在 fail_timeout 内, 不再选用此 peer

      if (peer->fails >= peer->max_fails
          && now - peer->checked <= peer->fail_timeout)
      {
          continue;
      }
      
    • fail_timeout 间隔内,此 peer 未失败 (未被选用或成功连接),认为其可 用

      /* get */
      if (now - best->checked > best->fail_timeout) {
          best->checked = now;
      }
      
    • 如果此 peer 确实可用 (在 fail_timeout 中再未失败),重置其失败计数

      if (peer->accessed < peer->checked) {
          peer->fails = 0;
      }
      
    • 如果此 peer 依然不可用,标记失败状态

      peer->fails++;
      peer->accessed = now;
      peer->checked = now;
      
  • 根据权值选择 peer 的算法说明 (1.0.15-1.2.9 之间的某个版本开始使用):

    changeset 4621:c90801720a0c  
    author  Maxim Dounin <mdounin@mdounin.ru>
    date    Mon, 14 May 2012 09:57:20 +0000 (20 months ago)
    
    Upstream: smooth weighted round-robin balancing.
    
    For edge case weights like { 5, 1, 1 } we now produce { a, a, b, a, c, a, a }
    sequence instead of { c, b, a, a, a, a, a } produced previously.
    
    Algorithm is as follows: on each peer selection we increase current_weight
    of each eligible peer by its weight, select peer with greatest current_weight
    and reduce its current_weight by total number of weight points distributed
    abababababababaaadddafdafdsafmong peers.
    
    In case of { 5, 1, 1 } weights this gives the following sequence of
    current_weight's:
    
         a  b  c
         0  0  0  (initial state)
    
         5  1  1  (a selected)
        -2  1  1  (before returning a)
    
         3  2  2  (a selected)
        -4  2  2  (before returning a)
    
         1  3  3  (b selected)
         1 -4  3  (before returning b)
    
         6 -3  4  (a selected)
        -1 -3  4  (before returning b)
    
         4 -2  5  (c selected)
         4 -2 -2  (before returning c)
    
         9 -1 -1  (a selected)
         2 -1 -1  (before returning c)
    
         7  0  0  (a selected)
         0  0  0  (before returning a)
    
    To preserve weight reduction in case of failures the effective_weight
    variable was introduced, which usually matches peer's weight, but is
    reduced temporarily on peer failures.
    
    上述算法在不考虑 peer 失败情况的实现
    
    peers->peer[n].effective_weight = server[i].weight;
    ...
    for (i = 0; i < rrp->peers->number; i++) {
        ...
        peer->current_weight += peer->effective_weight;
        total += peer->effective_weight;
        ...
        if (best == NULL || peer->current_weight > best->current_weight) {
            best = peer;
        }
    }
    ...
    best->current_weight -= total;
    

连接和失败

再回到 sticky 模块,upstream 模块使用 sticky 模块选取到合适的 peer 后, 开始尝试连接 peer

peer 选取操作由 r->upstream.peer.get,即 ngx_http_get_sticky_peer 函数完 成。

如果此请求并未带有任何 sticky 模块能够识别的 Cookie,ngx_http_get_sticky_peer 会调用 RR 模块的 get 方法获取 peer,这种情况下,sticky 还需要给请求响应添 加新的 Set-Cookie 字段标识此 peer,以便此客户端下次访问时直接使用。

peer 连接失败时,sticky 根据 no_fallback 标志决定是否再次调用 RR 模块 得到另外一个 peer

几个事实

  • 对任何请求来说,peer 的选择都按照 primary, backup 的顺序进行。

  • peer 是否失效,由 upstream 模块的使用者定义,比如,通过 proxy_next_upsteam。 目前 upstream 能够区分的 peer 失败类型有:

    #define NGX_HTTP_UPSTREAM_FT_ERROR           0x00000002
    #define NGX_HTTP_UPSTREAM_FT_TIMEOUT         0x00000004
    #define NGX_HTTP_UPSTREAM_FT_INVALID_HEADER  0x00000008
    #define NGX_HTTP_UPSTREAM_FT_HTTP_500        0x00000010
    #define NGX_HTTP_UPSTREAM_FT_HTTP_502        0x00000020
    #define NGX_HTTP_UPSTREAM_FT_HTTP_503        0x00000040
    #define NGX_HTTP_UPSTREAM_FT_HTTP_504        0x00000080
    #define NGX_HTTP_UPSTREAM_FT_HTTP_404        0x00000100
    #define NGX_HTTP_UPSTREAM_FT_UPDATING        0x00000200
    #define NGX_HTTP_UPSTREAM_FT_BUSY_LOCK       0x00000400
    #define NGX_HTTP_UPSTREAM_FT_MAX_WAITING     0x00000800
    #define NGX_HTTP_UPSTREAM_FT_NOLIVE          0x40000000
    #define NGX_HTTP_UPSTREAM_FT_OFF             0x80000000
    
  • peer 的失败处理都由 ngx_http_upstream_next 完成,它会调用负载均衡模块提供 的 free 回调函数。

  • 注释太多会影响代码可读性。

遗留问题

  1. sticky 模块未考虑 primary 后端 peer 全部失效,backup 被启用的情况?
  2. sticky 模块还有许多可以修改的地方,留作以后的项目。
Comments

不要轻轻地离开我,请留下点什么...


Published

Category

Nginx

Tags

Contact