nginx构建static location tree的过程

作者:djjsindy 和c/c++相关  

    nginx在处理location的配置的时候,用到了一种三叉排序树,加速了通过request的url和location的映射速度。


    可以从图中看到,static location tree的结构中,left指向的node是比parent的节点小的,right指向的node是比parent节点大的,tree指向拥有parent前缀的节点。初始的static location的类型包括extact_match,前缀匹配。看一下nginx是如何uri和location之间快速做映射的。

  1. static ngx_int_t
  2. ngx_http_core_find_static_location(ngx_http_request_t *r,
  3.     ngx_http_location_tree_node_t *node)
  4. {
  5.     u_char *uri;
  6.     size_t len, n;
  7.     ngx_int_t rc, rv;

  8.     len = r->uri.len;  //request的请求路径长度
  9.     uri = r->uri.data; //request请求的地址

  10.     rv = NGX_DECLINED; //默认精准匹配和前缀匹配 匹配不到,需要匹配后面的正则

  11.     for ( ;; ) {

  12.         if (node == NULL) {
  13.             return rv;
  14.         }

  15.         ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
  16.                        "test location: "%*s"", node->len, node->name);
  17.         //n是uri的长度和node name长度的最小值,好比较他们的交集
  18.         n = (len <= (size_t) node->len) ? len : node->len;
  19.         //比较uri和node 的name交集
  20.         rc = ngx_filename_cmp(uri, node->name, n);
  21.         //不得0表示uri和node的name不相等,这时候三叉树就能加速查找的效率,选择node的左节点或者右节点
  22.         if (rc != 0) {     
  23.             node = (rc < 0) ? node->left : node->right;

  24.             continue; //更新节点后重新开始比较匹配
  25.         }
  26.          //如果交集相等,如果uri的长度比node的长度还要长
  27.         if (len > (size_t) node->len) {

  28.             if (node->inclusive) {//如果这个节点是前缀匹配的那种需要递归tree节点,因为tree节点后面的子节点拥有相同的前缀。
  29.         //因为前缀已经匹配到了,所以这里先暂且把loc_conf作为target,但是不保证后面的tree节点的子节点是否有和uri完全匹配或者更多前缀匹配的。例如如果uri是/abc,当前node节点是/a,虽然匹配到了location /a,先把/a的location配置作为target,但是有可能在/a的tree节点有/abc的location,所以需要递归tree节点看一下。

  30.                 r->loc_conf = node->inclusive->loc_conf;
  31.         //设置成again表示需要递归嵌套location,为什么要嵌套递归呢,因为location的嵌套配置虽然官方不推荐,但是配置的话,父子location需要有相同的前缀。所以需要递归嵌套location
  32.                 rv = NGX_AGAIN;

  33.                 node = node->tree; //node重新变为tree节点
  34.                 uri += n;
  35.                 len -= n;
  36.        continue;
  37.             }

  38.             /* exact only */
  39.         //对于精确匹配的location不会放在公共前缀节点的tree节点中,会单拉出来一个node和前缀节点平行。也就是说对于精确匹配 =/abcd 和前缀匹配的/abc两个location配置,=/abcd不会是/abc节点的tree节点。=/abcd 只能是/abc的right节点
  40.             node = node->right;

  41.             continue;
  42.         }

  43.         if (len == (size_t) node->len) { //如果是uri和node的name是完全相等的

  44.             if (node->exact) {           //如果是精确匹配,那么就是直接返回ok了
  45.                 r->loc_conf = node->exact->loc_conf;
  46.                 return NGX_OK;

  47.             } else {                 //如果还是前缀模式的location,那么需要递归嵌套location了,需要提前设置loc_conf,如果嵌套有匹配的再覆盖
  48.                 r->loc_conf = node->inclusive->loc_conf;
  49.                 return NGX_AGAIN;
  50.             }
  51.         }

  52.         /* len < node->len */

  53.         if (len + 1 == (size_t) node->len && node->auto_redirect) {

  54.             r->loc_conf = (node->exact) ? node->exact->loc_conf:
  55.                                           node->inclusive->loc_conf;
  56.             rv = NGX_DONE;
  57.         }
  58.         //如果前缀相等,uri的长度比node的长度还要小,比如node的name是/abc ,uri是/ab,这种情况是/abc 一定是精确匹配,因为如果是前缀匹配那么/abc 肯定会再/ab的tree 指针里面。
  59.         node = node->left;
  60.     }

    可以从上面的代码看出,三叉树优化了static location的查找过程,防止了O(n)的复杂度来匹配location。location tree的建立过程比较复杂,首先在解析完所有location后,ngx_http_core_loc_conf_t的locations保存了所有的location配置,形成了一个链表,在merge完server和main的配置之后,就开始建立这个static location tree了。


    首先从locations链表中去掉那些正则匹配,还有named和nonamed的location节点。那么location链表中只剩下精准匹配和前缀匹配的那些location节点了,从这些节点中产生static location tree。

