1. 当前缓存系统方案:Openresty + Linux Page Cache
当前系统使用 Openresty 作为 CDN 节点,缓存使用的是 Nginx 的原生模块,未对其进行过二次更改。
但 Nginx Cache 模块并不提供内存缓存的能力,仅仅缓存文件的一些句柄元数据(如键值、有效期、访问状态等)信息。
可喜的是,Nginx 没有,但 Linux 总是能变着花样的让人满足:Linux Page Cache。
以下仅展开 Linux Page Cache 文档,其他更多信息请参考:Openresty 缓存管理调研
1.1 Linux Page Cache
尽管 Nginx 没有提供将文件缓存到内存中的直接能力,但是在 Nginx 中可以设置我们还是发现在实际运行过程中,内存确实在缓存中发挥了其作用。
经过调研跟排查,证实了磁盘中的缓存文件,被 Linux 的 buffer/page cache 缓存了。
在 Nginx 中,默认情况下就会利用操作系统的 Page Cache(通过内核的文件缓存机制),无需特殊配置即可自动缓存频繁访问的静态文件。
Linux Page Cache 最开始是 Buffer/Page Cache,后面迭代为 Page cache。从以下图中可以清楚的看到 Page Cache的位置。
Page Cache 完全位于内核层中,跟 VFS(Virtual Filesystem)一个虚拟文件系统的软件抽象层同一个位置。属于内核不属于用户。
前面提到的 fuse 就是位于用户层的实现了 vfs 的一种文件系统。
1.1.1 核心配置:sendfile,aio,directio
sendfile:传统文件传输是从数据需从磁盘→内核缓冲区→用户空间→Socket 缓冲区→网络,存在多次拷贝。
使用了 sendfile 之后通过系统调用直接将文件从内核缓冲区传输到 Socket 缓冲区,跳过用户空间拷贝,减少 CPU 和内存开销。
注意:sendfile 通常与 tcp_nopush 搭配,填充数据包再发送。
详见下图:
aio:异步 I/O,通过内核异步 I/O 或线程池(Linux)非阻塞处理文件,避免 Worker 进程阻塞。搭配 DirectIO:绕过内核缓存直接读写磁盘(需配置 directio),适合大文件。
通过在 Nginx 上搭配使用以上三个核心配置,可以实现小文件热文件从内核缓存区直接读取(page cache位于内核缓冲区),大文件则直接从磁盘读取,防止文件过大带来的额外内存挤占。
1.1.2 示例配置
|
|
1.1.3 vmtouch:一个可以查看内存缓存的工具
|
|
从下面这张截图可以看出(/var/auth-cdn/cache 为节点的缓存目录):
磁盘缓存目录中有两个文件 7 个条目包括目录,其中被 page cache 缓存的内存页有 84 页,一页大小为 4KB,已经在内存中缓存的大小为 336k,总共的大小为 336k,内存缓存率为 100%。
这个数据为测试数据,在 Nginx 上只配置了缓存这两个文件,所以缓存率能达到100%。
经过实测,在生产环境中,一共有71万个缓存文件,总大小为600GB左右,内存缓存了 100多GB 缓存率为 15%(根据内存大小而定)
更多排查的命令,如 top,free,cat /proc/meminfo 等都能印证磁盘缓存中的部分文件被缓存进了 page cache 中。
1.1.4 Page Cache 淘汰策略
Linux 的 Page Cache 使用 LRU 算法为淘汰算法,但是也不是传统意义上的 LRU 中间经过一些改良。存在两个列表 two-list 一个"热表",一个"冷表",优先淘汰"冷表"中的数据。
具体介绍的文章:Linux Memory Management
2. 新的缓存系统方案
为了拜托 Nginx 在缓存上的局限性,并且获得对缓存进一步控制,能够灵活设置缓存规则、缓存策略、缓存持久化等,能够游刃有余的应对业务的扩展跟挑战,需要重新调整缓存系统架构。本文档就 Openresty + ATS 跟 Fuse 方案展开讨论。
2.1 Openresty + ATS
代理使用 Openresty,将缓存交给Apache Traffic Server 去做。本来的候选列表有 ATS,Varnish,以及 Squid。但ATS 在我们之前的工作中,已经经过实践,也累积了一定的经验。ATS 作为Apache TOP项目,值得信赖。
并且ATS 拥有相对灵活的缓存能力,能够完美的结合内存跟磁盘缓存,并且拥有多种灵活策略。
以下是针对是否将 ATS 安装到同一台机器的示意图。
两种方案的详细对比请参考文档:节点 Openresty + ATS 部署方案对比
2.2 Fuse 方案
FUSE(Filesystem in Userspace)是一种用户态文件系统框架,允许开发者无需编写内核代码即可在用户空间实现自定义文件系统。通过 FUSE,开发者可以灵活地定义文件系统的行为(如文件读写、权限控制等),而无需关心底层内核模块的复杂性。其核心组件包括:
- 内核模块:负责与操作系统 VFS(虚拟文件系统)交互,将文件操作请求转发到用户态守护进程。
- 用户态守护进程:处理实际文件操作逻辑,并通过 /dev/fuse 设备与内核通信。
- 开发库(如 libfuse 或 Go-FUSE):提供 API 简化文件系统的实现
Nginx 可以使用 proxy_cache_path 配置缓存目录,那么我们就可以使用 Fuse 方案接管这个目录。FUSE(Filesystem in Userspace)是用户空间文件系统,允许开发者无需内核编程就能创建文件系统。
Fuse 实现了 VFS 抽象接口,使得我们可以接管来自 syscall 中的各类动作,如 stat/open/close/delete 等。
但是 Fuse 的缺点也很明显:是在用户态,那么就无可避免的会多出来数据拷贝。
这是一个简单的 go 实现的 Fuse的示例。main.go,在 Mac 上请注意要安装 macfuse:https://github.com/macfuse/macfuse。并且要开启允许加载内核扩展。windows 上没戏。
终端中的效果如下:
文件中的效果:
浏览器中的效果:
3. 其他方案
需要排除一些以前认为可能得方案。为了解决能在内存缓存文件,调研跟尝试过其他很多方案,但是都或多或少存在一些问题。
例如:tmpfs (虚拟文件系统),将内存虚拟为磁盘目录,使用 proxy_cache_path 指向这个目录,可以实现缓存文件的加速。但是局限性也很明显,重启之后内容的缺失,无法满足持久化的要求。
更多请转到文档:使用 Nginx 作为 CDN 节点时,利用内存进行缓存的技术方案