直播预约通道开启!2021腾讯数字生态大会邀您共探产业发展新机遇! 了解详情
写点什么

SSR 页面 CDN 缓存实践

2021 年 6 月 16 日

SSR 页面 CDN 缓存实践

SSR 是一项资源密集型任务,要抵抗更大流量、提供更快的服务,缓存是其中的必修课。


而 CDN 缓存——作为静态资源的首要支撑,适合武装到 SSR 页面吗?


开始之前


大家对 CDN 应该已经耳熟能详,如果不甚了解也没关系,我们先通过一系列问答带诸位走近这个话题。


为什么接入 CDN?


抽象一个简单的请求链路,方便理解 CDN 的定位。


接入前:

用户 -> Nginx -> App Server

接入后:

用户 -> CDN -> Nginx -> App Server


看似增加了一层传输成本,其实不然。


CDN 利用自身广大的服务器资源,能动态优化访问路由、就近提供访问节点,以更低延迟、更高带宽从源站获取数据,优化了网络层面的用户体验。


为什么开启 CDN 缓存?


开启前:浏览器 -> CDN -> Nginx -> App Server1 -> App Server2 -> …

开启后:浏览器 <-> CDN


CDN 能够缓存用户请求到的资源,并且可以包含 HTTP 响应头。在下一次任意用户请求同样的资源时,用缓存的资源直接响应用户,节省了本该由源站处理的所有后续步骤。


更直观的表达,就是截短了请求链路。


如何开启 CDN 缓存?

在不考虑自研 CDN 的情况下,开启 CDN 缓存的步骤非常简单:

  1. 域名接入 CDN 服务,同时针对路径启用缓存

  2. 在源站设置 Cache-Control 响应头,为了更灵活地控制缓存规则,但并不是必须

哪些服务可以开启 CDN 缓存?

大部分网站都适合接入 CDN,但 SSR 页面只有满足一定条件才可以开启 CDN 缓存

  • 无用户状态

  • 对时效性要求不高,至少能接受分钟级的延迟

怎样判断是否命中缓存?

不同 CDN 平台检测的方法略有不同,本质上都是判断响应头的标识字段。以腾讯 CDN 为例,响应头 X-Cache-Lookup 分别表示

  • Hit From MemCache: 命中 CDN 节点的内存

  • Hit From Disktank: 命中 CDN 节点的磁盘

  • Hit From Upstream: 未命中缓存,回源

如果该字段不存在,说明该页面没有配置 CDN,或未开启缓存。


CDN 缓存优化


用来衡量缓存效果的重要指标是缓存命中率,在正式设置 CDN 缓存之前,我们再来了解几个提高缓存命中率的要点。这些要点也适合作为评估系统是否应该接入 CDN 缓存的标准。


延长缓存时间


提高 Cache-Control 的时间是最有效的措施,缓存持续时间越久,缓存失效的机会越少。

即使页面访问量不大的时候也能显著提高缓存命中率。

需要注意,Cache-Control 只能告知 CDN 该缓存的时间上限,并不影响它被 CDN 提早淘汰。流量过低的资源,很快会被清理掉,CDN 用逐级沉淀的缓存机制保护自己的资源不被浪费。

忽略 URL 参数


用户访问的完整 URL 可能包含了各种参数,CDN 默认会把它们当作不同的资源,每个资源又是独立的缓存。

而有些参数是明显不合预期的,例如,页面链接在微信等渠道分享后,末尾被挂上各种渠道自身设置的统计参数。平均到单个资源的访问量就会大大降低,进而降低了缓存效果。

CDN 支持后台开启 过滤参数 选项,来忽略 URL ? 后面的参数。此时同一个 URL 一律当作同一个资源文件。

在腾讯 CDN 中,忽略参数的功能无法针对某个 URL,仅支持整个域名生效,这让过滤参数成为了极具风险的操作。除非域名缓存专用,否则不建议开启这个选项,即便同域名内所有已接入 CDN 缓存的资源都不依赖 URL 参数,也不能保证将来不会因此踩坑。


主动缓存

化被动为主动,才有可能实现 100% 的缓存命中率。

常用的主动缓存是资源预热,更适合 URL 路径明确的静态文件,动态路由无法交给 CDN 智能预热,除非依次推送具体的地址。


代码演进


谈过 CDN 缓存优化的几个要点,便可得知 CDN 后台的配置是需要谨慎对待的。我在实际操作中,也经过了几个阶段的调整,可毕竟具体配置方式取决于 CDN 服务商,因此本文不再深入讨论。

现在,我们要把目光转到代码层的演进了。


一、掌控缓存

代码配置有一个前提,即 CDN 后台需要开启读取源站 Cache-Control 的支持。

而后,只要简单地添加响应头,就能从运维手中接管设置 CDN 缓存规则的主动权。

以 Node.js Koa 中间件为例,全局的初始化版本如下

app.use((ctx, next) => {  ctx.set('Cache-Control', `max-age=300`)})
复制代码

当然,上述代码的疏漏是非常多的。在 SSR 应用中,不太需要缓存所有的页面,这就要补充路径的判断条件。


