架构:应用多级缓存模式支撑海量读服务。

本文不涉及缓存数据结构优化、缓存空间利用率跟业务数据相关的细节问题,主要从架构和提升命中率等层面来探讨缓存方案;本文也不讨论写服务,而是聚焦读服务。这里将基于多级缓存模式来介绍应用缓存时需要注意的问题和一些解决方案,其中一些方案已经在业务中实施。

多级缓存介绍

所谓多级缓存,即在整个系统架构的不同系统层级进行数据缓存,以提升访问效率,这也是应用最广的方案之一。我们应用的整体架构如下图所示。

整体流程分析如下:

  1. 首先接入Nginx将请求负载均衡到应用Nginx,此处常用的负载均衡算法是轮询或者一致性哈希,轮询可以使服务器的请求更加均衡,而一致性哈希可以提升应用Nginx的缓存命中率,相对于轮询,一致性哈希会存在单机热点问题,一种解决办法是热点直接推送到新入层Nginx,另一种办法是设置一个阀值,当超过阀值,改为轮询算法。
  2. 接着应用Nginx读取本地缓存,如果本地缓存命中则直接返回。应用Nginx本地缓存可以提升整体的吞吐量,降低后端的压力,尤其应对热点问题非常有效。本地缓存可以使用Lua Shared Dict、Nginx Proxy Cache(磁盘/内存)、Local Redis实现。
  3. 如果Nginx本地缓存没命中,则会读取相应的分布式缓存(如Redis缓存,另外可以考虑使用主从架构来提升性能和吞吐量),如果分布式缓存命中则直接返回相应数据(并回写到Nginx本地缓存)。
  4. 如果分布式缓存也没有命中,则会回源到Tomcat集群,在回源到Tomcat集群时也可以使用轮询和一致性哈希作为负载均衡算法。
  5. 在Tomcat应用中,首先读取本地堆缓存,如果有则直接返回(并会写到主Redis集群)。
  6. 作为可选部分,如果步骤4没有命中可以再尝试一次读主Redis集群操作,目的是防止当从Redis集群有问题时的流量冲击。
  7. 如果所有缓存都没有命中,只能查询DB或相关服务相关数据并返回。
  8. 步骤7返回的数据异步写到主Redis集群,此处可能有多个Tomcat实例同时写主Redis集群,造成数据错乱。

应用整体分了三部分缓存:应用Nginx本地缓存、分布式缓存、Tomcat堆缓存,每一层缓存都用来解决相关的问题,如应用Nginx本地缓存用来解决热点缓存问题,分布式缓存用来减少访问回源率、Tomcat堆缓存用于防止相关缓存失效/奔溃之后的冲击。
虽然都是加缓存,但是怎么加、怎么用,细想下来还是有很多问题需要权衡和考量的,接下来我们就详细来讨论一些缓存相关的问题。

如何缓存数据

下面将从缓存过期、维度化缓存、增量缓存、大Value缓存、热点缓存几个方面来详细介绍如何缓存数据。

过期与不过期

对于缓存的数据我们可以考虑不过期缓存和带过期时间缓存,什么场景应该选择哪种模式则需要根据业务和数据量等因素来决定。

不过期缓存机制

场景一般思路如下图所示。

使用Cache-Aside模式,首先写数据库,如果成功,则写缓存。这种场景下存在事务成功、缓存写失败但无法回滚事务的情况。另外,不要把写缓存放在事务中,尤其写分布式缓存,因为网络抖动可能导致写缓存响应时间很慢,引起数据库事务阻塞。如果对缓存数据一致性要求不是那么高,数据量也不是很大,则可以考虑定期全量同步缓存。
也有提到如下思路:先删缓存,然后执行数据库事务;不过这种操作对于如商品这种查询非常频繁的业务不适用,因为在删缓存的同时,已经有另一个系统来读缓存了,此时事务还没有提交。当然对于如用户维度的业务是可以考虑的。
不过为了更好的解决以上多个事务的问题,可以考虑使用订阅数据库日志的架构,如使用canal订阅MySQL的binlog实现缓存同步。
对于长尾访问的数据,大多数数据访问频率都很高的场景,若缓存空间足够则可以考虑不过期缓存,比如用户、分类、商品、价格、订单等,当缓存满了可以考虑LUR机制驱逐老的缓存数据。

过期缓存机制

