nginx開發筆記_ngx_hash源碼解析

ngx_hash源碼解析

ngx_hash是nginx中的hash表結構,具備如下特色:nginx

  • 靜態結構,hash表建立後沒法動態添加/刪除KV。
  • 採用連續存儲方式解決碰撞問題。即出現碰撞的KV存放在連續地址。
  • 支持前綴和後綴通配符匹配。

以上特色決定了其高效性與功能侷限性。算法

內存結構&hash_find

根據結構體定義與ngx_hash_find函數能夠看出其內存存放結構數組

typedef struct {
    void             *value;
    u_short           len;
    u_char            name[1];
} ngx_hash_elt_t;

typedef struct {
    //hash表分多個桶,每一個桶內存放hash(key)碰撞的元素
    ngx_hash_elt_t  **buckets;
    ngx_uint_t        size;
} ngx_hash_t;

void *
ngx_hash_find(ngx_hash_t *hash, ngx_uint_t key, u_char *name, size_t len)
{
    ngx_uint_t       i;
    ngx_hash_elt_t  *elt;

    //key % hash->size 選擇桶
    elt = hash->buckets[key % hash->size];
    if (elt == NULL) {
        return NULL;
    }
    while (elt->value) {
        if (len != (size_t) elt->len) {
            goto next;
        }
        //比對key
        for (i = 0; i < len; i++) {
            if (name[i] != elt->name[i]) {
                goto next;
            }
        }
        return elt->value;
    next:
        //計算下一個ele地址,每一個ele長度不固定。
        elt = (ngx_hash_elt_t *) ngx_align_ptr(&elt->name[0] + elt->len, sizeof(void *));
        continue;
    }
    return NULL;
}

示意圖以下:函數

整個hash表結構分紅若干個bucket,每一個bucket內存放key值碰撞的元素。ui

  • 每一個bucket的大小是初始化時指定的一個值(bucket_size),要求大於最大元素的大小。即bucket_size約束了元素的大小。但實際的桶大小還要根據各類信息具體肯定,詳見下文初始化部分。
  • bucket的數量時初始化時根據各類信息計算獲得,詳見下文初始化部分。

每一個元素內保存了完整的key值,注意ngx_hash_elt_t.name實際存儲的內容包括完成的key,不只是1個字節,len表示其真實長度。因此每一個元素的大小是不一致的,根據key的實際長度決定。指針

hash表結構初始化

初始化使用的是ngx_int_t ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)函數。code

ngx_hash_init_t *hinit結構以下:blog

typedef struct {
    ngx_hash_t       *hash; //出參,初始化好的hash表,後續經過ngx_hash_find()函數使用
    ngx_hash_key_pt   key;  //hash計算函數,經常使用選項有ngx_hash_key和ngx_hash_key_lc
    ngx_uint_t        max_size; //最大桶數量,實際數量在函數中計算。
    ngx_uint_t        bucket_size; //每一個桶的大小。
    char             *name; //表名詞
    ngx_pool_t       *pool; //數據pool
    ngx_pool_t       *temp_pool; //臨時pool,僅在須要通配符的hash表初始化是使用,ngx_hash_init()不須要使用
} ngx_hash_init_t;

ngx_hash_key_t *namesngx_uint_t nelts組成一組key不重複的KV集合。nginx提供了另一組函數ngx_hash_keys_array_init()ngx_hash_add_key()用於創造不重複的KV集合列表。遞歸

typedef struct {
    ngx_str_t         key;
    ngx_uint_t        key_hash;
    void             *value;
} ngx_hash_key_t;

ngx_hash_init()邏輯以下dns

//計算元素大小,元素結構參考ngx_hash_elt_t
#define NGX_HASH_ELT_SIZE(name)                                               \
    (sizeof(void *) + ngx_align((name)->key.len + 2, sizeof(void *)))