二、控制路径

虽然 CDN 后台也可以配置路径,但配置方式乃至路径数量都有局限性,不如代码形式灵活。

假如我们只需要缓存 /foo 页面,就加入 if 判断

app.use((ctx, next) => {  if (ctx.path === '/foo') {    ctx.set('Cache-Control', `max-age=300`)  }})
复制代码

这就陷入了第一个陷阱,一定要注意路由对 path 的处理。一般地,'/foo' 和 '/foo/' 是两个独立的 path。可能因为 ctx.path === '/foo' 而漏掉了请求 path 为 /foo/ 的处理。


三、补充路径

伪代码如下

app.use((ctx, next) => {  if ([ '/foo', '/foo/' ].includes(ctx.path)) {    ctx.set('Cache-Control', `max-age=300`)  }})
复制代码

此外,CDN 后台的配置也需要规避这个问题。在腾讯 CDN 中,目录和文件适用于不同的页面路径。


四、忽略降级页面

在服务端渲染失败时,为了提高容错,我们会返回降级之后的页面,转为客户端渲染。如果因为偶然的网络波动,导致 CDN 缓存了降级页面,将在一段时间内持续影响用户体验。

所以我们又引入了 ctx._degrade 自定义变量,标识页面是否触发了降级

app.use(async (ctx, next) => {  if ([ '/foo', '/foo/' ].includes(ctx.path)) {    ctx.set('Cache-Control', `max-age=300`)  }
  await next()
  // 页面降级时,取消缓存  if (ctx._degrade) {    ctx.set('Cache-Control', 'no-cache')  }})
复制代码

没错,这并不是最后一个陷阱。


五、Cookie 和状态治理

上面已经提到了 CDN 可以选择性地缓存 HTTP 响应头,可是此选项是对整个域名生效,又普遍需要开启。

新的问题正是来自一个不希望被缓存的响应头。

应用 Cookie 的设置依赖于响应头 Set-Cookie 字段,Set-Cookie 的缓存直接会导致所有用户的 Cookie 被刷新为同一个。

有多个解决方案,一是该页面不要设置任何 Cookie,二是代理层过滤掉 Set-Cookie 字段。可惜腾讯 CDN 目前还不支持对响应头的过滤,这步容错必须自己操作。

app.use(async (ctx, next) => {  const enableCache = [ '/foo', '/foo/' ].includes(ctx.path)
  if (enableCache) {    ctx.set('Cache-Control', `max-age=300`)  }
  await next()
  // 页面降级时,取消缓存  if (ctx._degrade) {    ctx.set('Cache-Control', 'no-cache')  }  // 缓存页面不设 Set-Cookie  else if (enableCache) {    ctx.res.removeHeader('Set-Cookie')  }})
复制代码

上面增加的代码旨在页面响应前移除 Set-Cookie,但是中间件的加载顺序是难以控制的。特别是一些(中间件)插件,会隐式地创建 Cookie,这让 Cookie 的清理工作异常麻烦。如果后续维护人员不知情,很可能将 Set-Cookie 重新加入到响应头中。所以,这种擦屁股的工作,尽量在代理层处理,而不是放在代码逻辑中。

除了 Cookie,还可能面临其他状态信息管理问题。比如在 Vuex 的 renderState 中存放请求用户的登录状态,此时 HTML 页面嵌入了用户信息,如果被 CDN 缓存,在客户端将发生和未清除 Set-Cookie 相似的问题。类似的例子还有很多,它们的解决思路非常相像,接入 CDN 缓存前务必对状态信息做好全面的排查。


六、定制缓存路径

现在功能总算趋于正常,然而缓存规则复杂多变,如果想设置更多页面,还要单独定制缓存时间呢?这段代码仍需要不断地变动。

例如,我们只想缓存 /foo/:id,而不缓存 /foo/foo、/foo/bar 等路径。

注意 CDN 后台可能只支持配置一个 /foo/ 开头的缓存路径,这就要求我们需要将 ctx.set('Cache-Control', 'no-cache') 做为默认处理,加在中间件的第一行。

又比如,我们想缓存 /foo 页面 5 分钟,/bar 页面 1 天,又需要引入一个时间配置表。

这个中间件和相应的配置就会变得越来越难以维护。

因此,我们换一种思路,缓存规则不再交给中间件,而是转到 Vue SSR 的 entry-server,通过 metadata 可以做到页面级别的配置。由于 SSR 方案的差异性,不再赘述具体实现。


七、缓存失效

缓存失效是个中性词,如何处理 CDN 缓存失效,此中利弊不得不慎重权衡。

一方面,它会间歇增加服务压力,在 Serverless 应用中还会提高计算成本。而另一方面,许多场景我们不得不主动触发它,才能真正更新资源。

CDN 缓存的黑暗面无法让人忽视。对用户而言,缓存是透明的,对产品、技术却很可能成为阻碍。

如果处理不当,它将影响新功能能否及时发布、阻断后置所有服务的埋点、提高风险感知的成本,以及无法保障一致性,增加了线上问题的排查难度。

因此,十分有必要设立一个负责缓存刷新、预热的触发式服务,用以改进开发人员的体验。可是 CDN 缓存可控性很低,刷新也不能做到全然实时生效。

处于频繁变化的页面,最好考虑进入稳定期再开启 CDN 缓存。即使是稳定的、大流量的页面,也还需要考虑 CDN 缓存穿透的防范措施。

一旦 CDN 缓存在 SSR 架构中得到重用,就要做好长期调整决策的准备。


总结


CDN 缓存是一把利刃,在大流量的场景下,可以替源站拦截几乎所有的请求,能提供极强伸缩性的负载。


那么 SSR 应用适合接入 CDN 缓存吗?再一次细数上面提到的诸多问题…


  • 路径控制

  • 页面降级

  • 状态治理

  • 缓存失效


答案得你自己说了算。

实际上,极少数 SSR 页面场景才需要 CDN 缓存,如门户首页。

流量不高、路径分散的一般业务,只需要使用动态的 CDN 加速和静态文件缓存,就能基本满足 CDN 代理层的优化需要。



头图:Unsplash

作者:齐云雷

原文:https://mp.weixin.qq.com/s/2jW4Xc94IHeRsgHLl6hU_g

原文:SSR 页面 CDN 缓存实践

来源:微医大前端技术 - 微信公众号 [ID:wed_fed]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021 年 6 月 16 日 08:001448

评论

发布
暂无评论
发现更多内容

入职两周,怀疑自己进了假百度!跟传说中完全不一样!难道真有两家百度公司?

程序员生活志

百度 程序员生活

一款基于 Python 语言的 Linux 资源监视器!

JackTian

Python GitHub Linux bashtop bpytop

程序员陪娃系列——育儿路上二三事续

孙苏勇

程序员人生 陪伴 随笔杂谈

Linkerd 2.8 - 實現超級簡單又安全的多叢集(multicluster) Kubernetes 架構

Rammus

Kubernetes DevOps 运维 云原生 Service Mesh

程序员陪娃系列——和孩子聊生死

孙苏勇

程序员人生 陪伴 随笔杂谈

ARTS打卡 第11周

引花眠

ARTS 打卡计划

《Java并发编程的艺术》读书笔记1:说说并发编程

Jason

多线程 并发

太赞了!程序员应该访问的最佳网站都在这里了!

JackTian

GitHub 学习 编程 程序员 网站平台

学生党学编程,有这个开源项目就够了!

JackTian

GitHub 学习 编程 程序员 学生党

BIGO技术:实时计算平台建设

InfoQ_3597a20b53cc

互联网 BIGO

程序的机器级表示-控制

引花眠

计算机基础

计算机网络基础(十六)---传输层-可靠传输的基本原理

书旅

计算机网络 网络 协议族 网络层

品质网络的迭变之路,以及运营商的未来之匙

脑极体

边云协同!EM-BOX视频分析盒加速安全生产场景落地AI应用

百度大脑

人工智能 人脸识别 图像识别 百度大脑 人体识别

ARTS打卡Week 10

teoking

十年一梦,小米的原罪得到救赎了吗?

脑极体

面试这么撩准拿offer,HashMap深度学习,扰动函数、负载因子、扩容拆分,原理和实践验证,让懂了就是真的懂!

小傅哥

Java 面试 hashmap 负载因子 扰动函数

多角度分析,通讯时序数据的预测与异常检测挑战

华为云开发者社区

时序数据库 即时通讯 异常检测 网络智能体 时序预测

一次好的聊天可以超过自己努力啃几周的书籍

良知犹存

程序人生

云图说 | 3分钟创建一个游戏类工作负载

华为云开发者社区

Docker 容器 华为云 工作负载 2048游戏

内存总是不够?HBase&GeoMesa配置优化了解一下

华为云开发者社区

内存模型 内存 HBase 大集群 GeoMesa

程序员陪娃系列——育儿路上二三事

孙苏勇

程序员人生 陪伴 随笔杂谈

十多位全球技术专家,为你献上近十个小时的.Net微服务介绍

newbe36524

微服务 .net core netcore 容器化

学了那么多技术,为何依然成不了架构师

菜根老谭

架构设计原则

“云”上教与学,让教育不止步于课堂

Geek_116789

程序员陪娃系列——小娃的到来

孙苏勇

程序员人生 陪伴 随笔杂谈

程序员陪娃系列——见你的第一面

孙苏勇

程序员人生 陪伴 随笔杂谈

【API进阶之路】帮公司省下20万调研费!如何巧用情感分析API实现用户偏好调研

华为云开发者社区

反馈 API 华为云 API Explorer平台 用户调研

Android 原生 SQLite 数据库的一次封装实践

vivo互联网技术

sqlite android 数据库

趣文:那天我被拉入 C++ 亲友群

程序员生活志

c c++ 程序员趣事

程序员陪娃系列——小小免费按摩师

孙苏勇

程序员人生 陪伴 随笔杂谈

技术为帆,纵横四海- Lazada技术东南亚探索和成长之旅

技术为帆,纵横四海- Lazada技术东南亚探索和成长之旅

SSR 页面 CDN 缓存实践-InfoQ