生成location tree的必经一步是生成location list,也是一种前缀list,大概的产生步骤是这样,从第一个location节点开始,找到一个与第一个location节点前缀不相同的节点,然后把这个节点之前的list到location,全部作为第一个location的location list,然后递归这个location list,同时继续递归后面剩下的location,说下上图中原始location 的分布。


   从a1开始寻找和a1前缀相同的location,表示没有,所以a1就没有前缀list,继续aa节点,从aa节点到aad节点都是以aa为前缀的,所以location变为了下图。


   然后递归分离的aa节点的list ,aac和aad节点,看aac的节点的后继节点有没有是aac前缀的。然后主location继续递归ab节点的后继节点。最后形成如下的location list。


    最终location list的节点分布如上图,list指针的链表后面的元素都是拥有相同前缀的。

    再说下构建list元素的时候经常用到的一个函数:ngx_queue_split(locations, q, &tail),作用是把location切割成两个双向循环队列,location队列和tail队列,location队列从原始的头元素到q元素之前的元素,tail队列从q元素开始到原location队列的最后一个元素。split操作节点示意图如下:



    看下location list 的建立过程


  1. static void
  2. ngx_http_create_locations_list(ngx_queue_t *locations, ngx_queue_t *q)
  3. {
  4.     u_char *name;
  5.     size_t len;
  6.     ngx_queue_t *x, tail;
  7.     ngx_http_location_queue_t *lq, *lx;

  8.     if (q == ngx_queue_last(locations)) { //如果location为空就没有必要继续走下面的流程了,尤其是递归到嵌套location
  9.         return;
  10.     }

  11.     lq = (ngx_http_location_queue_t *) q;

  12.     if (lq->inclusive == NULL) {
  13.         ngx_http_create_locations_list(locations, ngx_queue_next(q)); //如果这个节点是精准匹配那么这个节点,就不会作为某些节点的前缀,不用拥有tree节点
  14.         return;
  15.     }

  16.     len = lq->name->len;
  17.     name = lq->name->data;

  18.     for (x = ngx_queue_next(q);
  19.          x != ngx_queue_sentinel(locations);
  20.          x = ngx_queue_next(x))
  21.     {
  22.         lx = (ngx_http_location_queue_t *) x;
  23.          //由于所有location已经按照顺序排列好,递归q节点的后继节点,如果后继节点的长度小于后缀节点的长度,那么可以断定,这个后继节点肯定和后缀节点不一样,并且不可能有共同的后缀;如果后继节点和q节点的交集做比较,如果不同,就表示不是同一个前缀,所以可以看出,从q节点的location list应该是从q.next到x.prev节点
  24.         if (len > lx->name->len
  25.             || (ngx_strncmp(name, lx->name->data, len) != 0))
  26.         {
  27.             break;
  28.         }
  29.     }

  30.     q = ngx_queue_next(q);

  31.     if (q == x) { //如果q和x节点直接没有节点,那么就没有必要递归后面了产生q节点的location list,直接递归q的后继节点x,产生x节点location list
  32.         ngx_http_create_locations_list(locations, x);
  33.         return;
  34.     }
  35.     ngx_queue_split(locations, q, &tail); //location从q节点开始分割,那么现在location就是q节点之前的一段list
  36.     ngx_queue_add(&lq->list, &tail); //q节点的list初始为从q节点开始到最后的一段list

  37.     //原则上因为需要递归两段list,一个为p的location list(从p.next到x.prev),另一段为x.next到location的最后一个元素,这里如果x已经是location的最后一个了,那么就没有必要递归x.next到location的这一段了,因为这一段都是空的。

  38.     if (x == ngx_queue_sentinel(locations)) {
  39.         ngx_http_create_locations_list(&lq->list, ngx_queue_head(&lq->list));
  40.         return;
  41.     }
  42.     //到了这里可以知道需要递归两段location list了
  43.     ngx_queue_split(&lq->list, x, &tail);//再次分割,lq->list剩下p.next到x.prev的一段了
  44.     ngx_queue_add(locations, &tail); // 放到location 中去

  45.     ngx_http_create_locations_list(&lq->list, ngx_queue_head(&lq->list)); //递归p.next到x.prev

  46.     ngx_http_create_locations_list(locations, x); //递归x.next到location 最后了
  47. }

    最终static location tree的生成是从这个static location list中得到的也就是上图中的list。


     location list的结构中,原始的那个location 的队列中直升下了a1,aa,ab,ac,ad,ae这几个location节点,tree的构建是个递归的过程,首先从location队列中取中间节点,就认为是tree的root节点,它的list指针认为是tree节点,中间节点之前的那段list ,a1 ,aa认为是ab节点的左节点,ac,ad,ae认为是ab节点的右节点。形成了如下形式:



     然后递归每个container再进行刚才的操作,最终就能成为文章中最一开始的那个图的样子,对于一个tree的生成最重要的就是,把当前的location list折中,中间的节点的前驱list作为左节点,后继list作为右节点,list指针作为tree节点,然后递归每个节点。

