EVCache是一个开源、快速的分布式缓存,是基于Memcached的内存存储和Spymemcached客户端实现的,是Netflix(网飞)公司开发的
- Rend服务:是一个代理服务,用GO语言编写,能够高性能的处理并发。
- Memcached:基于内存的键值对缓存服务器
- Mnemonic:基于硬盘(SSD)的嵌入式键值对存储服务器,封装了RocksDB(是一种SSD的技术)
- EVCache集群在峰值每秒可以处理200kb的请求,
- Netflix生产系统中部署的EVCache经常要处理超过每秒3000万个请求,存储数十亿个对象,
- 跨数千台memcached服务器。整个EVCache集群每天处理近2万亿个请求。
- EVCache集群响应平均延时大约是1-5毫秒,最多不会超过20毫秒。
- EVCache集群的缓存命中率在99%左右。
EVCache 是线性扩展的,可以在一分钟之内完成扩容,在几分钟之内完成负载均衡和缓存预热。
EVCache的跨可用区复制
- EVCache客户端库包发送SET到缓存系统的本地地区的一个实例服务器中。
- EVCache客户端库包同时也将写入元数据(包括key,但是不包括要缓存的数据本身)到复制消息队列(Kafka)
- 本地区的“复制转播”的服务将会从这个消息队列中读取消息。
- 转播服务会从本地缓存中抓取符合key的数据
- 转播会发送一个SET请求到远地区的“复制代理”服务。
- 在远地区,复制代理服务会接受到请求,然后执行一个SET到它的本地缓存,完成复制。
- 在接受地区的本地应用当通过GET操作以后会在本地缓存看到这个已经更新的数据值。
由于Netflix没有开源EVCache的服务器部分,这里采用Memcached作为服务器。
- #安装libevent库
- yum install libevent libevent-devel gcc-c++
-
- #下载最新的memcached
- wget http://memcached.org/latest
-
- #解压
- tar -zxvf latest
-
- #进入目录
- cd memcached-1.6.6
-
- #配置
- ./configure --prefix=/usr/memcached
-
- #编译
- make
-
- #安装
- make install
-
- #启动
- memcached -d -m 1000 -u root -l 192.168.127.131 -p 11211 -c 256 -P /tmp/memcached.pid
- -d 选项是启动一个守护进程
- -m 是分配给Memcache使用的内存数量,单位是MB,我这里是10MB
- -u 是运行Memcache的用户,我这里是root
- -l 是监听的服务器IP地址,我这里指定了服务器的IP地址192.168.127.131
- -p 是设置Memcache监听的端口,我这里设置了11211(默认),最好是1024以上的端口
- -c 选项是最大运行的并发连接数,默认是1024,我这里设置了256,按照你服务器的负载量来设定
- -P 是设置保存Memcache的pid文件,我这里是保存在 /tmp/memcached.pid
(1)pom
-
- <dependency>
- <groupId>com.netflix.evcachegroupId>
- <artifactId>evcache-clientartifactId>
- <version>4.139.0version>
- dependency>
-
-
- <dependency>
- <groupId>net.spygroupId>
- <artifactId>spymemcachedartifactId>
- <version>2.12.3version>
- dependency>
-
-
- <dependency>
- <groupId>com.netflix.eurekagroupId>
- <artifactId>eureka-clientartifactId>
- <version>1.5.6version>
- <scope>runtimescope>
- dependency>
-
- <dependency>
- <groupId>com.netflix.spectatorgroupId>
- <artifactId>spectator-nflx-pluginartifactId>
- <version>0.80.1version>
- <scope>runtimescope>
- dependency>
-
- <dependency>
- <groupId>com.netflix.spectatorgroupId>
- <artifactId>spectator-apiartifactId>
- <version>0.80.1version>
- dependency>
-
-
- <dependency>
- <groupId>com.netflix.rxjavagroupId>
- <artifactId>rxjava-coreartifactId>
- <version>0.20.7version>
- dependency>
-
- <dependency>
- <groupId>com.netflix.servogroupId>
- <artifactId>servo-coreartifactId>
- <version>0.12.25version>
- dependency>
-
- <dependency>
- <groupId>com.google.code.findbugsgroupId>
- <artifactId>annotationsartifactId>
- <version>3.0.1version>
- dependency>
-
-
- <dependency>
- <groupId>com.netflix.nebulagroupId>
- <artifactId>nebula-coreartifactId>
- <version>4.0.1version>
- <scope>runtimescope>
- dependency>
-
- <dependency>
- <groupId>com.netflix.archaiusgroupId>
- <artifactId>archaius2-coreartifactId>
- <version>2.3.13version>
- <scope>runtimescope>
- dependency>
-
- <dependency>
- <groupId>com.netflix.archaiusgroupId>
- <artifactId>archaius-awsartifactId>
- <version>0.6.0version>
- dependency>
-
- <dependency>
- <groupId>javax.injectgroupId>
- <artifactId>javax.injectartifactId>
- <version>1version>
- dependency>
-
- <dependency>
- <groupId>io.reactivexgroupId>
- <artifactId>rxjavaartifactId>
- <version>1.3.8version>
- dependency>
(2)编写EVCache代码
- import com.netflix.evcache.EVCache;
- import com.netflix.evcache.EVCacheException;
-
- public class EVCacheDemo {
- public static void main(String[] args) throws Exception {
- // System.setProperty("EVCACHE_SERVER", "SERVERGROUP=192.168.127.128:11211");
- // EVCache evCache = new EVCache.Builder().setAppName("EVCACHE_SERVER").build();
-
- String deploymentDescriptor = System.getenv("EVCACHE_SERVER");
- if ( deploymentDescriptor == null ) {
- deploymentDescriptor = "SERVERGROUP1=192.168.127.131:11211";
- }
- System.setProperty("EVCACHE_1.use.simple.node.list.provider", "true");
- System.setProperty("EVCACHE_1-NODES", deploymentDescriptor);
- EVCache evCache = new EVCache.Builder().setAppName("EVCACHE_1").build();
- // s:key t :value i:ttl 秒
- evCache.set("name","zhangfei",10);
- String v = evCache.get("name");
- System.out.println(v);
- }
- }
Libevent 是一个用C语言开发的,高性能;轻量级,专注于网络,不如 ACE 那么臃肿庞大;源代码相当精炼、易读;跨平台,支持 Windows、 Linux、 BSD 和 Mac OS;支持多种 I/O 多路复用技术,epoll、 poll、 select 和 kqueue 等;支持 I/O,定时器和信号等事件;注册事件优先级。
Memcached | Redis | |
多线程 | 是 | 否(6.0以下版本) |
数据结构 | 简单kv,V为一个值 | V多种,List、Set、Hash.... |
持久化 | 否 | 是 |
集群 | 客户端或代理端(twemproxy) | 客户端、代理端(codis)、服务器端(RedisCluster) |
性能 | 读多写少,大数据 大于100K | 读写都行 |
通信协议 | 文本(telnet)、二进制 | RESP |
集群内通信 | 无 | Gossip、订阅 |
数据安全 | 无 | 有 |
集群failover | 无 | 有 |
传统的内存分配是通过对所有记录简单地进行malloc(动态内存分配)和free(释放) 来进行的。是以Page(M)为存储单位的(BuddySystem)。这种方式会导致内存碎片,加重操作系统内存管理器的负担。
memcached采用Slab(块) Allocation的方式分配和管理内存
Slab Allocation的原理:
注:块越小存的数据越多,块越来越大,是由growth factor决定的(1.25)
Item就是我们要存储的数据。是以双向链表的形式存储的。
- /**
- * Structure for storing items within memcached.
- */
- typedef struct _stritem {
- struct _stritem *next; //next即后向指针
- struct _stritem *prev; //prev为前向指针
- struct _stritem *h_next; /* hash chain next */
- rel_time_t time; /* least recent access */
- rel_time_t exptime; /* expire time */
- int nbytes; /* size of data */
- unsigned short refcount;
- uint8_t nsuffix; /* length of flags-and-length string */
- uint8_t it_flags; /* ITEM_* above */
- uint8_t slabs_clsid;/* which slab class we're in */
- uint8_t nkey; /* key length, w/terminating null and padding */
-
- union {
- uint64_t cas;
- char end;
- } data[];
- } item;
item 的结构分两部分:
Memcached有两种过期机制:Lazy Expiration(惰性过期)和LRU
Lazy Expiration
Memcached在get数据时,会查看exptime,根据当前时间计算是否过期(now-exptime>0),如果过期则删除该数据
LRU
主要有以下特性:
- API接口:提供同步或异步接口调用,异步接口返回Future
- 任务封装:将访问的操作及callback封装成Task
- 路由分区:通过Sharding策略(后面讲),选择Key对应的连接(connection)
- 将Task放到对应连接的队列中
- Selector异步获取队列中的Task,进行协议的封装和发送相应Memcached
- 收到Memcached返回的包,会找到对应的Task,回调callback,进行协议解析并通知业务线程Future.get
- //获取一个连接到几个服务端的memcached的客户端
- MemcachedClient c = new MemcachedClient(AddrUtil.getAddresses("192.168.127.123:11211"));
- //获取值,如果在5秒内没有返回值,将取消
- Object myObj = null;
- Future
- try{
- //异步获取 阻塞
- myObj = f.get(5,TimeUnit.SECONDS);
- }catch(TimeoutException e){
- //退出
- f.cancel(false);
- }
- import net.spy.memcached.MemcachedClient;
-
- import java.net.InetSocketAddress;
- import java.util.concurrent.Future;
-
- public class MemcachedDemo {
- public static void main(String[] args) {
-
- try{
- // 连接本地的 Memcached 服务
- MemcachedClient mcc = new MemcachedClient(new InetSocketAddress("192.168.127.128", 11211));
- System.out.println("Connection to server sucessful.");
-
- // 存储数据
- Future fo = mcc.set("runoob", 900, "Free Education");
-
- // 查看存储状态
- System.out.println("set status:" + fo.get());
-
- // 输出值
- System.out.println("runoob value in cache - " + mcc.get("runoob"));
-
- // 关闭连接
- mcc.shutdown();
-
- }catch(Exception ex){
- System.out.println( ex.getMessage() );
- }
- }
- }
SpyMemcached有两类线程:业务线程和selector线程
业务线程:
Selector线程
路由机制
容错
key路由到服务节点,服务节点宕机,有两种方式处理:
- 失败的节点从正常的队列中摘除
- 添加到重连队列中
- Selector线程定期对该节点进行重连
- Redistribute(推荐):遇到失败节点后选择下一个节点,直到选到正常的节点大量回源
- Retry:继续访问,一般就失败了
- Cancel:抛异常退出访问
Memcache的集群处理容错比较差
我们知道在日常存储中数据往往是对象,那么在Memcached中会以二进制的方式存储,所以在存数据时要进行对象的序列化(jdk序列化),在取数据时要进行对象的反序列化是业务线程处理。
序列化后会判断长度是否大于阈值16384个byte,如果大于将采用Gzip的方式进行数据压缩