21. 深入 Nginx HTTP 缓存源码:CDN功能

1. 前言

Nginx 作为高性能的 Web 服务器和反向代理,其 HTTP 缓存功能在降低后端负载、加速内容分发上起着至关重要的作用。proxy_cache_pathproxy_cache 等配置指令背后,是一套精心设计的缓存管理机制。本文将从 源码角度 拆解 Nginx HTTP 缓存的核心实现,覆盖 cache zone 内存组织内存不足时的 LRU 淘汰缓存命中判断命中缓存发送流程缓存文件内部结构与 cache manager 进程 ,以及 冷启动时缓存元数据加载 等常见问题。

文中源码基于 Nginx 1.25.x 版本(核心逻辑长期稳定),涉及的源文件主要为 src/http/ngx_http_file_cache.csrc/http/ngx_http_cache.hsrc/core/ngx_rbtree.h 等。阅读本文后,你将能清晰回答:一个 HTTP 请求是如何命中磁盘缓存的、共享内存里到底存了哪些数据,以及缓存空间不足时 Nginx 如何优雅地挤掉旧数据。

2. Cache Zone 内存组织

2.1 共享内存布局

proxy_cache_path keys_zone=NAME:SIZE 声明的 Zone 会创建一块共享内存(ngx_shm_zone_t),多个 worker 进程通过这块内存共享缓存索引。其核心结构体在 ngx_http_cache.h 中:

c 复制代码
typedef struct {
    ngx_http_file_cache_t  *cache;
    ngx_str_t               name;
    ngx_shm_zone_t         *shm_zone;
} ngx_http_file_cache_shm_t;

而真正的缓存主结构 ngx_http_file_cache_t 包含:

c 复制代码
typedef struct {
    ngx_rbtree_t            rbtree;        // 红黑树,key 为缓存 key 的 CRC32
    ngx_rbtree_node_t       sentinel;      // 红黑树哨兵节点
    ngx_queue_t             queue;         // 双向队列,用于 LRU
    ngx_atomic_t            cold;          // 1 = 冷缓存,仅插入不读取
    off_t                   max_size;      // 缓存目录最大容量
    size_t                  bsize;         // 共享内存块大小
    ngx_msec_t              inactive;      // inactive 时间
    time_t                  fail_time;
    ngx_uint_t              files;         // 当前缓存文件总数
    ngx_uint_t              loader_files;  // loader 已加载文件数
    // ... 其他字段
} ngx_http_file_cache_t;

共享内存的初始地址 shm.addr 会被转换为该结构体指针,之后的读写都在此块内存上进行。

2.2 缓存节点结构

每一份缓存条目在共享内存中对应一个 ngx_http_file_cache_node_t 节点,它同时被挂载到两棵"树"上:红黑树 (快速查找)和 LRU 双向队列(淘汰顺序)。

c 复制代码
typedef struct {
    ngx_rbtree_node_t       node;       // 红黑树节点
    ngx_queue_t             queue;      // LRU 队列节点
    u_char                  key[0];     // 变长的缓存 key(紧接结构体之后)
    unsigned                count:20;   // 引用计数
    unsigned                uses:12;    // 访问次数(用于 LRU 权重)
    // 缓存的元信息
    ngx_http_cache_key_t    crc;        // key 的 CRC32
    time_t                  expire;     // 过期时间
    time_t                  valid_sec;  // 有效期
    size_t                  body_start; // body 起始偏移
    off_t                   fs_size;    // 文件大小
    ngx_msec_t              lock_time;  // 锁定时间
    // ...
} ngx_http_file_cache_node_t;
  • 红黑树节点 node 的 key 是 (crc << 32) | key_hash(实际 64 位 key),通过比较这个 key 来查找缓存条目。
  • LRU 队列节点 queue 将所有节点串联成一个双向链表,头部是最近最少使用的节点,尾部是最近最常使用的节点。
  • key 字段为柔性数组,紧跟在节点结构体后面存放完整的 proxy_cache_key 生成的字符串。

这样,每份缓存的内存开销 = sizeof(ngx_http_file_cache_node_t) + key长度,通常约 200~400 字节。一个 10MB 的 keys_zone 大约可以容纳 2~3 万个缓存条目。

