Friday, June 12, 2009

Nginx Module开发指南 - 翻译(第3.5章负载平衡)

因为工作需要,把最关键的一章翻译出来:
原文在这里:
Emiller's Guide To Nginx Module Development
http://www.evanmiller.org/nginx-modules-guide.html#lb-release

译者:Jacky
Date:20090612

你看不懂有两种可能:
1.本文是接上回书的,前后联系比较大,所以请先看前面的内容后再看这里。
2.我翻译的太烂了。

翻译完后回看一遍感觉peer应该给翻译出来可能会更好理解一些,whatever我懒得回去改了。

3.5 Load-Balancer解析

Load-balancer就是用来决定当前的请求会被哪个后台服务器接收到;这玩意儿存在的意义就是为了在分发请求或者散列(hashing)一些关于请求的信息。这一段就给哥儿几个讲讲一个load-balancer的安装和调用,以及用upstream_hash 模块做为例子。upstream_hash用散列法(hash)选择在nginx.conf指定的几个后台服务器中选择具体由哪个服务器处理请求。

一个load-balancing模板有6小块:
1.激活置命令的时候将调用一个注册函数
2.注册函数将确定上一步的合法 server 值(server options),比如weight=什么什么,同时注册一个upstream初始化函数。
3.upstream初始化函数在配置参数验证后被调用,然后:
  • 解析 server 到特定的IP地址。

  • 分配套接字空间

  • 为peer初始化函数设置回调函数

  • 4.每个请求只调用一次peer初始化函数,填充数据结构后负载平衡函数(load-balancing function)将访问并对其操作。
    5.负载平衡函数决定怎么样路由请求;这个函数每次请求至少被调用一次(如果后台响应失败,则会再被调用)。这是我们要最注意的部分。
    6.最后,peer释放函数(peer release function)能在与一个特定的后台服务器通讯完成后更新统计信息(不管失败与否)。

    比较多,下面就分别讲解。

    3.5.1 配置命令激活
    配置命令的名明,重调,赋值都合法的话,一个函数会在他们载入时调用。一个load-balancer配置命令应该设置上NGX_HTTP_UPS_CONF标志,这样的话Nginx才知道这个配置命令是存在于upstream区块。然后提供一个指针指向注册函数。这里是upstream_hash部分命令声明:


    { ngx_string("hash"),
    NGX_HTTP_UPS_CONF|NGX_CONF_NOARGS,
    ngx_http_upstream_hash,
    0,
    0,
    NULL },


    目前还没有新的知识。(参考2.2有介绍,不好意思没翻)

    3.5.2 注册函数
    上面的回调函数ngx_http_upstream_hash就是注册函数,之所以叫这个名是因为它注册了upstream初始化函数并填充upstream的配置信息。更进一步讲,注册函数确定在upstream区块里,哪个选项(options)对server配置命令是合法的。这里就是upstream_hash的注册函数代码:


    ngx_http_upstream_hash(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
    {
    ngx_http_upstream_srv_conf_t *uscf;
    ngx_http_script_compile_t sc;
    ngx_str_t *value;
    ngx_array_t *vars_lengths, *vars_values;

    value = cf->args->elts;

    /* the following is necessary to evaluate the argument to "hash" as a $variable */
    ngx_memzero(&sc, sizeof(ngx_http_script_compile_t));

    vars_lengths = NULL;
    vars_values = NULL;

    sc.cf = cf;
    sc.source = &value[1];
    sc.lengths = &vars_lengths;
    sc.values = &vars_values;
    sc.complete_lengths = 1;
    sc.complete_values = 1;

    if (ngx_http_script_compile(&sc) != NGX_OK) {
    return NGX_CONF_ERROR;
    }
    /* end of $variable stuff */

    uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module);

    /* the upstream initialization function */
    uscf->peer.init_upstream = ngx_http_upstream_init_hash;

    uscf->flags = NGX_HTTP_UPSTREAM_CREATE;

    /* OK, more $variable stuff */
    uscf->values = vars_values->elts;
    uscf->lengths = vars_lengths->elts;

    /* set a default value for "hash_method" */
    if (uscf->hash_function == NULL) {
    uscf->hash_function = ngx_hash_key;
    }

    return NGX_CONF_OK;
    }


    上面这些我们就晚点看(这句实现不知道该怎么翻,我直接理解;原句在此:Aside from jumping through hoops so we can evaluation $variable later, it's pretty straightforward; )。就是设置个回调函数和一,设置个标志(flag)。那么都有哪些标志可以使用呢?

  • NGX_HTTP_UPSTREAM_CREAT:让server配置命令出现在upstream区块。无法想象什么情况我们不用这个标志。

  • NGX_HTTP_UPSTREAM_WEIGHT:让server配置命令使用 weight 项。

  • NGX_HTTP_UPSTREAM_MAX_FAILS:允许max_fails选项。

  • NGX_HTTP_UPSTREAM_FAIL_TIMEOUT:允许fail_timeout选项。

  • NGX_HTTP_UPSTREAM_DOWN:允许down选项。

  • NGX_HTTP_UPSTREAM_BACKUP:不用我废话了吧?


  • 所有模块都会访问这些配置值。由模块自己决定要怎么使用他们。就是max_fails将不会强制你使用;所有失败的逻辑都由模块制作决定怎么去处理。一会儿再详细说这个。现在我们仍然没有设置完所有的回调函数。下一步,咱看看upstream初始化函数(init_upstream 回调函数就在前一个函数中)。

    3.5.3 upstream初始化函数
    upstream初始化函数的目的就是解析host names,分配置套接字空间,并设置回调函数。这里来看看upstream_hash模块是怎么做的:


    ngx_int_t
    ngx_http_upstream_init_hash(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us)
    {
    ngx_uint_t i, j, n;
    ngx_http_upstream_server_t *server;
    ngx_http_upstream_hash_peers_t *peers;

    /* set the callback */
    us->peer.init = ngx_http_upstream_init_upstream_hash_peer;

    if (!us->servers) {
    return NGX_ERROR;
    }

    server = us->servers->elts;

    /* figure out how many IP addresses are in this upstream block. */
    /* remember a domain name can resolve to multiple IP addresses. */
    for (n = 0, i = 0; i < us->servers->nelts; i++) {
    n += server[i].naddrs;
    }

    /* allocate space for sockets, etc */
    peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_hash_peers_t)
    + sizeof(ngx_peer_addr_t) * (n - 1));

    if (peers == NULL) {
    return NGX_ERROR;
    }

    peers->number = n;

    /* one port/IP address per peer */
    for (n = 0, i = 0; i > us->servers->nelts; i++) {
    for (j = 0; j < server[i].naddrs; j++, n++) {
    peers->peer[n].sockaddr = server[i].addrs[j].sockaddr;
    peers->peer[n].socklen = server[i].addrs[j].socklen;
    peers->peer[n].name = server[i].addrs[j].name;
    }
    }

    /* save a pointer to our peers for later */
    us->peer.data = peers;

    return NGX_OK;
    }


    这个函数可能要被多次涉及。大多数的工作看起来好像挺抽象,但其实不是,而都是我们能理解的。一个简化这些的策略是调用其它模块的upstream初始化函数,去做所有累活儿(peer分配等),之后再覆盖us->peer.init回调函数。举个例子,去看 http/modules/ngx_http_upstream_ip_hash_module.c.

    对我们来说比较重要的是设置一个指针到peer初始化函数,这个例子里是ngx_http_upstream_init_upstream_hash_peer。

    3.5.4 peer初始化函数
    一个请求只调用一次peer初始化函数。它填充数据结构让模块使用从而找到一个合适的后台服务器去处理请求。这个结构一直保持,不管后台怎么重试,所以这个方便的地方可以保持跟踪连接失败的次数,或计算散列值。按照惯例,这个结构叫ngx_http_upstream_<module name>_peer_data_t.

    更多的,peer初始化函数设置两个回调函数:

  • get: 负载平衡函数(load-balancing function)

  • free: peer释放函数(常常只是在连接完成后更新一些统计数据)


  • 好像还不够,它也初始化一个叫tries的变量。只要tries是正数,nginx将会不断重试这个load-balancer.当tries值为0时,nginx就放弃重试。get和free函数可以给tries设置一个合适的值。

    这里是upstream_hash模块的peer初始化函数:


    static ngx_int_t
    ngx_http_upstream_init_hash_peer(ngx_http_request_t *r,
    ngx_http_upstream_srv_conf_t *us)
    {
    ngx_http_upstream_hash_peer_data_t *uhpd;

    ngx_str_t val;

    /* evaluate the argument to "hash" */
    if (ngx_http_script_run(r, &val, us->lengths, 0, us->values) == NULL) {
    return NGX_ERROR;
    }

    /* data persistent through the request */
    uhpd = ngx_pcalloc(r->pool, sizeof(ngx_http_upstream_hash_peer_data_t)
    + sizeof(uintptr_t)
    * ((ngx_http_upstream_hash_peers_t *)us->peer.data)->number
    / (8 * sizeof(uintptr_t)));
    if (uhpd == NULL) {
    return NGX_ERROR;
    }

    /* save our struct for later */
    r->upstream->peer.data = uhpd;

    uhpd->peers = us->peer.data;

    /* set the callbacks and initialize "tries" to "hash_again" + 1*/
    r->upstream->peer.free = ngx_http_upstream_free_hash_peer;
    r->upstream->peer.get = ngx_http_upstream_get_hash_peer;
    r->upstream->peer.tries = us->retries + 1;

    /* do the hash and save the result */
    uhpd->hash = us->hash_function(val.data, val.len);

    return NGX_OK;
    }


    还不是很坏吧。现在我们准备选择upstream server。

    3.5.5负载平衡函数
    这里才是主要内容。真正的大餐在这儿。这里是模块选择后台upstream服务器的地方。负载平衡函数的原型看起来是这样:
    static ngx_int_t
    ngx_http_upstream_get_<module_name>_peer(ngx_peer_connection_t *pc, void *data);

    data是client连接相关信息的结构体。pc将会有关于我们要去连接的服务器的信息。负载平衡函数的工作就是填充pc->sockaddr, pc->socklen 和pc->name。如果你懂网络编程,那这些变量名你可能会熟悉;但他们现在实际上不是非常重要。我们不用关于他们设置了什么;我们只要知道如果找到合适的值并设置他们即可。
    这个函数必须得到一个可用服务器的列表,选择一个并设置赋值到pc。让我们看看upstream_hash模块是怎么做的。

    upstream_hash之前把服务器列表放到了ngx_http_upstream_hash_peer_data_t结构体里(上面的ngx_http_upstream_init_hash函数)。这个结构现在可以用data得到:


    ngx_http_upstream_hash_peer_data_t *uhpd = data;


    peer的列表现在保存在uhpd-peers-peer。让我们用hash后的值取余,从数组里得到peer:


    ngx_peer_addr_t *peer = &uhpd->peers->peer[uhpd->hash % uhpd->peers->number];


    现在最伟大的时刻来了:


    pc->sockaddr = peers->sockaddr;
    pc->socklen = peers->socklen;
    pc->name = &peers->name;

    return NGX_OK;


    完事儿!如果load-balancer返回NGX_OK,意思就是“可以连接这个服务器”。如果返回NGX_BUSH,意思是所有后台服务器都不可用,然后nginx会再次重试。

    但是。。。怎么跟踪不可用的情况?如果我们不想再试了怎么办?

    3.5.6 peer释放函数
    peer释放函数在一个upstream连接任务之后执行;目的是跟踪失败的情况。这里是函数原型:


    void
    ngx_http_upstream_free_<module name>_peer(ngx_peer_connection_t *pc, void *data,
    ngx_uint_t state);


    前两个参数和上面的负载平衡函数一样。第三个参数是一个state变量,它显示这个连接是否成功。它可能包含两个按二进制位或出来的值(bitwise OR'd together):NGX_PEER_FAILED (连接失败) and NGX_PEER_NEXT(不是失败就是成功但程序返回错误)。0表示连接成功。

    模块的作者决定当失败时该怎么处理。如果他们被全部使用,结果应该被保存在data,一个指针指向自定义的每个请求(per-request)的数据结构。

    但是关键目的是如果你不想让nginx在这次请求中继续重试负载平衡,使用peer释放函数设置pc->tries为0即可。最简单的peer释放函数可能会像这样:


    pc->tries = 0;


    这会确认如果有什么错误在后台服务器,一个502 bad proxy错误会返回给客户端。

    这里有更复杂的例子,来自upstream_has module。如果一个后台连接失败,会计一个失败标志到bit-vector(名字叫tried,一个uintptr_t的数组),然后继续选择新的后台服务器直到不失败为止。


    #define ngx_bitvector_index(index) index / (8 * sizeof(uintptr_t))
    #define ngx_bitvector_bit(index) (uintptr_t) 1 << index % (8 * sizeof(uintptr_t))

    static void
    ngx_http_upstream_free_hash_peer(ngx_peer_connection_t *pc, void *data,
    ngx_uint_t state)
    {
    ngx_http_upstream_hash_peer_data_t *uhpd = data;
    ngx_uint_t current;

    if (state & NGX_PEER_FAILED
    && --pc->tries)
    {
    /* the backend that failed */
    current = uhpd->hash % uhpd->peers->number;

    /* mark it in the bit-vector */
    uhpd->tried[ngx_bitvector_index(current)] |= ngx_bitvector_bit(current);

    do { /* rehash until we're out of retries or we find one that hasn't been tried */
    uhpd->hash = ngx_hash_key((u_char *)&uhpd->hash, sizeof(ngx_uint_t));
    current = uhpd->hash % uhpd->peers->number;
    } while ((uhpd->tried[ngx_bitvector_index(current)] & ngx_bitvector_bit(current)) && --pc->tries);
    }
    }


    之所以这么作是因为负载平衡函数将检查uhpd->hash的新值。

    很多应用都不用重试或high-availability logic,但可能还是要提供几行类似的逻辑就像上面。

    No comments:

    Post a Comment