极客时间已完结课程限时免费阅读

02 | 如何才能做好动静分离?有哪些方案可选?

02 | 如何才能做好动静分离?有哪些方案可选?-极客时间

02 | 如何才能做好动静分离?有哪些方案可选?

讲述:秭明

时长16:36大小7.60M

上一篇文章中,我介绍了秒杀系统在架构上要考虑的几个原则,我估计你很快就会问:“知易行难,这些原则应该怎么应用到系统中呢?”别急,从这篇文章开始,我就会逐一介绍秒杀系统的各个关键环节中涉及的关键技术。
今天我们就先来讨论第一个关键点:数据的动静分离。不知道你之前听过这个解决方案吗?不管你有没有听过,我都建议你先停下来思考动静分离的价值。如果你的系统还没有开始应用动静分离的方案,那你也可以想想为什么没有,是之前没有想到,还是说业务体量根本用不着?
不过我可以确信地说,如果你在一个业务飞速发展的公司里,并且你在深度参与公司内类秒杀类系统的架构或者开发工作,那么你迟早会想到动静分离的方案。为什么?很简单,秒杀的场景中,对于系统的要求其实就三个字:快、准、稳
那怎么才能“快”起来呢?我觉得抽象起来讲,就只有两点,一点是提高单次请求的效率,一点是减少没必要的请求。今天我们聊到的“动静分离”其实就是瞄着这个大方向去的。
不知道你是否还记得,最早的秒杀系统其实是要刷新整体页面的,但后来秒杀的时候,你只要点击“刷新抢宝”按钮就够了,这种变化的本质就是动静分离,分离之后,客户端大幅度减少了请求的数据量。这不自然就“快”了吗?

何为动静数据

那到底什么才是动静分离呢?所谓“动静分离”,其实就是把用户请求的数据(如 HTML 页面)划分为“动态数据”和“静态数据”。
简单来说,“动态数据”和“静态数据”的主要区别就是看页面中输出的数据是否和 URL、浏览者、时间、地域相关,以及是否含有 Cookie 等私密数据。比如说:
很多媒体类的网站,某一篇文章的内容不管是你访问还是我访问,它都是一样的。所以它就是一个典型的静态数据,但是它是个动态页面。
我们如果现在访问淘宝的首页,每个人看到的页面可能都是不一样的,淘宝首页中包含了很多根据访问者特征推荐的信息,而这些个性化的数据就可以理解为动态数据了。
这里再强调一下,我们所说的静态数据,不能仅仅理解为传统意义上完全存在磁盘上的 HTML 页面,它也可能是经过 Java 系统产生的页面,但是它输出的页面本身不包含上面所说的那些因素。也就是所谓“动态”还是“静态”,并不是说数据本身是否动静,而是数据中是否含有和访问者相关的个性化数据。
还有一点要注意,就是页面中“不包含”,指的是“页面的 HTML 源码中不含有”,这一点务必要清楚。
理解了静态数据和动态数据,我估计你很容易就能想明白“动静分离”这个方案的来龙去脉了。分离了动静数据,我们就可以对分离出来的静态数据做缓存,有了缓存之后,静态数据的“访问效率”自然就提高了。
那么,怎样对静态数据做缓存呢?我在这里总结了几个重点。
第一,你应该把静态数据缓存到离用户最近的地方。静态数据就是那些相对不会变化的数据,因此我们可以把它们缓存起来。缓存到哪里呢?常见的就三种,用户浏览器里、CDN 上或者在服务端的 Cache 中。你应该根据情况,把它们尽量缓存到离用户最近的地方。
第二,静态化改造就是要直接缓存 HTTP 连接。相较于普通的数据缓存而言,你肯定还听过系统的静态化改造。静态化改造是直接缓存 HTTP 连接而不是仅仅缓存数据,如下图所示,Web 代理服务器根据请求 URL,直接取出对应的 HTTP 响应头和响应体然后直接返回,这个响应过程简单得连 HTTP 协议都不用重新组装,甚至连 HTTP 请求头也不需要解析。
图 1 静态化改造
第三,让谁来缓存静态数据也很重要。不同语言写的 Cache 软件处理缓存数据的效率也各不相同。以 Java 为例,因为 Java 系统本身也有其弱点(比如不擅长处理大量连接请求,每个连接消耗的内存较多,Servlet 容器解析 HTTP 协议较慢),所以你可以不在 Java 层做缓存,而是直接在 Web 服务器层上做,这样你就可以屏蔽 Java 语言层面的一些弱点;而相比起来,Web 服务器(如 Nginx、Apache、Varnish)也更擅长处理大并发的静态文件请求。

