动态网站如何进行加速?CDN、反向代理与数据库缓存协同方案

静态资源的加速相对简单——设长缓存、上 CDN 就八九不离十了。但动态网站完全是另一回事:页面内容因人而异、需要实时计算,缓存基本用不上,CDN 也帮不上太多忙。于是很多人卡在这里:我的网站每个页面都要登录才能看,每个数据都要查库,那还能怎么加速?

答案是能加速的,只是思路要从"缓存结果"转向"分层消化"。动态网站慢,通常是慢在几个确定的地方:后端程序反复执行、数据库反复查询、以及用户与源站之间的网络往返。针对这三点,各有对应的手段,组合起来就是一套协同方案。这篇文章把这些手段逐个讲清楚,并给出如何组合的落地建议。

一、先搞清楚动态网站慢在哪

优化之前要定位瓶颈,否则容易使错力。动态请求的耗时大致由这几段组成:

  1. 网络往返:用户到服务器的距离,跨地域、跨运营商时十几到上百毫秒;
  2. Web 服务器处理:接收请求、解析、路由分发;
  3. 应用代码执行:业务逻辑、模板渲染;
  4. 数据库查询:SQL 执行、关联查询、锁等待;
  5. 外部依赖:调用第三方接口、下游服务。
# 用 curl 拆解一次请求的各阶段耗时,先定位瓶颈段
curl -o /dev/null -s -w 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' https://www.example.com/
# connect→ttfb 的差值主要是服务端处理时间(含数据库)
# 如果连接阶段就很慢,瓶颈在链路而非服务端

经验上,动态网站慢,比例最高的两类是"数据库查询慢"和"应用重复计算"。其次是网络往返。明确了这一点,优化的优先级也就清楚了:先把重复的查询和计算用缓存消掉,再优化链路。

二、手段一:页面级缓存与片段缓存

动态内容默认不能整页缓存,因为内容因人而异。但很多页面的很大一部分其实是"对所有人一样"的。抓住这个落差,就有了页面级和片段级缓存的空间。

页面级缓存:把整个页面渲染好的 HTML 缓存起来,一定时间内所有用户共享同一份。适合内容更新不频繁、且不区分用户的页面,比如新闻详情、商品详情(不含个性化推荐的部分)。实现方式可以是应用自己写文件缓存,也可以是 Nginx 的 FastCGI/反向代理缓存。

片段缓存:页面整体不缓存,但把里面通用的部分(公共头部、侧边栏、热门榜单、页脚)单独缓存,组装时用缓存内容填充,只对个性化部分实时渲染。适合"框架通用、局部个性化"的页面。

边缘包含(ESI,Edge Side Includes):一种更工程化的片段缓存思路,用特殊标记把页面拆成可独立缓存的小块,由反向代理或 CDN 分别缓存和组装。思路很好,但要额外配置,中小站点未必值得。

判断一个页面能不能做缓存,看两个问题:内容多久变一次?不同用户看到的是否一样?两者都"友好"(变化慢、内容一致)就能设缓存;只有局部友好就做片段缓存。

# 用 Nginx 做页面级反向代理缓存(后端是应用服务)
# http 块:
proxy_cache_path /var/cache/nginx/page levels=1:2 keys_zone=page_cache:100m max_size=5g inactive=30m use_temp_path=off;

# server 块:
location /article/ {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host $host;
    proxy_cache page_cache;
    proxy_cache_valid 200 5m;          # 详情页缓存 5 分钟
    proxy_cache_use_stale updating error timeout http_500 http_502 http_503;
    # 登录用户(带会话 Cookie)跳过缓存,直接回后端
    proxy_cache_bypass $cookie_sessionid;
    proxy_no_cache     $cookie_sessionid;
    add_header X-Cache-Status $upstream_cache_status;
}

配置要点:proxy_cache_bypass / proxy_no_cache 配 Cookie 是关键,它保证登录用户的个性化页面不会被缓存,也不会污染公共缓存。改配置前备份并 nginx -t 验证。

cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak.$(date +%F)
nginx -t && systemctl reload nginx
curl -sI https://www.example.com/article/123 | grep -i x-cache-status

三、手段二:PHP opcache 与代码级优化

如果后端是 PHP,opcache 是必开且收益极大的一项。PHP 默认每次请求都要把源码编译成字节码再执行,opcache 把编译结果缓存在内存里,省掉重复编译。

