nginx的文件缓存分为两种,内存缓存和硬盘缓存。
内存缓存指文件句柄等信息进行缓存,减少使用open等的系统调用。
硬盘缓存指文件被缓存到硬盘上,一般是因为当作反向代理用才会有这种需求。
内存缓存是全模块都可以调用的,因为封装在core/ngx_open_file_cache.c中。
而硬盘缓存只有http模块可以调用,因为封装在http/ngx_http_file_cache.c中,最常见的就是upstream模块的cache了。
硬盘缓存也对内存缓存的进行调用。
因此proxy_cache_path这个命令同时会指定可用内存大小和可用硬盘大小。
我刚接触这块代码的时候也是看的我一头雾水,我还以为http的这个缓存机制是core的缓存机制的一层封装呢。
这篇文章仅仅讨论内存缓存,硬盘缓存相当复杂,hold的住就下篇进行讨论。
文件内存缓存在nginx中的使用
内存缓存也可以说是缓存了句柄,因为nginx进行发送的系统调用基本用的是sendfile,因此缓存了文件句柄也就缓存了文件。
当然,除了缓存了文件句柄还存了一些其他信息(修改时间信息,存在的目录信息,搜索文件的错误信息:文件不存在无权限读取等信息)
nginx自带的http_core_module将在使用了如下指令后使用文件缓存,而默认情况下是关闭的。
open_file_cache max=102400 inactive=20s;
这个将为打开文件指定缓存,默认是没有启用的,max指定缓存数量,建议和打开文件数一致,inactive是指经过多长时间文件没被请求后删除缓存
open_file_cache_valid 30s;
这个是指多长时间检查一次缓存的有效信息。
open_file_cache_min_uses 1;
open_file_cache指令中的inactive参数时间内文件的最少使用次数,如果超过这个数字,文件描述符一直是在缓存中打开的。
当open_file_cache打开后,在默认的http_static_module或者http_index_module中就会自动去调用
当配置了mp4,flv模块时也会在这些模块中被调用,配置gzip后会在gzip_static中被调用
如果是自定义模块,直接调用ngx_open_file_cache作为文件内存缓存也是可以的。
只要使用初始化函数对文件内存缓存的数据结构做一次初始化,随后打开任意文件时使用ngx_open_file_cache函数,在第一次打开该文件时会把该文件加入缓存,第二次打开会直接返回对应的结构。
源码分析
文件内存缓存机制主要使用了两个数据结构,红黑树和队列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ngx_open_file_cache_t *ngx_open_file_cache_init (ngx_pool_t *pool, ngx_uint_t max, time_t inactive) { ... ngx_rbtree_init(&cache->rbtree, &cache->sentinel, ngx_open_file_cache_rbtree_insert_value); ngx_queue_init(&cache->expire_queue); cln = ngx_pool_cleanup_add(pool, 0 ); ... cln->handler = ngx_open_file_cache_cleanup; cln->data = cache; return cache; }
初始化函数主要初始化了红黑树和队列两个数据结构,并且定义了ngx_open_file_cache_cleanup作为内存池销毁的回调函数,这个回调函数会依次去清理红黑和队列的信息,依次进行ngx_close_cached_file的调用来关闭文件。
紧接着的ngx_open_cached_file函数是整个的灵魂函数。
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 ngx_int_t ngx_open_cached_file (ngx_open_file_cache_t *cache, ngx_str_t *name, ngx_open_file_info_t *of, ngx_pool_t *pool) { time_t now; uint32_t hash; ngx_int_t rc; ngx_file_info_t fi; ngx_pool_cleanup_t *cln; ngx_cached_open_file_t *file; ngx_pool_cleanup_file_t *clnf; ngx_open_file_cache_cleanup_t *ofcln; ... if (cache == NULL ) { if (of->test_only) { if (ngx_file_info_wrapper(name, of, &fi, pool->log ) == NGX_FILE_ERROR) { return NGX_ERROR; } ... return NGX_OK; } cln = ngx_pool_cleanup_add(pool, sizeof (ngx_pool_cleanup_file_t )); if (cln == NULL ) { return NGX_ERROR; } rc = ngx_open_and_stat_file(name, of, pool->log ); if (rc == NGX_OK && !of->is_dir) { cln->handler = ngx_pool_cleanup_file; ... } return rc; }
上面这个过程主要是对cache没有初始化的情况下的调用,也就是直接打开文件就返回,不会使用文件内存缓存机制。
1 2 3 4 5 now = ngx_time(); hash = ngx_crc32_long(name->data, name->len); file = ngx_open_file_lookup(cache, name, hash);
对文件进行hash后进行查找,这里查找使用的红黑树
未找到的逻辑很清晰,来看下
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 if (file) { ... } rc = ngx_open_and_stat_file(name, of, pool->log ); if (rc != NGX_OK && (of->err == 0 || !of->errors)) { goto failed; } create: if (cache->current >= cache->max) { ngx_expire_old_cached_files(cache, 0 , pool->log ); } file = ngx_alloc(sizeof (ngx_cached_open_file_t ), pool->log ); ... file->name = ngx_alloc(name->len + 1 , pool->log ); ... ngx_cpystrn(file->name, name->data, name->len + 1 ); file->node.key = hash; ngx_rbtree_insert(&cache->rbtree, &file->node); cache->current++; add_event: ngx_open_file_add_event(cache, file, of, pool->log ); update: file->fd = of->fd; file->err = of->err; if (of->err == 0 ) { file->uniq = of->uniq; file->mtime = of->mtime; file->size = of->size; file->close = 0 ; file->is_dir = of->is_dir; file->is_file = of->is_file; file->is_link = of->is_link; file->is_exec = of->is_exec; file->is_directio = of->is_directio; if (!of->is_dir) { file->count++; } } file->created = now; found:. file->accessed = now; ngx_queue_insert_head(&cache->expire_queue, &file->queue ); if (of->err == 0 ) { if (!of->is_dir) { cln->handler = ngx_open_file_cleanup; ofcln = cln->data; ofcln->cache = cache; ofcln->file = file; ofcln->min_uses = of->min_uses; ofcln->log = pool->log ; } return NGX_OK; } return NGX_ERROR; }
这里主要使用了红黑树进行描述符的查询,这很容易理解。
但是队列又在哪儿使用呢?这就是超时机制的构成基础了。
超时机制并没有使用定时器,而是利用上一章提到的连接的内存池被销毁时会使用一个回调函数。这里这个回调函数为ngx_open_file_cleanup。
1 2 3 4 5 6 7 8 9 10 11 12 static void ngx_open_file_cleanup (void *data) { ngx_open_file_cache_cleanup_t *c = data; c->file->count--; ngx_close_cached_file(c->cache, c->file, c->min_uses, c->log ); ngx_expire_old_cached_files(c->cache, 1 , c->log ); }
将文件的引用计数count减一后,ngx_close_cached_file会尝试去关闭这个文件。
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_close_cached_file (ngx_open_file_cache_t *cache, ngx_cached_open_file_t *file, ngx_uint_t min_uses, ngx_log_t *log ) { if (!file->close) { file->accessed = ngx_time(); ngx_queue_remove(&file->queue ); ngx_queue_insert_head(&cache->expire_queue, &file->queue ); if (file->uses >= min_uses || file->count) { return ; } } ngx_open_file_del_event(file); if (file->count) { return ; } if (file->fd != NGX_INVALID_FILE) { if (ngx_close_file(file->fd) == NGX_FILE_ERROR) { ngx_log_error(NGX_LOG_ALERT, log , ngx_errno, ngx_close_file_n " \"%s\" failed" , file->name); } file->fd = NGX_INVALID_FILE; } if (!file->close) { return ; } ngx_free(file->name); ngx_free(file); }
然后看ngx_expire_old_cached_files
参数n说明是强制删除还是非强制删除
可以看到刚才create标记后,如果文件总量大于设置值,那么就会用0来强制释放1或者2个文件
而ngx_open_file_cleanup是常规检查,不会去强制释放
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 static void ngx_expire_old_cached_files (ngx_open_file_cache_t *cache, ngx_uint_t n, ngx_log_t *log ) { time_t now; ngx_queue_t *q; ngx_cached_open_file_t *file; now = ngx_time(); while (n < 3 ) { if (ngx_queue_empty(&cache->expire_queue)) { return ; } q = ngx_queue_last(&cache->expire_queue); file = ngx_queue_data(q, ngx_cached_open_file_t , queue ); if (n++ != 0 && now - file->accessed <= cache->inactive) { return ; } ngx_queue_remove(q); ngx_rbtree_delete(&cache->rbtree, &file->node); cache->current--; if (!file->err && !file->is_dir) { file->close = 1 ; ngx_close_cached_file(cache, file, 0 , log ); } else { ngx_free(file->name); ngx_free(file); } } }
核心的判断条件是
1 2 3 if (n++ != 0 && now - file->accessed <= cache->inactive) { return ; }
满足这个条件的会直接退出超时处理
不满足这个条件有两种情况
n = 0,然后不判断后面的表达式强制删除
n = 1,2 判断时间来进行删除
所以当强制删除传入时,会强制释放一个,然后删除1到2个文件。
常规检测时会删除1到2个文件
当文件占用最差情况的时候肯定会释放一个才去建立一个,因此不会出现泄漏的情况。
open_file_cache_events
&& 缓存file后的再次访问该key流程
上面有个没看明白的所以一开始没有分析,那就是ngx_open_file_add_event
而这个调用在红黑树找到文件以后调用特别频繁,所以这部分逻辑没分析,现在重新看下
首先是关于add_event这里是指什么event。
网上搜了一圈,没看到什么记录,终于在一个邮件记录里面看到了
What
does open_file_cache_events do?
可以看到这个兄弟和我一样困惑,然后得到回复。
open_file_cache_events activates event based watching for file
descriptor changes instead of periodic checks; avaliable with kqueue
event method.
It's intentionally left undocumented. Don't use it, its unfinished
code (there are known race which leads to SIGSEGV).
所以这里的event指的就是open_file_cache_events,只有在kqueue里面才有用。是unfinished
code。作用是监控文件描述符的变化。
所以这个命令少用为妙。
虽然他实际上没什么用,但是对于我分析代码会造成困惑,现在得到了解答。
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 file = ngx_open_file_lookup(cache, name, hash); if (file) { file->uses++; ngx_queue_remove(&file->queue ); if (file->fd == NGX_INVALID_FILE && file->err == 0 && !file->is_dir) { rc = ngx_open_and_stat_file(name, of, pool->log ); if (rc != NGX_OK && (of->err == 0 || !of->errors)) { goto failed; } goto add_event; } if (file->use_event || (file->event == NULL && (of->uniq == 0 || of->uniq == file->uniq) && now - file->created < of->valid ))
这里使用了两种机制,这两种机制是互斥的。
一个是文件事件检查机制,是kqueue下才有的。
一个是定时检查机制(now - file->created < of->valid)
如果定时检查没有问题,如果of没有uniq值那么就算检查通过了,否则对比uniq值
这个值就是文件属性中的st_ino(同一个设备中的每个文件,这个值都是不同的)。
这个值主要用于判断文件是否被修改(不过这个修改是覆盖这类的,如果你用open打开,然后写入的话,这个值还是一样的)
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 103 104 105 106 107 108 109 110 111 112 113 114 if (file->use_event || (file->event == NULL && (of->uniq == 0 || of->uniq == file->uniq) && now - file->created < of->valid )) { if (file->err == 0 ) { of->fd = file->fd; of->uniq = file->uniq; of->mtime = file->mtime; of->size = file->size; of->is_dir = file->is_dir; of->is_file = file->is_file; of->is_link = file->is_link; of->is_exec = file->is_exec; of->is_directio = file->is_directio; if (!file->is_dir) { file->count++; ngx_open_file_add_event(cache, file, of, pool->log ); } } else { of->err = file->err; of->failed = ngx_open_file_n; } goto found; } if (file->is_dir) { of->test_dir = 1 ; } of->fd = file->fd; of->uniq = file->uniq; rc = ngx_open_and_stat_file(name, of, pool->log ); if (rc != NGX_OK && (of->err == 0 || !of->errors)) { goto failed; } if (of->is_dir) { if (file->is_dir || file->err) { goto update; } } else if (of->err == 0 ) { if (file->is_dir || file->err) { goto add_event; } if (of->uniq == file->uniq) { if (file->event) { file->use_event = 1 ; } of->is_directio = file->is_directio; goto update; } } else { if (file->err || file->is_dir) { goto update; } } if (file->count == 0 ) { ngx_open_file_del_event(file); if (ngx_close_file(file->fd) == NGX_FILE_ERROR) { ngx_log_error(NGX_LOG_ALERT, pool->log , ngx_errno, ngx_close_file_n " \"%V\" failed" , name); } goto add_event; } ngx_rbtree_delete(&cache->rbtree, &file->node); cache->current--; file->close = 1 ; goto create; }
大致就是这样了,对缓存KEY的访问主要是判断缓存的key是否有变化或者过期。如果有要update然后再转入found。
否则直接转入found。
小结
这里的句柄cache的处理方式在其他系统可以引申为一切需要缓存系统调用返回数据的处理。
利用红黑树做数据的插入和查询,然后利用队列来做过期数据的删除,这种策略值得学习。