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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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函数,这个函数中可以看到上面的结构都是如何被初始化的。函数比较长,我们来看关键的部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 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函数。这个函数更简单,就是简单的查找出上面创建的共享内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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子系统,我们来看这个函数的片段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 //启动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这个函数,这个函数比较简单,就是设置定时器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 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 的第三个参数,这里由于是两个子进程,所以有两个结构:
1 2 3 4 5 6 7 8 9 10 11 12 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毫秒。
在看这两个handler之前,先看一个结构就是ngx_http_file_cache_node_t,一个cache文件对应一个node,这个node中主要保存了cache 的key和uniq, uniq主要是关联文件,而key是用于红黑树。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 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,我们就来看这个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 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,我觉得这个值设置为可调或许更好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 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。它的遍历也是从后到前的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 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这个函数,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 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()
*
不过这里要注意在nginx的当前cache实现中,只有file_handler和spec_handler被设置了,其他的都是空,因此我们着重来看ngx_http_file_cache_manage_file,这里ngx_walk_tree就不分析了,这个函数主要是遍历所有的cache目录,然后对于每一个cache文件调用file_handler回调。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 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函数比较简单,就是休眠,并重置对应的域:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 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的。