本文从缓存的演进,分析了Couchbase分布式缓存的架构

单机时代

单机时代一切都是美好的,缓存只是为了解决磁盘访问速度问题,大多数本地缓存基本上都是个HashMap.

存储型应用内部都会内置一个缓存,复杂度一般不在缓存本身,而在于存储型应用提供的访问方式.(比如mysql缓存的复杂在于sql查询转换成缓存的key-value查询).

当单机纵向扩展(Scaling Up)遇到瓶颈时,必须探寻横向扩展(Scaling out)的方案.

多机分片(sharding)

横向扩展最简单的方式就是分片,无论存储还是缓存.分片最简单的方式就是

node=nodes[hash(key)/%nodes.length]

n表示机器的节点数.但这样的方式用在缓存上有个问题,当新增加节点或者减少节点的时候,n变化基本上会导致所有的key对应的节点都发生变化,造成缓存震荡.这个大多数应用都是不可接受的.

另外一种分片就是静态key分片,应用配置中给每个节点设置静态hash区间,解决缓存震荡的问题.但这种方式带来的问题是如果某个节点宕机,不能自动摘除,无法实现高可用。 于是有了一致性哈希(Consistent Hashing)

一致性哈希 (图片来自网络)

具体参看一致性哈希

一般的实现方式是(代码仅是示例,做了部分精简):

//构造hash环,一般用二叉树(java中TreeMap是红黑树)实现,便于便于增删节点
TreeMap<Long, Node> nodeMap = new TreeMap<Long, Node>();
for (Node node : nodes) {
    for (int i = 0; i < numReps; i++) {
        nodeMap.put(hash(node, i), node);
    }
}
//获取节点
Long nodeKey = nodeMap.ceilingKey(hash(key));
Node node = nodeKey == null?nodeMap.firstEntry().value:nodeMap.get(nodeKey);

分片的方式有以下优点:

  • 机器之间互相独立,运维部署简单(分片方案相对集群的优势)
  • 一致性哈希避免了减少或者增加节点导致的缓存震荡
  • 一致性哈希的漂移机制一定程度上实现了高可用(相对静态分片)

但也存在以下问题:

  • 如果请求量非常大,宕机后1/n的数据穿透也不可接受
  • 下线或者新增节点比较麻烦,需要应用修改配置升级,同时也会导致上面的问题
  • 无法动态增减节点

代理方案

通过代理实现高可用也是一种可选方式,具体可参看Redis HA 方案选型. 不过代理方式存在的问题:

  • 性能瓶颈
  • 增加运维复杂度
  • 不能很好解决单点问题(Redis代理一般要依赖Redis的主从复制机制,memcached代理无法实现分组多写)

分组多写方案

为了解决高可用问题,应用发展过程中逐步演化出来的方案. 同一个memcached集群,配置多组,写的时候多写,读的时候随机读或者根据配置规则读.一组中读取不到,则读取另一组. memcached-multi-group

增加新节点的时候,将新节点和旧节点合在一起作为一组(A),全部的旧节点作为另外一组(B),同时启用两组,读取数据的时候先从A读取,如果读取不到则从B读取.写的时候同时写两组.这样新增的节点会逐步热起来,等待合适的时候将B组下线.

这个方案虽然有点笨,但也能解决集群方案尚未成熟的时候的缓存的高可用问题. 这个方案存在以下问题:

  • 应用层的配置比较复杂,上线下线节点更加麻烦
  • 写的时候要多写,对性能有影响
  • 纯内存型缓存,如果数据量比较大,内存数据丢失(重启服务等),热缓存的成本比较高

分布式缓存

总结一下,我们期望中的理想缓存:

  • 自动分片,不需要应用关心
  • failover,高可用,无单点问题
  • 可动态扩容,可自动迁移数据实现均衡
  • 自动复制(Replication),无需应用通过多写解决
  • 可持久化

以上需求就是分布式缓存需要解决的问题

Couchbase的实现

要解决自动分片以及动态扩容问题,首先面临的问题就是客户端怎么知道数据存到那个节点上了?因为这个是动态变化的.初步想到的可能是用个中心节点去保存数据和节点的映射关系.这个方式在分布式文件系统中用的多,但用在缓存上明显不合适了,因为缓存的数据都是小数据,这样中心节点就成瓶颈了,但思路可以借鉴.于是Couchbase引入了vBucket的概念.

  • vBucket vBucket其实就是key的分组,然后配置中心只需维护vBucket到节点的映射关系.复制以及数据迁移都以vBucket为单位,这样降低了复制和数据迁移的成本.原来复制迁移数据,需要将整个节点的数据都迁移完成后才能提供服务,而有了vBucket的概念后,一个vBucket迁移完即可提供对该vBucket下的数据的访问服务. 注:Couchbase中的vBucket和Bucket是不同的概念.Bucket是Couchbase的数据分区,相当于虚拟的数据库集群.二者没有任何关系
  • TAP协议 memcached本身没有提供数据同步机制,于是Couchbase引入了TAP协议来实现数据复制.http://www.couchbase.com/wiki/display/couchbase/TAP+Protocol
  • ep-engine(Eventually Persistent in-memory database) 持久化 https://github.com/membase/ep-engine ep-engine来源于membase,是memcached的持久化插件。
  • ns_server 基于Erlang OTP框架实现,承担着集群管理监控协调的任务,提供web界面以及REST管理接口 https://github.com/couchbase/ns_server
  • smartclient 顾名思义,这个client需要”聪明”一些.client需要将vBucket的映射表缓存起来,访问的时候就不需要从配置中心查询对应的vBucket在哪个节点上.同时需要连接到集群上,监听vBucket映射的变化,动态变更本地的client配置.
  • moxi Memcached代理,主要提供给不支持smartclient的编程语言应用使用,如:php

这样Couchbase几乎实现了理想中的分布式缓存。

Couchbase的缺点

  1. 逐渐倾向于闭源,社区版本和商业版本之间差距比较大。
  2. 各种组件拼接而成,都是c++实现,导致复杂度过高,遇到奇怪的性能问题排查比较困难。可能这也是Redis的集群采取了一种保守方案的原因吧。