2.3 初始化与插入

配置解析时调用 ngx_http_file_cache_set_slot 创建 ngx_http_file_cache_shm_t,然后由 ngx_http_file_cache_init 初始化共享内存中的 ngx_http_file_cache_t,并将红黑树和 LRU 队列准备好。

当有新的缓存文件生成时,ngx_http_file_cache_update 会创建或更新缓存节点:

c 复制代码
static ngx_int_t
ngx_http_file_cache_update(ngx_http_request_t *r, ngx_temp_file_t *tf)
{
    // 1. 计算 crc 和 hash
    // 2. 在红黑树中查找或新建 ngx_http_file_cache_node_t
    // 3. 更新元信息并使用 LRU 算法调整队列
    // 4. 写入缓存文件头
}

关键一点:Nginx 并不立即从共享内存中清除节点,即使磁盘文件已被删除,节点也可能暂时保留,仅在有新文件插入且空间不足时才真正回收。

3. 内存不足时的释放策略

3.1 LRU 淘汰入口

当共享内存中的空闲块不足以分配新节点时,会触发淘汰逻辑,主要入口是 ngx_http_file_cache_free()

c 复制代码
static ngx_http_file_cache_node_t *
ngx_http_file_cache_free(ngx_http_file_cache_t *cache)
{
    ngx_rbtree_node_t       *node;
    ngx_http_file_cache_node_t  *fcn;

    while (!ngx_queue_empty(&cache->queue)) {
        fcn = ngx_queue_data(ngx_queue_last(&cache->queue),
                             ngx_http_file_cache_node_t, queue);
        if (fcn->count == 0) {
            // 从红黑树删除
            ngx_rbtree_delete(&cache->rbtree, &fcn->node);
            // 归还给 slab 分配器
            ngx_slab_free(cache->shpool, fcn);
            return fcn;
        }
        // count > 0,说明节点正在被访问,移动到队首并再次尝试
        ngx_queue_remove(&fcn->queue);
        ngx_queue_insert_head(&cache->queue, &fcn->queue);
    }
    return NULL;
}

这段代码会从 LRU 队列尾部 (即最近最少被使用的一端)向前扫描,找到一个引用计数 count == 0 的节点。找到后将其从红黑树和队列中移除,并通过 ngx_slab_free 归还内存。如果当前节点 count > 0,则将其移动到队列头部,继续尝试下一个,避免长时间等待锁。

3.2 引用计数与 inactive 参数

inactive 参数(例如 inactive=1d)指定了缓存文件的"保鲜期"。但它的作用并不是单纯的文件过期时间,而是在 LRU 淘汰和扫描时决定一个节点是否值得保留。

  • ngx_http_file_cache_free 中并不会检查 inactive,即内存淘汰只看引用计数和 LRU 位置。
  • 真正的过期清理由 Cache Manager 进程负责,它扫描每个缓存文件的元数据,将 access_time + inactive < now 的文件从磁盘和共享内存中删除。
  • 此外,当某次请求尝试更新缓存时,也会对过期的旧条目标记 expired = 1,并可能触发删除。

因此,内存不足时的淘汰是纯 LRU + 引用计数 驱动,而 inactive 时间是磁盘文件的老化标准,两者配合使用。

3.3 关于内存碎片

Nginx 使用内部的 ngx_slab_pool_t 管理共享内存,分配小块内存时采用 slab 分类器,可以很好地避免碎片。但频繁分配和释放依然会产生一定程度的外部碎片。因此官方建议预留 keys_zone 比实际估算稍大一些,避免频繁淘汰引发的性能抖动。