如何做动静分离的改造

理解了动静态数据的“why”和“what”,接下来我们就要看“how”了。我们如何把动态页面改造成适合缓存的静态页面呢?其实也很简单,就是去除前面所说的那几个影响因素,把它们单独分离出来,做动静分离。
下面,我以典型的商品详情系统为例来详细介绍。这里,你可以先打开京东或者淘宝的商品详情页,看看这个页面里都有哪些动静数据。我们从以下 5 个方面来分离出动态内容。
URL 唯一化。商品详情系统天然地就可以做到 URL 唯一化,比如每个商品都由 ID 来标识,那么 http://item.xxx.com/item.htm?id=xxxx 就可以作为唯一的 URL 标识。为啥要 URL 唯一呢?前面说了我们是要缓存整个 HTTP 连接,那么以什么作为 Key 呢?就以 URL 作为缓存的 Key,例如以 id=xxx 这个格式进行区分。
分离浏览者相关的因素。浏览者相关的因素包括是否已登录,以及登录身份等,这些相关因素我们可以单独拆分出来,通过动态请求来获取。
分离时间因素。服务端输出的时间也通过动态请求获取。
异步化地域因素。详情页面上与地域相关的因素做成异步方式获取,当然你也可以通过动态请求方式获取,只是这里通过异步获取更合适。
去掉 Cookie。服务端输出的页面包含的 Cookie 可以通过代码软件来删除,如 Web 服务器 Varnish 可以通过 unset req.http.cookie 命令去掉 Cookie。注意,这里说的去掉 Cookie 并不是用户端收到的页面就不含 Cookie 了,而是说,在缓存的静态数据中不含有 Cookie。
分离出动态内容之后,如何组织这些内容页就变得非常关键了。这里我要提醒你一点,因为这其中很多动态内容都会被页面中的其他模块用到,如判断该用户是否已登录、用户 ID 是否匹配等,所以这个时候我们应该将这些信息 JSON 化(用 JSON 格式组织这些数据),以方便前端获取。
前面我们介绍里用缓存的方式来处理静态数据。而动态内容的处理通常有两种方案:ESI(Edge Side Includes)方案和 CSI(Client Side Include)方案。
ESI 方案(或者 SSI):即在 Web 代理服务器上做动态内容请求,并将请求插入到静态页面中,当用户拿到页面时已经是一个完整的页面了。这种方式对服务端性能有些影响,但是用户体验较好。
CSI 方案。即单独发起一个异步 JavaScript 请求,以向服务端获取动态内容。这种方式服务端性能更佳,但是用户端页面可能会延时,体验稍差。

动静分离的几种架构方案

