nginx中cache的设计和实现

原文链接http://www.pagefault.info/?p=375#more-375


                              nginx中cache的设计和实现


Nginx的cache实现比较简单,没有使用内存,全部都是使用文件来对后端的response进行cache,因此nginx相比varnish以及squid之类的专门做cache的server,可能效果不会那么好。特别如果cache内容比较大的话。不过还有一种折衷的处理,那就是挂载一个内存盘,然后让nginx cache到这个盘。我这里的看的代码是1.1.17。

首先来看Nginx中如何开启cache,http cache主要是应用在upstream中的,因此upstream对应的两个命令来启用cache,一个是xxx_cache_path(比如proxy_cache_path),它主要是用来创建管理cache的共享内存数据结构(红黑树和队列);一个是xxx_cache,它主要是使用前面创建的zone。

先来看第一个命令,xxx_cache_path,它会调用ngx_http_file_cache_set_slot函数,在看这个函数之前,先来看ngx_http_file_cache_t这个数据结构,它主要用来管理所有的cache文件,它本身不保存cache,只是保存管理cache的数据结构。每一个xxx_cache_path都会创建一个ngx_http_file_cache_t。

typedef struct {
    ngx_rbtree_t                          rbtree;
    ngx_rbtree_node_t                sentinel;
    ngx_queue_t                         queue;
    //cold表示这个cache是否已经被loader进程load过了
    ngx_atomic_t                         cold;
    //那个进程正在load这个cache
    ngx_atomic_t                        loading;
    //文件大小
    off_t                                      size;
} ngx_http_file_cache_sh_t;
 
 
struct ngx_http_file_cache_s {
    ngx_http_file_cache_sh_t         *sh;
    ngx_slab_pool_t                       *shpool;
    //cache的目录
    ngx_path_t                               *path;
    //当前的path下的所有cache文件的最大值
    off_t                                          max_size;
    size_t                                        bsize;
    //如果多久不使用就被删除
    time_t                                        inactive;
    //当前有多少个cache文件(超过loader_files之后会被清0)
    ngx_uint_t                                 files;
    //这个值也就是一个阈值,当load的文件个数大于这个值之后,

    //load进程会短暂的休眠(时间位loader_sleep)
    ngx_uint_t                                 loader_files;
    //最后被manage或者loader访问的时间
    ngx_msec_t                               last;
    //和上面的loader_files配合使用,当文件个数大于loader_files,就会休眠
    ngx_msec_t                               loader_sleep;
    //配合上面的last,也就是loader遍历的休眠间隔。
    ngx_msec_t                               loader_threshold;
    //共享内存的地址
    ngx_shm_zone_t                      *shm_zone;
};


然后这里有一个很关键的结构就是ngx_path_t,它保存了当前的cache的一些信息,以及对应的cache管理回调函数manger和loader。

typedef struct {
    //cache名字
    ngx_str_t                             name;
    size_t                                  len;
    size_t                                  level[3];
    //对应的回调,以及回调数据
    ngx_path_manager_pt        manager;
    ngx_path_loader_pt            loader;
    void                                   *data;
    u_char                              *conf_file;
    ngx_uint_t                          line;
} ngx_path_t;


然后我们来看ngx_http_file_cache_set_slot函数,这个函数中可以看到上面的结构都是如何被初始化的。函数比较长,我们来看关键的部分:

    ngx_http_file_cache_t  *cache;
    .......
    //初始化cache对象,这两个回调后续会详细分析
    cache->path->manager = ngx_http_file_cache_manager;
    cache->path->loader = ngx_http_file_cache_loader;
    cache->path->data = cache;
    cache->path->conf_file = cf->conf_file->file.name.data;
    cache->path->line = cf->conf_file->line;
    //初始化这几个阈值
    cache->loader_files = loader_files;
    cache->loader_sleep = (ngx_msec_t) loader_sleep;
    cache->loader_threshold = (ngx_msec_t) loader_threshold;
    //将path添加到全局的路径管理中
    if (ngx_add_path(cf, &cache->path) != NGX_OK) {
        return NGX_CONF_ERROR;
    }
    //创建共享内存
    cache->shm_zone = ngx_shared_memory_add(cf, &name, size, cmd->post);
    if (cache->shm_zone == NULL) {
        return NGX_CONF_ERROR;
    }
 
    if (cache->shm_zone->data) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                           "duplicate zone \"%V\"", &name);
        return NGX_CONF_ERROR;
    }
 
    //设置初始化函数
    cache->shm_zone->init = ngx_http_file_cache_init;
    cache->shm_zone->data = cache;
    //设置cache
    cache->inactive = inactive;
    cache->max_size = max_size;

    ...........


