Nginx源代码分析-radix tree

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

radix tree是一种字典树,可以很得心应手地构建关联数组。在信息检索中可用于生成文档的倒排索引,另外,在IP路由选择中也有其特别的用处。

在Nginx中实现了radix tree,其主要用在GEO模块中,这个模块中只有一个指令即geo,通过这个指令可以定义变量,而变量的值依赖于客户端的IP地址(默认使用($remote_addr,但也可设定为其他变量),通过这个模块可以实现负载均衡,对不同区段的用户请求使用不同的后端服务器。一个例子:

 geo  $country  {
   default          no; 
   127.0.0.0/24     us;    #/之前为IP地址address,/之后是地址掩码mask
   127.0.0.1/32     ru;
   10.1.0.0/16      ru;
   192.168.1.0/24   uk;    #当ip地址为192.168.1.23时,变量country的值为uk
 }

nginx在解析上面这段配置时,会构建一个数据结构,并在接受请求后根据客户端IP地址查找对应的变量值,这个数据结构就是radix tree,它是一棵二叉树,其结构图如下所示,每条边对应1bit是0或1。

radix tree

<!-- lang: cpp -->
typedef struct ngx_radix_node_s  ngx_radix_node_t;

struct ngx_radix_node_s {
    ngx_radix_node_t  *right;
    ngx_radix_node_t  *left;
    ngx_radix_node_t  *parent;
    uintptr_t          value;
};

typedef struct {
    ngx_radix_node_t  *root;
    ngx_pool_t        *pool;
    ngx_radix_node_t  *free;
    char              *start;
    size_t             size;
} ngx_radix_tree_t;

为避免频繁地为ngx_radix_node_t分配和释放空间,实现节点的复用,ngx_radix32tree_delete删除节点后并没有释放空间,而是利用ngx_radix_tree_t中的成员free把删除的节点连接成了一个单链表结构,在调用ngx_radix_alloc创建新节点时就先看free右孩子指针所指向的链表是否为空,如果不为空,就从中取出一个节点返回其地址。另外,为radix tree分配空间是以Page为单位的,start指向Page中可用内存的起始位置,size是page中剩余可用的空间大小。

radix tree的创建、插入一节点、删除一节点、查找这四个操作的函数声明如下:

<!-- lang: cpp -->
ngx_radix_tree_t *ngx_radix_tree_create(ngx_pool_t *pool,
    ngx_int_t preallocate);
ngx_int_t ngx_radix32tree_insert(ngx_radix_tree_t *tree,
    uint32_t key, uint32_t mask, uintptr_t value);
ngx_int_t ngx_radix32tree_delete(ngx_radix_tree_t *tree,
    uint32_t key, uint32_t mask);
uintptr_t ngx_radix32tree_find(ngx_radix_tree_t *tree, uint32_t key);

插入节点

geo指令中的“192.168.1.0/24 ru;”这样一条配置就对应了radix tree中的一个节点,那程序中是如何实现的呢?首先看函数ngx_radix32tree_insert中的参数,key是对应in_addr_t类型的ip地址转换成主机字节序后的四个字节,mask即网络掩码,对应于24的是0xFFFFFF00四个字节,value是对应ru的一个 ngx_http_variable_value_t类型的指针。

将value插入那个位置呢?从key&mask的最高位开始,若是0,则转向左孩子节点,否则转向右孩子节点,以此类推沿着树的根节点找到要插入的位置(对应上面例子的要插入的节点在第24层)。若到了叶子节点仍没到达最终位置,那么在叶子节点和最终位置之间空缺的位置上插入value=NGX_RADIX_NO_VALUE的节点。如果对应位置已经有值,返回NGX_BUSY,否则设置对应的value,返回NGX_OK。

创建

为radix tree树结构及其root节点分配空间,并根据preallocate的值向树中插入一定数量的节点,当preallocate等于-1时,会重新为preallocate设置适当的值,不同平台下会插入不同数量的节点。

preallocate的具体含义是,在树中插入第1层到第preallocate层所有的节点,即创建树之后树中共有2^(preallocate+1)-1个节点。那么,当preallocate=-1时,应该为不同的平台设定怎样的值呢?这是由num=ngx_pagesize/sizeof(ngx_radix_node_t)决定的,当为num=128时,preallocate=6,这是因为预先插入节点生成的树是完全二叉树,树的第6层节点都插满时,树共有127个节点占用正好不大于1页内存的空间,增加preallocate继续预先插入节点就会得不偿失。这里我也说不太清楚,贴上注释:

<!-- lang: cpp -->
 /*
 * Preallocation of first nodes : 0, 1, 00, 01, 10, 11, 000, 001, etc.
 * increases TLB hits even if for first lookup iterations.
 * On 32-bit platforms the 7 preallocated bits takes continuous 4K,
 * 8 - 8K, 9 - 16K, etc.  On 64-bit platforms the 6 preallocated bits
 * takes continuous 4K, 7 - 8K, 8 - 16K, etc.  There is no sense to
 * to preallocate more than one page, because further preallocation
 * distributes the only bit per page.  Instead, a random insertion
 * may distribute several bits per page.
 *
 * Thus, by default we preallocate maximum
 *     6 bits on amd64 (64-bit platform and 4K pages)
 *     7 bits on i386 (32-bit platform and 4K pages)
 *     7 bits on sparc64 in 64-bit mode (8K pages)
 *     8 bits on sparc64 in 32-bit mode (8K pages)
 */

查找

现在给定一个ip,应该在radix tree中怎样找到对应的变量值呢?首先将ip地址转换成主机字节序的四个字节,然后调用uintptr_t ngx_radix32tree_find即可,在这个函数中,会将从32位的key的最高位开始,若是0,就转向左孩子,若是1,就转向右孩子,这样从树的根节点开始,直到找到对应的叶子节点为止,在此查找路径上最后一个值不为NGX_RADIX_NO_VALUE的node的value就是所返回的值。
代码如下:

<!-- lang: cpp -->
 uintptr_t
ngx_radix32tree_find(ngx_radix_tree_t *tree, uint32_t key)
{
    uint32_t           bit;
    uintptr_t          value;
    ngx_radix_node_t  *node;

    bit = 0x80000000;
    value = NGX_RADIX_NO_VALUE;
    node = tree->root;

    while (node) {
       if (node->value != NGX_RADIX_NO_VALUE) {
            value = node->value;
       }

        if (key & bit) {
            node = node->right;

        } else {
            node = node->left;
        }

        bit >>= 1;
    }  

    return value;
 }

删除节点

删除过程,首先要先找到要删除的节点,其过程同插入一节点时相同,如果找不到,返回NGX_ERROR,否则就分两种情况:

  • 如果要删除的节点是叶子节点,那么将此节点删除,并插入到free右孩子指针所指向的链表中,留在以后复用,如果删除之后,其父节点成了叶子节点且其值为NGX_RADIX_NO_VALUE,那么也将其父节点执行同样的删除操作,以此类推直到根节点为止;

  • 如果要删除的节点有至少一个孩子,并且这个要删除的节点的值不是NGX_RADIX_NO_VALUE,则只需设定其值为NGX_RADIX_NO_VALUE即可,这样子处理,减少了删除操作的复杂度,这个节点也只有等遇到第一种情况时才会真正地从树中删除。

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