前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >微服务前端数据加载的最佳实践

微服务前端数据加载的最佳实践

作者头像
lucifer210
发布2021-05-10 15:27:42
9270
发布2021-05-10 15:27:42
举报
文章被收录于专栏:脑洞前端脑洞前端

作者:Starkwang https://zhuanlan.zhihu.com/p/351044054

目前在不少团队里已经逐步实践落地了微服务架构,比如前端圈很流行的 BFF(Backend For Frontend)其实就是微服务架构的一种变种,即让前端团队维护一套“胶水层/接入层/API层”的服务,调用后台团队提供的若干个微服务,将微服务的结果进行逻辑组装,从而包装出对外的 API。

在这种架构下,服务在大体上可以分为两种角色:

  1. 前端服务(Frontend),包装底层的微服务,对外直接暴露可调用的 API。例如在 BFF 架构里,很可能就是一个 Node.js 写成的 HTTP Server。
  2. 后台微服务(Microservices),通常由后端团队提供的单体服务,承载不同模块的功能,提供一系列的内部调用接口。

这篇文章主要分享这种架构下,前端服务进行数据加载的几种最佳实践。

最简单的情形

我们先考虑一种最简单的情形,也就是每当有外部请求进来,那么前端服务都会向若干个后台微服务请求数据,然后进行逻辑处理,返回响应:

这种朴素的模型明显存在一个问题:每个外部请求都会触发多次内部服务调用,这样的做法非常浪费资源,因为对于大多数内部微服务而言,请求的结果在一定的时间内都是可缓存的。

比如用户的头像可能几天几周甚至几个月才更新一次,这种情况下前端服务完全可以缓存用户的头像一段时间,这段时间内,读取用户头像可以从直接从缓存内读取,而不需要请求后台,很大程度上节省了后台服务的负担。

加入本地缓存

于是我们在前端服务中加入了本地内存缓存(Local Cache),让大多数请求都命中本地缓存,从而减少了后台服务的负担:

本地缓存通常是放置在内存里的,而内存空间比较有限,所以我们需要引入缓存淘汰的机制,限制内存最大容量。具体的缓存淘汰算法就有很多了,比如 FIFO、LFU、LRU、MRU、ARC 等等,可以根据业务的实际情况来选择合适的算法。

引入本地缓存之后,依然会有一个问题:缓存只能在单个服务实例(服务实例可以理解为服务器、K8S Pod之类的概念)上生效,而大多数前端服务为了能够横向扩容,一般都是无状态的,所以会有大量并存的实例。

也就是说,本地缓存可能只会在某台服务器上生效,而其他平行的服务器上没有缓存,如果请求打到了没有缓存的服务器上,那么依然无法命中缓存。

另外一个问题就是,缓存逻辑和应用逻辑是耦合的,每一个接口的代码里可能都会存在类似这样的逻辑:

代码语言:javascript
复制
var cachedData = cache.get(key)
if (cachedData !== undefined) {
  return cachedData
} else {
  return fetch('...')
}

Don't Repeat Yourself! 我们显然需要把这段缓存逻辑抽象出来,避免重复代码。

加入 Cache 层和中心化缓存

为了解决上面两个问题,我们继续改进我们的架构:

  1. 加入中心化的远程缓存(比如 Redis、Memcache),让远程缓存可以作用到所有实例上面;
  2. 将缓存、RPC 等非应用层的逻辑抽象为单独的组件(Cache Layer),用来封装后台微服务的读写、本地缓存、远程缓存相关的逻辑。

抽象出这样一层 Cache Layer 之后,我们便可以进一步演进我们的服务。

加入缓存刷新机制

虽然我们有了中心化的缓存,但缓存毕竟只是短期内有效的。一旦缓存失效,那就还是得向后台服务请求数据,在这种临界条件下,请求耗时就会增加,出现耗时的毛刺现象(每隔一段时间,有小部分请求耗时变大)。

那么有没有办法可以让缓存一直保持“新鲜”呢?这就需要缓存刷新的机制了,大体上讲,缓存刷新分为主动刷新和被动刷新两种:

主动刷新

主动刷新即每当数据有更新的时候,刷新缓存,下游服务永远只读取缓存内的数据。

读多写少的后台服务非常适合这种模式,因为读请求永远不会打到数据库里,而是被分流到性能、扩展性高几个档次的缓存组件上面,从而很大程度上减轻数据库的压力。

当然主动刷新也并不是完美无缺的,它意味着前后端服务必须要在缓存组件上产生耦合(比如需要约定缓存 key 的命名、数据结构等),这就带来了一定的隐患,一旦后端微服务错误地写入了缓存,或者缓存组件出现可用性问题,结果很可能是灾难性的。所以这种模式更适合单个服务内部,而不是多个服务之间。

被动刷新

被动刷新即读取缓存数据的时候,根据缓存的剩余有效期或者类似指标,决定要不要异步刷新缓存(类似 HTTP 协议的 stale-while-revalidate)。

这种模式相比于主动刷新,优点是服务间的耦合更少一些,但缺点在于 1. 只能根据访问热点进行缓存,无法全量缓存;2. 只能根据相关指标被动刷新,降低了数据的即时性。

如果团队的前端服务(如 BFF)和后台服务是由两套人员开发维护,比较适合使用这样的缓存模式。当然具体选择哪种模式,得根据实际情况来决定。

缓存是一个非常灵活并且万金油的组件,这里篇幅有限就不再深入,更多关于缓存的设计模式,可以参考这里:

donnemartin/system-design-primergithub.com

请求收敛

对于大流量的业务而言,可能同时会有成百上千的请求打到同一个前端服务实例上,这些请求会触发大量的对缓存、后台服务的读请求,大多数情况下,这些并发的读请求是可以收归为少数几个请求的。

这种思路和 Facebook 开源的 dataloader 非常相似,将并行的、参数相同的请求收归到一起,从而降低后端服务的压力(在 GraphQL 的使用场景下很容易出现这种问题)。

容灾缓存

我们不妨考虑一种极端的情况:如果后台服务全挂了,前端服务能不能使用缓存里的来“撑住”一段时间?这就是容灾缓存的概念,即在服务异常的时候,降级到使用缓存中的数据来响应外部请求,保证一定的可用性。容灾缓存的逻辑,同样可以抽象到 Cache Layer 中。

降级、容灾的逻辑是非常灵活的,需要根据实际业务情况来制定,可以参考一些更深入的文章。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-05-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 脑洞前端 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 最简单的情形
  • 加入本地缓存
  • 加入 Cache 层和中心化缓存
  • 加入缓存刷新机制
  • 请求收敛
  • 容灾缓存
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档