Nginx源代码分析-hash

本文分析基于Nginx-1.2.6,与旧版本或将来版本可能有些许出入,但应该差别不大,可做参考

上篇对nginx中的hash分析还没有结束,因为带有wildcard或wc字样的结构体和函数还没有说明。但我也不知道该从何说起。(其实这篇我理解的可能有错,因为涉及到http module中的代码,还没读透,所以还不吝赐教)

从配置文件的server_name指令说起

在这里有这个指令的详解,这里也有个例子。这个指令有一个动作是比较HTTP报文头中的HOST与server_name的参数,选择最匹配的那个server配置,匹配的先后顺序为:

  • 精确匹配,即HOST与server_name中某个参数相同
  • 与带前置通配符的域名匹配,例如sub.domain.com匹配*.domain.com
  • 与带后置通配符的域名匹配,例如www.example.com匹配www.example.*
  • 与某个正则表达式匹配

比如有配置如下:

#下面这两个配置等价,因为.example.com等价于 example.com  *.example.com
server {
  server_name   example.com  *.example.com  www.example.*;
}

server {
  server_name  .example.com  www.example.*;
}

当读取完配置加载到内存后,应该维持一个什么样的数据结构,才能在每个HTTP请求到来时按上述顺序快速寻找到最佳匹配?

哈希表的功效

容易想到的是hash表,其实nginx中正是利用了三个hash表实现这项功能,一个是普通hash表对应不带通配符的server_name配置,另两个对应带通配符的配置。

为方便描述称存放带通配符的key的哈希表为wildcard hash,一般hash为exact hash。

系统处理过程大致为:

  • 解析出请求的HOST

  • 然后调用ngx_http_find_virtual_server(r,host,len),其中r是封装了HTTP请求的一个结构体,找到最佳匹配的配置cscf(类型为ngx_http_core_srv_conf_t),并在r结构体中记录下来,对应代码如下所示。

  • 继续往下处理

第二步代码:

 <!-- lang: cpp -->
cscf = ngx_hash_find_combined(&r->virtual_names->names,
                              ngx_hash_key(host, len), host, len);
.....
r->srv_conf = cscf->ctx->srv_conf;
r->loc_conf = cscf->ctx->loc_conf;

在寻找最佳匹配时调用了ngx_hash.c中定义的一个函数,要重点关注这个调用:

<!-- lang: cpp -->
 cscf = ngx_hash_find_combined(&r->virtual_names->names,
                              ngx_hash_key(host, len), host, len);

/*r->virtual_names是这个类型,与server_name指令的匹配顺序可以对着看*/
typedef struct {
     ngx_hash_combined_t              names;

     ngx_uint_t                       nregex;
     ngx_http_server_name_t          *regex;
} ngx_http_virtual_names_t;

typedef struct {
    ngx_hash_t            hash;
    ngx_hash_wildcard_t  *wc_head;
    ngx_hash_wildcard_t  *wc_tail;
} ngx_hash_combined_t;

现在回到正题,ngx_hash_combined_t中有三个hash表,调用ngx_hash_find_combined(hash,key,name,len)时,就是在按先后顺序在三个hash表中查找对应的项。

<!-- lang: cpp -->
void *
ngx_hash_find_combined(ngx_hash_combined_t *hash, ngx_uint_t key, u_char *name,
    size_t len)
{
    void  *value;

    /*在普通hash表中查找是否有精准匹配*/
    if (hash->hash.buckets) {
        value = ngx_hash_find(&hash->hash, key, name, len);

        if (value) {
            return value;
        }
    }

    if (len == 0) {
        return NULL;
    }
    /*在wc_head哈希表中查找是否能与某个带前置通配符的项匹配*/
    if (hash->wc_head && hash->wc_head->hash.buckets) {
        value = ngx_hash_find_wc_head(hash->wc_head, name, len);

        if (value) {
        return value;
        }
    }
    /*在wc_tail哈希表中查找是否能与某个带后置通配符的项匹配*/
    if (hash->wc_tail && hash->wc_tail->hash.buckets) {
        value = ngx_hash_find_wc_tail(hash->wc_tail, name, len);

        if (value) {
            return value;
        }
    }

    return NULL;
}

