nginx对静态文件的读取效率远剩于Apache,而linux上的aio实现又不尽如人意。那么它是 通过什么方式来读取静态文件的呢?
Benchmark on serving static files
本篇分析为了简化和抓住主线,只考虑在nginx配置中禁用sendfile
和directio
后,代
码处理静态文件的代码逻辑。
a typical start
静态文件由模块ngx_http_static_module
处理,它是一个 content handler
。在nginx
启动时,此模块会注册 NGX_HTTP_CONTENT_PHASE
阶段回调函数
ngx_http_static_handler
。
下面主要分析此函数,为了清晰,在分析过程中,暂时忽略掉异常处理等非主干代码。
/* ngx_http_module_static.c: 48 */
static ngx_int_t
ngx_http_static_handler(ngx_http_request_t *r)
{
u_char *last, *location;
size_t root, len;
ngx_str_t path;
ngx_open_file_info_t of;
ngx_http_core_loc_conf_t *clcf;
ngx_buf_t *b;
...
last = ngx_http_map_uri_to_path(r, &path, &root, 0);
path.len = last - path.data;
of.read_ahead = clcf->read_ahead;
of.directo = clcf->directio;
of.valid = clcf->open_file_cache_valid;
of.min_uses = clcf->open_file_cache_min_uses;
of.errors = clcf->open_file_cache_errors;
of.events = clcf->open_file_cache_events;
ngx_open_cached_file(clcf->open_file_cache, &path, &of, r->pool);
...
r->headers_out.status = NGX_HTTP_OK;
r->headers_out.content_length_n = of.size;
r->headers_out.last_modified_time = of.mtime;
ngx_http_set_content_type(r);
...
b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t);
rc = ngx_http_send_header(r);
b->file_pos = 0;
b->file_last = of.size;
b->in_file = b->file_last ? 1: 0;
b->last_buf = (r == r->main) ? 1: 0;
b->last_in_chain = 1;
b->file_fd = of.fd;
b->file->name = path;
b->file->log = log;
b->file->directio = of.is_directio;
out.buf = b;
out.next = NULL;
return ngx_http_output_filter(r, &out);
}
open_file_cache
open_file_cache_events
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).
所以,events相关的代码我们也可以暂时忽略了。
open_file_cache
core/ngx_open_file_cache.c: 129
open_file_cache
用来存储nginx已经打开的文件的信息。这些信息包括,
- Open file dscriptors, information with their size and modification time;
- Information about the existence of directories;
- Error information when searches for a file - no file, do have rights to read, etc.
open_file_cache
本质上是一个结点类型为 ngx_cached_open_file_t
的二叉对
(rbtree) 和 queue
的 LRU 结构。在配置文件中指令open_file_cache
中的 max
参
数用于指定此 LRU 结构的最大节点个数。
上面的主流程中可以看到,当客户端请求一个文件时,ngx_http_static_handler
会先去
此cache中,查看文件是否已经被打开。如果已经打开,是否能被重用。如果没有打开,打
开文件,并将其相关的信息存入此cache,这个逻辑由ngx_open_cached_file
实现。
在进行上述逻辑之前,nginx先要检查是否开始了open_file_cache
。如果,没有开启
open_file_cache
,直接打开文件并返回。代码如下:
145 if (cache == NULL) {
...
171 rc = ngx_open_and_stat_file(name->data, of, pool->log);
...
182 return rc;
183 }
ngx_open_and_stat_file
打开文件,并且填充 of
的各相应字段。对应信息填充如下:
of->uniq, `st_ino`
of->fd, file descriptor。
of->is_directio, `fcntl`, `O_DIRECT`。
of->mtime, `st_mtime`.
of->size, `st_size`.
of->is_dir, `S_ISDIR((sb)->st_mode)`
of->is_file, `S_ISREG((sb)->st_mode)`
of->is_link, `S_ISLINK((sb->st_mode)`
of->is_exec, `(((sb)->st_mode & S_IXUSER) == S_IXUSER)`
如果开启了open_file_cache
,将执行下面的逻辑。
of->fd = NGX_INVALID_FILE;
of->err = 0;
...
hash = ngx_crc32_long(name->data, name->len);
file = ngx_open_file_lookup(cache, name, hash);
上一段代码很好理解,将文件名crc32后的值做为key,在 open_file_cache
中查找对应
的 ngx_cached_open_file_t
是否存在 (由于crc32的值范围较小,很容易出现碰撞,
nginx对这类问题针对 rbtree 做了特殊处理,即对 key 相等的节点还要判断另外的附加字
段 (在 open_file_cache
附加字段为文件名本身),key相同但是附加字段值不同的,按
照附加字段值的大小规则,继续在二叉树中进行查找)。
在找到文件名对应的 ngx_cached_open_file_t
节点后,
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->data, of, pool->log);
goto add_event;
}
if (file->use_event
|| (file->event == NULL
&& (of->uniq == 0 || of->uniq == file->uniq)
&& now - file->created < of->valid))
{
if (file->err == 0) {
/* fill of's fields with values from file */
of->fd = file->fd;
of->uniq = file->uniq;
of->mtime = file->mtime;
of->size = file->size;
of->is_dir = file->is_dir;
... is_file, is_link, is_exec, 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; /* error msg "open()" */
}
goto found;
}
...
of->fd = file->fd;
of->uniq = file->uniq;
rc = ngx_open_and_stat_file(name->data, of, pool->log);
}
根据 LRU 的定义,最近访问的节点要放到队首。上面的操作,先将刚刚定义的
ngx_cached_open_file_t
节点从queue中删除,以便操作成功后,再将此节点放到 queue
的前端。
if (file->use_event
|| (file->event == NULL
&& (of->uniq == 0 || of->uniq == file->uniq)
&& now - file->created < of->valid))
在使用文件事件检查 (file->use_event == 1) 或者使用定期检查机制
(now - file->created < of->valid) 时,如果文件被cache的信息有效,并且在它对应的
文件上未出现任何错误 (file->err == 0), 那么直接使用 ngx_cached_open_file_t
上
包含的信息。
事件检查机制,只在启用了kqueue机制配置下使用,它使用操作系统接口对文件的inode节
点进行检查,以保证此 ngx_cached_open_file_t
的有效性。
如果从 open_file_cache
中并未找到对应请求文件的 ngx_cached_open_file_t
节点
的话,nginx 将会根据磁盘文件的信息创建一个新的 ngx_cached_open_file_t
, 并写入
open_file_cache
中。具体代码如下,
/* not found */
rc = ngx_open_and_stat_file(name->data, of, pool->log);
...
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++;
file->uses = 1;
file->count = 0;
file->use_event = 0;
file->event = NULL;
add_event:
ngx_open_file_add_event(cache, file, of, pool->log);
update:
...
renew:
file->created = now;
found:
file->accessed = now;
ngx_queue_insert_head(&cache->expire_queue, &file->queue);
if (of->err == 0) {
...
return NGX_OK;
}
...
新创建的 ngx_cached_open_file_t
被插到 LRU queue 的头节点位置,更新相应的时间
戳字段。最后,整个 ngx_open_cached_file
顺利返回。
output-chain
下面回到 ngx_http_static_handler
,在上面描述的 ngx_open_cached_file
执行完毕
后,nginx 开始准备返回文件数据。首先,准备必要的 HTTP 包头,
212 r->headers_out.status = NGX_HTTP_OK;
213 r->headers_out.content_length_n = of.size;
214 r->headers_out.last_modified_time = of.mtime;
215
216 if (ngx_http_set_content_type(r) != NGX_OK) {
...
224 r->allow_ranges = 1;
然后,将文件包装成 ngx_buf_t
,并通过它在 output filter 中读取文件数据。
228 b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
233 b->file = ngx_pcalloc(r->pool, sizeof(ngx_file_t));
238 rc = ngx_http_send_header(r);
237
240 if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
241 return rc;
242 }
243
244 b->file_pos = 0;
245 b->file_last = of.size;
246
247 b->in_file = b->file_last ? 1: 0;
248 b->last_buf = (r == r->main) ? 1: 0;
249 b->last_in_chain = 1;
250
251 b->file->fd = of.fd;
252 b->file->name = path;
253 b->file->log = log;
254 b->file->directio = of.is_directio;
255
256 out.buf = b;
257 out.next = NULL;
258
259 return ngx_http_output_filter(r, &out);
ngx_http_sender_header
调用header filter chain中的各个函数,最终构造并发送HTTP
包头,此函数正常情形最终将返回 NGX_OK
(包头处理正常并且发送完成) 或者
NGX_AGAIN
(包头处理正常但未完全发送)。
然后,使用 ngx_buf_t
生成 ngx_chain_t
类型的变量 out
并调用
ngx_http_output_filter
,此函数调用包体相关的 filter chain
ngx_http_top_body_filter
。在 reponse body 到达其它 filter 比如 (gzip) 之前,需
要用统一的方式存入内存,这样第三方 filters 就不用关心数据到底是在内存中产生的还
是来自磁盘文件。数据来源统一化 (暂且这么叫吧) 由 nginx 标准必选模块
ngx_http_copy_filter_module
完成。
在调用 ngx_http_output_filter
时,out (ngx_chain_t
) 只有唯一的一个buf (
ngx_buf_t
)。明确这一点可以简化后续的代码分析。
ngx_http_copy_filter_module
ngx_http_copy_filter_module
只有一个配置项 ngx_bufs_t
, 其结构为:
64 typedef struct {
65 ngx_int_t num;
66 size_t size;
67 } ngx_bufs_t;
在 nginx 启动时,这个结构体的两个字段分别被初始化为1和32768 (32K)。
此模块是 output filter chain 中第一个对响应包体进行处理的标准模块。它的处理
函数是 ngx_http_copy_filter
。
/* ngx_http_copy_filter_module.c */
79 static ngx_int_t
80 ngx_http_copy_filter(ngx_http_request_t *r, ngx_chain_t *in)
81 {
...
84 ngx_output_chain_ctx_t *ctx;
...
93 ctx = ngx_http_get_module_ctx(r, ngx_http_copy_filter_module);
...
95 if (ctx == NULL) {
...
130 }
136 for ( ;; ) {
137 rc = ngx_output_chain(ctx, in);
139 if (ctx->in == NULL) {
140 r->buffered &= ~NGX_HTTP_COPY_BUFFERED;
141
142 } else {
143 r->buffered |= NGX_HTTP_COPY_BUFFERED;
144 }
...
}
...
195 }
上面摘出的代码可以看出,关键逻辑由 ngx_output_chain
完成。
/* ngx_output_chain.c */
40 ngx_int_t
41 ngx_output_chain(ngx_output_chain_ctx_t *ctx, ngx_chain_t *in)
42 {
...
47 if (ctx->in == NULL && ctx->busy == NULL) {
48
49 /*
50 * the short path for the case when the ctx->in and ctx->busy chains
51 * are empty, the incoming chain is empty too or has the single buf
52 * that does not require the copy
53 */
54
55 if (in == NULL) {
56 return ctx->output_filter(ctx->filter_ctx, in);
57 }
59 if (in->next == NULL
60 #if (NGX_SENDFILE_LIMIT)
61 && !(in->buf->in_file && in->buf->file_last > NGX_SENDFILE_LIMIT)
62 #endif
63 && ngx_output_chain_as_is(ctx, in->buf))
64 {
65 return ctx->output_filter(ctx->filter_ctx, in);
66 }
67 }
68
69 /* add the incoming buf to the chain ctx->in */
70
71 if (in) {
72 if (ngx_output_chain_add_copy(ctx->pool, &ctx->in, in) == NGX_ERROR) {
73 return NGX_ERROR;
74 }
75 }
87 for ( ;; ) {
88
89 while (ctx->in) {
134 if (ctx->buf == NULL) {
135
136 rc = ngx_output_chain_align_file_buf(ctx, bsize);
158 } else if (ngx_output_chain_get_buf(ctx, bsize) != NGX_OK) {
159 return NGX_ERROR;
160 }
161 }
162 }
163
164 rc = ngx_output_chain_copy_buf(ctx);
/* ngx_output_chain.c */
456 static ngx_int_t
457 ngx_output_chain_copy_buf(ngx_output_chain_ctx_t *ctx)
458 {
459 off_t size;
460 ssize_t n;
461 ngx_buf_t *src, *dst;
462 ngx_uint_t sendfile;
475 #if (NGX_SENDFILE_LIMIT)
476
477 if (src->in_file && src->file_pos >= NGX_SENDFILE_LIMIT) {
478 sendfile = 0;
479 }
480
481 #endif
526 #if (NGX_HAVE_FILE_AIO)
527
528 if (ctx->aio_handler) {
529 n = ngx_file_aio_read(src->file, dst->pos, (size_t) size,
530 src->file_pos, ctx->pool);
536 } else {
537 n = ngx_read_file(src->file, dst->pos, (size_t) size,
538 src->file_pos);
539 }
540 #else
541
542 n = ngx_read_file(src->file, dst->pos, (size_t) size, src->file_pos);
543
544 #endif
由以上代码可以看到,在禁用sendfile和directio后 (默认配置),nginx 最终调用
blocking-read
读取数据。所以,这种情况下存在因为磁盘寻址问题造成的进程吞吐量下
降。
为了理解上述代码,下面简单归纳一下上述代码中出现的函数的主要功能:
What if We Trun sendfile
on?
nginx IO Under Linux
24 typedef struct {
25 ngx_recv_pt recv;
26 ngx_recv_chain_pt recv_chain;
27 ngx_recv_pt udp_recv;
28 ngx_send_pt send;
29 ngx_send_chain_pt send_chain;
30 ngx_uint_t flags;
31 } ngx_os_io_t;
17 static ngx_os_io_t ngx_linux_io = {
18 ngx_unix_recv,
19 ngx_readv_chain,
20 ngx_udp_unix_recv,
21 ngx_unix_send,
22 #if (NGX_HAVE_SENDFILE)
23 ngx_linux_sendfile_chain,
24 NGX_IO_SENDFILE
25 #else
26 ngx_writev_chain,
27 0
28 #endif
29 };
Tests
Q. How about the latency while nginx serving a big file with sendfile and directio off?
Comments
不要轻轻地离开我,请留下点什么...