# 确认 opcache 是否安装(PHP 5.5+ 通常内置,作为扩展)
php -m | grep -i opcache
# Debian/Ubuntu: apt install php-opcache  (或随 php 主包自带)
# RHEL/Rocky:    yum install php-opcache  (或 php-pecl-zendopcache)
; php.ini 或 /etc/php.d/10-opcache.ini
opcache.enable=1
opcache.memory_consumption=256       ; 共享内存大小,按项目规模调
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000  ; 要大于项目文件数
opcache.validate_timestamps=1        ; 开发环境为 1,生产可设 0 并靠部署时 reload
opcache.revalidate_freq=60           ; 生产环境适当加大,减少 stat 开销

生产环境常用的做法是把 validate_timestamps 设为 0,彻底不做文件时间检查(最快),代价是每次发布代码后要重启 PHP-FPM 才能生效。这个取舍在"更新频率低"的站点上很划算。

其他语言也有类似机制:Java 有 JIT 和预编译,Node.js 有 V8 的代码缓存,Python 有字节码缓存。思路一致——把"每次都做"的编译/初始化工作只做一次。

另外要优化数据库查询本身:加合适索引、避免 N+1 查询、避免 SELECT *、分页不要用大偏移量。SQL 慢查询即使有缓存也拖后腿,缓存只是对冲,不是根治。

四、手段三:数据库缓存与 Redis

数据库是动态网站最常见的瓶颈,这一层要用缓存消掉重复查询。

数据库自己的缓存:InnoDB Buffer Pool 会把热数据页缓存在内存里,把 innodb_buffer_pool_size 设成机器内存的一半到七成(专用数据库服务器),能显著减少磁盘 IO。这是"免费"的一层,必须配好。

# my.cnf / mysqld.cnf
innodb_buffer_pool_size = 4G      ; 经验值:专用库可设为物理内存的 50%-70%
innodb_buffer_pool_instances = 4  ; 大内存实例可拆分成多个实例减少争用

应用层缓存(Redis):把热点查询的结果缓存到 Redis,读时先查 Redis、未命中再查库并回填;写时更新库并删除对应缓存。这是解决数据库压力的主力手段,具体配置思路(数据结构、TTL、淘汰策略、一致性)在上面的 Redis 专题里有详细展开。

查询结果缓存:除了对象级缓存,还可以缓存"查询结果集"。比如首页的商品列表、排行榜、统计数字,这类结果在多用户间共享,缓存价值很高。要注意的是一旦底层数据变化,要主动失效相关缓存。

# 观察 MySQL 缓存相关指标(在数据库上执行)
mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';"
# Innodb_buffer_pool_reads 是物理读,read_requests 是逻辑读
# 两者比值越接近 0 说明 Buffer Pool 命中越好

mysql -e "SHOW ENGINE INNODB STATUS\G" | grep -A5 'BUFFER POOL'

五、手段四:CDN 动态加速

CDN 对动态内容默认不缓存,但可以优化"回源链路"。动态加速(有的叫全站加速)的思路不是缓存结果,而是让 CDN 节点到源站之间的这一段走更优的路径:服务商自建的骨干网、优化的路由、长连接复用、协议优化(如 QUIC)。它减少的是跨运营商、跨地域带来的延迟和丢包。

动态加速不改变业务逻辑,源站的每个请求还是要实时处理,收益来自链路质量。所以它适合这样的场景:源站本身处理很快,瓶颈主要在用户到源站之间的网络往返。

判断方法还是前面那条 curl 命令:如果 connect 阶段耗时占比高,说明链路是瓶颈,动态加速有用;如果 ttfb 明显大于 connect,说明是源站处理慢,先优化源站(缓存、索引、代码)比开动态加速更有效。

# 对比开关动态加速前后的连接与首字节时间(多地拨测更有说服力)
curl -o /dev/null -s -w 'connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer}\n' https://www.example.com/

一个实用组合是:CDN 只负责链路优化和静态资源缓存,动态请求走"不缓存但优化链路"的通道回源,源站负责实时渲染。这样既享受就近接入的低延迟,又不牺牲动态性。

六、手段五:SSR 缓存与应用内缓存

服务端渲染(SSR)的页面虽然是动态生成的,但渲染结果在一段时间内对多数用户是一样的,可以缓存。

SSR 结果缓存:把渲染好的 HTML 缓存起来,一段时间内的相同请求直接返回缓存的 HTML,不再执行渲染。适合内容型页面(文章、商品详情)。注意要按正确维度分(比如按 URL、按语言),不能让个性化内容串味。