wildcard hash 初始化

初始化过程是ngx_hash_wildcard_init中进行的,但我们只知道这个函数的第二个参数是ngx_hash_key_t数组,其实这个数组中存放的并不是带通配符的域名,而是经过转换之后的。因为对sub.example.com选择匹配时可能会要判断是否有*.example.com或sub.example.*之类的server_name配置,那怎么才能把sub.example.com快速匹配到*.example.com而不是*.example.org呢?

而且当给出的key值既有不带通配符的记做A,又有带前置通配符的记为B,又有带后置通配符的记为C,我们希望从中筛选出A存放到exact hash中,B存放到_head wildcard hash中,C存放到tail wildcard hash中,ngx_hash_keys_array_init和ngx_hash_add_key是做这个的,源码里面有注释。在说明这两个函数之前,先看下涉及到的数据结构:

<!-- lang: cpp -->

typedef struct {
    ngx_uint_t        hsize;

    ngx_pool_t       *pool;
    ngx_pool_t       *temp_pool;

    ngx_array_t       keys;
    ngx_array_t      *keys_hash;

    ngx_array_t       dns_wc_head;
    ngx_array_t      *dns_wc_head_hash;

    ngx_array_t       dns_wc_tail;
    ngx_array_t      *dns_wc_tail_hash;
} ngx_hash_keys_arrays_t;

这个数据结构的说明在这里,下面这段是从他那里复制过来的

hsize: 将要构建的hash表的桶的个数。对于使用这个结构中包含的信息构建的三种类型的hash表都会使用此参数。

pool: 构建这些hash表使用的pool。

temp_pool:在构建这个类型以及最终的三个hash表过程中可能用到临时pool。该temp_pool可以在构建完成以后,被销毁掉。这里只是存放临时的一些内存消耗。

keys: 存放所有非通配符key的数组。

keys_hash: 这是个二维数组,第一个维度代表的是bucket的编号,那么keys_hash[i]中存放的是所有的key算出来的hash值对hsize取模以后的值为i的key。假设有3个key,分别是key1,key2和key3假设hash值算出来以后对hsize取模的值都是i,那么这三个key的值就顺序存放在keys_hash[i][0],keys_hash[i][5], keys_hash[i][6]。该值在调用的过程中用来保存和检测是否有冲突的key值,也就是是否有重复。

dns_wc_head: 存放前向通配符key被处理完成以后的值。比如:“*.abc.com”被处理完成以后,变成“com.abc.”被存放在此数组中。

dns_wc_tail: 存放后向通配符key被处理完成以后的值。比如:“mail.xxx.*”被处理完成以后,变成“mail.xxx.”被存放在此数组中。

dns_wc_head_hash: 该值在调用的过程中用来保存和检测是否有冲突的前向通配符的key值,也就是是否有重复。
dns_wc_tail_hash: 该值在调用的过程中用来保存和检测是否有冲突的后向通配符的key值,也就是是否有重复。

注:keys,dns_wc_head,dns_wc_tail,三个数组中存放的元素时ngx_hash_key_t类型的,而keys_hash,dns_wc_head_hash,dns_wc_tail_hash,三个二维数组中存放的元素是ngx_str_t类型的。

ngx_hash_keys_array_init就是为上述结构分配空间。

ngx_hash_add_key是将带或不带通配符的key转换后存放在上述结构中的,其过程是

  • 先看传入的第三个参数标志标明的key是不是NGX_HASH_WILDCARD_KEY,
    • 如果不是,则在ha->keys_hash中检查是否冲突,冲突就返回NGX_BUSY,否则,就将这一项插入到ha->keys中。
    • 如果是,就判断通配符类型,支持的统配符有三种”*.example.com”, “.example.com”, and “www.example.*“,然后将第一种转换为"com.example.“并插入到ha->dns_wc_head中,将第三种转换为"www.example"并插入到ha->dns_wc_tail中,对第二种比较特殊,因为它等价于”*.example.com”+“example.com”,所以会一份转换为"com.example.“插入到ha->dns_wc_head,一份为"example.com"插入到ha->keys中。当然插入前都会检查是否冲突。

