Codis是起源于豌豆荚的redis Proxy项目,其主要目的是为了解决redis使用中的两个痛点:
- 难以动态的平行扩展增加新的redis服务.
- 难以运维管理.
它主要的架构是这样的:
主要包括几个组件:
- dashboard:主要用于管理服务,主要通过ZK向codis-proxy下发命令,比如新增/减少服务器.
- zookeeper:用于集群服务之间同步配置.
- codis-proxy:代理层,实现了大部分redis协议命令,对客户端而言与一个真正的redis服务没有太大区别.同时监听ZK的命令,如果有配置的变更就更新自己内部的配置信息,收到redis命令之后根据ZK下发的配置找到对应的redis服务转发命令.
- redis:经过修改过的redis服务,主要新增了用于数据迁移用的命令.换言之,为了支持动态变更redis服务,是不能使用原生的redis服务的.
proxy层由于使用了ZK来同步配置,所以是无状态的可以进行平行扩展的.一般而言,要实现一个集群服务的分布式,归根到底本质上有两种方式:
- smart-client方式:将分布式逻辑放在client,client层自己感知数据的分布情况.这种方式优点是性能不会有太大的损耗,缺点在于提供服务的同时也需要提供对应的客户端库给用户,让使用者对这个分布式逻辑完全透明,另外对已有的不能修改协议的服务如redis/memcached等显然不适合这种方式了.已知的项目这样做的有tair,aerospike.
- proxy方式:将分布式逻辑放在代理层,接收到请求之后由这个代理层负责路由请求到对应的服务上去.优点是不用提供和修改客户端协议,缺点是经过多一层转发势必造成性能的损耗.
Codis使用的Proxy方式,根据他们提供的测试数据,比twemproxy的性能低20%左右.
来谈里面最核心的变更服务器时需要做的迁移数据和相应的一致性问题如何处理.
codis将所有的数据预分配为1024个slot,做一次典型的数据迁移,其最小单位就是一个slot,其流程大致是这样的(以下文字主要来自Codis作者huangdongxu的博客):
- 由dashboard通过ZK向所有proxy下发一个pre_migrate命令,如pre_migrate slot_1 to group 2.
- 当所有proxy都收到并且回复了pre_migrate命令时,标记slot_1的状态为 migrate,服务该slot的server group改为group2, 同时codis-config向group1的redis机器不断发送 SLOTSMGRT 命令, target参数是group2的机器, 直到group1中没有剩余的属于slot_1的key.
- 迁移过程中, 如果客户端请求 slot_1 的 key 数据, proxy 会将请求转发到group2上, proxy会先在group1上强行执行一次 MIGRATE key 将这个键值提前迁移过来. 然后再到group2上正常读取
- 迁移完成, 标记slot_1状态为online
这是一个典型的两阶段提交的流程,首先通知所有的proxy准备迁移了,然后再开始数据的迁移.需要注意的问题是,此时的选择了怎样的数据一致性模型以及如何保证这一点的?
Codis使用的是强一致性的数据模型,而不是类似Dynamo那样的当发现有不一致的数据都返回给使用者由使用者来解决冲突的方式.具体策略是:
- 在pre_migrate阶段,proxy如果收到这个slot的请求,会block住直到migrate状态了才开始处理.
- 在migrate阶段,如果新的处理该slot的proxy接收到了该slot的请求,首先会同步一份该slot的数据过来,再返回给客户端.
可以看到,无论哪个阶段,只要处理到了正在处理的slot,这样的处理都势必造成请求的处理变慢,整个服务的可用性降低,这也是没有办法的事情,具体看业务而定,是否需要更看重可用性还是一致性了.
总体来看,codis并不是有太大难度的项目,毕竟最难的两个部分ZK和Redis是使用已有的项目,但是由于解决了最开始的两大痛点,这个项目由此流行起来.尤其是加上了dashboard之后,方便了使用和管理,使得这个项目更像是一个完全闭环的产品,我觉得这个思路值得借鉴.