分布式架构之缓存系统

  一个大型稳健成熟的分布式系统的背后,往往会设计众多的支撑组件,将这些支撑系统成为分布式系统的基础设施。进行系统架构设计所依赖的基础设施,还包括分布式协作及配置管理组件、分布式缓存组件、持久化存储组件、分布式消息系统、搜索引擎、以及CDN系统、负载均衡系统、运维自动化系统等,还有实时计算系统、离线计算系统、分布式文件系统、日志收集系统、监控系统、数据仓库等。此处主要讲讲缓存系统组件。

缓存组件层

缓存系统带来的好处:

  1. 加速读写。缓存通常是全内存的,比如Redis、Memcache。对内存的直接读写会比传统的存储层如MySQL,性能好很多。由于单台机器的内存资源和承载能力有限,并且如果大量使用本地缓存,也会使相同的数据被不同的节点存储多份,对内存资源造成较大的浪费,因此才催生出了分布式缓存。
  2. 降低后端的负载。在高并发环境下,大量的读、写请求涌向数据库,磁盘的处理速度与内存显然不在一个量级,从减轻数据库的压力和提供系统响应速度两个角度来考虑,一般都会在数据库之前加一层缓存。

缓存系统带来的成本:

  1. 数据不一致性:在分布式环境下,数据的读写都是并发的,上游有多个应用,通过一个服务的多个部署(为了保证可用性,一定是部署多份的),对同一个数据进行读写,在数据库层面并发的读写并不能保证完成顺序,也就是说后发出的读请求很可能先完成(读出脏数据)
  2. 代码维护成本:加入缓存后,需要同时处理缓存层和存储层的逻辑,增加了开发者维护代码的成本。
  3. 运维成本:引入缓存层,比如Redis。为保证高可用,需要做主从,高并发需要做集群。

 

缓存的更新

缓存的数据一般都是有生命时间的,过了一段时间之后就会失效,再次访问时需要重新加载。缓存的失效是为了保证与数据源真实的数据保证一致性和缓存空间的有效利用性。常见的缓存更新策略:

  1. LRU/LFU/FIFO等算法:三种算法都是属于当缓存不够用时采用的更新算法。只是选出的淘汰元素的规则不一样:LRU淘汰最久没有被访问过的,LFU淘汰访问次数最少的,FIFO先进先出。适合内存空间有限,数据长期不变动,基本不存在数据一不致性业务。比如一些一经确定就不允许变更的信息。要清理哪些数据是由具体的算法定的,开发人员只能选择其中的一种,一致性比较差。
  2. 超时剔除:给缓存数据手动设置一个过期时间,比如Redis expire命令。当超过时间后,再次访问时从数据源重新加载并设回缓存。开发维护成本不是很高,很多缓存系统都自带过期时间API(比如Redis expire)。不能保证实时一致性,适合于能够容忍一定时间内数据不一致性的业务。
  3. 主动更新:如果数据源的数据有更新,则主动更新缓存。一致性比较好,只要能确定正确更新,一致性就能有保证。业务数据更新与缓存更新藕合一起,需要处理业务数据更新成功而缓存更新失败的情景,为了解耦一般用来消息队列的方式更新。不过为了提高容错性,一般会结合超时剔除方案,避免缓存更新失败,缓存得不到更新的场景。一般适用于对于数据的一致性要求很高,比如交易系统,优惠劵的总张数。

 

 缓存系统常见问题及解决

一般情况下,按如下的方式使用缓存功能:当业务系统发起某一个查询请求时,首先判断缓存中是否有该数据;如果缓存中存在,则直接返回数据;如果缓存中不存在,则再查询数据库,然后返回数据。

  1.  缓存穿透
    1. 描述:业务系统要查询的数据根本就存在,当业务系统发起查询时,按照上述流程,首先会前往缓存中查询,由于缓存中不存在,然后再前往数据库中查询。由于该数据压根就不存在,因此数据库也返回空。这就是缓存穿透。如果在高并发场景中大量的缓存穿透,请求直接落到存储层数据库,稍微不慎后端系统就会被压垮。
    2. 出现原因:恶意攻击,故意营造大量不存在的数据请求服务,由于缓存中并不存在这些数据,因此海量请求均落在数据库中。
    3. 针对缓存穿透的优化方案
      1. 缓存空对象:发生缓存穿透,是因为缓存中没有存储这些空数据的key,导致这些请求全都到数据库。那么,可以将数据库查询结果为空的key也存储在缓存中,当后续又出现该key的查询请求时,缓存直接返回null,而无需查询数据库。不过这个方案还是不能应对大量高并发且不相同的缓存穿透,如果在清楚业务有效范围情况下,一瞬间发起大量不相同的请求,第一次查询还是会穿透到DB。另外这个方案的一种缺点就是:每一次不同的缓存穿透,缓存一个空对象,导致大量不同的穿透缓存大量空对象,内存被大量白白占用,使真正有效的数据不能被缓存起来。所以对于这种方案需要做到:第一,做好业务过滤。将不属于业务范围内的请求过滤掉,系统直接返回,直接不走查询。第二,给缓存的空对象设置一个较短的过期时间,在内存空间不足时可以被有效快速清除。
      2. 布隆过滤器:布隆过滤器是一种结合hash函数与bitmap的一种数据结构。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中,它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。当业务系统有查询请求的时候,首先去BloomFilter中查询该key是否存在,若不存在,则说明数据库中也不存在该数据,因此缓存都不用查,直接返回null;若存在,则继续执行后续的流程,先前往缓存中查询,缓存中没有的话再前往数据库中的查询。
      3. 一般情况下,建议对于空数据的key各不相同、key重复请求概率低的场景而言,应该选择第二种方案;而对于空数据的key数量有限、key重复请求概率较高的场景而言,应该选择第一种方案。适当情况下,布隆过滤器和缓存空对象也是完全可以结合起来的,布隆过滤器用本地缓存实现,内存占用极低,不命中时再走redis/memcache这种远程缓存查询。
  2. 缓存雪崩
    1. 描述:当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力,造成数据库后端故障,从而引起应用服务器雪崩。
    2. 出现情景:缓存服务器挂了;高峰期缓存局部失效;热点缓存失效等等原因
    3. 缓存雪崩优化方案:
      1. 保证缓存层服务的高可用性,比如一主多从,Redis Sentine机制。
      2. 依赖隔离组件为后端限流并降级,比如netflix的hystrix。
      3. 项目资源隔离。避免某个项目的bug,影响了整个系统架构,有问题也局限在项目内部。
      4. 避免缓存集中失效,缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
      5. 如果可以,则设置热点数据永远不过期。
  3. (缓存击穿) 热点数据集中失效
    1. 描述:缓存击穿是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,导致数据库扛不住高并发量而引起宕机。
    2. 缓存击穿优化方案:
      1. 在现实情况下,很难对数据库服务器造成压垮性的压力,类似缓存击穿是不常发生的。如果真的存在这种情况,简单的做法就是让热门数据缓存永不过期即可。
      2. 使用互斥锁。缓存中没有数据,获取锁并从数据库去取数据,防止短时间内过多的请求发往数据库,导致数据库挂掉。
  4. 缓存无底洞问题(此问题不常发生,由facebook发现的,此处不记录,有篇博客写的很不错:https://blog.csdn.net/yonggeit/article/details/72862134)

 

memcache实现分布式缓存系统

  memcache是一款开源的高性能的分布式内容对象缓存系统,被许多大型网站所采用,用于在应用中减少对数据库的访问,提高应用的访问速度,并降低数据库的负载。为了在内存中提供数据的高速查找能力,memcache使用key-value形式存储和访问数据,在内存中维护一张巨大的HashTable,使得对数据查询的时间复杂度降低到O(1),保证了对数据的高性能访问。内存的空间总是有限的,当内存没有更多的空间来存储新的数据时,memcache就会使用LRU(Least Recently Used)算法,将最近不常访问的数据淘汰掉,以腾出空间来存放新的数据。memcache存储支持的数据格式也是灵活多样的,通过对象的序列化机制,可以将更高层的对象转换成为二进制数据,存储在缓存服务器中,当前端应用需要时,又可以通过二进制内容反序列化,将数据还原成原有对象。memcache客户端与服务端通过构建在TCP协议之上的memcache协议来进行通信,协议支持两种数据的传递,这两种数据分别为文本行和非结构化数据。文本行主要用来承载客户端的命令及服务端的响应,而非结构化数据则主要用于客户端和服务端数据的传递。由于非结构化数据采用字节流的形式在客户端和服务端之间进行传输和存储,因此使用方式非常灵活,缓存数据存储几乎没有任何限制,并且服务端也不需要关心存储的具体内容及字节序。

  memcache本身并不是一种分布式的缓存系统,它的分布式是由访问它的客户端来实现的。一种比较简单的实现方式是根据缓存的key来进行Hash,当后端有N台缓存服务器时,访问的服务器为hash(key)%N,这样可以将前端的请求均衡地映射到后端的缓存服务器。但这样也会导致一个问题,一旦后端某台缓存服务器宕机,或者是由于集群压力过大,需要新增缓存服务器时,大部分的key将会重新分布。对于高并发系统来说,这可能会演变成一场灾难,所有的请求将如洪水般疯狂地涌向后端的数据库服务器,而数据库服务器的不可用,将会导致整个应用的不可用,形成所谓的“雪崩效应”。

  使用consistent Hash算法能够在一定程度上改善上述问题。该算法早在1997年就在论文Consistent hashing and random trees中被提出,它能够在移除/添加一台缓存服务器时,尽可能小地改变已存在的key映射关系,避免大量key的重新映射。consistent Hash的原理是这样的,它将Hash函数的值域空间组织成一个圆环,假设Hash函数的值域空间为0~(2的32次方-1),也就是Hash值是一个32位的无符号整型,整个空间按照顺时针的方向进行组织,然后对相应的服务器节点进行Hash,将他们映射到Hash环上,假设有4台服务器分别为node1,node2,node3,node4,它们在环上的位置如图所示。

  接下来使用相同的Hash函数,计算出对应的key的Hash值在环上对应的位置。根据consistent Hash算法,按照顺时针方向,分布在node1与node2之间的key,它们的访问请求会被定位到node2,而node2与node4之间的key,访问请求会被定位到node4,以此类推。假设有新的节点node5增加进来时,假设它被Hash到node2与node4之间,那么受影响的只有node2和node5之间的key,它们将被重新映射到node5,而其他key的映射关系将不会发生改变,这样避免了大量key的重新映射。

  当然上面描绘的知识一种理想的情况,各个节点在环上分布得十分均匀。正常情况下,当节点数据较少时,节点的分布可能十分不均匀,从而导致数据访问的倾斜,大量的key被映射到同一台服务器上。为了避免这种情况的出现,可以引入虚拟节点的机制,对每一个服务器节点都计算多个Hash值,每一个Hash值都对应环上一个节点的位置,该节点称为虚拟节点,而key的映射方式不变,只是多了一步从虚拟节点再映射到真实节点的过程。这样,如果虚拟节点的数量足够多,即使只有很少的实际节点,也能够使key分布得相对均衡。

posted @ 2019-09-01 14:48  追求沉默者  阅读(1880)  评论(0编辑  收藏  举报