本文分析基于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。
<!-- 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即可,这样子处理,减少了删除操作的复杂度,这个节点也只有等遇到第一种情况时才会真正地从树中删除。