然后是每个upstream模块的xxx_cache命令,这个命令对应xxx_cache函数,我们这里就看看ngx_http_fastcgi_cache函数。这个函数更简单,就是简单的查找出上面创建的共享内存。

static char *
ngx_http_fastcgi_cache(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
     ................
    //查找出共享内存
    flcf->upstream.cache = ngx_shared_memory_add(cf, &value[1], 0,
                                                 &ngx_http_fastcgi_module);
    if (flcf->upstream.cache == NULL) {
        return NGX_CONF_ERROR;
    }
 
    return NGX_CONF_OK;
}


然后来看cache系统的启动。


当配置解析完毕之后就会进入进程的初始化部分也就是ngx_master_process_cycle这个函数,而在这个函数中将会启动nginx的worker,并且会启动对应的cache manger和cache loader,也就是说cache manger和cache loader是一对子进程,并且独立于worker。

cache manger的作用是用来定时删除无用的cache文件(引用计数为0),一般来说只有manger会删除无用的cache(特殊情况,比如在loader中分配共享内存失败可能会强制删除一些cache, 或者说 loader的时候遇到一些特殊文件)。

cache loader的的主要作用是遍历cache目录,然后加载一些没有被加载的文件(比如nginx重启后,也就是上次遗留的文件),或者说将cache文件重新插入(因为删除是使用LRU算法),后续能够看到代码细节。

接下来就来看代码,首先在中 ngx_master_process_cycle会调用ngx_start_cache_manager_processes来启动cache子系统,我们来看这个函数的片段:

    //启动manger子进程
    ngx_spawn_process(cycle, ngx_cache_manager_process_cycle,
                      &ngx_cache_manager_ctx, "cache manager process",
                      respawn ? NGX_PROCESS_JUST_RESPAWN :

                                       NGX_PROCESS_RESPAWN);
    ..........
    //启动loader子进程
    ngx_spawn_process(cycle, ngx_cache_manager_process_cycle,
                      &ngx_cache_loader_ctx, "cache loader process",
                      respawn ? NGX_PROCESS_JUST_SPAWN :

                                       NGX_PROCESS_NORESPAWN);


从上面可以看到会fork两个子进程,然后子进程的回调是ngx_cache_manager_process_cycle这个函数,这个函数比较简单,就是设置定时器:

static void
ngx_cache_manager_process_cycle(ngx_cycle_t *cycle, void *data)
{
    ngx_cache_manager_ctx_t *ctx = data;
 
    void         *ident[4];
    ngx_event_t   ev;
    .......
    ngx_memzero(&ev, sizeof(ngx_event_t));
    //最核心的在这里了设置传递进来的ctx->handler为定时器回调
    ev.handler = ctx->handler;
    ev.data = ident;
    ev.log = cycle->log;
    ident[3] = (void *) -1;
    ............
    //添加定时器
    ngx_add_timer(&ev, ctx->delay);
    //进入事件循环
    for ( ;; ) {
 
        if (ngx_terminate || ngx_quit) {
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting");
            exit(0);
        }
 
        if (ngx_reopen) {
            ngx_reopen = 0;
            ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reopening logs");
            ngx_reopen_files(cycle, -1);
        }
 
        ngx_process_events_and_timers(cycle);
    }
}


上面的函数可以看到定时器的回调会设置为传递给ngx_cache_manager_process_cycle函数的data,而这个data是什么呢,就是上面ngx_start_cache_manager_processes函数中传递给spawn 的第三个参数,这里由于是两个子进程,所以有两个结构:

static ngx_cache_manager_ctx_t  ngx_cache_manager_ctx = {
    ngx_cache_manager_process_handler, "cache manager process", 0
};
 
static ngx_cache_manager_ctx_t  ngx_cache_loader_ctx = {
    ngx_cache_loader_process_handler, "cache loader process", 60000
};


也就是说manger 和 loader的定时器会分别调用ngx_cache_manager_process_handler和ngx_cache_loader_process_handler,不过可以看到manager的定时器初始时间是0,而loader是60000毫秒。也就是说,manager在nginx一启动时就启动了,但是,loader是在nginx启动了1分钟后才会启动。


在看这两个handler之前,先看一个结构就是ngx_http_file_cache_node_t,一个cache文件对应一个node,这个node中主要保存了cache 的key和uniq, uniq主要是关联文件,而key是用于红黑树。