前面我们通过改造把静态数据和动态数据做了分离,那么如何在系统架构上进一步对这些动态和静态数据重新组合,再完整地输出给用户呢?
这就涉及对用户请求路径进行合理的架构了。根据架构上的复杂度,有 3 种方案可选:
实体机单机部署;
统一 Cache 层;
上 CDN。
方案 1:实体机单机部署
这种方案是将虚拟机改为实体机,以增大 Cache 的容量,并且采用了一致性 Hash 分组的方式来提升命中率。这里将 Cache 分成若干组,是希望能达到命中率和访问热点的平衡。Hash 分组越少,缓存的命中率肯定就会越高,但短板是也会使单个商品集中在一个分组中,容易导致 Cache 被击穿,所以我们应该适当增加多个相同的分组,来平衡访问热点和命中率的问题。
这里我给出了实体机单机部署方案的结构图,如下:
图 2 Nginx+Cache+Java 结构实体机单机部署
实体机单机部署有以下几个优点:
没有网络瓶颈,而且能使用大内存;
既能提升命中率,又能减少 Gzip 压缩;
减少 Cache 失效压力,因为采用定时失效方式,例如只缓存 3 秒钟,过期即自动失效。
这个方案中,虽然把通常只需要虚拟机或者容器运行的 Java 应用换成实体机,优势很明显,它会增加单机的内存容量,但是一定程度上也造成了 CPU 的浪费,因为单个的 Java 进程很难用完整个实体机的 CPU。
另外就是,一个实体机上部署了 Java 应用又作为 Cache 来使用,这造成了运维上的高复杂度,所以这是一个折中的方案。如果你的公司里,没有更多的系统有类似需求,那么这样做也比较合适,如果你们有多个业务系统都有静态化改造的需求,那还是建议把 Cache 层单独抽出来公用比较合理,如下面的方案 2 所示。
方案 2:统一 Cache 层
所谓统一 Cache 层,就是将单机的 Cache 统一分离出来,形成一个单独的 Cache 集群。统一 Cache 层是个更理想的可推广方案,该方案的结构图如下:
图 3 统一 Cache
将 Cache 层单独拿出来统一管理可以减少运维成本,同时也方便接入其他静态化系统。此外,它还有一些优点。
单独一个 Cache 层,可以减少多个应用接入时使用 Cache 的成本。这样接入的应用只要维护自己的 Java 系统就好,不需要单独维护 Cache,而只关心如何使用即可。
统一 Cache 的方案更易于维护,如后面加强监控、配置的自动化,只需要一套解决方案就行,统一起来维护升级也比较方便。
可以共享内存,最大化利用内存,不同系统之间的内存可以动态切换,从而能够有效应对各种攻击。
这种方案虽然维护上更方便了,但是也带来了其他一些问题,比如缓存更加集中,导致:
Cache 层内部交换网络成为瓶颈;
缓存服务器的网卡也会是瓶颈;
机器少风险较大,挂掉一台就会影响很大一部分缓存数据。
要解决上面这些问题,可以再对 Cache 做 Hash 分组,即一组 Cache 缓存的内容相同,这样能够避免热点数据过度集中导致新的瓶颈产生。
方案 3:上 CDN
在将整个系统做动静分离后,我们自然会想到更进一步的方案,就是将 Cache 进一步前移到 CDN 上,因为 CDN 离用户最近,效果会更好。
但是要想这么做,有以下几个问题需要解决。
失效问题。前面我们也有提到过缓存时效的问题,不知道你有没有理解,我再来解释一下。谈到静态数据时,我说过一个关键词叫“相对不变”,它的言外之意是“可能会变化”。比如一篇文章,现在不变,但如果你发现个错别字,是不是就会变化了?如果你的缓存时效很长,那用户端在很长一段时间内看到的都是错的。所以,这个方案中也是,我们需要保证 CDN 可以在秒级时间内,让分布在全国各地的 Cache 同时失效,这对 CDN 的失效系统要求很高。
命中率问题。Cache 最重要的一个衡量指标就是“高命中率”,不然 Cache 的存在就失去了意义。同样,如果将数据全部放到全国的 CDN 上,必然导致 Cache 分散,而 Cache 分散又会导致访问请求命中同一个 Cache 的可能性降低,那么命中率就成为一个问题。
发布更新问题。如果一个业务系统每周都有日常业务需要发布,那么发布系统必须足够简洁高效,而且你还要考虑有问题时快速回滚和排查问题的简便性。
从前面的分析来看,将商品详情系统放到全国的所有 CDN 节点上是不太现实的,因为存在失效问题、命中率问题以及系统的发布更新问题。那么是否可以选择若干个节点来尝试实施呢?答案是“可以”,但是这样的节点需要满足几个条件:
靠近访问量比较集中的地区;
离主站相对较远;
节点到主站间的网络比较好,而且稳定;
节点容量比较大,不会占用其他 CDN 太多的资源。
最后,还有一点也很重要,那就是:节点不要太多。
基于上面几个因素,选择 CDN 的二级 Cache 比较合适,因为二级 Cache 数量偏少,容量也更大,让用户的请求先回源的 CDN 的二级 Cache 中,如果没命中再回源站获取数据,部署方式如下图所示:
图 4 CDN 化部署方案
使用 CDN 的二级 Cache 作为缓存,可以达到和当前服务端静态化 Cache 类似的命中率,因为节点数不多,Cache 不是很分散,访问量也比较集中,这样也就解决了命中率问题,同时能够给用户最好的访问体验,是当前比较理想的一种 CDN 化方案。
除此之外,CDN 化部署方案还有以下几个特点:
把整个页面缓存在用户浏览器中;
如果强制刷新整个页面,也会请求 CDN;
实际有效请求,只是用户对“刷新抢宝”按钮的点击。
这样就把 90% 的静态数据缓存在了用户端或者 CDN 上,当真正秒杀时,用户只需要点击特殊的“刷新抢宝”按钮,而不需要刷新整个页面。这样一来,系统只是向服务端请求很少的有效数据,而不需要重复请求大量的静态数据。
秒杀的动态数据和普通详情页面的动态数据相比更少,性能也提升了 3 倍以上。所以“抢宝”这种设计思路,让我们不用刷新页面就能够很好地请求到服务端最新的动态数据。