下面用一张流程图概括两种清理路径:
#mermaid-svg-orNVYohPWCT9ieKi{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-orNVYohPWCT9ieKi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-orNVYohPWCT9ieKi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-orNVYohPWCT9ieKi .error-icon{fill:#552222;}#mermaid-svg-orNVYohPWCT9ieKi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-orNVYohPWCT9ieKi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-orNVYohPWCT9ieKi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-orNVYohPWCT9ieKi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-orNVYohPWCT9ieKi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-orNVYohPWCT9ieKi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-orNVYohPWCT9ieKi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-orNVYohPWCT9ieKi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-orNVYohPWCT9ieKi .marker.cross{stroke:#333333;}#mermaid-svg-orNVYohPWCT9ieKi svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-orNVYohPWCT9ieKi p{margin:0;}#mermaid-svg-orNVYohPWCT9ieKi .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-orNVYohPWCT9ieKi .cluster-label text{fill:#333;}#mermaid-svg-orNVYohPWCT9ieKi .cluster-label span{color:#333;}#mermaid-svg-orNVYohPWCT9ieKi .cluster-label span p{background-color:transparent;}#mermaid-svg-orNVYohPWCT9ieKi .label text,#mermaid-svg-orNVYohPWCT9ieKi span{fill:#333;color:#333;}#mermaid-svg-orNVYohPWCT9ieKi .node rect,#mermaid-svg-orNVYohPWCT9ieKi .node circle,#mermaid-svg-orNVYohPWCT9ieKi .node ellipse,#mermaid-svg-orNVYohPWCT9ieKi .node polygon,#mermaid-svg-orNVYohPWCT9ieKi .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-orNVYohPWCT9ieKi .rough-node .label text,#mermaid-svg-orNVYohPWCT9ieKi .node .label text,#mermaid-svg-orNVYohPWCT9ieKi .image-shape .label,#mermaid-svg-orNVYohPWCT9ieKi .icon-shape .label{text-anchor:middle;}#mermaid-svg-orNVYohPWCT9ieKi .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-orNVYohPWCT9ieKi .rough-node .label,#mermaid-svg-orNVYohPWCT9ieKi .node .label,#mermaid-svg-orNVYohPWCT9ieKi .image-shape .label,#mermaid-svg-orNVYohPWCT9ieKi .icon-shape .label{text-align:center;}#mermaid-svg-orNVYohPWCT9ieKi .node.clickable{cursor:pointer;}#mermaid-svg-orNVYohPWCT9ieKi .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-orNVYohPWCT9ieKi .arrowheadPath{fill:#333333;}#mermaid-svg-orNVYohPWCT9ieKi .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-orNVYohPWCT9ieKi .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-orNVYohPWCT9ieKi .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-orNVYohPWCT9ieKi .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-orNVYohPWCT9ieKi .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-orNVYohPWCT9ieKi .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-orNVYohPWCT9ieKi .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-orNVYohPWCT9ieKi .cluster text{fill:#333;}#mermaid-svg-orNVYohPWCT9ieKi .cluster span{color:#333;}#mermaid-svg-orNVYohPWCT9ieKi div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-orNVYohPWCT9ieKi .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-orNVYohPWCT9ieKi rect.text{fill:none;stroke-width:0;}#mermaid-svg-orNVYohPWCT9ieKi .icon-shape,#mermaid-svg-orNVYohPWCT9ieKi .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-orNVYohPWCT9ieKi .icon-shape p,#mermaid-svg-orNVYohPWCT9ieKi .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-orNVYohPWCT9ieKi .icon-shape .label rect,#mermaid-svg-orNVYohPWCT9ieKi .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-orNVYohPWCT9ieKi .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-orNVYohPWCT9ieKi .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-orNVYohPWCT9ieKi :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 场景二:Cache Manager 定时清理
场景一:内存不足触发 LRU 淘汰








Worker 尝试分配新节点
共享内存空闲块不足
调用 ngx_http_file_cache_free()
从 LRU 队列尾部取节点
引用计数 count == 0 ?
从红黑树删除节点
ngx_slab_free 归还内存
新节点成功分配
将节点移至 LRU 队列头部
master 定时器到期
唤醒 Cache Manager 进程
遍历缓存目录,读取文件头
access_time + inactive < now ?
引用计数 count == 0 ?
删除磁盘缓存文件
从共享内存移除节点
跳过,等待下次扫描
总缓存大小 > max_size ?
按 LRU + 过期时间排序删除
暂不清理
manager_sleep 后重新注册定时器
节点从红黑树与 LRU 队列中移除

