概述
云服务为软件系统的开发和部署带来更多的敏捷性,且提供更多创新的可能性。
EVCache是一个基于Memcached内存存储和Spymemcached客户端的开源(参考GitHub)的快速的分布式缓存解决方案,主要用在亚马逊弹性计算云服务(AWS EC2)的基础设施上,专为云计算而优化,能够顺畅而高效地提供数据层服务。
EVCache实现的主要功能包括分布式键值对存储,亚马逊云服务的跨区域数据复制以及注册和自动发现新节点或者新服务。典型的应用场景是对上下文一致性要求不高的业务,其高扩展性可处理超大流量,同时提供健壮的应用编程接口。
EVCache是以下几个单词的缩写:
- Ephemeral:数据存储是短暂的,有自身的存活时间;
- Volatile:数据可在任何时候消失;
- Cache:一个内存型的键值对存储系统。
EVCache的特性:
- 分布式键值对存储,缓存可以跨越多个实例;
- 数据可以跨越亚马逊云服务的AZ进行复制
- 通过Netflix内部的命名服务进行注册,自动发现新节点和服务;
- 为了存储数据,键是非空字符串,值可以是非空的字节数组,基本类型,或序列化对象,且小于1MB;
- 作为通用的缓存集群被各种应用使用,支持可选的缓存名称,通过命名空间避免主键冲突;
- 缓存命中率在99%以上;
- 与Netflix驻留数据框架能够良好协作,典型的访问次序:内存→EVCache→Cassandra/SimpleDB/S3。
架构
在Netflix架构中有两个基本元素,一个是控制平面,运行在亚马逊云服务(AWS)之上,用于用户登录,浏览和播放以及一般性服务。另一个是数据平面,叫做Open Connect,这是一个全球性的视频分发网络。EVCache是位于控制平面的。
EVCache使用Memcached操作接口,基于数据大小和网络容量可以线性扩展,支持任意数量的数据备份。所有操作都拥有对拓扑结构的感知、重试、回退,以及其他机制来保障操作的完整性,同时优化亚马逊云服务的架构。每个主键中的数据通过数据分块技术处理后可以是任意大小的。
Memcached是一个单进程应用,在单台主机上工作的很好,而EVCache使用它作为一个基础模块,Memcached是EVCache的一个子集。
典型部署
EVCache是线性扩展的,通过容量监控,可以在一分钟内扩容,在几分钟内完成重新均衡和数据预热。Netflix有一个漂亮的容量模型,所有容量的改变并不频繁,在管理缓存命中率时有更好的扩容方法。
单节点部署
启动时,EVCache服务器将各个实例注册到命名服务(Netflix内部的命名服务包含所有运行主机的列表)。在Web应用启动时,初始化EVCache客户端库,查询命名服务中的EVCache服务器列表,然后建立链接。当Web应用需要执行CRUD操作的时候,客户端通过键值选择一个实例来执行这些操作,使用一致性哈希将数据分片到集群上。
多可用区部署
集群启动时,可用区(AZ,Available Zone)A的一个EVCache服务器实例向命名服务注册,声明可用区A和B都可以使用。在可用区A中的Web应用启动期间,Web应用初始化EVCache客户端来查询命名服务中的所有EVcache服务器实例,建立跨AZ的连接。当Zone-A需要从一个键读取数据的时候,EVCache客户端查询可用区A中的EVCache服务器实例,并从这个实例获取数据。当可用区A中的Web应用需要写或删除一个键的数据的时候,EVCache客户端查询可用区A和B中的EVCache服务器实例,并且写入或删除它。
高可用
通过无数据丢失的高速缓存部署,EVCache可以避免缓存故障。尽管个别实例可能会消亡,并且可能会定期这样做,但不会影响客户端应用程序。在Netflix,有很多服务使用EVCache作为一个独立的存储机制,这意味着数据除了EVCache没有更适合的地方存放,这归功于EVCache拥有强大的容错能力。
AWS的多可用区
AWS云服务在全球不同的地方都有数据中心。与此对应,根据地理位置把某个地区的基础设施服务集合称为一个区域。通过AWS的区域,一方面可以使得AWS云服务在地理位置上更加靠近用户,另一方面使得用户可以选择不同的区域存储他们的数据以满足法规遵循方面的要求。
AWS的每个区域一般由多个AZ组成,而AZ一般是由多个数据中心组成。AWS引入AZ设计主要是为了提升用户应用程序的高可用性。因为AZ之间在设计上是相互独立的,也就是说它们会有独立的供电、独立的网络等,这样假如一个AZ出现问题时也不会影响其他AZ。在一个区域内,AZ之间通过高速网络连接,从而保证很低的延时。
EVCache实例通过将Amazon EC2放到多个AZ,能够预防应用的单点故障。无论在相同的物理区域内还是在不同的物理区域之间,在多个AZ上运行独立的应用都是非常重要的。如果一个AZ失效,在其他AZ上的应用可以继续运行,从而实现高可用性。
EVCache对AWS高可用性的增强
由于跨越多个亚马逊云服务AZ,EVCache集群是不会挂掉的。当其中的实例偶然挂掉时,通过一致性哈希跨集群分片来使缓存的影响降到最低。
在保持高可用性的同时,操作EVCache集群的总体成本很低,因为缓存没有命中时访问亚马逊云服务的成本较高,如访问SimpleDB,AWS S3,EC2上的Cassandra等。EVCache集群的总体成本在高稳定、线性扩展的条件下还是令人满意的。
隐藏在需求后面的是数据或状态所需要的每个请求服务,必须是跨地区可用的。高可靠性数据库和高性能缓存是支持分布式架构的基础设施,一个典型场景是将缓存架构于数据库前面或其他持久存储前面。如果没有缓存的全局复制,一个地区的会员切换到另外一个地区时,会在新的地区缓存中没有原地区的数据,这种情况称为冷缓存。处理这种缓存数据丢失的办法只有重新从数据库加载,但是这种方式会延长响应时间并对数据库形成巨大冲击,EVCache除了跨AZ复制之外,还提供跨区域复制,对基于AWS的高可用性进行增强。
跨地域的数据复制
跨地域的数据复制示意图:
复制路径对于调用者是透明的:
- EVCache客户端库发送SET到缓存系统的本地地区的一个实例服务器中;
- EVCache客户端库同时也将写入元数据(包括key,但是不包括要缓存的数据本身)到Kafka;
- 本地区的复制中继服务将会从这个消息队列中读取消息;
- 中继服务会从本地缓存中抓取符合key的数据;
- 中继服务会发送一个SET请求到另一个地域的复制中继服务;
- 在另一个区域中,复制中继服务会接受请求,然后执行SET操作到它的本地缓存,完成复制;
- 在接受地区的本地应用当通过GET操作以后会在本地缓存上看到这个已经更新的数据值。
高性能
TODO
SpyMemcached
一个用Java开发的异步、单线程的Memcached客户端。Spymemcached中的每一个节点,用MemcachedNode表示,这个对象内部含有一个同等网络连接到该节点。根据主键的哈希值查找某个节点,Spymemcached中使用了NodeLocator,默认的定位器是ArrayModNodeLocator,这个对象内部包含所有的MemcachedNode。spy使用的哈希算法都在对象DefaultHashAlgorithm中,默认使用NATIVE_HASH,也就是String.hashCode()。定位器和客户端中间还有一个对象,叫MemcachedConnection,它代表客户端到Memcached集群的连接,内部持有定位器。客户端内部会持有MemcachedConnection。spy使用NIO实现,因此有一个选择器,这个对象也存在于MemcachedConnection中。与服务器之间进行的各种操作如协议数据发送,数据解析等,spy中抽象为Operation,文本协议的get操作最终实现为net.spy.memcached.protocol.ascii.GetOperationImpl
。为了实现工作线程和IO线程之间的调度,spy抽象出一个GetFuture,内部持有一个OperationFuture。TranscodeService执行字节数据和对象之间的转换,在spy中的实现方式为任务队列和线程池,这个对象的实例在客户端中。
关联
EVCache与Spymemcached的继承关系如下图所示:
EVCacheConnection继承自MemcachedConnection,重写了shutdown方法、队列和广播操作,保证每个连接线程的执行。EVCacheMemcachedNodeROImpl是MemcachedNode的子类,实现了与Memcached服务器节点间的相关操作。EVCacheNodeImpl继承自BinaryMemcachedNodeImpl并实现了EVCacheNodeImplMBean和CompositeMonitor两个接口,不仅有节点的相关操作,而且实现了对节点的监控。EVCacheMemcachedClient继承自MemcachedClient,作为实体类实现队列的异步操作。
EVCache开源项目中采用的是SpyMemcached来支持Memcached的协议。Memcached服务器和客户端之间采用TCP的方式通信,自定义一套字节流的格式,里面分成命令行和数据块行,命令行里指明数据块的字节数目,命令行和数据块后都跟随\r\n
。重要的一点是服务器在读取数据块时是根据命令行里指定的字节数目,因此数据块中含有\r
或\n
并不影响服务器读块操作,数据块后必须跟随\r\n
。
Moneta架构
Moneta项目在EVCahce服务器中引入2个新的进程:Rend和Mnemonic。Rend是用Go语言写的一个高性能代理,Mnemonic是一个基于RocksDB的硬盘型键值对存储。Mnemonic重用Rend服务器组件来处理协议解析(如Memcached协议),连接管理和并行锁。这三种服务器都使用Memcached的文本和二进制协议,所以客户端与它们的交互有着相同的语法,给调试和一致性检查带来便捷性。
Rend代理服务
Rend作为另外两个真正存储数据进程的代理,是一个高性能服务器,使用二进制和文本Memcached协议进行通信。它是Go语言写的,具有对并发处理的高性能。这个项目已经在Github上开源了。使用Go是不错的选择,因为需要比Java更好的低时延(垃圾回收时的暂停是个问题),以及比C更好的生产效率,同时能处理成千上万的客户端连接,Go非常适合这样的场景。
Rend的职责是管理L1和L2缓存的关系,根据不同的内部使用场景采用不同的策略,还具有裁剪数据的特性,能够将数据分割成固定的大小插入到Memcached中以避免内存分配时的病态行为。这种服务器侧的分片代替了客户端分片,已经证明是可行的。
Rend的设计是模块化的,并且可配置。在内部,有这样一些分层:连接管理,服务器循环,通信协议,请求编排和后台处理器。Rend也有着独立用来测试的客户端代码库,能够集中发现协议中的bug或者其他错误,例如错误对齐,未清除的缓存以及未完成的响应等。
Mnemonic存储
Mnemonic是基于RocksDB的L2解决方案,在硬盘上存储数据。协议解析、连接管理、Mnemonic的并发控制等所有的管理都使用了和Rend相同的库。Mnemonic是嵌入到Moneta服务器的一个后台服务,Mnemonic项目暴露出一个定制化的C API供Rend处理器使用。
Mnemonic中有趣的部分是在C++的核心层封装了RocksDB。Mnemonic处理Memcached协议风格的请求,实现了Memcached的所需操作,包括TTL支持。它包含了一个重要的特性:将请求分发到一个本地系统的多个RocksDB数据库,减少了每个RocksDB数据库实例的负载。
Netflix选择了RocksDB,一个嵌入式键值对存储,它使用了日志结构合并树的数据结构。写操作首先插入到一个内存数据结构中(一个内存表),当写满的时候再写入到硬盘上。当写入硬盘的时候,内存表是一个不可修改的SST文件。这样形成了批量的序列化写入SSD的操作,减少了大量的内部垃圾回收,改善了SSD在长时间运行实例上的时延。
使用场景
推荐相似内容
过程如下:
- 一个客户向Web应用发起一个页面请求,处理这一请求需要得到一个电影或电视节目的相似性列表;
- Web应用查询EVCache来得到这些数据,这样场景的典型缓存命中率高于99.9%;
- 如果缓存没有命中,Web应用将调用相似性计算服务来计算这些数据;
- 如果已经计算过的数据也没有命中的话,相似性计算服务将从SimpleDB中读取数据。如果在SimpleDB中没有,相似性计算服务根据给出的电影或电视节目重新计算相似性;
- 相似性计算服务在计算出电影或电视节目的数据后,将数据写入到EVCache中;
- 最后,相似性计算服务生成客户端所需要的响应并返回给客户端。
源码
查看GitHub,不难得知,主要有4部分组成:
4个模块:
- evcache-core:核心功能
- evcache-client:客户端
- evcacheproxy:提供RESTful风格的编程接口,方便其他语言使用EVCache缓存服务
- evcache-zipkin-tracing:集成zipkin做分布式链路跟踪
下面的源码分析,基于如下GAV:
<dependency>
<groupId>com.netflix.evcache</groupId>
<artifactId>evcache-client</artifactId>
<version>5.21.0</version>
</dependency>
事件监听
EVCacheEvent源码如下:
public class EVCacheEvent {
public static final String CLIENTS = "clients";
private final EVCache.Call call;
private final String appName;
private final String cacheName;
private final EVCacheClientPool pool;
private final long startTime;
private long endTime = 0L;
private String status = "success";
private Collection<EVCacheClient> clients = null;
private Collection<EVCacheKey> evcKeys = null;
private int ttl = 0;
private CachedData cachedData = null;
private Map<Object, Object> data;
}
EVCacheEvent描述EVCache调用过程中的相关事件,主要功能包括:
- 获得一个调用,Call的实体,应用名称,缓存名称;
- 设置/获取键的列表;
- 设置/获取TTL;
- 设置/获取缓存数据;
- 设置/获取Latch;
- 设置/获取其他属性等。
EVCacheEventListener接口继承自java.util.EventListener
,定义启动、完成、错误和流量条件四种方法,代码如下:
public interface EVCacheEventListener extends EventListener {
void onStart(EVCacheEvent e);
void onComplete(EVCacheEvent e);
void onError(EVCacheEvent e, Throwable t);
boolean onThrottle(EVCacheEvent e) throws EVCacheException;
}
度量与监控
EVCache提供三个接口用于定义度量与监控的维度:
- Stats:操作完成的时间,以及每次的缓存命中和没有命中;
- Operation:记录启动和停止的时间,以及操作时长;
- EVCacheMetricsMBean:用于获得度量的具体参数。
EVCacheOperation实现Operation接口中的所有方法。EVCacheMetrics实现EVCacheMetricsMBean和Stats接口,计算各种计数器,并建立与Netflix后台通信实现监控,
TODO
连接管理
连接模块主要以工厂模式来实现客户端与服务器的连接管理,EVCache的BaseConnection-Factory同样继承自SpyMemcached的BinaryConnectionFactory,主要功能如下:
- 创建/获取NodeLocator;
- 创建Memcached的节点和连接,以及连接的观察者;
- 创建读写操作的队列;
- 获得客户端连接池的管理器;
- 获得连接与操作间的相关属性,例如队列长度、重连时间、读数据的缓存大小、操作超时的时间、哈希算法、服务器组所在的时区等。
只有在运行时,才能知道具体要使用哪一个实现类。因此需要元工厂类或服务定位器来增强工厂模式。为了更方便灵活的使用工厂模式,抛弃switch语句,EVCache使用JDK的依赖注入API完成关联关系。
参考源码:DIConnectionFactoryBuilderProvider。TODO:深入讲解。
异步
EVCacheFuture实现JDK里的泛型Future接口:
public class EVCacheFuture implements Future<Boolean> {
private final Future<Boolean> future;
private final String app;
private final ServerGroup serverGroup;
private final String key;
private final EVCacheClient client;
// 省略其他几个基于JDK Future的方法
public boolean cancel(boolean mayInterruptIfRunning) {
return this.future.cancel(mayInterruptIfRunning);
}
}
EVCacheFutures实现ListenableFuture<Boolean,OperationCompletionListener>
和OperationCompletionListener
两个接口,对一个Future的集合列表完成类似EVCacheFuture的操作,增加对每个Future的监听。
BulkGetFuture是SpyMemcached中的一个类,为了实现大数据集的获取。EVCacheBulkGetFuture继承自BulkGetFuture,完成大数据集的异步获取。
OperationFuture也是SpyMemcached中的一个类,用于实现增删改等行为的异步操作,在EVCache中对应的是EVCacheOperationFuture。对于一个OperationFuture,应用的代码层面判断一个给定异步操作行为的状态。例如,如果更新一系列键值对,且希望验证操作是否正确,就可能要发起多个IO操作。
EVCacheLatchImpl实现EVCacheLatch接口,为了实现一个特殊的目的:向EVCache服务器组中的多台设备发起增删改的异步操作,同时根据相关策略统计挂起、失败、成功和已经完成的异步操作个数。
工具类
包括如下四个:
- EVCacheConfig:主要用于从文件或列表中动态获得各种类型的属性,数据类别有Int,Long,String,boolean以及这些基本属性的链表,同时可以获取有关监控的配置信息
- ServerConfigCircularIterator:一个副本集的循环迭代器,它保证了所有副本集与请求的数量相等。
- Sneaky:提供了两个静态方法,可以静默地检查并不属于本方法声明的异常,这实际上是有争议的,要慎重使用。
- ZoneFallbackIterator:一个基于AZ回退的循环迭代器,用于保证在AZ回退的情况下,请求可以跨AZ分发。
EVCacheClient
EVCacheClient,即EVCache客户端,内部持有EVCacheMemcachedClient对象,连接工厂ConnectionFactory,连接的观察者EVCacheConnectionObserver,服务器节点的地址列表,服务器群组的配置信息,以及与操作处理相关的诸多属性。源码将近2000行,省略。
作为EVCache的核心类,为了保证数据的可靠性,EVCacheClient提供读写队列的验证方法validateReadQueueSize和ensureWriteQueueSize,以及对服务器节点的有效性检测validateNode。为了保证数据的有效传输,EVCacheClient提供Chunk装配方法的多态,还提供循环冗余校验。为保证数据的高效操作,EVCacheClient使用静态内部类SuccessFuture和DefaultFuture,并基于Future实现读写操作的一系列方法。
EVCacheClient类的初始化流程:
EVCacheClientPool
如果每个EVCacheClient是一个线程的话,EVCacheClientPool就是线程池的核心,主要功能如下:
- 状态清除;
- 从线程池中取一个用于读的EVCacheClient;
- 从线程池中选择一个空闲的EVCacheClient;
- 从指定的服务器群组中获得一个EVCacheClient;
- 从线程池中获得用于写或者只用于写的EVCacheClient;
- 查询服务器群组中的服务器实例是否有变化;
- 获得Memcached的socket地址列表;
- 关闭一个AZ内的若干客户端;
- 通过服务器群组建立若干个新的客户端;
- 更新一个AZ内的Memcached读实例;
- 是否强制清除Memcached的实例;
- 刷新/异步刷新线程池;
- ping服务器是否活着;
- 禁用服务器群组;
- 获取读/写/可用实例的数量;
- 建立监控。
EVCacheClientPoolManager
一个管理类,管理着每个应用的客户端线程池。当EVCacheClientPoolManager被初始化时,所有定义在evcache.appsToInit
中的属性也将被初始化并添加到线程池。如果一个服务知道所有的应用都将使用它,会被定义到属性列表里,然后被初始化。构造函数初始化流程:
一个EVCache应用也可通过EVCacheClientPoolManager调用initEVCache(String app)
来初始化。
线程池
线程池模块由四部分组成:线程池管理,节点管理,服务器群组配置和连接检测:
线程池管理
参考上面的EVCacheClient、EVCacheClientPool、EVCacheClientPoolManager等类。
连接检测
EVCacheConnectionObserverMBean是一个接口,用于获得活跃/非活跃服务器的个数和名称、连接的数量等:
public interface EVCacheConnectionObserverMBean {
int getActiveServerCount();
Set<SocketAddress> getActiveServerNames();
int getInActiveServerCount();
Set<SocketAddress> getInActiveServerNames();
long getLostCount();
long getConnectCount();
}
EVCacheConnectionObserver实现接口ConnectionObserver和EVCacheConnectionObserverMBean,源码略,主要功能如下:
- 计算已经建立的连接数量;
- 计算所丢失的连接数量;
- 获得活跃/非活跃服务器的个数和名称,名称以Socket地址表示;
- 获得连接和丢失的次数;
- 获得应用名称和服务器群组;
- 注册活跃节点/取消注册非活跃节点;
- 建立监控。
服务器群组配置
服务器群组配置有两个类ServerGroup和EVCacheServerGroupConfig。
ServerGroup类型实现Comparable接口。Comparable接口强行对实现它的每个类对象进行整体排序。此排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。实现此接口的对象列表(和数组)可以通过Collections.sort
和Arrays.sort
进行自动排序。ServerGroup对象可以用作有序映射表中的键或有序集合中的元素,无需指定比较器。
EVCacheServerGroupConfig内部持有ServerGroup对象,主要是封装获取相关信息的方法,如getServerGroup、getRendPort、getUdsproxyMemcachedPort等。
节点管理
EVCacheNodeList是一个接口,返回值是ServerGroup和EVCacheServerGroupConfig的映射。EVCacheNodelist接口有两个实现类:
- SimpleNodeListProvider:通过系统属性来获取节点列表,具有调试的色彩。系统属性的格式如下:
<EVC ACHE_APP>NODES=setname0=instance01:port, instance02:port, instance03:port; setname1=instance11:port, instance12:port, instance13:port; setname2=instance21:port, instance22:port, instance23:port
- DiscoveryNodeListProvider:通过Netflix的发现服务来发现EVCacheServer的实例,也是与亚马逊云服务紧密关联的部分,是需要学习和关注的:
参考
- 深入分布式缓存:从原理到实践