typedef struct {
    //红黑树和queue结构
    ngx_rbtree_node_t                node;
    ngx_queue_t                      queue;
    //cache key
    u_char                           key[NGX_HTTP_CACHE_KEY_LEN
                                                - sizeof(ngx_rbtree_key_t)];
    //引用计数
    unsigned                         count:20;
    //多少请求在使用
    unsigned                         uses:10;
    unsigned                         valid_msec:10;
    //cache的状态
    unsigned                         error:10;
    //是否存在对应的cache文件
    unsigned                         exists:1;
    //是否正在更新
    unsigned                         updating:1;
    //是否正在删除
    unsigned                         deleting:1;
                                     /* 11 unused bits */
    //文件的uniq
    ngx_file_uniq_t                  uniq;
    //cache失效时间
    time_t                           expire;
    //比如cache control中的max-age
    time_t                           valid_sec;
    //其实应该是body大小
    size_t                           body_start;
    //文件大小
    off_t                            fs_size;
} ngx_http_file_cache_node_t;


然后先来看manager的handler:


static void
ngx_cache_manager_process_handler(ngx_event_t *ev)
{
    time_t        next, n;
    ngx_uint_t    i;
    ngx_path_t  **path;
 
    next = 60 * 60;
 
    path = ngx_cycle->pathes.elts;
    //遍历所有的cache目录
    for (i = 0; i < ngx_cycle->pathes.nelts; i++) {
 
        if (path[i]->manager) {
            //调用manger回调
            n = path[i]->manager(path[i]->data);
            //取得下一次的定时器的时间,可以看到是取n和next的最小值
            next = (n <= next) ? n : next;
 
            ngx_time_update();
        }
    }
 
    if (next == 0) {
        next = 1;
    }
 
    ngx_add_timer(ev, next * 1000);
}


上面最核心的就是path[i]->manager,而这个回调是在一开始介绍的ngx_http_file_cache_set_slot中设置的,它设置manager回调为ngx_http_file_cache_manager,我们就来看这个函数:

static time_t
ngx_http_file_cache_manager(void *data)
{
    ngx_http_file_cache_t  *cache = data;
 
    off_t   size;
    time_t  next, wait;
    //如果有超时的cache,就超时对应的cache
    next = ngx_http_file_cache_expire(cache);
    //最后访问时间
    cache->last = ngx_current_msec;
    cache->files = 0;
 
    for ( ;; ) {
        ngx_shmtx_lock(&cache->shpool->mutex);
 
        size = cache->sh->size;
 
        ngx_shmtx_unlock(&cache->shpool->mutex);
 
        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, ngx_cycle->log, 0,
                       "http file cache size: %O", size);
        //如果没有超过最大的大小限制,则直接返回。
        if (size < cache->max_size) {
            return next;
        }
        //否则遍历所有的cache文件,然后删除过期的cache.
        wait = ngx_http_file_cache_forced_expire(cache);
 
        if (wait > 0) {
            return wait;
        }
 
        if (ngx_quit || ngx_terminate) {
            return next;
        }
    }
}

上面的函数中主要调用了两个函数,一个是ngx_http_file_cache_expire,一个是ngx_http_file_cache_forced_expire,他们有什么区别呢,主要区别是这样子,前一个只有过期的cache才会去尝试删除它(引用计数为0),而后一个不管有没有过期,只要引用计数为0,就会去清理。来详细看这两个函数的实现。

首先是ngx_http_file_cache_expire,这里注意nginx使用了LRU,也就是队列最尾端保存的是最长时间没有被使用的,并且这个函数返回的就是一个wait值,这个值的计算不知道为什么nginx会设置为10ms,我觉得这个值设置为可调或许更好。