总结一下

今天,我主要介绍了实现动静分离的几种思路,并由易到难给出了几种架构方案,以及它们各自的优缺点。可以看到,不同的架构方案会引入不同的问题,比如我们把缓存数据从 CDN 上移到用户的浏览器里,针对秒杀这个场景是没问题的,但针对一般的商品可否也这样做呢?
你可能会问,存储在浏览器或 CDN 上,有多大区别?我的回答是:区别很大!因为在 CDN 上,我们可以做主动失效,而在用户的浏览器里就更不可控,如果用户不主动刷新的话,你很难主动地把消息推送给用户的浏览器。
另外,在什么地方把静态数据和动态数据合并并渲染出一个完整的页面也很关键,假如在用户的浏览器里合并,那么服务端可以减少渲染整个页面的 CPU 消耗。如果在服务端合并的话,就要考虑缓存的数据是否进行 Gzip 压缩了:如果缓存 Gzip 压缩后的静态数据可以减少缓存的数据量,但是进行页面合并渲染时就要先解压,然后再压缩完整的页面数据输出给用户;如果缓存未压缩的静态数据,这样不用解压静态数据,但是会增加缓存容量。虽然这些都是细节问题,但你在设计架构方案时都需要考虑清楚。
最后,欢迎在留言区分享讨论你对于数据动静分离的一些关键认知,我会第一时间给你反馈。
分享给需要的人,Ta购买本课程,你将得16
生成海报并分享

赞 22

提建议

上一篇
01 | 设计秒杀系统时应该注意的5个架构原则
下一篇
03 | 二八原则:有针对性地处理好系统的“热点数据”
unpreview
 写留言

