以前,我总是抱有一些奇怪的迷思,认为无需系统地学习,只要随意地按照各种教程进行配置就能够很好地设置缓存。然而,这种做法浪费了大量时间,直到最近我才发现之前在很多地方都踩了坑。我以为自己做对了很多事情,但实际上在访客那边并没有什么效果。

接下来,我还是把自己之前遇到的问题记录一下吧,如果有人看到可以作为参考也是不错的。不过,这篇文章绝对不能当作教程。我只会提及在设置缓存时会用到的一些代码片段。尽管我的网站算不上超快,但将动态博客与缓存相结合,基本上也能实现页面秒开的效果了。TTFB基本都在几十毫秒左右,已经足够了。


配置 Nginx 缓存的 inactive:

在 Nginx 配置文件顶部配置缓存区域 proxy_cache_path 时,大部分教程会指导配置「位置」、「缓存层级」、「缓存大小限制」等,但对于 inactive 参数,虽然会介绍它的作用,但是鲜少说明它是有默认值的。

proxy_cache_path /nginx_cache/blog_cache levels=1:2 keys_zone=blog_cache:64m max_size=512m inactive=180d;

结尾的 inactive 这个参数用于告诉 Nginx,在缓存内容超过多长时间没有被访问后,应该被主动清理。

如果不配置这个参数,Nginx 是可以正常运行的。我曾天真地以为只要不配置它,Nginx 就只会在缓存文件超过「缓存大小限制」后才会清理,结果就是每次短时间测试都正常,时间隔长一点再测试,就总是没有命中缓存。

事实就是如此,因为 inactive 参数实际上有默认值,且只有 10 分钟,如果不明确指定,即便缓存文件还没有达到大小的限制,只要超过了 10 分钟没被访问到,它也是会被 Nginx 主动清理掉的。

因此,我目前将这个参数设置得很长,足足 180 天。当缓存文件超过 180 天没有被访问才会被清理。事实上这个值还可以设置得更长,对于一般个人小站来说,更新频率很低,增加缓存重用机会也可减少资源浪费。

当然,时间设置太长也可能导致部分无用缓存积累,比如某篇文章修改 URL 地址后,Nginx 会为它生成新的缓存文件,旧的缓存只能等超出 inactive 设置的时间后才会被清理。对于频繁更改 URL 地址的人(应该很少见就是了),问题也不大,因为页面缓存文件一般都很小,即便频繁修改 URL,也不会导致缓存文件爆掉。

如果真有洁癖,受不了过期缓存存在,那就根据自身情况缩短 inactive 值就好。

至于需要缓存更新怎么办?下面会提到我之前遇到的另一个坑。


配置 Nginx 更新缓存內容:

对于一般的个人博客和小站点来说,其实是没有太大的必要去主动的清理缓存的。但是以前在查找教程的时候,有看过很多文章是在指导大家通过 ngx_cache_purge 这个插件来进行缓存清理。可用,但是其实是很没有必要的。

因为 ngx_cache_purge 这个插件其实用的人不多,所以大家在通过包管理器安装 Nginx,或者是到 GitHub 之类的站点找到的预编译安装包,基本都没有集成这个插件。想用它,就需要自己手动去编译安装。大部分人 Nginx 又都是放在性能比较一般的 VPS,如果在小机器上编译安装,那是真的费时间,且帮助不大。 

如果不使用这个插件,内容更新了,缓存要怎么处理呢?其实 Nginx 是可以直接更新缓存内容的,只需要简单的在配置文件里面加几条命令就好了:

# 根據緩存區名称進行調用
proxy_cache blog_cache;
# 配置緩存鍵
proxy_cache_key $uri$is_args$args;

# 配置特定响应的缓存有效期
proxy_cache_valid 200 302 3m;
proxy_cache_valid 404 1m;

# 缓存过期时返回过期缓存同时请求请求更新
proxy_cache_revalidate on;
# 后台更新缓存
proxy_cache_background_update on;

这段配置中的内容,和其他常见的教程示例比较不同的配置有以下几个点: 

  1. proxy_cache_valid 200 302 3m; 意思是为 200/302 响应的内容缓存 3 分钟。一般的教程会设置 5 分钟或者更长。这个参数配置的有效期,只是用来标记缓存有效期。缓存超过配置时间后会被标注成「过期」,但不会被删除。
  2. proxy_cache_revalidate on; 用来告诉 Nginx 在用户访问的时候,只要系统存在缓存文件,无论缓存是否过期,优先把缓存内容提供给用户。同时向上游发送请求,以判断内容是否更新。如果没更新,会更新缓存有效时间;如果内容有更新,拉取新内容存为缓存,用户下次访问时就会提供新的缓存给用户了。
  3. proxy_cache_background_update on; 后台更新,搭配 revalidate 的。

配置了这几项之后,每次更新了新的内容,就只需要自己在对应的页面刷新两下,Nginx 就会更新缓存内容了,ngx_cache_purge 也就用不上啦。


多服务器用 lsyncd 同步缓存:

如果拥有多台服务器,那么在 Nginx 配置文件设置相同的情况下,缓存文件就可以当静态文件来部署在所有服务器上,实现 CDN 的效果。

