Caching a Front Page That Nobody Meant to Cache

这篇内容暂时只有 English 版本,以下是原文。

Two winters ago I ran the read side of a news aggregator not unlike this one: a PHP front page, a Python ranker writing into MySQL 5.7, and nginx 1.18 in front doing fastcgi_cache. The symptom was subtle and nasty. During the morning traffic spike, a story that had already dropped off the front page kept reappearing for about forty minutes, and the ranker’s score for it had been overwritten twice with an older value. No errors in the logs. Just a stale top slot that a handful of users kept seeing.

What was actually happening: nginx was caching responses keyed by the request path only. The front page renders based on a page query parameter, and we had a Vary-like set of variants (logged in vs anonymous, mobile vs desktop) that nginx did not know about. On top of that, the ranker pushed updates asynchronously, so there was a window where a cached page and a freshly ranked score disagreed. The cache TTL was 60 seconds, but a background purge job was clearing keys with a glob that matched more than intended, occasionally removing the new entry and leaving the old one to be re-served.

Here is the cache config I ended up with, simplified but functional:

proxy_cache_path /var/cache/nginx/front levels=1:2 keys_zone=front:64m
                 max_size=2g inactive=10m use_temp_path=off;

map $http_cookie $cache_bypass {
    default 0;
    "~*(session|uid)=" 1;
}

server {
    location = / {
        proxy_cache front;
        proxy_cache_key "$scheme$host$request_uri$is_args$args$http_x_device";
        proxy_cache_bypass $cache_bypass;
        proxy_no_cache $cache_bypass;
        proxy_cache_valid 200 30s;
        add_header X-Cache-Status $upstream_cache_status;
        proxy_pass http://ranker_front;
    }
}

And I stopped letting the purge job guess. Instead of globbing, the ranker writes a version integer to a small Redis key on each successful rank cycle; nginx includes that version in the cache key via a map from a header the app sets. When the version bumps, old keys simply age out under inactive=10m rather than being explicitly deleted. The cost is more cold misses after a big rank change and a slightly larger cache footprint. That was fine for our traffic; it would not be fine for a site with very high write frequency and low tolerance for recompute.

The part that bit me: proxy_cache_bypass with a cookie regex means every request that carries a stale tracking cookie skips the cache entirely, and I discovered this only after watching origin load double. Also, if you put anything user-specific into the response (a personalized “your topics” strip), no key scheme saves you; you have to fragment that out of the cached HTML or render it client-side. I have not tested this on nginx 1.25’s newer cache directives, so treat the exact syntax as version-sensitive.

My question for anyone running something similar: how do you decide what belongs in the cache key versus what belongs in a short-lived version token? I keep flip-flopping between the two, and I suspect the right answer depends on how bursty your rank updates are.

0
赞同度 100% · 0 条评论 · 1 阅读

0 条评论

支持 Markdown · 回复会实时推送给对方

还没有评论,写下第一条。