(nginx源码系列五)--nginx文件的内存缓存分析

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;

...

//如果cache结构没有被初始化
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) {
...
//找到
}
/* not found */

//打开该文件,保存信息
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);

/* drop one or two expired open files */
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);

//如果n不为0而且这个文件没有过期,那么直接返回
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
))
{
//没问题就直接到found标记做找到的操作了
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) {

/*
* chances that directory became file are very small
* so test_dir flag allows to use a single syscall
* in ngx_file_info() instead of three syscalls
*/
//源码的解释很清楚了,是为了防止目录变成文件,给目录加了标记

of->test_dir = 1;
}

//重新打开加入of信息
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) {
//目录还是目录,直接update,found
if (file->is_dir || file->err) {
goto update;
}
//文件变成目录,检查不通过
/* file became directory */

} else if (of->err == 0) { /* file */
//文件变成目录,重新添加事件进行检查,而后update,found
if (file->is_dir || file->err) {
goto add_event;
}
//文件的uniq值未发生变化,进行update,found
if (of->uniq == file->uniq) {
if (file->event) {
file->use_event = 1;
}

of->is_directio = file->is_directio;

goto update;
}
//文件变化了,检查不通过
/* file was changed */

} else { /* error to cache */
//文件发生了错误,如果以前也是错误,那么update,found
if (file->err || file->is_dir) {
goto update;
}
//文件以前没有错误,说明文件被删除了,那么检查不通过
/* file was removed, etc. */
}
//检查不通过并且引用计数为0,那么关闭文件并且加入事件监听,然后update,found
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;
}
//引用技术不为0,那么在红黑树上删除这个节点,把cache到的数目减一,
//给文件设置close标记,然后当作前文的no found处理(会重新添加到文件cache中)
ngx_rbtree_delete(&cache->rbtree, &file->node);

cache->current--;

file->close = 1;

goto create;
}

大致就是这样了,对缓存KEY的访问主要是判断缓存的key是否有变化或者过期。如果有要update然后再转入found。

否则直接转入found。

小结

这里的句柄cache的处理方式在其他系统可以引申为一切需要缓存系统调用返回数据的处理。

利用红黑树做数据的插入和查询,然后利用队列来做过期数据的删除,这种策略值得学习。