大型互联网应用的数据缓存架构设计和客户实践
无论是 Netflix 这样的在线流媒体服务还是亚马逊全球电商系统,在业务规模不断发展的过程中,系统应用架构不断优化的同时,数据缓存服务对于大规模在线系统的用户体验越发重要。在近期不断的客户碰撞中,无论是经典的分库分表还是云原生的分布式架构数据库方案,客户特别关注当未来业务发展不确定,未来有可能爆发的情况下,有没有一个“银弹”的数据服务可以满足客户的需求?本文是上篇,主要探讨数据缓存服务,尝试从实际客户案例出发,客观分析多个典型大规模互联网业务发展过程中,数据缓存架构最佳实践和经过生产验证的落地方案;
回归初心:按应用场景选择合适的数据存储
从我个人接触和研究的 AWS 典型互联网客户来看,目前应用系统数据存储使用最广泛的是内存缓存,NoSQL 数据库,对象存储以及关系数据库,如下图所示,通常应用的数据层分为五个典型场景(1)简单高效的数据缓存服务,通常支持简单的数据结构和键值操作,比如接下来我们要继续探讨的被客户广泛应用的 Memcached 项目(2)第二种是类似 Redis 这样的内存数据库,支持丰富的数据结构,Pub/Sub 订阅者模式,以及类似关系数据库的主从复制,可持久化到磁盘等特性(3)NoSQL 数据库,大家比较熟悉的 MongoDB,Cassandra 以及 Amazon DynamoDB 服务等等,相比前两种内存类数据存储,NoSQL 通常基于磁盘存储,而且支持分布式架构,多数据中心复制等特性;(4)关系数据库,在事务型场景中,应用在订单、支付等领域;(5)对象存储服务,最早出现的云服务,5G 时代,适合存储大量的图片,视频等各种非结构化数据,是 AWS 数据湖的核心存储服务,可以直接对接用户移动应用,后端各种大数据和 AI 任务。
应用层对于读写缓存服务和数据库数据有如下可选择的缓存策略(1)延迟加载,即需要的时候将数据从数据库加载到缓存,即当缓存未命中时,访问后端数据库,再把数据存储到缓存中,以便下次读取时缓存命中提高性能;优点是仅对请求的数据缓存,节点故障对于应用程序而言并不致命;缺点是缓存不命中情况下的性能损失,多了两次次缓存操作,大量的缓存不命中对数据库也会增加很大压力(2)直写(Write Through):最终用户通常更能容忍写入和更新数据时的延迟,因此,每次数据更新都是同时写入数据库再写入缓存,优点很明显,缓存中的数据永远最新,永不过时;缺点是新节点缺失数据,另外大部分数据通常是很少被读取,浪费了大量内存资源;无论哪种策略,都可以结合缓存数据生存时间(TTL)来优化内存空间使用;更多详情可以参考 AWS 官方文档:缓存策略和最佳实践以及比较 Memcached 和 Redis;
大规模分布式缓存服务参考架构
目前除了基于内存的缓存服务比如 Memcached 和 Redis,基于磁盘的键值数据库引擎也在蓬勃发展比如 LevelDB, RocksDB, LMDB, BadgerDB 等等;高速本地 SSD 磁盘的随机读取延迟一般在150微秒左右,基于AWS r5系列机器换算,同样的 5000万的 Key,带有 1KB大小的字符串值对象,基于磁盘的成本只有内存的1/3(参考:https://medium.com/@maheshsenni/disk-based-key-value-stores-5e0da4c9bdea)因此大规模缓存数据,同时利用内存和本地高速SSD 磁盘是非常具有价值的技术架构;同时,如何将单机的缓存服务,改造成分布式可扩展高容错又低延迟的高性能缓存服务是大型互联网应用的必然遇到的技术挑战,那是否有已经成功上线并开源的解决方案呢?首先看两个架构图,技术细节在后续章节一一展开:
内存缓存服务架构演进
内存缓存服务使用的最广泛的是 Memcached 和 Redis,Memcached 的大型客户包括 Netflix 和 Facebook 等,我们已经在前几篇 Netflix 案例分享中(案例分享:,)详细拆解了基于Memcached的EVCache 缓存服务的技术演进,在我看来EVCache 服务已经是一个非常优化的内存缓存服务,但 Netflix 技术团队依然不满足只局限在 Memcached 上的分布式架构,他们受 Amazon Dynamo 论文启发,期望实现一个支持多种键值数据库引擎比如 Memcached、Redis及RocksDB 等,快速构建一个分布式数据存储的通用服务 Dynomite;
第一个阶段:数据无分区
这个阶段,一个缓存服务的最大数据存储空间受限于单个节点(AWS 环境中通常是一台 EC2 实例)的内存和磁盘容量;Memcached1.5.4 版本+可以通过 Extstore 组件将缓存服务数据空间从内存存储扩大到本地SSD磁盘存储,内存中存放 Key 值,SSD 磁盘上存放缓存数据;RedisOn Flash(RoF) 版本同样也支持类似的内存和 SSD 的二级缓存策略服务;
第二个阶段:数据复制和读副本
很多时候,我们期望如果一个缓存服务节点挂掉,能够快速恢复,即如何实现缓存服务节点的高可用性,大家都非常熟悉经典的关系数据库的主从架构,缓存服务中也类似提供这样的主从机制;如 Redis 支持 slaveof 命令创建多个副本,这种架构中,主节点提供读和写的入口,副本通常只提供读操作,如果主节点挂掉,某一个副本可以提升为主节点继续提供服务;
但 Memcached 不太一样,一个 Memcached 服务器进程遵循单一职责原则设计,聚焦简单高效进行键值对的存取操作,不同的服务器进程间没有通信机制;因此,如何在 Memcached 缓存服务中实现多份数据复制?Netflix 技术团队的做法是在 EVCache 客户端识别 Memcached 服务端是否是多可用区部署,如果是,对于写操作,客户端会同时多写多个可用区的 Memcached 存储,而读请求正常会操作本可用区的 Memcached 服务节点。
第三阶段:数据分片和缓存数据扩张
虽然经历了第一第二阶段我们可以利用本地存储和主从复制提升了缓存服务的可用性和容错能力,但还会遇到两个挑战(1)单个缓存服务数据存储有上限(一台物理机支撑的内存和 SSD 磁盘大小),如果缓存数据增长到TB甚至PB级别,缓存服务存储该如何扩展?(2)读写性能无法线性扩展,读请求可以通过冗余的数据副本来提升,但过多的副本引入,对于主节点性能有影响,同时同步或跨可用区双写会有时延,另外数据写入的性能如何在多个节点上共同分担?因此,对于大规模互联网应用而言,需要一个可扩展的分布式的缓存服务满足:
分布式存储:缓存数据分布存储在多个节点,即支持分片
性能支持线性扩展:快速加入和删除节点,在新节点数据预热的前提下,数据访问性能线性伸缩;
响应延迟:最优 1毫秒以内
可用性:永远在线,如果有部分缓存节点挂掉,可以利用一致性哈希算法降低数据重新分片的影响范围
数据冗余复制:多个可用区甚至多区域复制保障数据节点的高可用性
数据一致性:缓存服务性能优先,最大程度保证一致性或用户可配置数据一致性
简单,尽可能低的运维成本
Facebook:可扩展的分布式memcached缓存服务实践
接着我们分别看看两个典型的互联网公司基于Memcached设计的可扩展的分布式缓存服务;第一家是 Facebook,大家可以阅读一篇公开论文《ScalingMemcache at Facebook》来了解具体背景和详细的架构设计权衡,该服务的目标是支撑 Facebook 及 Instagram 每秒数十亿次请求访问;幸运的是,Facebook开源了该服务的核心组件mcrouter ( https://github.com/facebook/mcrouter ),它是一个实现了 Memcached 协议的智能路由代理,Web 应用不需要任何修改只需要从原来直接访问 Memcached 节点改成访问本地的 mcrouter 代理,再通过该代理智能处理数据路由,TCP 连接到后端的 Memcached 节点进行通信,该代理程序大部分代码基于 C++ 语言实现,下图是它的整体架构:
该代理程序支持非常丰富的分布式内存缓存服务特性:(1)任何支持Memcached 协议的客户端都可以直接跟 mcrouter 直接通信,无需任何修改(2)连接池,多个应用服务客户端可以连接到同一个 mcrouter 实例,共享后端到 Memcached 节点的连接,从而降低 Memcached 节点的总连接数(3)内置一致性哈希算法(4)前缀路由(5)池数据复制,即两个缓存服务池(Pool)拥有相同的数据,提升读性能及容错(6)Memcached 节点的监控检查和快速自动恢复(7)新缓存池数据预热,自动从一个临近的有数据的缓存池获取数据填充新建的空缓存池服务(8)缓存操作广播到多个缓存池及集群等等;详情请参考 https://github.com/facebook/mcrouter/wiki;
举个例子,在缓存服务中,很多时候,应用方期望可以将相同业务的缓存数据存放在一起,这个时候就可以利用“前缀路由”功能,如下图所示,应用只需要定义特定的前缀来代表不同的业务,mcrouter 根据配置的路由规则,将数据分布到不同的缓存池进行存取:
Netflix:可扩展的分布式缓存服务EVCache
Netflix 的 EVCache 总规模非常庞大,是整个在线流媒体服务的核心服务之一,总数据量达 Petabytes 级别,百亿数据条目,每天承载万亿次操作。
我们回顾下 Netflix 的 EVCache 架构,一个 EVCache 集群包含多个分片(Shard)一个分片就是一个独立的 Memcached 进程,EVCache 客户端实现ketama 一致性哈希算法,将数据分配到不同的分片上进行存取;由于集群中包含多个分片节点,EVCache 客户端要负责计算数据落在哪个分片,因此客户端需要发现所有的分片节点信息,EVCache 服务中利用自家的 Eureka 服务进行Memcached节点的自动注册和发现,所以在每个 EC2 节点上,有一个Sidecar 监控 Memcached 进程的健康状态,并向 Eureka 注册中心更新 Memcached 终端节点信息。
对比 Facebook 的实现,Netflix 开源的 EVCache 组件,主要实现了一个EVCache 客户端,包含一致性哈希算法,AWS 可用区感知,EVCache App(一个集群)的多可用区数据多写复制,而空集群数据预热和多区域数据复制等高级功能由单独的模块实现,而且并没有完全开源;
Redis是否支持可扩展的分布式架构?
用过 Amazon 托管的 ElastiCache for Redis 服务的同学都知道,Redis 分为集群模式和非集群模式,如下图所示,非集群模式不支持数据分片,缓存数据规模受限于单个虚拟机的内存资源,而集群模式支持数据分片,从而支持大规模数据缓存,从官方的常见问答文档可查,最多可支持 250个节点,可存储 170.6TB 的内存缓存数据;从技术上来看,Amazon 托管的 ElastiCache for Redis 服务可以满足大部分客户基于 Redis 的缓存服务需求;不过客户还会面临两个挑战(1)成本挑战,TB 甚至 PB 级的内存资源在云环境中的成本非常有压力(2)如果还需要更多的缓存服务空间,怎么扩展?
对于成本压力,我们在前文《》中已经获知,Netflix 改造了 EVCache 支持内存 L1 和本地 SSD L2 的二级缓存架构,在满足 Netflix EVCache 服务的 1毫秒的 SLA 要求的同时降低了60%左右的内存需求,从而降低了整体成本,那 Redis 服务可以利用实例的本地磁盘吗?同时如果有一个中间件可以支持构建可扩展的分布式架构 Redis 服务,那就可以不断添加新的实例节点来支持更高的吞吐要求和更大量的缓存空间要求。
本文前面已经提到,商业版的Redis提供一个 Redison Flash(RoF)功能支持内存和SSD的二级存储架构,而磁盘数据的操作引擎采用了 RocksDB 项目,但 RocksDB 参数繁多,公开的一篇论文《Optimization of RocksDB for Redis on Flash》的作者详细描述了,如何优化 RocksDB 参数提升 11倍的 RoF 的性能,大家如果实际使用中可以参考;那有没有开源的实现呢?国内的互联网公司奇虎 360 同样基于 RocksDB 引擎开源了一个支持直接持久化到本地 SSD 磁盘的 Pika 项目(https://github.com/Qihoo360/pika)根据文档介绍 “目前 Pika 在线上部署并运行了 20多个巨型(承载数据与Redis相比)集群 粗略的统计如下:当前每天承载的总请求量超过100亿,当前承载的数据总量约3TB”。
Redis 的数据可以通过本地磁盘进一步扩展可支撑的数据量大小,唯一的问题就是如果实现分布式高可用?这里就不得不提到已经在 Netflix 生产服役的Dynomite(https://github.com/Netflix/dynomite)项目,是 Amazon Dynamo 的一个实现,可以实现类似 Amazon DynamoDB 类似分布式架构的中间件,目前支持 Redis 和 Memcached 两种数据引擎;如下图所示,在每一台实例上,Dynomite 前置在 Redis 节点,负责在多个数据中心,多个数据节点之间复制数据;在数据一致性方面,Dynomite提供三种可选的配置,DC_ONE:在本可用区同步读或写,异步同步到其他可用区节点,DC_QUORUM:在本可用区同步读或写到 quorum(法定)数量的数据节点,异步复制到剩下的节点;和 DC_SAFE_QUORUM:类似 DC_QUORUM 区别在于读或写操作成功的前提是 quorum(法定)数量的数据节点都成功;读和写的一致性可分开设定;成员节点的状态机制可采用集中式的成员管理策略或 Gossip协议,前者每个节点定期通过本地的一个代理访问集中的 Dynomite-Manager中的令牌服务获取令牌;
因此,一个大胆的但未验证的想法是,结合 Pika 和 Dynomite 可以实现一个支持本地 SSD 磁盘的可扩展的分布式兼容 Redis 协议的缓存服务。
第四阶段:缓存数据跨区域复制
缓存数据的跨区域的复制,是实现多区域多活架构的一个必备特性,Netflix 的 EVCache 服务通过新的Kafka队列和更新的 EVCache 客户端实现了多区域百毫秒延迟的双向复制,但该组件并未开源;目前开源方案中唯有Dynomite 是可以同时支持 Memcached 和 Redis 两种引擎的多区域复制中间件,大家可以尝试是否满足自身的业务要求;
总结
缓存服务对于大型互联网应用的用户体验非常重要,类似应用开发的微服务架构,可扩展的分布式缓存服务本质上是有状态的微服务架构,相对于无状态的应用微服务难度要大很多,本文分解了几个典型互联网公司满足大型数据缓存需求,设计和上线的分布式缓存服务,主要两个方向(1)同时利用内存和本地高速 SSD 磁盘扩展缓存空间(2)利用智能路由代理或抽象的 Amazon Dynamo 中间件实现节点的弹性伸缩,数据分片,多可用区多区域复制,失败恢复等通用分布式难题;庆幸的是我们可以基于 AWS 基础设施结合这些开源的项目打造适合我们自己应用场景的缓存服务,站在巨人的肩膀上。
下篇预告
解决了数据缓存服务的可扩展分布式架构的探讨,下篇中,我们将继续展开对于 NoSQL 数据库以及关系数据库在大型互联网客户业务发展中的技术架构演进和落地实践,主要聚焦 AmazonDynamoDB、Cassandra、MongoDB和MySQL/Postgresql。