应用内缓存:在应用进程内存里缓存配置、字典、不常变的基础数据,避免每次请求都去查库或读 Redis。进程内缓存的访问比 Redis 还快(没有网络开销),但多实例之间不同步,适合几乎不变的数据。

对象池与连接复用:数据库连接池、Redis 连接池、HTTP 客户端连接池,都要配好,避免每次请求都建立新连接。连接建立的成本在动态请求里占比不小。

# 观察 PHP-FPM 进程与连接情况
systemctl status php-fpm 2>/dev/null || systemctl status php8.1-fpm
# 查看慢日志定位慢请求
tail -f /var/log/php-fpm/www-slow.log 2>/dev/null

七、组合协同:一个完整方案

把上面的手段组合起来,一个动态网站的加速架构大致是这样:

用户浏览器
  ↓ 静态资源 → CDN(缓存 + 就近)
  ↓ 动态请求 → CDN 动态加速(优化回源链路,不缓存)
Nginx 反向代理
  ↓ 页面级/片段缓存(可缓存的页面直接返回)
应用(PHP-FPM / Java / Node)
  ↓ opcache / 代码缓存 + 进程内缓存
Redis(对象缓存、查询结果缓存)
  ↓ 未命中才查
数据库(Buffer Pool 兜底)

分层消化的原则:能在越靠前的层解决就越省资源。静态资源在 CDN 层解决;可缓存页面在 Nginx 层解决;重复查询在 Redis 层解决;只有真正必要的数据操作才落到数据库。

落地顺序建议(按投入产出比):

优先级 措施 预期收益
1 开启 PHP opcache、配好数据库 Buffer Pool 低成本,普遍有效
2 静态资源走 CDN + 长缓存 显著降低源站压力
3 热点查询上 Redis 解决数据库瓶颈
4 可缓存页面做页面级/片段缓存 减少应用执行
5 开 CDN 动态加速 降低跨地域延迟
6 慢 SQL 优化、加索引 根治数据库问题

先做前几项通常就能看到明显改善,后面的按实际瓶颈补。不要一上来就上最复杂的方案。

常见问题

Q:动态页面完全不缓存,还能加速吗?
能。一是通过链路优化降低网络往返(CDN 动态加速),二是用 Redis 等缓存底层数据减少重复查询,三是优化代码和 SQL。缓存不是唯一的加速手段。

Q:登录用户的页面会不会被误缓存?
只要正确配置了"带会话 Cookie 的请求跳过缓存",就不会。这是页面级缓存必须处理的细节,配错会把一个人的私人页面缓存给所有人。

Q:opcache 开了以后改了代码不生效?
生产环境常见把 validate_timestamps 设为 0,这时改了代码需要重启 PHP-FPM 或 reload 才生效。发布流程里要加上这一步。

Q:Redis 和数据库缓存重复吗?
不重复,是两级。数据库 Buffer Pool 缓存的是数据页(磁盘 IO 层面),Redis 缓存的是查询结果或业务对象(应用层面)。前者自动,后者要写代码维护。

Q:CDN 动态加速贵吗?会不会没效果?
它按流量或请求计费,成本取决于服务商和用量。效果取决于瓶颈是否在链路——源站处理本来就快、延迟主要来自距离时收益明显;源站自己就慢的话,收益有限。

总结

动态网站加速的关键是转变思路:不能靠"缓存结果",而要靠"分层消化"。把重复的查询、计算和网络往返分别用合适的层去消除。

页面级和片段缓存是第一类手段,适合内容变化慢、局部通用的页面,配合 Cookie 判断避免误缓存登录用户内容。PHP 站点务必开 opcache,把重复编译消掉。

数据库是主要瓶颈,先用 Buffer Pool 兜底,再用 Redis 缓存热点查询结果和业务对象。慢 SQL 要根治,缓存只是对冲。

CDN 对动态内容的价值在于"优化回源链路"而不是"缓存"。用 curl 拆解耗时判断瓶颈在链路还是源站:链路慢用动态加速,源站慢先优化源站。

组合落地的顺序是:先做低成本的 opcache 和 Buffer Pool,再让静态资源走 CDN,然后热点查询上 Redis,接着做页面级缓存,最后按需开动态加速和优化 SQL。分层协同,动态网站同样可以做到又快又稳。