在NUMA架构的服务器上运行Apache反向代理时,进程调度器可能把httpd子进程迁移到任意CPU核心,而进程正在读写的缓存数据仍然留在最初分配内存的节点上。一旦发生跨节点访问,内存延迟会从约90纳秒上升到140纳秒以上,对于高并发代理缓存来说,这种累积效应会直接拉低吞吐量。要解决这个问题,不能只盯着缓存命中率,还需要把Apache进程、缓存内存页以及磁盘IO中断绑定到同一个NUMA节点。下文将从NUMA基础、绑定方法、Apache内部配置和效果验证四个层面展开。

NUMA节点对Apache代理缓存的具体影响
NUMA即非统一内存访问架构,每个CPU插座拥有独立的本地内存控制器,访问本地内存速度快,访问远端内存则需要经过芯片间互联总线。Linux默认的内存分配策略通常是本地节点优先,但进程调度器并不保证线程始终留在最初分配内存的节点上。Apache的mod_cache把响应体缓存到磁盘或内存,磁盘页缓存由内核管理,通常跟随写入线程所在节点。如果写入的httpd进程在节点0,读取的进程被调度到节点1,页缓存数据就要跨节点传输,每次访问都比本地多出几十纳秒。
除了数据缓存,Apache共享内存段如SSL会话缓存、代理统计信息等也会出现同样问题。这些共享段通过mmap或shmget创建,默认在首次访问的节点分配物理页。后续在其他节点访问同样带来延迟。可以用numastat -p <pid>观察,许多页面落在节点0但进程主要运行在节点1,这就说明亲和性不匹配。网络方面,网卡中断通常由某个NUMA节点处理,如果httpd进程不在该节点,处理完中断后的数据包需要跨节点传递到用户空间,进一步放大延迟。因此,完整的NUMA亲和性绑定应该把网卡中断、工作进程和缓存内存放在同一节点。
使用taskset和numactl绑定Apache进程
最直接的方法是修改Apache启动命令,用taskset限定CPU集合,numactl限定内存分配节点。假设服务器有两个NUMA节点,节点0包含CPU 0-15,节点1包含16-31。想把一组httpd进程固定在节点0,可以执行:
# 绑定后续启动的httpd及子进程到NUMA节点0 numactl --cpunodebind=0 --membind=0 /usr/sbin/httpd -DFOREGROUND
Apache使用fork模型,父进程绑定后子进程继承同样的CPU亲和性掩码和NUMA内存策略,因此需要确保MPM配置在启动时生成足够多的子进程,并且每个子进程都在被绑定的节点上运行。如果使用prefork模型,父进程负责监听,子进程处理请求,继承有效。但主进程的监听socket和accept队列如果跨节点,会造成额外开销,建议也把父进程固定到同一个节点。
对于更细致的控制,可以在systemd服务单元中直接使用以下配置:
[Service] ExecStart=/usr/bin/numactl --cpunodebind=0 --membind=0 /usr/sbin/httpd -DFOREGROUND CPUAffinity=0-15 NUMAPolicy=bind NUMAMask=0
这里CPUAffinity和NUMAMask是systemd的原生支持,比在ExecStart里再包一层更加干净。NUMAMask使用节点掩码,0表示节点0,1表示节点1,3表示节点0和1。如果配置了NUMAPolicy=bind,systemd会在exec之后为进程设置内存绑定。需要确认所使用的systemd版本支持这些指令。如果希望不同httpd子进程绑定不同节点,可以启动多个Apache实例或使用systemd模板单元,分别指定不同NUMAMask,前端通过负载均衡分发。例如缓存目录按节点分开,每个实例只挂载本地NVMe盘,这样读写都无需跨节点。
Apache内部MPM与缓存目录的节点化配置
MPM event模式使用一个父进程和多个子进程,每个子进程内多个线程。由于线程共享进程的地址空间,无法单独为每个线程绑定不同NUMA节点,因此需要把整个子进程绑定到同一节点。对于高并发代理,合理做法是启动两个httpd实例,分别绑定节点0和节点1,并给每个实例分配独立的缓存根目录,放在各自节点的本地盘上。配置示例如下:
<IfModule mod_cache.c>
CacheRoot /var/cache/apache-node0
CacheDirLevels 2
CacheDirLength 1
CacheEnable disk /
</IfModule>
在另一个实例中使用/var/cache/apache-node1。注意mod_cache的CacheRoot需要预先创建,Apache会自动生成子目录。磁盘页缓存会跟随写入线程的NUMA节点,因此绑定后数据基本留在本地。如果两个节点必须共享一个缓存目录,可以使用tmpfs并设置numactl --interleave=all,但这种做法会牺牲本地内存优势,除非缓存对象极小且内存充足。
除了磁盘缓存,Apache的共享内存缓存如SSL会话缓存也涉及NUMA。共享内存段在第一个进程创建时分配,如果之后其他节点的进程也要访问,同样有跨节点问题。如果必须跨节点使用,可以考虑使用numactl --interleave=0,1让页面在节点间交错分配,这样访问延迟平均化,避免单节点带宽过载。不过这类共享段通常容量小,影响有限。
还需要检查MPM的ThreadsPerChild和MaxRequestWorkers等参数。绑定到单个NUMA节点后,该节点可用的CPU核心数就限制了最大并发线程数。假设每个节点16核,event模式下ThreadsPerChild设为25,两个节点各一个实例,总并发约800。不要在一个节点上设置超过本地核心数过多的线程,否则上下文切换会抵消亲和性收益。建议ThreadsPerChild略高于核心数,留出IO等待空间。
验证NUMA绑定效果与调优注意点
绑定之后需要用工具确认是否生效。numastat可以查看进程的内存分配分布:
numastat -p $(pgrep -f httpd | head -1)
输出中Node0和Node1的数值应显示大部分页面落在目标节点。还可以用numactl --hardware确认节点与CPU的映射关系。如果看到远端分配仍很多,检查numactl的内存策略是否被后续代码覆盖,比如某些库显式调用mbind或set_mempolicy。
性能对比可以借助perf统计跨节点访问事件,命令如下:
perf stat -e node-loads,node-load-misses -p $(pgrep -f httpd | head -1) sleep 10
node-loads是本地节点内存访问次数,node-load-misses是跨节点访问次数。绑定合理时miss比例应低于5%。如果miss比例高,说明还有内存页分配在其他节点,需要排查共享段和文件页缓存是否被回收后重新分配到了错误节点。对于磁盘缓存文件,可以使用dd或fio测试本地盘和远端盘的延迟差异,辅助判断。
绑定并非没有副作用。如果所有请求都聚集在一个节点,该节点的内存带宽和CPU会先达到瓶颈,另一个节点空闲。为了避免这种情况,可以在入口负载均衡层使用一致性哈希,把不同虚拟主机或URL前缀分发到不同Apache实例。也可以启动两个实例分别绑定两个节点,并挂载不同缓存目录。需要监控每个节点的CPU利用率、内存带宽和缓存命中率,动态调整实例数。还可以结合irqbalance的禁用与手动设置网卡中断亲和性,让每个网卡队列的中断落在对应节点,形成完整的本地化路径。
总之,Apache代理缓存的NUMA亲和性绑定不是单一配置就能完成,需要从进程绑定、内存策略、缓存目录布局、网卡中断四个环节协同。通过taskset、numactl和systemd参数可以快速上手,但生产环境中必须用numastat与perf持续验证,避免出现局部过载。
Apache代理缓存NUMA亲和性性能调优修改时间:2026-09-18 06:08:47