4. 缓存命中判断

4.1 Cache Key 的生成与查找

Nginx 所使用的 cache key 由 proxy_cache_key 指令定义,默认值为 $scheme$proxy_host$request_uri。生成 key 后,通过 ngx_crc32_long 计算 CRC32,然后调用 ngx_http_file_cache_lookup 在红黑树中查找:

c 复制代码
static ngx_http_file_cache_node_t *
ngx_http_file_cache_lookup(ngx_http_file_cache_t *cache, u_char *key,
    size_t len, ngx_uint_t hash)
{
    ngx_rbtree_node_t  *node, *sentinel;
    ngx_http_file_cache_node_t  *fcn;
    uintptr_t            val;

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, cache->shpool->log, 0, "cache lookup");

    val = ((uintptr_t) hash << 32) | len;
    node = cache->rbtree.root;
    sentinel = cache->rbtree.sentinel;

    while (node != sentinel) {
        if (val < node->key) {
            node = node->left;
            continue;
        }
        if (val > node->key) {
            node = node->right;
            continue;
        }
        // key 相同,进行字符串校验
        fcn = (ngx_http_file_cache_node_t *) node;
        if (len == fcn->node.data
            && ngx_memcmp(key, fcn->key, len) == 0)
        {
            return fcn;
        }
        // 继续在相同 key 的子树中查找
        node = (val < node->key) ? node->left : node->right;
    }
    return NULL;
}

首先使用 (hash << 32) | len 作为红黑树 key 的一部分;若相等,再通过字面比较来避免 hash 冲突。找到节点后,并不代表缓存一定"命中",还需要进行一系列有效性检查。

4.2 有效性验证

查找成功返回节点后,调用方(例如 ngx_http_file_cache_open)会继续验证:

  • 冷启动标志 :若 cache->cold 为 1,说明 loader 正在加载缓存,此时不认为命中,直接回源。
  • 文件状态:检查磁盘上的缓存文件是否存在、尺寸是否正确。
  • 新鲜度 :若节点设置了 expire 且当前时间已过期,则标记 expired = 1,并视配置决定是直接回源还是使用过期内容进行协商(proxy_cache_use_stale)。
  • 并发控制 :对于正在更新或刚生成的缓存文件,会有锁机制防止"惊群效应",通过 ngx_http_file_cache_lock 实现。

一旦所有检查通过,请求便确认"缓存命中",进入发送流程。

5. 命中缓存的发送过程

5.1 缓存发送总览

确定缓存命中后,Nginx 会构造一个特殊的 handler 链,调用 ngx_http_cache_send

c 复制代码
ngx_int_t
ngx_http_cache_send(ngx_http_request_t *r)
{
    // 从缓存节点中获取文件信息
    // 设置响应头,包括 Content-Type、Content-Length、Last-Modified 等
    // 启动文件发送
    return ngx_http_upstream_cache_send(r, &ctx->cache);
}

最终的发送使用了 ngx_http_send_headerngx_http_output_filter,对于文件内容,通过 ngx_http_cache_send_body 利用 sendfile 系统调用将磁盘文件直接写入 socket 缓冲区,零拷贝传输。

5.2 Range 请求与 Slice 模块

发送缓存文件时,还需要处理 Range 请求和 Slice 模块

  • 如果客户端请求 Range 头,并且缓存文件有效,Nginx 会读取文件对应的片段进行发送,而不是发送整个文件。
  • 当启用 slice 指令时,大文件被切割成多个切片,每个切片独立缓存。此时缓存命中需要拼接多个切片,并作为完整响应(或分段)发送。这部分涉及 ngx_http_slice_filter_modulengx_http_file_cache_slice_header 等逻辑,不在本文展开。

5.3 优化要点

  • open_file_cache 与缓存发送无关 ,那个指令针对静态文件的 inode 缓存,不适用于 proxy_cache
  • 高并发场景下,需要考虑 sendfile_max_chunk 限制单次传输大小,避免长时间占用 worker。

6. 缓存文件内容与 Cache Manager 进程

6.1 缓存文件内部格式

