---
title: 缓存的进化之路—Couchbase的分布式架构
date: 2014-06-30
draft: false
summary: '从单机缓存、分片、代理到分布式缓存，理解 Couchbase 这类系统到底解决了哪些问题。'
slug: couchbase
tags:
- couchbase
- redis-cluster
- 分布式缓存
topics:
- infra
type: post
---
本文从缓存的演进出发，分析 Couchbase 分布式缓存的架构。

### 单机时代

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

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

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

### 多机分片(sharding)

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

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

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

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

![一致性哈希][image-1]
(图片来自网络)

具体参看[一致性哈希][1]

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

    //构造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 方案选型][2].
不过代理方式存在的问题:

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

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

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

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

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

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

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

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

### Couchbase的实现

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

* vBucket
![][image-3]
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][3]
* ep-engine(Eventually Persistent in-memory database) 持久化
[https://github.com/membase/ep-engine][4]
ep-engine来源于membase,是memcached的持久化插件。
* ns\_server
基于Erlang OTP框架实现,承担着集群管理监控协调的任务,提供web界面以及REST管理接口 [https://github.com/couchbase/ns\_server][5]
* smartclient
顾名思义,这个client需要”聪明”一些.client需要将vBucket的映射表缓存起来,访问的时候就不需要从配置中心查询对应的vBucket在哪个节点上.同时需要连接到集群上,监听vBucket映射的变化,动态变更本地的client配置.
* moxi
Memcached代理,主要提供给不支持smartclient的编程语言应用使用,如:php

![][image-4]

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

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

[1]:    http://zh.wikipedia.org/wiki/%E4%B8%80%E8%87%B4%E5%93%88%E5%B8%8C "一致性哈希"
[2]:    /redis-ha/ "Redis HA 方案选型"
[3]:    http://www.couchbase.com/wiki/display/couchbase/TAP+Protocol
[4]:    https://github.com/membase/ep-engine
[5]:    https://github.com/couchbase/ns_server

[image-1]:    http://images.cnitblog.com/blog/312753/201304/13152329-84c656b0987f4d30bfa1bb3cb63a2a21.png "一致性哈希"
[image-2]:    ./memcached-multi-group.jpg "memcached-multi-group"
[image-3]:    ./vbucket2.png "vbucket"
[image-4]:    http://www.couchbase.com/sites/default/files/data_manager.png "couchbase-architecture"