ngx_int_t
ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)
{
    u_char          *elts;
    size_t           len;
    u_short         *test;
    ngx_uint_t       i, n, key, size, start, bucket_size;
    ngx_hash_elt_t  *elt, **buckets;

    //入參判斷
    if (hinit->max_size == 0) {
        ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,
                      "could not build %s, you should "
                      "increase %s_max_size: %i",
                      hinit->name, hinit->name, hinit->max_size);
        return NGX_ERROR;
    }

    //元素的大小都小於桶大小,保證1個桶能存放至少任意1個元素。
    for (n = 0; n < nelts; n++) {
        if (hinit->bucket_size < NGX_HASH_ELT_SIZE(&names[n]) + sizeof(void *))
        {
            ngx_log_error(NGX_LOG_EMERG, hinit->pool->log, 0,
                          "could not build %s, you should "
                          "increase %s_bucket_size: %i",
                          hinit->name, hinit->name, hinit->bucket_size);
            return NGX_ERROR;
        }
    }
    //test用於計算每一個桶所須要的大小,即hash(key)碰撞的幾個元素大小之和
    test = ngx_alloc(hinit->max_size * sizeof(u_short), hinit->pool->log);
    if (test == NULL) {
        return NGX_ERROR;
    }
    //計算一個初始的桶數量,算法含義沒理解。
    bucket_size = hinit->bucket_size - sizeof(void *);

    start = nelts / (bucket_size / (2 * sizeof(void *)));
    start = start ? start : 1;

    if (hinit->max_size > 10000 && nelts && hinit->max_size / nelts < 100) {
        start = hinit->max_size - 1000;
    }
    //逐步調整,找到一個能放下全部元素的桶數量。
    for (size = start; size <= hinit->max_size; size++) {

        ngx_memzero(test, size * sizeof(u_short));

        for (n = 0; n < nelts; n++) {
            if (names[n].key.data == NULL) {
                continue;
            }

            key = names[n].key_hash % size;
            test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
            //test[key] > bucket_size 表示hash(key)相同的元素總大小 > 桶大小
            //則調整桶數量(size++),減小碰撞,減小hash(key)相同的元素總大小
            if (test[key] > (u_short) bucket_size) {
                goto next;
            }
        }

        goto found;

    next:

        continue;
    }

    size = hinit->max_size;

    ngx_log_error(NGX_LOG_WARN, hinit->pool->log, 0,
                  "could not build optimal %s, you should increase "
                  "either %s_max_size: %i or %s_bucket_size: %i; "
                  "ignoring %s_bucket_size",
                  hinit->name, hinit->name, hinit->max_size,
                  hinit->name, hinit->bucket_size, hinit->name);

