深入分布式缓存
读完需要
15分钟速读仅需5分钟
云服务不仅为软件系统的开发和部署带来了更多的敏捷性,而且提供了更多创新的可能性。当分布式缓存技术遇到云服务会是怎样的情形呢?EVCache就是这样的一种技术。
EVCache是一个开源、快速的分布式缓存,是基于Memcached的内存存储和Spymem-cached客户端实现的解决方案,主要用在亚马逊弹性计算云服务(AWSEC2)的基础设施上,为云计算做了优化,能够顺畅而高效地提供数据层服务。图10-1所示是EVCache开源项目在Github上的表现。
图10-1EVCache开源项目的Star趋势
EVCache是一个缩写,包括:
Ephemeral:数据存储是短暂的,有自身的存活时间。
Volatile:数据可以在任何时候消失。
Cache:一个内存型的键值对存储系统。
EVCache实现的主要功能包括分布式键值对存储,亚马逊云服务的跨区域数据复制以及注册和自动发现新节点或者新服务。EVCache典型的应用是对上下文一致性要求不高的场景,其可扩展性已经可以处理非常大的流量,同时提供了健壮的应用编程接口。
1
EVCache项目介绍
EVCache是Netflix开源软件项目(OpenSourceSoftware,OSS)中的一部分,是Netflix多个关于数据存储的开源项目中的一个重要成员。在Netflix架构中有两个基本元素,一个是控制平面,运行在亚马逊云服务(AWS)之上,用于用户登录,浏览和播放以及一般性服务。另一个是数据平面,叫做OpenConnect,这是一个全球性的视频分发网络。
EVCache是位于控制平面的。
Netflix是微服务架构领域的实践者,在系统中部署了上百个微服务,每一个微服务只专注做一件事情。这使得Netflix所提供的软件系统能够做到高度均衡和松耦合。由于状态都存储在缓存或持久存储中,所以这些微服务大多数是无状态的,易于自动扩展。
EVCache在Netflix内部是一个被广泛使用的数据缓存服务,所提供的低延迟且高可用的缓存方案可以很好地满足Netflix微服务架构需要,也用来做一般数据的存储。EVCache能够使面向终端用户的应用,个性化算法和各种微服务都具备优良的性能。
EVCache具有如下的特性:
分布式的键值对存储,缓存可以跨越多个实例。
数据可以跨越亚马逊云服务的可用区进行复制。
通过Netflix内部的命名服务进行注册,自动发现新节点和服务。
为了存储数据,键是非空字符串,值可以是非空的字节数组,基本类型,或者序列化对象,且小于1MB。
作为通用的缓存集群被各种应用使用,支持可选的缓存名称,通过命名空间避免主键冲突。
一般的缓存命中率在99%以上。
与Netfix驻留数据框架能够良好协作,典型的访问次序:内存→EVCache→Cassandre/SimpleDB/S3。
使用缓存技术所带来的最大影响可能是数据的不一致性。出于性能优先的考虑,具体的应用会依赖于EVCache来处理数据的不一致性。对于存活时间很短的数据,用TTL设置数据的失效时间,对于长时间保留的数据,通过构建一致性检查来修复它们。
EVCache是使用了Memeached操作接口(如get、set、touch等),基于数据大小和网络容量可以线性扩展,支持任意数量的数据备份(有的集群支持2个,有的支持9个)。所有操作都拥有对拓扑结构的感知、重试、回退,以及其他机制来保障操作的完整性,同时优化了亚马逊云服务的架构。每个主键中的数据通过数据分块技术处理后可以是任意大小的。
简而言之,Memcached是一个单进程应用,在单台主机上工作的很好,而EVCache使用它作为一个基础模块,Memcached是EVCache的一个子集。
1.1
EVCache的由来
对于一个流媒体服务来说,提供一个以客户为中心的用户体验意味着要做很多事情,要包括优秀的内容库,直观的用户界面,个性化内容推荐,可以让用户获取所喜爱的内容并可高质量播放的快速服务,等等。
Netflix期待用户和系统服务交互时能有一个极致的用户体验,对云服务而言,所考虑的目标是:
与Netflix数据中心相对应的快速响应时间。
从面向会话的应用到云服务中的无会话状态应用。
使用NoSQL的数据驻留,如Cassandra/SimpleDB/S3。
从数据存储(如Cassandra,或其他的亚马逊云服务如S3或SimpleDB)中计算或提取数据,这样的数据存储操作大多需要花费数百毫秒,因此会影响用户体验。通过EVCache作为数据前端缓存,访问时间更加快速而且是线性的,同时削减了这些数据存储的负载,还能够更有效的分担用户请求。此外,数据加载服务经常是先于缓存响应,这保证了用户可以得到个性化的数据响应而不是通用响应。另外,使用EVCache缓存可以有效地削减操作的总体成本。
EVCache是典型的客户端/服务器结构。服务器端包括一个Memcached进程,这是一个流行的且久经考验的内存型键值对存储,还包括一个叫Prana的Java进程用于与发现服务(基于Eureka的实现)通信并托管本地管理,以及监控服务健康状态和统计状态的各种应用,并将统计信息发送给Netfix平台的统计服务。具体结构如图10-2所示。
图10-2EVCacheServer的基本结构
其中,面向微服务的Java应用提供了一个集成应用程序到微服务生态系统的HTTP接口,主要功能如下:
注册到发现系统。
其他服务的发现。
健康检查服务。
HTTPAPI和负载均衡要求。
动态属性加载。
EVCache客户端是一个Java的客户端,用于发现EVCache服务器并管理所有的增删改查(CRUD)操作,由客户端处理在集群中添加/删除服务器。基于亚马逊云服务可用区,客户端在执行创建、更新和删除操作的时候复制数据。另一方面,客户端的读操作直接从同一可用区的服务器读取数据。图10-3展示了EVCache的典型部署结构和单节点客户端实例与服务器的关系。
图10-3EVCache单节点客户端实例与服务器的关系
一个EVCache客户端连接了多个EVCache的服务器集群。在一个区域内,Netflix有多个全数据集的拷贝,由亚马逊云服务的可用区隔离开来。虚线框描述了区域内的副本,每个都拥有数据的全量镜像,作为隔离亚马逊云服务的自动伸缩组来管理这些镜像。某些缓存在一个区域内有两个镜像,有的拥有更多。这种高层架构长期来看是有效的,不会改变。每个客户端连接自己区域内所有可用区的所有服务器。写操作被发往所有实例,读操作优先选择离读请求近的服务器。
1.2
EVCache的发展
Netflix的服务在全球个多个国家上线了,很多国家都可以使用。为了应对用户和服务日益增长的需求,Netflix在全球建立了EVCache分布式系统。
Netflix的全球云服务遍布亚马逊各个服务区域,例如北弗吉尼亚、俄勒冈州和爱尔兰,为这些地区的会员提供就近服务,但是网络流量会因为各种原因改变,比如关键基础设施出了问题故障,或者地区之间进行失败恢复等,因此,Netflix采用无态应用服务器服务于来自任何地区的会员。
这些数据如果从持久层存储获得将会非常昂贵(造成频繁的数据库访问),Netflix需要将这种数据写入到本地缓存,而且必须复制到所有地区的缓存中,以便服务于各个地区的用户请求。微服务是依赖于缓存的,必须快速可靠地访问多种类型的数据,比如会员的观影历史,排行榜和个性化推荐等,这些数据的更新与改变都必须复制到全世界各个地区,以便这些地区的用户能够快速可靠地访问。
EVCache是专门为这些情况而设计的缓存产品,这是建立在于全局复制基础上的,同时也考虑了强一致性。例如,如果爱尔兰和弗吉尼亚的推荐内容有轻微差别,这些差别不会伤害到用户浏览和观看体验,对于非重要的数据,会严重依赖最终一致性模型进行复制,
本地和全局两个缓存的差别保持在一个可以容忍的很短的时间内,这就大大简化了数据的复制。EVCache并不需要处理全局锁、事务更新、部分提交回滚或其他分布式一致性有关的复杂问题。即使在跨区域复制变慢的情况下,也不会影响性能和本地缓存的可靠性,所有复制都是异步的,复制系统能够在不影响本地缓存操作的情况下悄悄地进行。复制延迟是另外一个问题,快得足够吗?在两个地区之间切换的会员流量有多频繁?什么情况会冲击导致不一致性?宁愿不从完美主义去设计一个复制系统,EVcache只要能最低限度满足应用和会员用户的要求即可。
图10-4介绍了EVCache跨地域的复制
图10-4EVCache跨地域的数据复制
这张图说明复制操作是在SET操作以后实现,应用程序调用EVCache客户端库的set方法,之后的复制路径对于调用者是透明的:
1)EVCache客户端库发送SET到缓存系统的本地地区的一个实例服务器中。
2)EVCache客户端库同时也将写人元数据(包括key,但是不包括要缓存的数据本身)到复制消息队列(Kafka)。
3)本地区的复制中继服务将会从这个消息队列中读取消息。
4)中继服务会从本地缓存中抓取符合key的数据。
5)中继服务会发送一个SET请求到另一个地域的复制中继服务。
6)在另一个区域中,复制中继服务会接受请求,然后执行SET操作到它的本地缓存,
完成复制。
7)在接受地区的本地应用当通过GET操作以后会在本地缓存上看到这个已经更新的数据值。
这是一个简单描述,需要注意的是,它只会对SET操作有效,对于其他DELETETOUCH或批mutation等操作不会复制,DELETE和TOUCH是非常类的,只有一点不同:它们不从本地缓存中读取已经存在的值。
跨区域复制主要是通过消息队列进行,一个地区的EVCache客户端不会注意到其他地区的复制情况,读写都是只使用本区域缓存,不会和其他地区缓存耦合,通过消息系统来解耦合。
1.3
EVCache的演进
EVCache作为Nethix系统中最大的子系统之一,在系统优化中占有相当的比例,地位独一无二。所有数据存储在内存的成本随着用户基数的增长而上扬,单日个性化批处理输出将加载超过5TB的数据到EVCache集群。数据存储成本是存储数据与全局副本个数的乘积。如前所述,不同的A/B测试和其他内部数据也增加了更多的数据。对于用户的工作集,如今已经有数十亿的键值,而且在持续增加,成本的压力逐渐显现了出来。
面向数据和时延的优化
在一般情况下,在Netflix的某个服务区域内可以看到同一个用户的反复区域切换对用户而言并不是常态。尽管数据在三个区域的内存中,只有一个区域中的数据被所在的用户正常使用。由此推断,在每个区域有着这些缓存的不同工作集,一个小的子集是热数据,其他是冷数据。
除了冷热数据的分类之外,所有在内存中的这些数据存储成本随着用户的基数在增加。
Netflix使用EVCache在内存中存储了若干TB的数据,包括了用于弹性的多个数据拷贝。
随着成本面临的压力。Netflix开始使用RocksDB来降低EVCache的存储成本,同时保持了相对低的请求延迟。Netflix引人了多级缓存机制,即同时使用RAM和SSD。
根据不同区域不同数据访问的情况,Netflix构建了一个系统将热数据存储在RAM,冷数据存储在硬盘。这是典型的两级缓存架构(L1代表RAM,L2代表硬盘),依赖于EVCache的强一致性和低时延性能。面对尽量低的时延需求,要使用更多的昂贵内存,使用低成本的SSD也要满足客户端对低时延的预期。
内存型EVCache集群运行在AWSr3系列的实例类型上,对大规模内存的使用进行了优化。通过转移到i2系列的实例上,在相同的RAM和CPU的条件下,可以获得比SSD存储(r3系)扩大十倍的增益(80→GB,从r3.xlarge到i2.xlarge)。Netflix也降级了实例的大小到小型内存实例上。结合这两点,就可以在数千台服务器上做优先的成本优化了。
基于EVCache,这种充分利用全局化请求发布和成本优化的项目叫做Moneta,源自拉丁记忆女神的名字,也是罗马神话中财富守护神——JunoMoneta。
Moneta架构
Moneta项目在EVCahce服务器中引入了2个新的进程:Rend和Mnemonic。Rend是用Go语言写的一个高性能代理,Mnemonic是一个基于RocksDB的硬盘型键值对存储。
Mnemnonic重用了Rend服务器组件来处理协议解析(如Memcached协议),连接管理和并行锁。这三种服务器都使用Memeached的文本和二进制协议,所以客户端与它们的交互有着相同的语法,给调试和一致性检查带来了便捷性。Moneta的系统结构如图10-5所示。
图10-5Moneta的结构组成
Rend代理服务
Rend作为另外两个真正存储数据进程的代理,是一个高性能服务器,使用二进制和文本Memcached协议进行通信。它是Go语言写的,具有对并发处理的高性能。这个项目已经在Github上开源了。使用Go是不错的选择,因为需要比Java更好的低时延(垃圾回收时的暂停是个问题),以及比C更好的生产效率,同时能处理成千上万的客户端连接,Go非常适合这样的场景。
Rend的职责是管理L1和L2缓存的关系,根据不同的内部使用场景采用不同的策略,还具有裁剪数据的特性,能够将数据分割成固定的大小插入到Memcached中以避免内存分配时的病态行为。这种服务器侧的分片代替了客户端分片,已经证明是可行的。
Rend的设计是模块化的,并且可配置。在内部,有这样一些分层:连接管理,服务器循环,通信协议,请求编排和后台处理器。Rend也有着独立用来测试的客户端代码库,能够集中发现协议中的bug或者其他错误,例如错误对齐,未清除的缓存以及未完成的响应等。Rend的基本结构如图10-6所示。
图10-6Rend的结构组成
作为Moneta服务的那些缓存,一个服务器就可以服务多种不同的客户端。一类是热路径上的在线分析流量,用户请求的个性化数据。其他是离线分析的流量和近期系统所产生的数据。这些典型的服务是整夜运行的巨量批处理和结束时几个小时的持续写操作。
模块化允许使用默认的实现来优化Netflix夜间的批处理计算,直接在L2中插入数据并且在L1中更换热数据,避免了在夜间预计算时引起L1缓存的写风暴。来自其他区域的副本数据通常不是热数据,所以也直接插人L2。
图10-7展示了一个Rend进程有多个端口连接了各种后台存储。
图10-7Rend多端口连接后台存储服务
鉴于Rend的模块化,很容易在不同的端口上引入其他的服务器,几行代码就能实现批处理和流量副本。允许不同后台的插件式嵌人,通过一个接口和一个构造函数即可。已经证明了这种设计的有效性,一个工程师在一天内熟悉了相关代码并学习了LMDB,把它集成起来作为了存储后台。这些代码参见