Nginx 的磁盘缓存文件并非直接保存后端原始响应体,而是在文件头部附加了一段元数据。结构定义在 ngx_http_cache.h

c 复制代码
typedef struct {
    ngx_http_file_cache_header_t  header;
    u_char                        body[1];  // 实际为一个占位,表示 body 开始
} ngx_http_file_cache_file_t;

typedef struct {
    ngx_uint_t                   version;      // 版本号
    time_t                       valid_sec;    // 缓存有效期
    time_t                       last_modified;// 最后修改时间
    time_t                       date;         // 日期
    ngx_http_cache_key_t         crc;          // key 的 CRC32
    size_t                       header_start; // 响应头起始偏移
    size_t                       body_start;   // 响应体起始偏移
    off_t                        content_length;// 原始响应体长度
    u_char                       etag[NGX_HTTP_CACHE_ETAG_LEN];
    ngx_str_t                    vary[NGX_HTTP_CACHE_VARY_LEN];
    char                         vary_header[...];
} ngx_http_file_cache_header_t;

写入缓存文件时,ngx_http_file_cache_write_header 会先写入 ngx_http_file_cache_header_t,再写入后端返回的 HTTP 响应头,最后才是响应体。因此一个缓存文件的布局为:

复制代码
|-- header (固定大小) --|-- 原始响应头(变长) --|-- 响应体 --|

读取时,通过 header_startbody_start 定位,配合 ngx_http_file_cache_read_header 解析,从而能够重构出完整的 HTTP 响应。

6.2 Cache Manager 进程

Cache Manager 并不是常驻进程,而是一个由 master 定时唤醒的任务(通过 ngx_process_events_and_timers 中的定时器)。核心函数为 ngx_http_file_cache_manager

c 复制代码
static void
ngx_http_file_cache_manager(void *data)
{
    ngx_http_file_cache_t  *cache = data;

    for ( ;; ) {
        // 1. 计算应当清理的体积阈值 (watermark)
        // 2. 遍历所有缓存文件
        while (cache->files < max) {
            if (ngx_http_file_cache_manager_delete(cache) != NGX_OK) {
                break;
            }
        }
        // 3. 重新注册定时器
        ngx_add_timer(&cache->manager_event, cache->manager_sleep);
        return;
    }
}

主要工作:

  • 扫描缓存目录,读取每个文件头。
  • 对比 access_time + inactive 判断是否过期。
  • 若过期且引用计数为 0,删除文件并从共享内存中移除节点。
  • 若总缓存大小超过 max_size,则以 LRU + 过期时间结合的方式删除文件,直到降至安全水位。
  • 为了避免影响正常请求,manager 在每次扫描后都会 sleep(默认 60s,可配置 manager_sleep)。

6.3 与 Worker 协作

当 worker 进程发现缓存文件过期,但 proxy_cache_use_stale 允许使用过期内容时,它会先发送过期数据,同时在后台标记该条目,等待 Manager 进程下次扫描时统一清理。这体现了 Nginx "先服务再清理"的设计哲学。

7. 冷启动时如何处理命中的缓存

7.1 Cache Loader 进程

Nginx 启动或 reload 时,共享内存中的数据全部丢失,但磁盘上仍存有大量缓存文件。为了让 worker 能够命中这些缓存,需要一个 Cache Loader 进程(ngx_http_file_cache_loader)将磁盘元数据加载到共享内存。

c 复制代码
static void
ngx_http_file_cache_loader(void *data)
{
    ngx_http_file_cache_t  *cache = data;

    for ( ;; ) {
        // 1. 打开缓存目录,遍历文件
        // 2. 读取每个缓存文件的 header
        // 3. 在共享内存中创建 ngx_http_file_cache_node_t
        // 4. 限制每次加载文件数,避免长时间阻塞
        ngx_add_timer(&cache->loader_event, cache->loader_sleep);
        return;
    }
}

关键点:

  • Loader 在多个 worker 启动之前运行(由 master 发起),也可以配置 loader_files 限制每次加载的最大文件数,剩余的留待下次 sleep 后再加载。
  • 加载过程中,cache->cold 被置为 1,所有 worker 的缓存查找都会直接返回"未命中",强制回源。这避免了 worker 在索引尚不完整时读到不一致的数据。
  • 当 loader 完成全部扫描后,cold 置为 0,worker 即可正常使用缓存。