static time_t
ngx_http_file_cache_expire(ngx_http_file_cache_t *cache)
{
    ........
 
    now = ngx_time();
 
    ngx_shmtx_lock(&cache->shpool->mutex);
 
    for ( ;; ) {
        //如果cache队列为空,则直接退出返回
        if (ngx_queue_empty(&cache->sh->queue)) {
            wait = 10;
            break;
        }
        //从最后一个开始
        q = ngx_queue_last(&cache->sh->queue);
 
        fcn = ngx_queue_data(q, ngx_http_file_cache_node_t, queue);
 
        wait = fcn->expire - now;
        //如果没有超时,则退出循环
        if (wait > 0) {
            wait = wait > 10 ? 10 : wait;
            break;
        }
        ......


        //如果引用计数为0,则删除这个cache节点
        if (fcn->count == 0) {
            ngx_http_file_cache_delete(cache, q, name);
            continue;
        }
        //如果当前节点正在删除,则退出循环
        if (fcn->deleting) {
            wait = 1;
            break;
        }
 
        p = ngx_hex_dump(key, (u_char *) &fcn->node.key,
                         sizeof(ngx_rbtree_key_t));
        len = NGX_HTTP_CACHE_KEY_LEN - sizeof(ngx_rbtree_key_t);
        (void) ngx_hex_dump(p, fcn->key, len);
 
        /*
         * abnormally exited workers may leave locked cache entries,
         * and although it may be safe to remove them completely,
         * we prefer to just move them to the top of the inactive queue
         */
        //将当前节点放入队列最前端
        ngx_queue_remove(q);
        fcn->expire = ngx_time() + cache->inactive;
        ngx_queue_insert_head(&cache->sh->queue, &fcn->queue);


        .............


    }
 
    ngx_shmtx_unlock(&cache->shpool->mutex);
 
    ngx_free(name);
 
    return wait;
}


然后是ngx_http_file_cache_forced_expire,顾名思义,就是强制删除cache 节点,它的返回值也是wait time,它的遍历也是从后到前的。

static time_t
ngx_http_file_cache_forced_expire(ngx_http_file_cache_t *cache)
{
     ...............
 
    path = cache->path;
    len = path->name.len + 1 + path->len + 2 * NGX_HTTP_CACHE_KEY_LEN;
 
    name = ngx_alloc(len + 1, ngx_cycle->log);
    if (name == NULL) {
        return 10;
    }
 
    ngx_memcpy(name, path->name.data, path->name.len);
 
    wait = 10;
    //删除节点尝试次数
    tries = 20;
 
    ngx_shmtx_lock(&cache->shpool->mutex);
    //遍历队列
    for (q = ngx_queue_last(&cache->sh->queue);
         q != ngx_queue_sentinel(&cache->sh->queue);
         q = ngx_queue_prev(q))
    {
        fcn = ngx_queue_data(q, ngx_http_file_cache_node_t, queue);
 
        ..............................................
        //如果引用计数为0则删除cache
        if (fcn->count == 0) {
            ngx_http_file_cache_delete(cache, q, name);
            wait = 0;
 
        } else {
            //否则尝试20次
            if (--tries) {
                continue;
            }
 
            wait = 1;
        }
 
        break;
    }
 
    ngx_shmtx_unlock(&cache->shpool->mutex);
 
    ngx_free(name);
 
    return wait;
}


上面的分析中还有一个函数就是ngx_http_file_cache_delete,这个函数这里就不分析了,它主要有2个功能,一个是删除cache文件,一个是删除cache管理节点。

然后来看loader的handle,ngx_http_file_cache_loader这个函数:

static void
ngx_http_file_cache_loader(void *data)
{
    ngx_http_file_cache_t  *cache = data;
 
    ngx_tree_ctx_t  tree;
 
    if (!cache->sh->cold || cache->sh->loading) {
        return;
    }
 
    if (!ngx_atomic_cmp_set(&cache->sh->loading, 0, ngx_pid)) {
        return;
    }
 
    ...........
    //设置回调,后续会介绍这几个回调的含义
    tree.init_handler = NULL;
    tree.file_handler = ngx_http_file_cache_manage_file;
    tree.pre_tree_handler = ngx_http_file_cache_noop;
    tree.post_tree_handler = ngx_http_file_cache_noop;
    tree.spec_handler = ngx_http_file_cache_delete_file;
    //回调数据就是cache
    tree.data = cache;
    tree.alloc = 0;
    tree.log = ngx_cycle->log;
    //last为最后load时间
    cache->last = ngx_current_msec;
    cache->files = 0;
    //开始遍历
    if (ngx_walk_tree(&tree, &cache->path->name) == NGX_ABORT) {
        cache->sh->loading = 0;
        return;
    }
 
    cache->sh->cold = 0;
    cache->sh->loading = 0;
    ................................
}


上面有一个很核心的数据结构,就是ngx_tree_ctx_t,它的结构在nginx里面有注释,我们可以看下注释:
* ctx->init_handler() – see ctx->alloc
* ctx->file_handler() – file handler
* ctx->pre_tree_handler() – handler is called before entering directory
* ctx->post_tree_handler() – handler is called after leaving directory
* ctx->spec_handler() – special (socket, FIFO, etc.) file handler
*
* ctx->data – some data structure, it may be the same on all levels, or
* reallocated if ctx->alloc is nonzero
*
* ctx->alloc – a size of data structure that is allocated at every level
* and is initilialized by ctx->init_handler()
*
* ctx->log – a log


