nginx对静态文件的读取效率远剩于Apache,而linux上的aio实现又不尽如人意。那么它是 通过什么方式来读取静态文件的呢?

Benchmark on serving static files

本篇分析为了简化和抓住主线,只考虑在nginx配置中禁用sendfiledirectio后,代 码处理静态文件的代码逻辑。

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

不要轻轻地离开我,请留下点什么...

comments powered by Disqus

Published

Category

Nginx

Tags

Contact