7.2 冷启动期间的命中策略

在大型站点中,可能有几十万个缓存文件,loader 可能需要若干分钟才能完成加载。这段时间内,所有请求都会回源,后端压力瞬间增大。为了解决这个问题,Nginx 提供了一种折中:

  • loader_threshold :当 loader 加载的文件数达到此阈值后,会提前将 cold 置为 0。此时未加载的缓存文件对应的请求依然回源,但已经加载的部分可以正常命中。
  • loader_sleep:每次加载若干文件后暂停一段时间(默认 200ms),将 CPU 和 IO 让给其他进程,避免 loader 抢占过多资源。

另外,Nginx 商业版提供了 cache_key_zone_file 指令,可以将共享内存 dump 到文件,下次启动直接从文件恢复,避免 loader 扫描,不过开源版需要通过 nginx-modules 扩展或自行实现。

7.3 冷启动场景实战

假设缓存目录 cache/ 下有 20 万个文件,keys_zone=one:10mloader_files=200loader_sleep=50ms。冷启动过程如下:

  1. master 进程等待配置加载完毕。
  2. 调用 ngx_spawn_process 创建 loader 子进程,并置 cold=1
  3. loader 进程每次读取 200 个文件的头部,在共享内存中创建节点。
  4. 每处理完一批,调用 ngx_msleep(50) 休眠。
  5. 当全部文件扫描完毕(或达到 loader_threshold),loader 进程退出,cold 被置为 0。
  6. 此时 worker 可以命中已加载的缓存。

8. 总结

本文深入了 Nginx HTTP Cache 的内部实现,核心要点回顾:

  • 内存组织 :基于共享内存的红黑树 + LRU 双向队列,每个缓存条目以 ngx_http_file_cache_node_t 存储,key 为 CRC32 + hash。
  • 内存不足淘汰ngx_http_file_cache_free 从 LRU 队尾扫描引用计数为 0 的节点,直接归还 slab 内存,与 inactive 参数解耦。
  • 命中判断 :通过红黑树查找,再校验文件存在性、过期状态、cold 标志等。
  • 发送流程 :读取 header + 原始响应头,使用 sendfile 零拷贝发送,支持 Range 和 Slice。
  • Cache Manager :定期扫描磁盘缓存文件,按 inactive + max_size 清理过期条目,避免影响正常请求。
  • 冷启动 :Cache Loader 进程从磁盘重建共享内存索引,期间 cold 标志阻止命中,加载分批进行以降低冲击。

理解这些源码逻辑,不仅有助于排查 Nginx 缓存的奇怪行为(比如"为什么 reload 后缓存全部回源"),也能针对高并发环境设计更合理的 keys_zone 大小、inactive 时间和 loader 参数。希望本文能成为你深入 Nginx 内部的一把钥匙。

相关推荐
时空无限19 小时前
vllm 缓存对模型启动时间的影响
缓存·vllm
张小姐的猫19 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
2501_916007471 天前
深入理解HTTPS对称与非对称加密机制及Charles抓包实践
网络协议·http·ios·小程序·https·uni-app·iphone
BelongPanda1 天前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
2501_916008891 天前
HTTPS 抓包遇到证书绑定怎么办,使用 TraceEagle 解除 App 证书校验
网络协议·计算机网络·http·网络安全·ios·adb·https
青山木1 天前
Redis 缓存一致性全解:从 Cache Aside 到延迟双删、Canal+MQ 的进阶之路
java·数据库·redis·后端·缓存
咏方舟【长江支流】1 天前
【开源】跨语言·跨平台·跨数据库(6) ——一种ORM缓存的接口实现和容错处理
数据库·缓存·开源·咏方舟-长江支流·用宝框架
郝亚军1 天前
nginx的三个基础库:PCRE、OpenSSL、Zlib的安装
linux·服务器·nginx
CodexDave1 天前
MySQL事务隔离级别与MVCC机制解析
前端·数据库·mysql·nginx·性能优化·负载均衡