不过这里要注意在nginx的当前cache实现中,只有file_handler和spec_handler被设置了,其他的都是空,因此我们着重来看ngx_http_file_cache_manage_file,这里ngx_walk_tree就不分析了,这个函数主要是遍历所有的cache目录,然后对于每一个cache文件调用file_handler回调。

static ngx_int_t
ngx_http_file_cache_manage_file(ngx_tree_ctx_t *ctx, ngx_str_t *path)
{
    ngx_msec_t              elapsed;
    ngx_http_file_cache_t  *cache;
 
    cache = ctx->data;
    //将文件添加进cache
    if (ngx_http_file_cache_add_file(ctx, path) != NGX_OK) {
        (void) ngx_http_file_cache_delete_file(ctx, path);
    }
    //如果文件个数太大,则休眠并清理files计数
    if (++cache->files >= cache->loader_files) {
        ngx_http_file_cache_loader_sleep(cache);
 
    } else {
        ngx_time_update();


        //否则看loader时间是不是过长,如果过长则又进入休眠
        elapsed = ngx_abs((ngx_msec_int_t) (ngx_current_msec - cache->last));
 
        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, ngx_cycle->log, 0,
                       "http file cache loader time elapsed: %M", elapsed);
 
        if (elapsed >= cache->loader_threshold) {
            ngx_http_file_cache_loader_sleep(cache);
        }
    }
 
    return (ngx_quit || ngx_terminate) ? NGX_ABORT : NGX_OK;
}


上面有两个函数没有分析,分别是ngx_http_file_cache_add_file和ngx_http_file_cache_loader_sleep。其中sleep函数比较简单,就是休眠,并重置对应的域:

static void
ngx_http_file_cache_loader_sleep(ngx_http_file_cache_t *cache)
{
    ngx_msleep(cache->loader_sleep);
 
    ngx_time_update();
 
    cache->last = ngx_current_msec;
    cache->files = 0;
}


然后来看ngx_http_file_cache_add_file,它主要是通过文件名计算hash,然后调用ngx_http_file_cache_add将这个文件加入到cache管理中(也就是添加红黑树以及队列),因此我们就主要来看ngx_http_file_cache_add。


static ngx_int_t
ngx_http_file_cache_add(ngx_http_file_cache_t *cache, ngx_http_cache_t *c)
{
    ngx_http_file_cache_node_t  *fcn;
 
    ngx_shmtx_lock(&cache->shpool->mutex);
    //首先查找
    fcn = ngx_http_file_cache_lookup(cache, c->key);
 
    if (fcn == NULL) {
        //如果不存在,则新建结构
        fcn = ngx_slab_alloc_locked(cache->shpool,
                                    sizeof(ngx_http_file_cache_node_t));
        if (fcn == NULL) {
            ngx_shmtx_unlock(&cache->shpool->mutex);
            return NGX_ERROR;
        }
 
        ngx_memcpy((u_char *) &fcn->node.key, c->key, sizeof(ngx_rbtree_key_t));
 
        ngx_memcpy(fcn->key, &c->key[sizeof(ngx_rbtree_key_t)],
                   NGX_HTTP_CACHE_KEY_LEN - sizeof(ngx_rbtree_key_t));


        //插入红黑树
        ngx_rbtree_insert(&cache->sh->rbtree, &fcn->node);
 
        fcn->uses = 1;
        fcn->count = 0;
        fcn->valid_msec = 0;
        fcn->error = 0;
        fcn->exists = 1;
        fcn->updating = 0;
        fcn->deleting = 0;
        fcn->uniq = 0;
        fcn->valid_sec = 0;
        fcn->body_start = 0;
        fcn->fs_size = c->fs_size;
 
        cache->sh->size += c->fs_size;
 
    } else {
        //否则删除queue,后续会重新插入
        ngx_queue_remove(&fcn->queue);
    }
 
    fcn->expire = ngx_time() + cache->inactive;
    //重新插入
    ngx_queue_insert_head(&cache->sh->queue, &fcn->queue);
 
    ngx_shmtx_unlock(&cache->shpool->mutex);
 
    return NGX_OK;
}


下一篇我将会介绍在upstream中如何使用cache的。


期待。。。


(全文完)

你可能感兴趣的:(nginx中cache的设计和实现)