found:
    //從新賦值test[],若是是goto found,和以前的test[]是同樣的。
    //test[i]表示第i個桶的大小
    for (i = 0; i < size; i++) {
        test[i] = sizeof(void *);
    }

    for (n = 0; n < nelts; n++) {
        if (names[n].key.data == NULL) {
            continue;
        }

        key = names[n].key_hash % size;
        test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
    }
    //計算表的大小,且保證每一個桶起始地址能夠是cacheline對齊
    len = 0;
    for (i = 0; i < size; i++) {
        if (test[i] == sizeof(void *)) {
            continue;
        }
        test[i] = (u_short) (ngx_align(test[i], ngx_cacheline_size));
        len += test[i];
    }

    //申請hinit->hash和hinit->hash->buckets基本結構空間
    if (hinit->hash == NULL) {
        hinit->hash = ngx_pcalloc(hinit->pool, sizeof(ngx_hash_wildcard_t)
                                             + size * sizeof(ngx_hash_elt_t *));
        if (hinit->hash == NULL) {
            ngx_free(test);
            return NGX_ERROR;
        }
        buckets = (ngx_hash_elt_t **)((u_char *) hinit->hash + sizeof(ngx_hash_wildcard_t));
    } else {
        buckets = ngx_pcalloc(hinit->pool, size * sizeof(ngx_hash_elt_t *));
        if (buckets == NULL) {
            ngx_free(test);
            return NGX_ERROR;
        }
    }
    //分配元素空間,且保證元素起始地址是cacheline對齊的
    elts = ngx_palloc(hinit->pool, len + ngx_cacheline_size);
    if (elts == NULL) {
        ngx_free(test);
        return NGX_ERROR;
    }

    elts = ngx_align_ptr(elts, ngx_cacheline_size);
    //buckets[]與元素空間關聯
    for (i = 0; i < size; i++) {
        if (test[i] == sizeof(void *)) {
            continue;
        }

        buckets[i] = (ngx_hash_elt_t *) elts;
        elts += test[i];
    }

    for (i = 0; i < size; i++) {
        test[i] = 0;
    }
    //將names[]的KV列表複製到hash表結構中
    for (n = 0; n < nelts; n++) {
        if (names[n].key.data == NULL) {
            continue;
        }

        key = names[n].key_hash % size;
        elt = (ngx_hash_elt_t *) ((u_char *) buckets[key] + test[key]);

        elt->value = names[n].value;
        elt->len = (u_short) names[n].key.len;

        ngx_strlow(elt->name, names[n].key.data, names[n].key.len);

        test[key] = (u_short) (test[key] + NGX_HASH_ELT_SIZE(&names[n]));
    }
    //配置每一個桶內最後一個ele->value = NULL;
    for (i = 0; i < size; i++) {
        if (buckets[i] == NULL) {
            continue;
        }

        elt = (ngx_hash_elt_t *) ((u_char *) buckets[i] + test[i]);

        elt->value = NULL;
    }

    ngx_free(test);

    hinit->hash->buckets = buckets;
    hinit->hash->size = size;

    return NGX_OK;
}

輔助初始化

在使用ngx_int_t ngx_hash_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names, ngx_uint_t nelts)時要求names[]時一個key內容不重複列表。構造內容不重複的列表若是每次採用循環判斷當列表巨大時,時間開銷較大,nginx提供2個輔助函數ngx_int_t ngx_hash_keys_array_init(ngx_hash_keys_arrays_t *ha, ngx_uint_t type)ngx_int_t ngx_hash_add_key(ngx_hash_keys_arrays_t *ha, ngx_str_t *key, void *value, ngx_uint_t flags)經過一個簡易的鏈狀hash進行重複檢查。代碼中部分涉及通配符處理的先略過下文再說。

typedef struct {
    ngx_uint_t        hsize; //簡易hash表的桶數量
    ngx_pool_t       *pool;
    ngx_pool_t       *temp_pool;
    ngx_array_t       keys;         //精確匹配的key列表
    ngx_array_t      *keys_hash;    //使用二維數組構造的簡易hash表,用於檢查key是否重複。
    ...
} ngx_hash_keys_arrays_t;
ngx_int_t
ngx_hash_keys_array_init(ngx_hash_keys_arrays_t *ha, ngx_uint_t type)
{
    ...
    if (ngx_array_init(&ha->keys, ha->temp_pool, asize, sizeof(ngx_hash_key_t))
        != NGX_OK) {
        return NGX_ERROR;
    }

    if (ngx_array_init(&ha->dns_wc_head, ha->temp_pool, asize, sizeof(ngx_hash_key_t)) != NGX_OK) {
        return NGX_ERROR;
    }
    ...
}
ngx_int_t
ngx_hash_add_key(ngx_hash_keys_arrays_t *ha, ngx_str_t *key, void *value,
    ngx_uint_t flags)
{
    ...
    //計算hash(key)
    for (i = 0; i < last; i++) {
        if (!(flags & NGX_HASH_READONLY_KEY)) {
            key->data[i] = ngx_tolower(key->data[i]);
        }
        k = ngx_hash(k, key->data[i]);
    }
    k %= ha->hsize;

    /* check conflicts in exact hash */
    //在簡易hash表的桶中查找是否有相同key
    name = ha->keys_hash[k].elts;
    if (name) {
        for (i = 0; i < ha->keys_hash[k].nelts; i++) {
            if (last != name[i].len) {
                continue;
            }
            if (ngx_strncmp(key->data, name[i].data, last) == 0) {
                //經過簡易hash表判斷,找到相同key
                return NGX_BUSY;
            }
        }
    } else {
        if (ngx_array_init(&ha->keys_hash[k], ha->temp_pool, 4, sizeof(ngx_str_t)) != NGX_OK){
            return NGX_ERROR;
        }
    }
    //將key放入簡易hash表中
    name = ngx_array_push(&ha->keys_hash[k]);
    if (name == NULL) {
        return NGX_ERROR;
    }
    *name = *key;
    //將不重複的key放入結果ha->keys列表中
    hk = ngx_array_push(&ha->keys);
    if (hk == NULL) {
        return NGX_ERROR;
    }
    hk->key = *key;
    hk->key_hash = ngx_hash_key(key->data, last);
    hk->value = value;

    return NGX_OK;
    ...
}