调用ngx_hash_wildcard_init初始化后,生成的哈希表结构(是个决策树吧)如下图所示:

Nginx源代码分析-hash_第1张图片
Nginx源代码分析-hash_第2张图片
Nginx源代码分析-hash_第3张图片

注释:

<!-- lang: cpp -->    
    /*
     * the 2 low bits of value have the special meaning:
     *     00 - value is data pointer for both "example.com"
     *          and "*.example.com";
     *     01 - value is data pointer for "*.example.com" only;
     *     10 - value is pointer to wildcard hash allowing
     *          both "example.com" and "*.example.com";
     *     11 - value is pointer to wildcard hash allowing
     *          "*.example.com" only.
     */

在ngx_http_server_names中有使用。

<!-- lang: cpp -->    

 if (ngx_hash_keys_array_init(&ha, NGX_HASH_LARGE) != NGX_OK) {
    goto failed;
}
......
rc = ngx_hash_add_key(&ha, &name[n].name, name[n].server,
                              NGX_HASH_WILDCARD_KEY);

 if (ha.keys.nelts) {
    hash.hash = &addr->hash;
    hash.temp_pool = NULL;

    if (ngx_hash_init(&hash, ha.keys.elts, ha.keys.nelts) != NGX_OK) {
        goto failed;
    }
}

if (ha.dns_wc_head.nelts) {

    /*这里有排序*/
    ngx_qsort(ha.dns_wc_head.elts, (size_t) ha.dns_wc_head.nelts,
              sizeof(ngx_hash_key_t), ngx_http_cmp_dns_wildcards);

    hash.hash = NULL;
    hash.temp_pool = ha.temp_pool;

    if (ngx_hash_wildcard_init(&hash, ha.dns_wc_head.elts,
                               ha.dns_wc_head.nelts)
        != NGX_OK)
    {
        goto failed;
    }

    addr->wc_head = (ngx_hash_wildcard_t *) hash.hash;
}

if (ha.dns_wc_tail.nelts) {
    /*这里有排序*/
    ngx_qsort(ha.dns_wc_tail.elts, (size_t) ha.dns_wc_tail.nelts,
              sizeof(ngx_hash_key_t), ngx_http_cmp_dns_wildcards);

    hash.hash = NULL;
    hash.temp_pool = ha.temp_pool;

    if (ngx_hash_wildcard_init(&hash, ha.dns_wc_tail.elts,
                               ha.dns_wc_tail.nelts)
        != NGX_OK)
    {
        goto failed;
    }

    addr->wc_tail = (ngx_hash_wildcard_t *) hash.hash;
}

wildcard hash 查找

有了上面wildcard hash的树状的结构图,会容易地读懂代码。

  • ngx_hash_find_wc_head:

    • 比如查找sub.example.com,会首先在wildcard hash中查找com,并根据com的value的低两位判断,发现是11,就继续在级联的hash中查找example,发现其value低两位是01,而待查找的key还有sub,则返回((uintptr_t) value & (uintptr_t) ~3),即sub.example.com匹配*.example.com。
    • 但当你要查找的是example.com时,在第二级hash对应项example中发现value低两位是01,而待查找的key已从后往前遍历到头了,则返回NULL,说明example.com不匹配*.example.com。
    • 而当查找domain.com时,先查找com,其value低两位是11,继续在级联hash中查找domain,发现其value是10,且待查找key已从后往前遍历到头,则返回级联的wildcard hash中的value值和~3的与。说明domain.com 匹配 .domain.com。
      
  • ngx_hash_find_wc_tail:后置通配符与前置通配符的处理类似,而且更简单,因为后置通配符没有类似”*.example.com"与”.example.com"的区别。

这篇写的杂乱。。哎。。。

你可能感兴趣的:(nginx,hash,源码分析,wildcard)