当前位置: 首页 > 科技观察

微服务前端数据加载的优秀实践

时间:2023-03-21 00:33:55 科技观察

目前,已经有不少团队逐步实现了微服务架构。比如在前端圈很流行的BFF(BackendForFrontend),其实就是微服务架构的一个变种。前端团队维护一套“胶水层/接入层/API层”的服务,调用后台团队提供的若干个微服务,将微服务的结果进行逻辑组装,打包对外API。在这种架构下,服务大致可以分为两种角色:前端服务(Frontend),对底层微服务进行包装,直接对外暴露可调用的API。比如在BFF架构中,很可能是一个用Node.js写的HTTPServer。后台微服务(Microservices),通常是后端团队提供的单个服务,承载不同模块的功能,提供一系列内部调用接口。本文主要分享该架构下前端服务加载数据的几种最佳实践。最简单的情况,我们先考虑最简单的情况,就是每当有外部请求进来,前端服务会向后台的几个微服务请求数据,然后进行逻辑处理并返回响应:这个简单模型显然存在一个问题:每次外部请求都会触发多次内部服务调用,这是一种资源浪费,因为对于大多数内部微服务来说,请求的结果在一定时间内是可以缓存的。例如,用户的头像可能每隔几天、几周甚至几个月才更新一次。在这种情况下,前端服务可以将用户的头像缓存一段时间。这期间可以直接从缓存中读取用户的头像。无需请求后台,大大减轻了后台服务的负担。添加本地缓存所以我们在前端服务中添加了本地内存缓存(LocalCache),这样大部分请求都会命中本地缓存,从而减轻后台服务的负担:本地缓存通常放在内存中,而且内存空间比较大。有限,所以我们需要引入缓存淘汰机制来限制内存的最大容量。具体的缓存淘汰算法有很多,比如FIFO、LFU、LRU、MRU、ARC等,可以根据业务的实际情况选择合适的算法。引入本地缓存后,还有一个问题:缓存只能对单个服务实例生效(服务实例可以理解为服务器、K8SPod等概念),而大多数前端服务都是通常不可用,以便能够水平扩展。状态,所以会有大量的并发实例。也就是说,本地缓存可能只在某台服务器上生效,其他并行服务器上是没有缓存的。如果请求命中没有缓存的服务器,它仍然无法命中缓存。还有一个问题就是缓存逻辑和应用逻辑是耦合的,在各个接口的代码中可能会有这样的逻辑:varcachedData=cache.get(key)if(cachedData!==undefined){returncachedData}else{returnfetch('...')}不要重复自己!我们显然需要抽象这个缓存逻辑来避免代码重复。加入Cache层和集中缓存为了解决以上两个问题,我们不断完善我们的架构:增加一个集中的远程缓存(如Redis、Memcache),让远程缓存可以应用到所有实例;将应用层的逻辑抽象成一个单独的组件(CacheLayer),用于封装后台微服务的读写、本地缓存、远程缓存相关的逻辑。抽象出这么一层CacheLayer之后,我们就可以进一步演进我们的服务了。添加缓存刷新机制虽然我们有一个集中缓存,但是缓存毕竟只有很短的时间有效。一旦缓存失效,还是要向后台服务请求数据。在这种临界条件下,请求时间会增加,会出现耗时毛刺(每隔一段时间,少量请求会耗时更长)。那么有没有办法让缓存保持“新鲜”呢?这就需要缓存刷新机制。一般来说,缓存刷新分为主动刷新和被动刷新两种:主动刷新和主动刷新,即每当有数据更新时,就刷新缓存,下游服务始终只读取缓存中的数据。数据。读多写少的后台服务非常适合这种模式,因为读请求永远不会命中数据库,而是分流到性能和扩展性更高的缓存组件,从而大大降低数据库的负载。压力。当然,主动刷新并不完美。这意味着前后端服务必须耦合在缓存组件上(例如缓存key的名称、数据结构等需要约定),这带来了一定的隐患。一旦后端微服务写入缓存错误,或者某个缓存组件出现可用性问题,结果很可能是灾难性的。所以这种模式更适合单个服务,而不是多个服务之间。PassiveRefresh被动刷新是指在读取缓存数据时,根据缓存的剩余有效期或类似指标来决定是否异步刷新缓存(类似于HTTP协议中的stale-while-revalidate)。与主动刷新相比,这种模式的优点是服务之间的耦合度较小,缺点是1.只能根据访问热点进行缓存,不能全量缓存;2、只能根据相关指标被动刷新,减少了数据消耗。直接。如果团队的前端服务(比如BFF)和后端服务由两套人员开发和维护,使用这样的缓存模式比较合适。当然,具体选择哪种模式要根据实际情况而定。缓存是一个非常灵活、万能的组件,这里限于篇幅就不展开深入了。关于缓存的设计模式,可以参考这里:donnemartin/system-design-primergithub.com针对大流量业务的请求汇聚,同一前端服务实例可能有上百个请求在同时,这些请求会触发缓存和后台服务的大量读取请求。在大多数情况下,这些并发读请求可以归类为少数请求。这个思路很像Facebook开源的dataloader,将相同参数的并行请求收集在一起,从而减轻后端服务的压力(这个问题在GraphQL的使用场景中很容易出现)。容灾缓存我们不妨考虑一个极端的情况:如果后台服务全部宕机,前端服务能否利用缓存“撑”一段时间?这就是容灾缓存的概念,即当服务出现异常时,降级使用缓存中的数据来响应外部请求,保证一定的可用性。容灾缓存的逻辑也可以抽象到CacheLayer中。