本文分析基於Nginx-1.2.6,與舊版本或未來版本可能有些許出入,但應該差異不大,可作參考node
radix tree是一種字典樹,能夠很駕輕就熟地構建關聯數組。在信息檢索中可用於生成文檔的倒排索引,另外,在IP路由選擇中也有其特別的用處。nginx
在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設置適當的值,不一樣平臺下會插入不一樣數量的節點。dom
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便可,這樣子處理,減小了刪除操做的複雜度,這個節點也只有等遇到第一種狀況時纔會真正地從樹中刪除。