精选留言(64)

  • james
    2019-02-14
    现在不都是前后端分离嘛, 不知道为啥还要将动静分离拿出来说事, 前端页面部署CDN早就标配了, 对于秒杀操作, 没有哪个平台是让用户刷新页面的, 都是到时间后点击按钮来秒杀, 而且这个缓存策略不应该是第一次访问不到再查询缓存, 应该在商家创建完秒杀产品后, 缓存就有了, 如果修改内容, 就直接更新缓存, 秒杀开始后, 商家就不能再修改了, 不想抬杠, 反正我认为这篇文章没啥干货
    共 20 条评论
    182
  • J.M.Liu
    2018-10-23
    Hash 分组越少,缓存的命中率肯定就会越高,但短板是也会使单个商品集中在一个分组中,容易导致 Cache 被击穿,所以我们应该适当增加多个相同的分组,来平衡访问热点和命中率的问题。 老师,我也不是很明白hash分组的多少和命中率之间的关系。命中率不是只和热点数据是否被缓存有关吗,为什么和分组的多少也有关系啊?还有您说的cache分散是什么概念啊?这个问题好像很多人都问到了,看来很多人都不明白,希望老师可以比较详细地解答一下,谢谢老师。
    展开

    作者回复: 举个栗子,假如三次访问同一个商品,cache分别有3组,那么如果这3次访问都分别访问不同的3组cache那么是不是3次都没命中。如果只是一组cache的话,那么除了第一次没有命中外,后面两次就命中了。所以cache分组对命中率是不是有影响了。

    共 8 条评论
    28
  • 2018-11-13
    动静分离的思想暗含着,分而治之和重点把控变化的思维。 分而治之-几乎所有不能直接解决的问题,都会分割开来,直到比较容易解决为止,学习也类似,把不能理解的知识作分割,直到比较容易理解 把控变化-管理或者研究事物发展的趋势,就需要重点关注和把控变化的情况,静态的容易处理,所以,变化的就对整体起到了决定性的作用 对于编程缓存是提高性能的杀手锏,静态资源自然是越靠前越好,动态资源也是越靠前越好,但是需要注意处理好变化的一致性问题
    展开

    作者回复: 抽象总结能力很强😉

    共 2 条评论
    21
  • 阿毛
    2018-10-02
    分离时间因素。服务端输出的时间也通过动态请求获取。 这里的时间为什么也要动态请求呢?

    作者回复: 因为时间要以服务端为准,客户端的时间用户可以自己修改

    18
  • 落叶飞逝的恋
    2018-11-09
    老师对于目前而言,你讲的架构方面,比如CDN、Nginx这些对于目前我的工作内容而言,涉及不到,哪怕上个新项目,我只要向运维申请机器,其他的不管,也没机会管,那么我如何快速的补充的这些东西,因为上面的讲的内容,可能对于我而言太空洞,应该如何避免?

    作者回复: 只要你想学一定能找到机会,至于如何向别人学习,最简单的一个办法,多和那个同学吃吃饭,一回生二回熟😊

    共 3 条评论
    16
  • 嚴脂红.*
    2018-10-04
    我想知道老师用的配图都是用什么软件做的呢?

    作者回复: Viso就行啊

    15
  • Rosso
    2018-10-03
    CDN二级cache缓存是一个什么概念?是一个CDN上的术语么?百度了一下没怎么查到

    作者回复: 二级cache是指cdn设置了多级回源机制,就是如果缓存没有命中再到二级缓存中去取,而不是直接回服务端来请求 本质是减少回原的请求量

    13
  • Hana
    2018-10-20
    对于浏览器端的动静分离,疑问 问题一:动静数据在服务器端合成的话,是用什么怎么合成的? 而对于app原生开发的话 问题二:商品详情返回动静数据都是json格式的,那么在服务端如何合成呢?还是只能前段来分开请求合成?
    展开

    作者回复: 动态数据在服务端合成是通过一种叫ESI的技术实现的,例如<esi:src="xx.htm"/>在静态页面中包含这种标签,然后由varnish等软件来解析这种标签,再发起动态请求,把动态请求的结果合成到静态页面中,最终形成一个完整的页面 服务端生成json格式的目的本身是方便让前端页面渲染时容易使用这些数据。可以把这些json数据在服务端合成一起输出,也可以在浏览器里单独发一个ajax请求再异步获取都可以

    共 2 条评论
    9
  • A星空之夜
    2018-10-02
    方案一和方案二中静态页面和动态资源的组装完整页面是在服务端吗?

    作者回复: 理论上都可以,各有优缺点,秒杀推荐在客户端做,普通的商品推荐在服务端做

    6
  • 星一
    2019-11-08
    我居然看不下去了,感觉很多地方都没有解释明白
    5
  • 杨晓维
    2019-07-17
    所以到这边还没有用到Redis吗?
    共 2 条评论
    5
  • 他城之途
    2018-11-04
    看评论,很多人被老师的“缓存命中率”整懵了,我觉得老师把“命中率”改成“利用率”可能会更好避免误解,老师的意思是分组多了,单个分组的“利用率”就低了。但在很多人的概念里,“命中率”是指缓存打中率,跟第一次和失效有关。

    作者回复: 😉,在单独的qa文章中有介绍

    共 3 条评论
    4
  • sgl
    2018-10-02
    想问一下,方案一和方案二里的cahe层,还有一个缓存,缓存是什么?

    作者回复: Cache层是web缓存可以直接缓存http请求,例如varnish 另外一个缓存就是redis这种

    4
  • escray
    2020-11-19
    即使不做秒杀系统,也需要做动静分离,但是秒杀系统可能需要把动静分离做到极致。 动静的差别主要是看数据中是否含有和访问者相关的个性化数据,更具体的说就是看,页面的 HTML 源码中是否含有个性化数据。 文中讲的三种动静分离架构方案,似乎的确有时间的影子,但是即使现在也不是每个秒杀系统都需要上 CDN 的,也可能是大家都被云厂商“惯坏了”。 没有做过秒杀系统,很多内容看的也不是很明白,抱着虚心学习的心态。看到留言里面不少“大佬”吐槽,有点好奇。按照资历和经验来看,国内能够超过作者的人可能并不多,但是似乎都自我感觉良好。 其实专栏的主旨并不是手把手教你搭建一个秒杀系统,而是讲秒杀系统的或者说类似的高并发、高性能、数据一致性的系统的设计思路。
    展开
    3
  • 🐻两脚兽จุ๊บ。
    2019-01-23
    许大大你好. 今天看了您这篇动静分离的干货, 受益匪浅. 想请教个问题. 方案1里面提到的把通常只需要虚拟机或者容器运行的 Java 应用换成实体机, 并且列出了实体机的几个优点. 这里面还是不大懂实体机相对于虚拟机为什么会存在这些差异. 我认为的是: 虚拟机只不过是实体机通过vps软件虚拟化出来的服务器而已, 同等配置的虚拟机跟实体机本质上没什么差别, 为什么会有网络瓶颈, 甚至大内存差别??? 希望得到徐老师的回复.谢谢

    作者回复: 多个虚拟机实际上就是共用一个实体机,所以每个虚拟机能用的资源就是少一些,因为每个虚拟机会竞争整个实体机的资源,例如网络和内存。所以单个实例来讲虚拟机和实体机能用的资源还是有很大差异的

    共 3 条评论
    3
  • 小喵喵
    2018-10-02
    缓存失效了怎么办呢?文中没有给出答案,请指导一下。

    作者回复: 失效需要一个失效系统来实现,一般有主动失效和被动失效 主动失效需要监控数据库数据的变化然后转成消息来发送失效消息,这个实现比较复杂,阿里有个系统叫metaq,大家可以去网上查查 被动失效就是只缓存固定时间,然后到期后自动失效

    3
  • 缪文
    2018-10-02
    统一cache层,就是类似于redis或者阿里的tair这样的中间件吧?缓存单点怎么解决的?同一个商品的缓存再怎么hash都是落到同一个tair节点的

    作者回复: 统一cache层的缓存是web型的缓存,如varnish 缓存单点是通过hash分组,即多个分组缓存一样的内容 Hash分组是通过nginx完成的,一个分组的机器配在nginx的stream里

    3
  • 小野
    2019-01-30
    Cache分组越多,命中率越低能理解。为什么分组越少越容易Cache被击穿呢。按我个人理解,Cache击穿就是同一个Key同一时间大量并发请求因为Cache中没有,都请求到后端系统去了。分组越多还是一样都击穿到后端系统去了啊

    作者回复: 正常情况下,你的理解是对的 这里说的问题是,如果一个key访问量过大,到导致单台的cache实例支持不了这么大的访问,从而导致cache失效,然后在把流量直接打到后端服务的情况

    共 3 条评论
    2
  • func
    2018-10-08
    许老师,架构图中的cache 是通过Varnish缓存的精通数据吗?nginx 也可以实现 为什么要单独加一层这个呢?

    作者回复: 主要是性能和稳定性考虑

    2
  • func
    2018-10-08
    没明白老师数的这个意思指的是?Cache层是web缓存可以直接缓存http请求,例如varnish?web缓存,缓存http请求?缓存的是请求后返回的数据?如果是同一个http请求就不会走到 redis cache了?

    作者回复: 你可以了解一下varnish的原理

    2