看下代码:

  1. static ngx_http_location_tree_node_t *
  2. ngx_http_create_locations_tree(ngx_conf_t *cf, ngx_queue_t *locations,
  3.     size_t prefix)
  4. {
  5.     size_t len;
  6.     ngx_queue_t *q, tail;
  7.     ngx_http_location_queue_t *lq;
  8.     ngx_http_location_tree_node_t *node;

  9.     q = ngx_queue_middle(locations);         //取中间节点

  10.     lq = (ngx_http_location_queue_t *) q;
  11.     len = lq->name->len – prefix;            //len是name减去prefix的长度

  12.     node = ngx_palloc(cf->pool,
  13.                       offsetof(ngx_http_location_tree_node_t, name) + len);
  14.     if (node == NULL) {
  15.         return NULL;
  16.     }

  17.     node->left = NULL;
  18.     node->right = NULL;
  19.     node->tree = NULL;
  20.     node->exact = lq->exact;
  21.     node->inclusive = lq->inclusive;

  22.     node->auto_redirect = (u_char) ((lq->exact && lq->exact->auto_redirect)
  23.                            || (lq->inclusive && lq->inclusive->auto_redirect));

  24.     node->len = (u_char) len;
  25.     ngx_memcpy(node->name, &lq->name->data[prefix], len);      //可以看到实际node的name是父节点的增量(不存储公共前缀,也许这是为了节省空间)

  26.     ngx_queue_split(locations, q, &tail);                  //location队列是从头节点开始到q节点之前的节点,tail是q节点到location左右节点的队列

  27.     if (ngx_queue_empty(locations)) {
  28.         /*
  29.          * ngx_queue_split() insures that if left part is empty,
  30.          * then right one is empty too
  31.          */
  32.         goto inclusive;
  33.     }

  34.     node->left = ngx_http_create_locations_tree(cf, locations, prefix); //递归构建node的左节点
  35. if (node->left == NULL) {
  36.         return NULL;
  37.     }

  38.     ngx_queue_remove(q);

  39.     if (ngx_queue_empty(&tail)) {
  40.         goto inclusive;
  41.     }

  42.     node->right = ngx_http_create_locations_tree(cf, &tail, prefix);//递归构建node的右节点
  43.     if (node->right == NULL) {
  44.         return NULL;
  45.     }

  46. inclusive:

  47.     if (ngx_queue_empty(&lq->list)) {
  48.         return node;
  49.     }

  50.     node->tree = ngx_http_create_locations_tree(cf, &lq->list, prefix + len);      //根据list指针构造node的tree指针
  51.     if (node->tree == NULL) {
  52.         return NULL;
  53.     }

  54.     return node;
  55. }
总结:
      static location tree大大优化了精准匹配和前缀匹配的location的查找过程,线性递归查找效率低下,三叉树的左节点代表当前比node节点的name小的节点,右节点代表比当前node节点name大的节点,tree节点表示拥有相同前缀的节点。
      

相关资料:

nginx构建static location tree的过程来源网络,如有侵权请告知,即处理!

编程Tags: