热点数据缓存技术

达芬奇密码2018-07-12 09:52

为何要引入热门数据缓存

我们先对比一下在数据库中,数据存放在内存和硬盘上的性能对比。

考虑SATA硬盘:10000rpm、150MB/s传输率、9ms寻道时间
MySQL Innodb pagesize:16kB
如果每一次数据请求,内存中都未缓存,则吞吐量为:60qps
如果数据均在内存中,查询语句为简单主键查询,返回结果集很小,则吞吐量为:50000qps。

从测试结果可知,纯内存的访问吞吐量,大约为硬盘的1000倍,这与我们的常识一致。那紧接着的问题是,在我们线上产品部署时,为何不直接使用数据库的块缓存来存储数据,而是要使用专门的数据缓存产品来存储热门数据呢。假设每条记录50bytes,数据库页大小 16kB,综合考虑每条记录的额外存储开销,则在内存中每个page大约存放260条记录。如果该页中只有一条热门数据,而其他均是冷门数据,则空间利用率为0.3%

主流数据缓存产品

Memcached

官方网站: http://memcached.org/

产品提供的主要操作类型:

  1. 数据存取命令:GET、SET、ADD、REPLACE、DELETE、INCR、DECR
  2. 统计命令:STATS
  3. CAS命令:CAS、GETS

产品优点:

  1. 高性能,适用于做热点数据缓存
  2. 有LRU机制,冷数据自动替换出内存,此特征非常适用于存储热门数据
  3. 产品较成熟,已广泛应用于web网站产品中

产品缺点:

  1. 无事务隔离机制,在有并发数据操作时,必须依赖于服务器提供的CAS原语来做并发控制,代码开发难度较大

Redis

官方网站: http://redis.io/  。产品特点如下:

  1. 提供非常丰富的数据类型:String、List、Set、Sorted set、Hash
  2. 自带数据持久化机制,可采用Snapshot把数据完全导出到硬盘,或者采用AOF(append only file)日志来记录增量更新
  3. 没有LRU机制,这点对于热门数据缓存来说影响较大。作为权衡,redis提供了一种近似的策略来做LRU,当内存空间不够时进行多次采样来选取一个上次访问时间最久的数据作为换出对象

MySQL 查询缓存

其实MySQL自身也带了热门数据缓存机制,某些场景下也能满足产品需求。首先在服务器上设置开启查询缓存以及缓存占用内存大小,然后可在用户SQL中设定此语句是否走查询缓存。另外,在服务器上还可设定当用户SQL没有明确说明查询缓存操作策略时,默认行为是什么。MySQL查询缓存的实现原理很简单,不做复杂语法分析,以用户SQL语句为key进行查找,如果匹配到,则直接返回上次查询获得的结果集。需要格外注意的是,当一张表中的任何一条数据被更新或插入删除后,整张表相关的查询缓存都会被清空。因此,查询缓存只适用于查询量非常频繁而更新操作很少发生的场景。

NTSE

NTSE是网易杭州研究院自主研发的一款数据库产品,可作为底层存储引擎安装到MySQL中。NTSE的一个特色是自带了热门数据行级缓存,其最大优点是用户使用非常方便,只需要在服务器上开启开关即可打开此功能,并且能保证缓存数据与实际数据强一致。根据项目组测试结果,对数据访问热点明显的应用与InnoDB相比通常可获得3~5倍甚至更高的性能提升。

扩展方案

数据保存在外部缓存服务器中,受限于服务器的内存容量及处理能力。下面讨论一下当我们决定使用memcached或redis时,有哪些水平扩展方案。 

一致性hash

一致性hash是大家都非常熟悉的一种水平扩展方案,优点是不需要服务器做任何改动即可使用,而且主流客户端如spymemcached、jedis都支持。缺点是一旦服务器数量发生变化,数据迁移的成本非常高。一致性hash的部署架构图如下: 

其他方案

除去一致性hash,下面介绍几个其他业界常用水平扩展方案:

  1. Gizzard。twitter开源的sharding framework, 可以接不同的存储系统(memcached, redis, mysql)。它充当网络中间件角色, 可管理分散在任意多台存储节点上的分片数据。https://github.com/twitter/gizzard
  2. Tair。Tair是淘宝开源的一款KV存储框架,支持多种存储引擎(redis、memcached、leveldb)。其功能比较强大,支持多机架、多数据中心,以及自动数据迁移和复制。http://tair.taobao.org
  3. Twemproxy。是一个twtter开源的一个redis和memcache代理服务器,可作为中间件管理底层多台数据缓存服务器。https://github.com/twitter/twemproxy

这些方案的共同优点是都以中间件形式管理底层多台数据缓存服务器,对产品开发人员来说,屏蔽分布式水平扩展的各种复杂问题。具体每种方案的优缺点及使用注意事项,可参考附件中的《开源数据缓存服务器扩展方案》。

数据一致性问题

如果我们用了NTSE或者MySQL查询缓存,那就不存在数据一致性问题,如果数据存储在memcached或redis之类的外部缓存系统中,则数据一致性需要我们格外关注。下面列举几种目前主流数据同步方案。 

客户端同步算法

此算法的基本思想,由客户端通过特定算法来保证当更新数据库记录时,同步更新缓存中的数据。由于同步算法只和SQL语句有关,带有普遍性,所以我们公司的DAO框架以及DDB中,都已经自带缓存数据同步算法。 

更新数据库语句

对更新数据库的SQL语句来说,update语句或delete语句在操作memcached上面没有本质不同。下面以delete语句为例来说明。 

根据SQL语句中的缓存key更新缓存

a)      从SQL语句或数据库中提取缓存key

b)      把缓存中与缓存key相关的数据删除

c)      在数据库中执行delete语句 

查询数据库语句

向memcached中添加记录的SQL,操作流程如下: 

a)      从SQL语句或数据库中提取缓存key

b)      尝试从缓存中获取数据,如果能够得到则直接返回结果

c)      从数据库中获取数据并返回给用户

d)      把新获得的数据插入到缓存中 

需要额外注意的是,即使我们应用了缓存数据同步算法,还是无法保证缓存数据与数据库中的数据严格一致。产生问题的原因有以下几种:

  1. 客户端并发更新。当客户端并发更新同一条记录时,数据库提供了锁机制来保证数据正确性。而涉及到数据库和缓存系统两个产品时,问题变得非常复杂,通常认为当存在并发更新时,缓存系统必然会出现与数据库数据的不一致情况。
  2. 客户端并发查询+更新操作。当有查询和更新操作并发时,一方面在把老数据填充到缓存中,另一方面数据库的数据正在更新到新版本,此时很容易引发数据不一致。另外,当数据库设置为repeatable read时,事务中读取到的永远为老数据,更容易引起数据不一致。
  3. 缓存服务器自身宕机。当系统中存在多台缓存服务器时,目前流行做法是采用客户端一致性hash。当产生网络瞬断,更新操作会被分发到一个临时节点,而网络恢复时,就变得比较尴尬。如果要保证数据强一致,则原有服务器的数据都将清空,如果不清空老数据,则一致性难以得到保障。 另外,当服务器发生宕机时,每个客户端各自决定何时切换到新服务器以及何时恢复到原有服务器,各个客户端切换时间点难以保证在同一个时间,这也会引发在临界时间点的更新操作产生数据不一致。 

附件的《Memcache数据一致性方案讨论》,对各种场景展开详细讨论,有兴趣读者可进一步查看。另外,文档中还提到一种基于CAS操作的算法,能够保证单机情况下数据强一致。 

通过日志异步更新

阅读前面的客户端一致算法可以发现,要保证缓存服务器数据一致性代价非常昂贵。在实际生产使用时,还有另一种方案:通过异步采集回放数据库binlog日志来更新缓存服务器数据。基本流程为:

  1. 从数据库中获取更新binlog(格式必须为row);
  2. 从binlog中提取缓存key,若语句为update或delete,则删除缓存服务器中的相应数据。 

此方案的缺点是缓存数据最终一致性而非强一致。 

数据过期时间

主流缓存服务器产品都带有数据过期时间判断机制。在插入缓存服务器时,数据带有一个时间戳。当再次获取时,服务器将比对过期时间条件是否满足,如果已过期则返回数据不存在。一般情况下,此方案可与客户端同步算法联合使用,能大大降低数据不一致的概率,将数据一致性级别提升为最终一致性。 

如何避免掉入陷阱

如果数据存放在外部缓存服务器,则很难保证数据强一致。因此,我们在编写程序时,需要注意以下原则:

  1. 缓存中的数据不可信
  2. 静态数据最安全,如果数据永远不更改,则没有一致性风险
  3. 更新数据库的源数据,不能依赖于缓存,避免数据库数据被污染

预热问题

业界常有一句话,热门数据缓存是万金油,如果发现哪里性能顶不住了就抹一下。这个万金油虽然好使,但用起来也需要格外注意。我们产品在发布时,都会做几轮性能测试,但回想一下我们以前做的测试,都是在系统运行一段时间稳定后产出的性能指标。数据一缓存,性能指标立马提升,但千万小心在某些情况下这只是一个假象。除了前面讲到的数据一致性问题,数据预热也需要考虑。

再回顾一下前面讲到的memcached行缓存和数据库块缓存之间的区别。行缓存虽然大大提高内存利用率,但也有副作用。当加载一条数据时,就需要一次数据库操作,至少一次IO,数据完全预热的时间非常长。而块缓存,最大好处是利用了热门数据局部性原理,往往可以在较短时间填充满整个buffer。当前端缓存服务器还在数据预热阶段,无法提供热门数据时,大量操作被穿透到底层数据库服务器上。尤其当前端缓存服务器宕机时,此情况很容易引发雪崩效应。

一般使用时为了尽量避免数据预热的风险,有以下措施:

  1. 如果热门数据呈现静态特征,最好在产品上线前就把数据加载到缓存服务器中,防止产品上线突发流量导致数据库服务器性能风险;
  2. 当我们配置了缓存服务器,并不意味着数据库的内存buffer可以完全忽略,这两者各司其职不可替代。
  3. 实际线上部署时,即使缓存服务器性能可以满足要求,也应极力杜绝只配置一台缓存服务器。俗话说不要把鸡蛋放在一个篮子里,配置多个缓存服务器后,能有效降低一台服务器宕机产生的不利影响。
  4. 如果热门数据非常重要,则可考虑为其保存多个缓存副本。当一台机器宕机时,还有另一台服务器提供服务,避免请求直接穿透到底层。
  5. Redis提供了数据持久化机制,某些场景下也可考虑使用。这样当服务器重启后可以直接加载之前的热门数据避免再去数据库中获取。此机制下最好配置数据失效时间,避免数据不一致。
  6. 一般缓存服务器日常重启操作,尽量安排在晚上进行。
  7. 防患于未然,重要产品需要做例行容量规划。需要测试在线上高压力仿真环境下,万一前端缓存服务器宕机,后端会出现什么情况。 

非结构化数据缓存

最后稍微提一下非结构化数据缓存方案。

对于图片视频等数据,如果用户访问很大,而且呈现二八特征,第一想到的应该是CDN。采用CDN后不仅可以有效降低后端服务器压力,还能大大改善用户响应时间;

如果需要在服务器端进行热点数据缓存,可选方案有:

  1. Apache Traffic Server(推荐):http://trafficserver.apache.org/
  2. Varnish:https://www.varnish-cache.org/
  3. Squid:http://www.squid-cache.org/; 

本文来自网易实践者社区,经作者邱似峰授权发布。