比如我的 Nginx 缓存统一存放在机器的 /nginx_cache 路径下,那么我只需要在多台服务器上安装 lsyncd 软件,然后让多台服务器互相同步这个文件夹就可以。之后,只要任一的一台服务器发生了缓存文件的变更,它就会自动把对应的文件改动,全部都同步到所有服务器的缓存文件夹中。

Lsyncd 的好处在于大部分操作系统自带的包管理器都可以直接安装,比如 Debian/Ubuntu 都可以直接通过 apt install lsyncd 来进行安装。关于如何配置,过段时间再补上吧,现在好晚了,先睡觉了。嘻嘻。


提前预缓存全站内容:

预先缓存对于特别少内容的站点其实必要性不大,因为站长自己手动去访问一下触发 Nginx 缓存就可以了。对于一些已经有不少文章内容的站点,手动去触发缓存就变得很繁琐了。这时候就可以考虑用一些脚本来对站点进行预先缓存。

大部分站点/博客都是使用现成的生成器来搭建的,比如 Wordpress / Ghost,亦或是一些静态博客生成器。正常来说,它们在构建站点的时候,也会生成站点对应的 Sitemap,这是一个记录了站点所有前台页面的文件。在做预缓存的时候,我们就可以用脚本读取这个文件,然后让脚本遍历访问所有页面,来触发 Nginx 缓存。

下面是一段 python 的脚本,脚本运行步骤是先运行一个 chrome 浏览器,然后解析队列中的 sitemap 文件,让浏览器浏览所有的页面。浏览会分两次,第一次访问因为没有缓存,会比较慢,给脚本 3 秒钟等待,之后会再刷新一次页面,确保缓存成功,等待 1 秒钟后,脚本会让浏览器开始访问下一个页面。一直到所有页面都访问完成,站点的预缓存工作就完成了。

一般站点 sitemap 文件就在域名后面添加 /sitemap.xml 即可获取,比如我自己博客的 sitemap 地址可以通过访问:https://imwc.me/sitemap.xml 获取。

下面是以我自己博客的 sitemap 为例做的 python 脚本:

import requests
import time
import xml.etree.ElementTree as ET
from selenium import webdriver

# 创建浏览器驱动
driver = webdriver.Chrome()

# 放入 sitemap 文件地址
sitemap_urls = [
    'https://imwc.me:443/sitemap-posts.xml',
    'https://imwc.me:443/sitemap-pages.xml',
    'https://imwc.me:443/sitemap-tags.xml',
]

# 遍历 sitemap 並访问每个 sitemap 的页面
for sitemap_url in sitemap_urls:
    print(f"开始访问 sitemap: {sitemap_url}")
    # 发送请求获取 sitemap 内容
    response = requests.get(sitemap_url)
    # 确保请求成功
    response.raise_for_status() 

    # 解析 XML
    root = ET.fromstring(response.content)

    # 遍历所有的 <url> 标签
    for url in root.findall('{http://www.sitemaps.org/schemas/sitemap/0.9}url'):
        # 提取 <loc> 标签内的 URL
        loc = url.find('{http://www.sitemaps.org/schemas/sitemap/0.9}loc').text
        print(f"访问页面: {loc}")
        # 打开页面
        driver.get(loc)
        # 等待页面加载完成
        time.sleep(2)
        # 刷新页面,确保正确缓存
        driver.refresh()
        time.sleep(1)

    print(f"完成访问 sitemap: {sitemap_url}")

# 关闭浏览器驱动
driver.quit()

print("所有 sitemap 的访问过程已完成。")

预缓存完成后,随便访问一个页面,都能获得很快的响应啦。查看状态的话,可以在 Nginx 配置里再加条指令:

add_header X-Cache-Status $upstream_cache_status;

添加这条指令之后,就可以在浏览器的控制台里面查看页面缓存状态了。位置在控制台-网络,然后点击主页面的项目,查看 header。可以看到,随便打开任何一页,缓存状态都是 hit 啦。

脚本还是建议在本机运行,因为在没有图形界面的服务器上调用浏览器会有一点点麻烦。多服务器上可以通过 lsyncd 同步缓存文件,那么在自己电脑上运行一次脚本也够了。反正单次运行用不了特别长时间,且也不需要再重复操作,就没必要强求要在无图形界面的服务器运行它了。

脚本本身也很简单,可以根据自己的需求随意更改优化一下,比如根据自己服务器延迟来调整访问/刷新的等待时间。目前来说对于我自己来说是完全够用了。


其他杂项可选配置:

# 打開緩存鎖,避免併發請求打架
proxy_cache_lock on;

# 开启代理缓冲区
proxy_buffering on;

# 上游返回错误响应(比如上游當機)使用缓存来响应请求,防止服务中斷
proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;

其实这些缓存配置好了之后,只要机器的线路不是特别的差,基本都可以让站点在很短的时间打开。比如美国西部走 9929 线路的机器,ICMP 延迟 160ms - 190ms,页面 TTFB 也可以到 200ms 以内,不会太慢。至于亚洲直连大陆的机器,比如香港/日本/韩国/新加坡,都可以做到大陆访问时 TTFB 100ms 以内,只会比机器直接的 ping 值稍微高一丁点。这样的速度,就完全够用了。