通配符匹配

nginx支持3種形式的通配符匹配。

  • .example.com能夠匹配example.comwww.example.com
  • *.example.com 只能夠匹配www.example.com不能匹配example.com
  • www.example.*能夠匹配www.example.com

內部是使用3張hash表分別保存精確匹配、頭部統配、尾部統配。再查找是也區分精確查找、頭部統配查找、尾部統配查找。

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

typedef struct {
    ngx_hash_t        hash;
    void             *value;
} ngx_hash_wildcard_t;//這個結構的含義見下文。


void * ngx_hash_find_combined(ngx_hash_combined_t *hash, ngx_uint_t key, u_char *name, size_t len) {
    void  *value;
    //在精確表查找
    if (hash->hash.buckets) {
        value = ngx_hash_find(&hash->hash, key, name, len);
        if (value) {
            return value;
        }
    }
    if (len == 0) {
        return NULL;
    }
    //在頭部統配表查找
    if (hash->wc_head && hash->wc_head->hash.buckets) {
        value = ngx_hash_find_wc_head(hash->wc_head, name, len);
        if (value) {
            return value;
        }
    }
    //在尾部統配表查找
    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;
}

關於在前綴表和後綴表種如何查找,須要先了解前綴表和後綴表的結構。

爲了查找方便,特別是爲了實現頭部匹配表的查找,對於3中統配形式會進行必定的變化。

  • .example.com形式的通配符會在 精確表中加入example.com 在頭部匹配中加入com.example
  • *.example.com形式的通配符會在頭部匹配中加入com.example.
  • www.example.*形式的通配符會在尾部匹配中加入www.example

處理後都就能實現成從左到右分段匹配。處理代碼詳見ngx_hash_add_key()函數的wildcard:部分該部分有註釋,比較好讀。

進行初步處理後,就要開始構造分段的hash結構了,相關代碼在ngx_hash_wildcard_init()

示例有如下三個處理後的統配符號和對應的value

{
  www.aaa.com  : X1,
  img.aaa.com  : X2,
  www.bbb.com. : X3,
}

將保存成形如這樣的結構

{
  www : {
    aaa : {
      com : X1
    },
    bbb : {
      com : X2
    }
  },
  img : {
    bbb : {
      com : X3
    }
  }
}

相關代碼以下:

ngx_int_t
ngx_hash_wildcard_init(ngx_hash_init_t *hinit, ngx_hash_key_t *names,
    ngx_uint_t nelts)
{
    size_t                len, dot_len;
    ngx_uint_t            i, n, dot;
    ngx_array_t           curr_names, next_names;
    ngx_hash_key_t       *name, *next_name;
    ngx_hash_init_t       h;
    ngx_hash_wildcard_t  *wdc;
...

    for (n = 0; n < nelts; n = i) {
        //按.進行拆分
        dot = 0;
        for (len = 0; len < names[n].key.len; len++) {
            if (names[n].key.data[len] == '.') {
                dot = 1;
                break;
            }
        }

        //第一段保存在curr_names中
        name = ngx_array_push(&curr_names);
        if (name == NULL) {
            return NGX_ERROR;
        }
        name->key.len = len;
        name->key.data = names[n].key.data;
        name->key_hash = hinit->key(name->key.data, name->key.len);
        name->value = names[n].value;

        dot_len = len + 1;
        if (dot) {
            len++;
        }
        //非第一段保存在next_names中
        next_names.nelts = 0;
        if (names[n].key.len != len) {
            next_name = ngx_array_push(&next_names);
            if (next_name == NULL) {
                return NGX_ERROR;
            }

            next_name->key.len = names[n].key.len - len;
            next_name->key.data = names[n].key.data + len;
            next_name->key_hash = 0;
            next_name->value = names[n].value;
        }

        for (i = n + 1; i < nelts; i++) {
            if (ngx_strncmp(names[n].key.data, names[i].key.data, len) != 0) {
                break;
            }
            //將第一段相同的 後面部分添加到next_name
            if (!dot && names[i].key.len > len && names[i].key.data[len] != '.') {
                break;
            }

            next_name = ngx_array_push(&next_names);
            if (next_name == NULL) {
                return NGX_ERROR;
            }

            next_name->key.len = names[i].key.len - dot_len;
            next_name->key.data = names[i].key.data + dot_len;
            next_name->key_hash = 0;
            next_name->value = names[i].value;
        }

        if (next_names.nelts) {
            h = *hinit;
            h.hash = NULL;
            //遞歸構造表
            if (ngx_hash_wildcard_init(&h, (ngx_hash_key_t *) next_names.elts, next_names.nelts) != NGX_OK) {
                return NGX_ERROR;
            }

            wdc = (ngx_hash_wildcard_t *) h.hash;
            if (names[n].key.len == len) {
                wdc->value = names[n].value;
            }
            //bit[0]表示最後是否有.
            //bit[1]是否指向中間hash結構,便是否爲根節點
            name->value = (void *) ((uintptr_t) wdc | (dot ? 3 : 2));
        } else if (dot) {
            name->value = (void *) ((uintptr_t) name->value | 1);
        }
    }

    if (ngx_hash_init(hinit, (ngx_hash_key_t *) curr_names.elts, curr_names.nelts) != NGX_OK)
    {
        return NGX_ERROR;
    }

    return NGX_OK;
}

理解內部存放結構後在看ngx_hash_find_wc_tail()ngx_hash_find_wc_head()就很是簡單了,經過value指針的bit[1]判斷是否爲根節點,根據bit[0]判斷後續段是否必須。

void * ngx_hash_find_wc_tail(ngx_hash_wildcard_t *hwc, u_char *name, size_t len)
{
    void        *value;
    ngx_uint_t   i, key;
    key = 0;
    for (i = 0; i < len; i++) {
        if (name[i] == '.') {
            break;
        }
        key = ngx_hash(key, name[i]);
    }
    if (i == len) {
        return NULL;
    }
    value = ngx_hash_find(&hwc->hash, key, name, i);
    if (value) {
        /*
         * the 2 low bits of value have the special meaning:
         *     00 - value is data pointer;
         *     11 - value is pointer to wildcard hash allowing "example.*".
         */
        if ((uintptr_t) value & 2) {
            i++;
            hwc = (ngx_hash_wildcard_t *) ((uintptr_t) value & (uintptr_t) ~3);
            //遞歸查找
            value = ngx_hash_find_wc_tail(hwc, &name[i], len - i);
            if (value) {
                return value;
            }
            return hwc->value;
        }
        return value;
    }
    return hwc->value;
}
相關文章
相關標籤/搜索