Apache 代理缓存模块在处理一次请求时,涉及缓存查找、响应头复制、桶数据传递和缓存写入等多个步骤。这些步骤产生的大量临时对象不会直接通过 malloc 逐个申请,而是由 APR 内存池统一分配。理解这套内存池管理机制,有助于排查代理缓存场景下的内存占用异常、响应延迟和并发吞吐问题。

一、APR 内存池如何支撑代理缓存请求
Apache 的可移植运行时 APR 提供了一种基于池的内存管理模型,核心结构是 apr_pool_t。每个请求会从连接池中派生出请求池,请求处理过程中涉及的解析结果、临时缓冲区、URI 副本以及缓存控制结构都优先从这个池中分配。mod_cache 在判断缓存命中时,需要构造缓存键、解析 Cache-Control 头、生成缓存句柄,这些对象都挂在请求池上,请求结束后统一释放,不需要逐个调用 free。
这种设计带来两个直接好处。第一是减少系统调用,小对象分配只是从内存块中移动指针;第二是降低内存泄漏风险,池销毁时自动回收所有子对象。但代价是池内对象生命周期被绑定到请求,如果某个缓存操作需要把数据保留到请求结束之后,就不能直接使用请求池,必须创建子池或使用独立池。比如 mod_cache 写入缓存时,需要在过滤链关闭后继续完成部分元数据落盘,这时会从独立缓存池中分配空间。
#include <apr_pools.h>
#include <httpd.h>
static apr_status_t cache_write_metadata(request_rec *r, cache_object_t *obj)
{
apr_pool_t *subpool;
apr_pool_create(&subpool, r->pool);
/* 从子池分配缓存元数据,避免被请求池提前释放 */
cache_meta_t *meta = apr_pcalloc(subpool, sizeof(cache_meta_t));
meta->key = apr_pstrdup(subpool, obj->key);
meta->expire = obj->expire;
/* 子池在写入完成后销毁 */
apr_pool_destroy(subpool);
return APR_SUCCESS;
}
上述代码中,apr_pool_create 创建的 subpool 是从请求池派生的子池。即使请求池被销毁,子池仍可独立控制生命周期。Apache 内部大量使用这种父子池关系,代理缓存模块会在需要延长对象存活时间时创建独立子池,而不是改变请求池的销毁时机。
二、代理缓存写入与读取路径中的内存流转
代理缓存写入通常发生在 mod_proxy 收到上游响应之后。响应数据以 bucket brigade 的形式在过滤器中流动,mod_cache 的缓存过滤器会拦截这些数据,把需要缓存的内容复制到缓存用内存池。对于磁盘缓存,数据最终通过文件 bucket 写入缓存文件,但响应头、缓存键和状态信息仍保留在内存池中。对于共享内存缓存,如 mod_socache_shmcb,对象会进入共享内存段,此时内存池只负责请求侧临时结构。
读取路径上,缓存命中后 mod_cache 会构造一个缓存桶组返回给输出过滤器。如果缓存内容较大,不会一次性全部读入内存,而是分块读取,每一块都可能从池中分配临时 bucket。这样虽然避免了为整个响应分配大块连续内存,但在高并发下,池中的小对象数量会快速上升。APR 池的内存块链表会不断增长,直到请求结束才归还给分配器。
这里需要区分两种分配方式。apr_palloc 从池的活跃内存块中分配小对象,通常小于 8KB;apr_palloc_large 用于大块内存,直接调用 malloc 并记录在池的大块链表中,释放时单独回收。代理缓存处理大文件时,数据块并不会走小对象路径,而是使用大块分配或文件 bucket 映射,因此不会迅速耗尽池内的连续空间。缓存键和头部字段这类频繁分配的小对象才是小对象路径的主要消费者。
#include <apr_pools.h>
static void *allocate_cache_buffer(apr_pool_t *pool, apr_size_t size)
{
if (size > 8192) {
/* 大对象直接走大块分配,避免撑爆池内连续内存 */
return apr_palloc_large(pool, size);
}
return apr_palloc(pool, size);
}
上面的逻辑在 Apache 内部并不少见。通过阈值区分大小对象,可以让内存池在频繁分配小对象的同时,不因为几个大响应而浪费大量连续空间。理解这一点有助于解释为什么代理缓存的内存占用并不总是随缓存文件大小线性增长。
三、内存池生命周期控制与缓存模块的边界
请求池的生命周期由 Apache 核心控制,从请求开始到请求结束。代理缓存模块不能在请求结束后继续使用请求池中的对象,否则会访问已释放内存。常见的做法是为缓存写入创建子池,并在写入完成后显式销毁。对于需要跨请求存在的数据,如缓存索引和统计信息,应放到服务器级池或进程级池中。mod_cache 的缓存目录扫描、htcacheclean 等工具则运行在独立进程中,不使用请求池。
如果开发自定义缓存提供者,需要注意不要引用请求池中的指针到缓存成功后阶段。比如把 cache handle 或 URI 字符串存入共享缓存时,必须复制数据而不是只保存指针。否则请求结束后指针悬空,后续请求读取缓存时会崩溃。Apache 的 mod_cache API 在设计上要求提供者自行管理缓存对象生命周期,核心模块只在请求期间内使用这些对象。
池的提前销毁还可能造成 bucket 数据失效。在过滤链返回前,任何需要跨过滤器的数据都必须引用有效的 bucket 和 brigade。若某个过滤器提前销毁了池,后续过滤器继续读取桶时就会访问非法内存。因此 mod_cache 在缓存过滤器中会谨慎处理子池的销毁时机,通常延迟到过滤链完全结束后再回收。
四、内存池相关的性能观察与调优思路
代理缓存场景下,内存池带来的开销往往表现为请求处理期间的内存峰值,而不是持续增长的内存泄漏。如果发现 Apache 进程 RSS 持续上升,先要区分是请求池未释放、缓存共享段配置过大还是连接池膨胀。使用 mod_status 可以观察活跃请求和连接数,但无法直接看到池的分配细节。APR 提供调试接口,可以在编译时启用池调试,跟踪每个池的创建和销毁。
对于磁盘缓存,内存池主要影响缓存命中和写入时的 CPU 与内存分配效率。频繁的小对象分配会带来内存碎片,尤其在长时间运行后,池内空闲块可能无法合并。虽然 APR 分配器会复用内存块,但在极端并发下,每个请求独立的内存块会导致进程地址空间增长。适当调整 MPM 的 MaxRequestWorkers 和每个子进程处理的请求数,可以限制内存峰值。
如果使用共享内存缓存,如 mod_socache_shmcb,需要根据缓存对象大小和数量设置合适的共享内存段容量。容量过小会导致频繁淘汰和重新缓存,容量过大则占用系统内存,压缩其他进程的可用空间。可以通过压测观察命中率和进程内存,找到适合当前业务的容量。磁盘缓存本身不需要大内存池,但 htcacheclean 扫描和缓存索引更新会暂时增加内存使用。
| 配置项 | 作用 | 调整建议 |
|---|---|---|
| MaxRequestWorkers | 限制同时处理的请求数 | 根据平均每个请求的内存池大小设置 |
| mod_socache_shmcb 容量 | 共享内存缓存段大小 | 设置为缓存对象总量的 1.2 到 1.5 倍 |
| CacheEnable / CacheRoot | 开启磁盘缓存并指定目录 | 目录层级不宜过深,减少扫描开销 |
总体来看,Apache 代理缓存的内存池管理并不需要频繁手工干预,它的默认行为在大多数场景下已经足够稳定。真正需要关注的是自定义缓存提供者是否错误延长了请求池生命周期,以及共享内存缓存配置是否与业务规模匹配。把握这两点,可以让代理缓存在高并发下保持可预测的内存占用。
Apache代理缓存内存池mod_cache修改时间:2026-10-05 01:17:53