即采用懒加载,一般用于缓存其他系统的数据(无法订阅变更消息,或者成本很高)、缓存空间有限、低频热点缓存等场景。常见步骤是:首先读取缓存,如果不命中则查询数据,然后异步写入缓存并过期缓存,设置过期时间,下次读取将命中缓存。热点数据经常使用,即在应用系统上缓存比较短的时间。这种缓存可能存在一段时间的数据不一致情况,需要根据场景来决定如何设置过期时间。如库存数据可以在前端应用上缓存几秒钟,短时间的不一致是可以忍受的。

维度化缓存与增量缓存

对于电商系统,一个商品可能拆成基础属性、图片列表、上下架、规格参数、商品介绍等;如果商品变更变更了,要把这些数据都更新一边,那么整个更新成本(接口调用量和带宽)很高。因此最好将数据进行维度化并增量更新(只更新变更的部分)。尤其如上下架这种只是一个状态变更,但是每天频繁调用的,维度化后能减少服务很大的压力。维度化缓存方案如下图所示。

按照不同维度接收MQ进行更新。

大Value缓存

要警惕缓存中的大Value,尤其是使用Redis时。遇到这种情况时可以考虑使用多线程实现的缓存(如Memcached)来缓存大Value;或者对Value进行压缩;或者将Value拆分为多个小Value,客户端再进行查询、聚合。

热点缓存

对于那些访问非常频繁的热点缓存,如果每次都去远程缓存系统中获取,可能会因为访问量太大导致远程缓存系统请求过多、负载过高或者带宽过高等问题,最终可能导致缓存响应慢,使客户端请求超时。一种解决方案是通过挂更多的从缓存,客户端通过负载均衡机制读取从缓存系统数据。不过也可以在客户端所在的应用/代理层本地存储一份,从而避免访问远程缓存,即使像库存这种数据,在有些应用系统中也可以进行几秒钟的本地缓存,从而降低远程系统的压力。

分布式缓存与应用负载均衡

此处说的分布式缓存一般采用分片实现,即将数据分散到多个实例或多台服务器。算法一般采用取模和一致性哈希。如之前说的做不过期缓存机制可以考虑取模机制,扩容时一般是新建一个集群;而对于可以丢失的缓存数据可以考虑一致性哈希,即使其中一个实例出问题只是丢一小部分,对于分片实现可以考虑客户端实现,或者使用如Twemproxy中间件进行代理(分片对客户端都是透明的)。如果使用Redis可以考虑使用redis-cluster分布式集群方案。
应用负载均衡一般采用轮询和一致性哈希,一致性哈希可以根据应用请求的URL或者URL参数将先沟通的请求转发到同一个节点;而轮询即将请求均匀的转发到每个服务器,如下图所示。

整体流程如下:

  1. 首先请求进入接入层Nginx。
  2. 根据负载均衡算法将请求转发给应用Nginx。
  3. 如果应用Nginx本地缓存命中,则直接返回数据,否则读取分布式缓存或者回源到Tomcat。
  • 轮询的优点:应用Nginx的请求更加均匀,使得每个服务器的负载基本均衡,不会因为热点问题导致其中某一台服务器负载过重。
  • 轮询的缺点:随着应用Nginx服务器的增加,缓存的命中率会下降,比如原来10台服务器命中率为90%,再加10台服务器将可能降低到45%。
  • 一致性哈希的优点:相同请求都会转发到同一台服务器,命中率不会因为增加服务器而降低。
  • 一致性哈希的缺点:因为相同的请求会转发到同一台服务器,因此可能造成某台服务器负载过重,甚至因为请求太多导致服务出现问题。

那么到底选择哪种算法呢?答案就是根据实际情况动态选择:

  • 负载较低时使用一致性哈希,比如普通商品访问。
  • 热点请求时降级一致性哈希为轮询,比如京东首页的商品访问。

当然,某些场景是将热点数据推送到接入层Nginx,直接响应给用户,比如秒杀商品的访问。

热点数据与更新缓存

热点数据会造成服务器压力过大,导致服务器性能、吞吐量、带宽达到极限,出现响应慢或者拒绝服务的情况,这肯定是不允许的。可以用如下几个方案去解决。

单机全量缓存+主从

如下图所示,所有缓存都存储在应用本机,回源之后会把数据更新到主Redis集群,然后通过主从复制到其他从Redis集群。缓存的更新可以采用懒加载或者订阅消息进行同步。

分布式缓存+应用本地热点

对于分布式缓存,我们需要在Nginx+Lua应用中进行应用缓存来减少Redis集群的访问冲击,即首先查询应用本地缓存,如果命中则直接缓存,如果没有命中则接着查询Redis集群、回源到Tomcat,然后将数据缓存到应用本地,如下图所示。


此处到应用Nginx的负载机制采用:正常情况采用一致性哈希,如果某个请求类型访问量突破了一定的阈值,则自动降级为轮询机制。另外对于一些秒杀活动之类的热点我们是可以提前知道的,可以把相关数据预先推送到接入层Nginx并将负载均衡机制降级到轮询。

另外可以考虑建立时实时热点发现系统来发现热点,如下图所示。

  1. 接入Nginx将请求转发给应用Nginx。
  2. 应用Nginx首先读取本地缓存,如果命中直接返回,不命中会读取分布式缓存、回源到Tomcat进行处理。
  3. 应用Nginx会将请求上报给实时热点发现系统(如使用UDP直接上报请求,或者将请求写到本地kafka,或者使用flume订阅本地Nginx日志),上报给实时热点发现系统后,他将进行热点统计(可以考虑storm实时计算)。
  4. 根据设置的阈值将热点数据推送到应用Nginx本地缓存。

因为做了本地缓存,因此对于数据一致性需要我们去考虑,即何时失效或更新缓存:

  • 如果可以订阅数据变更消息,那么可以订阅变更消息进行缓存更新。
  • 如果无法订阅消息或者订阅消息成本比较高,并且对短暂的数据一致性要求不严格(步如在商品详情页看到的库存,可以短暂的不一致,只要保证下单时一致即可),那么可以设置合理的过期时间,过期后在查询新的数据。
  • 如果是秒杀之类的,可以订阅活动开启消息,将相关数据提前推送到前端应用,并将负载均衡机制降级为轮询。
  • 建立实时热点发现系统来对热点进行统一推送和更新。

更新缓存与原子性

正如之前说的,如果多个应用同时操作一份数据很可能造成缓存数据是脏数据,解决办法有:

  • 更新数据时使用更新时间戳或者版本对比,如果使用Redis可以利用其单线程机制进行原子化更新。
  • 使用如canal订阅数据库binlog,如下图所示。此处把MySQL看成发布者,binlog是发布的内容,canal看成消费者,canal订阅binlog然后更新到Redis。

  • 将更新请求按照相应的规则分散到多个队列,然后每个队列进行单线程更新,更新时拉取最新的数据保存。
  • 用分布式锁,更新之前获取相关的锁。

缓存崩溃与快速修复

当我们使用分布式缓存时,应该考虑如何应对其中一部分缓存实例宕机的情况。接下来将介绍分布式缓存的常用算法。当缓存数据是可丢失的情况时,我们可以选择一致性哈希算法。

取模

对于取模机制如果其中一个实例故障,如果摘除此实例将导致大量缓存不命中,瞬间大流量可能导致后端DB/服务出现问题。对于这种情况可以采用主从机制来避免实例故障的问题,即其中一个实例故障可以用从/主顶上来。但是取模机制下如果增加一个节点将导致大量缓存不命中,所以一般是建立另一个集群,然后把数据迁移到新集群,然后把流量迁移过去。

一致性哈希

对于一致性哈希机制如果其中一个实例故障,摘除此实例将只影响一致性哈希环上的部分缓存不命中,不会导致瞬间大量回源到后端DB服务,但是也会产生一些影响。
另外也可能因为一些误操作导致整个缓存集群出现问题,如何快速恢复呢?

快速恢复

如果出现之前说到的一些问题,可以考虑如下方案:

  • 主从机制,做好冗余,即其中一部分不可用,将对等的部分补上去。
  • 如果因为缓存导致应用可用性已经下降可以考虑:部分用户降级,然后慢慢减少降级量;后台通过Worker预热缓存数据。

也就是如果整个缓存集群故障,而且没有备份,那么只能去慢慢将缓存重建。为了让部分用户还是可用的,可以根据系统承受能力,通过降级方案让一部分用户先用起来,将这些用户相关的缓存重建。另外通过后台Worker进行缓存数据的预热。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值