Codis 是一个分布式 Redis 解决方案, 对于上层的应用来说, 连接到 Codis Proxy 和连接原生的 Redis Server 没有显著区别 (不支持的命令列表), 上层应用可以像使用单机的 Redis 一样使用, Codis 底层会处理请求的转发, 不停机的数据迁移等工作, 所有后边的一切事情, 对于前面的客户端来说是透明的, 可以简单的认为后边连接的是一个内存无限大的 Redis 服务。
Codis 的架构图如下所示:
Codis Proxy 提供 Redis 集群的接入口,Dashboard 是 Codis的管理工具,下层提供 Redis 服务。也可以对 Codis Proxy 实现高可用,使得集群更加完善。
组件
1、Codis Proxy
客户端连接的 Redis 代理服务, 实现了 Redis 协议。除部分命令不支持以外,表现的和原生的 Redis 没有区别。
2、Codis Dashboard
集群管理工具,支持 codis-proxy、codis-server 的添加、删除,以及据迁移等操作。在集群状态发生改变时,codis-dashboard 维护集群下所有 codis-proxy 的状态的一致性。
3、Codis Server
基于 Redis 开发,增加了额外的数据结构,提供下层的 Redis 服务。
在大数据高并发场景下,单个 Redis 实例往往会显得捉襟见肘。首先体现在内存上,单个 Redis 的内存不宜过大,内存太大会导致 rdb 文件过大,进一步导致主从同步时全量同步时间过长,在实例重启恢复时也会消耗很长的数据加载时间,特别是在云环境下,单个实例内存往往都是受限的。其次体现在 CPU 的利用率上,单个 Redis 实例只能利用单个核心,这单个核心要完成海量数据的存取和管理工作压力会非常大。正是在这样的大数据高并发的需求之下,Redis 集群方案应运而生。它可以将众多小内存的 Redis 实例综合起来,将分布在多台机器上的众多 CPU 核心的计算能力聚集到一起,完成海量数据存储和高并发读写操作。Codis 是 Redis 集群方案之一,令我们感到骄傲的是,它是中国人开发并开源的,来自前豌豆荚中间件团队。绝大多数国内的开源项目都不怎么靠谱,但是 Codis 非常靠谱。有了Codis 技术积累之后,项目「突头人」刘奇又开发出来中国人自己的开源分布式数据库 ——TiDB,可以说 6 到飞起。从 Redis 的广泛流行到 RedisCluster 的广泛使用之间相隔了好多年,Codis 就是在这样的市场空缺的机遇下发展出来的。
大型公司有明确的 Redis 在线扩容需求,但是市面上没有特别好的中间件可以做到这一点。
其实codis原理和官方cluster基本一致,但是:
Codis 使用 Go 语言开发,它是一个代理中间件,它和 Redis 一样也使用 Redis 协议对外提供服务,当客户端向 Codis 发送指令时,Codis 负责将指令转发到后面的 Redis 实例
来执行,并将返回结果再转回给客户端。Codis 上挂接的所有 Redis 实例构成一个 Redis 集群,当集群空间不足时,可以通过动态增加 Redis 实例来实现扩容需求。客户端操纵 Codis 同操纵 Redis 几乎没有区别,还是可以使用相同的客户端 SDK,不需要任何变化。因为 Codis 是无状态的,它只是一个转发代理中间件,这意味着我们可以启动多个Codis 实例,供客户端使用,每个 Codis 节点都是对等的。因为单个 Codis 代理能支撑的QPS 比较有限,通过启动多个 Codis 代理可以显著增加整体的 QPS 需求,还能起到容灾功能,挂掉一个 Codis 代理没关系,还有很多 Codis 代理可以继续服务。
Codis 要负责将特定的 key 转发到特定的 Redis 实例,那么这种对应关系 Codis 是如
何管理的呢?Codis 将所有的 key 默认划分为 1024 个槽位(slot),它首先对客户端传过来的 key 进行 crc32 运算计算哈希值,再将 hash 后的整数值对 1024 这个整数进行取模得到一个余数,这个余数就是对应 key 的槽位。
每个槽位都会唯一映射到后面的多个 Redis 实例之一,Codis 会在内存维护槽位和Redis 实例的映射关系。这样有了上面 key 对应的槽位,那么它应该转发到哪个 Redis 实例
hash = crc32(command.key)
slot_index = hash % 1024
redis = slots[slot_index].redis
redis.do(command)
槽位数量默认是 1024,它是可以配置的,如果集群节点比较多,建议将这个数值配置大
一些,比如 2048、4096。
如果 Codis 的槽位映射关系只存储在内存里,那么不同的 Codis 实例之间的槽位关系就无法得到同步。所以 Codis 还需要一个分布式配置存储数据库专门用来持久化槽位关系。
Codis 开始使用 ZooKeeper,后来连 etcd 也一块支持了。
Codis 将槽位关系存储在 zk 中,并且提供了一个 Dashboard 可以用来观察和修改槽位关系,当槽位关系变化时,Codis Proxy 会监听到变化并重新同步槽位关系,从而实现多个
Codis Proxy 之间共享相同的槽位关系配置。
zookeeper依赖jdk语言包,描述:用于存放数据配置路由表。zookeeper简称zk。在生产环境中,zk部署越多,其可靠性越高。由于zk集群是以宕机个数过半才会让整个集群宕机,因此,奇数个zk更佳。
逻辑架构如下:
访问方式可以是类似keepalived集群的vip方式,或者是通过java代码调用jodis控件再连接上zookeeper集群,然后查找到可用的proxy端,进而连接调用不同的codis-proxy地址来实现高可用的LVS和HA功能。
中间层由codis-proxy和zookeeper处理数据走向和分配,通过crc32算法,把key平均分配在不同redis的某一个slot中。实现类似raid0的条带化,在旧版本的codis中,slot需要手工分配,在codis3.2之后,只要点一个按钮slot会自动分配,相当方便,但是也可以手动分配,需要另外调用codis-admin命令。
最后codis-proxy把数据存进真实的redis-server主服务器上,由于codis的作者黄东旭相当注重数据一致性,不允许有数据延时造成的数据不一致,所以架构从一开始就没考虑主从读写分离。从服务器仅仅是作为故障切换的冗余架构,由codis-dashboard监控各服务的状态,然后通过改写zookeeper数据和调用redis-sentinel实现故障切换功能。
刚开始 Codis 后端只有一个 Redis 实例,1024 个槽位全部指向同一个 Redis。然后一个 Redis 实例内存不够了,所以又加了一个 Redis 实例。这时候需要对槽位关系进行调整,
将一半的槽位划分到新的节点。这意味着需要对这一半的槽位对应的所有 key 进行迁移,迁移到新的 Redis 实例。
那 Codis 如果找到槽位对应的所有 key 呢?
Codis 对 Redis 进行了改造,增加了 SLOTSSCAN 指令,可以遍历指定 slot 下所有的key。Codis 通过 SLOTSSCAN 扫描出待迁移槽位的所有的 key,然后挨个迁移每个 key 到新的 Redis 节点。在迁移过程中,Codis 还是会接收到新的请求打在当前正在迁移的槽位上,因为当前槽位的数据同时存在于新旧两个槽位中,Codis 如何判断该将请求转发到后面的哪个具体实例呢?Codis 无法判定迁移过程中的 key 究竟在哪个实例中,所以它采用了另一种完全不同的思路。当 Codis 接收到位于正在迁移槽位中的 key 后,会立即强制对当前的单个 key 进行迁移,迁移完成后,再将请求转发到新的 Redis 实例。
Redis 新增实例,手工均衡 slots 太繁琐,所以 Codis 提供了自动均衡功能。自动均衡会在系统比较空闲的时候观察每个 Redis 实例对应的 Slots 数量,如果不平衡,就会自动进行迁移。
Codis 给 Redis 带来了扩容的同时,也损失了其它一些特性。因为 Codis 中所有的 key 分散在不同的 Redis 实例中,所以事务就不能再支持了,事务只能在单个 Redis 实例中完成。同样 rename 操作也很危险,它的参数是两个 key,如果这两个 key 在不同的 Redis 实例中,rename 操作是无法正确完成的。Codis 的官方文档中给出了一系列不支持的命令列表。同样为了支持扩容,单个 key 对应的 value 不宜过大,因为集群的迁移的最小单位是key,对于一个 hash 结构,它会一次性使用 hgetall 拉取所有的内容,然后使用 hmset 放置到另一个节点。如果 hash 内部的 kv 太多,可能会带来迁移卡顿。官方建议单个集合结构的总字节容量不要超过 1M。如果我们要放置社交关系数据,例如粉丝列表这种,就需要注意了,可以考虑分桶存储,在业务上作折中。Codis 因为增加了 Proxy 作为中转层,所有在网络开销上要比单个 Redis 大,毕竟数据包多走了一个网络节点,整体在性能上要比单个 Redis 的性能有所下降。但是这部分性能损耗不是太明显,可以通过增加 Proxy 的数量来弥补性能上的不足。Codis 的集群配置中心使用 zk 来实现,意味着在部署上增加了 zk 运维的代价,不过大部分互联网企业内部都有 zk 集群,可以使用现有的 zk 集群使用即可。
Codis 在设计上相比 Redis Cluster 官方集群方案要简单很多,因为它将分布式的问题交给了第三方 zk/etcd 去负责,自己就省去了复杂的分布式一致性代码的编写维护工作。而Redis Cluster 的内部实现非常复杂,它为了实现去中心化,混合使用了复杂的 Raft 和Gossip 协议,还有大量的需要调优的配置参数,当集群出现故障时,维护人员往往不知道从何处着手。
mget 指令用于批量获取多个 key 的值,这些 key 可能会分布在多个 Redis 实例中。Codis 的策略是将 key 按照所分配的实例打散分组,然后依次对每个实例调用 mget 方法,最后将结果汇总为一个,再返回给客户端。架构变迁Codis 作为非官方 Redis 集群方案,近几年来它的结构一直在不断变化,一方面当官方的 Redis 有变化的时候它要实时去跟进,另一方面它作为 Redis Cluster 的竞争方案之一,它还得持续提高自己的竞争力,给自己增加更多的官方集群所没有的便捷功能。比如 Codis 有个特色的地方在于强大的 Dashboard 功能,能够便捷地对 Redis 集群进行管理。这是 Redis 官方所欠缺的。另外 Codis 还开发了一个 Codis-fe(federation 联邦) 工具,可以同时对多个 Codis 集群进行管理。在大型企业,Codis 集群往往会有几十个,有这样一个便捷的联邦工具可以降低不少运维成本。
主机名 | ip | 服务 |
---|---|---|
server1 | 172.25.1.1 | zk,codis-dashboard,codis-proxy,codis-fe,codis-server |
server2 | 172.25.1.2 | codis-server |
server3 | 172.25.1.3 | codis-server |
真机 | 172.25.1.250 | web测试 |
在server1上:
步骤一:在网上下载go安装包,并解压到指定目录
go安装包下载地址
https://dl.google.com/go/go1.9.1.linux-amd64.tar.gz
tar zxf go1.9.1.linux-amd64.tar.gz -C /usr/local/
vim /etc/profile
export GOROOT=/usr/local/go
export GOPATH=/usr/local/codis
export PATH=$PATH:/usr/local/go/bin
source /etc/profile
go version #查看go版本
mkdir -p $GOPATH/src/github.com/CodisLabs
yum install unzip -y
unzip codis-3.2.2.zip -d $GOPATH/src/github.com/CodisLabs #此处加载环境变量中的设置
cd $GOPATH/src/github.com/CodisLabs
mv codis-3.2.2/ codis
步骤五:对codi进行源码编译前安装依赖项(go也是依赖项,已经安装好)
yum install gcc git autoconf -y
cd /usr/local/codis/src/github.com/CodisLabs/codis
make MALLOC=libc
codis源码编译完成之后:
组件的启动脚本在 G O P A T H / s r c / g i t h u b . c o m / C o d i s L a b s / c o d i s / a d m i n 目 录 下 配 置 文 件 在 GOPATH/src/github.com/CodisLabs/codis/admin目录下 配置文件在 GOPATH/src/github.com/CodisLabs/codis/admin目录下配置文件在GOPATH/src/github.com/CodisLabs/codis/config目录下
步骤七:安装JDK
yum install jdk-8u121-linux-x64.rpm -y
java -version #查看java版本
步骤八:安装zookeeper
zookeeper安装包下载地址
tar zxf zookeeper-3.4.9.tar.gz -C /usr/local/
cd /usr/local
mv zookeeper-3.4.9/ zookeeper
vim /etc/profile #编辑
79 export PATH=$PATH:/usr/local/go/bin:$ZOOKEEPER_HOME/bin
80 export ZOOKEEPER_HOME=/usr/local/zookeeper
source /etc/profile #重新加载环境变量
cd /usr/local/zookeeper/conf/
cp zoo_sample.cfg zoo.cfg
vim zoo.cfg
tickTime=2000 #一个周期(tick)的时长(单位:毫秒)
initLimit=10 #初始化同步阶段最多耗费tick个数
syncLimit=5 #等待应答的最大间隔tick个数
dataDir=/data/zookeeper/data #该目录需要手动建立。
clientPort=2181 #客户端连接server的端口,即对外服务端口,一般设置为 2181
maxClientCnxns=60 #最大连接数设置(单ip限制). 注:默认60,设成0即无限制
autopurge.purgeInterval=24 #ZK提供了自动清理事务日志和快照文件的功能,这个参数指定了清理频率,单位是小时
autopurge.snapRetainCount=500 #这个参数和上面的参数搭配使用,这个参数指定了需要保留的文件数目。默认是保留3个
注意:因为我这里只部署单个codis-proxy节点的环境,如果是多(比如3个)个,需要在zookeeper的配置文件里面写入:
server.1=codis-1:2888:3888
server.2=codis-2:2888:3888
server.3=codis-3:2888:3888
其中2888表示zookeeper程序监听端口,3888表示zookeeper选举通信端口
说明:
server.A=B:C:D
A 是一个数字,表示这个是第几号服务器;
B 是这个服务器的 ip 地址;
C 表示的是这个服务器与集群中的 Leader 服务器交换信息的端口;
D 表示的是万一集群中的 Leader 服务器挂 了,需要一个端口来重新进行选举,选出一个新的Leader,而这个端口就是用来执行选举时服务器相互通信的 端口。
设置myid准则:
设置myid在我们配置的dataDir指定的目录下面,创建一个myid文件,里面内容为一个数字,用来标识当前主机,conf/zoo.cfg文件配置的srver.X中的X为什么数字,则myid文件就输入这个数字,我只有一台zk,所以配置文件里可以不配置server.X,但还是要配置myid的,echo一个数字1进去即可。如果有多台zk,则分别在zk服务器上echo对应的数字进对应的myid文件。
mkdir /data/zookeeper/data -p
echo "1" > /data/zookeeper/data/myid
cd /usr/local/zookeeper/bin
./zkServer.sh start
./zkServer.sh status
步骤十二:对codis进行配置,并启动codis的各个组件。编辑/usr/local/codis/config.ini文件并启动codis-dashboard组件
vim /usr/local/codis/config.ini
zk=localhost:2181
product=test
proxy_id=proxy_1
net_timeout=5
dashboard_addr=localhost:18087
coordinator=zookeeper
参数解释:
zk=localhost:2181 //zookeeper的地址, 如果是zookeeper集群,可以这么写: zk=hostname1:2181,hostname2:2181,hostname3:2181,hostname4:2181,hostname5:2181,如果是etcd,则写成http://hostname1:port,http://hostname2:port,http://hostname3:port
product=test //产品名称, 这个codis集群的名字, 可以认为是命名空间, 不同命名空间的codis没有交集
proxy_id=proxy_1 //proxy会读取, 用于标记proxy的名字, 针对多个proxy的情况, 可以使用不同的config.ini, 只需要更改 proxy_id 即可
net_timeout=5 //检测状态时间间隔
dashboard_addr=localhost:18087 //dashboard 服务的地址,CLI 的所有命令都依赖于 dashboard 的 RESTful API,所以必须启动
coordinator=zookeeper //如果用etcd,则将zookeeper替换为etcd
步骤十三:启动codis组件
cd $GOPATH/src/github.com/CodisLabs/codis/admin
./codis-dashboard-admin.sh start
./codis-proxy-admin.sh start
./codis-fe-admin.sh start
./codis-server-admin.sh start
netstat -antuple
vim /etc/profile
export GOROOT=/usr/local/go
export GOPATH=/usr/local/codis
export PATH=$PATH:/usr/local/go/bin
source /etc/profile #加载环境变量
步骤二:解压go安装包到指定目录,并创建codis配置文件与启动脚本所存放的目录
tar zxf go1.9.1.linux-amd64.tar.gz -C /usr/local/
mkdir -p $GOPATH/src/github.com/CodisLabs
yum install unzip -y
yum install gcc git autoconf -y
unzip codis-3.2.2.zip -d $GOPATH/src/github.com/CodisLabs
cd $GOPATH/src/github.com/CodisLabs
mv codis-3.2.2/ codis
cd codis/
make MALLOC=libc
cd /usr/local/codis/src/github.com/CodisLabs/codis/config/
vim redis.conf
61 bind 0.0.0.0
cd /usr/local/codis/src/github.com/CodisLabs/codis/admin
./codis-server-admin.sh start
netstat -antlp #6379端口
vim /etc/profile
export GOROOT=/usr/local/go
export GOPATH=/usr/local/codis
export PATH=$PATH:/usr/local/go/bin
source /etc/profile #加载环境变量
tar zxf go1.9.1.linux-amd64.tar.gz -C /usr/local/
mkdir -p $GOPATH/src/github.com/CodisLabs
yum install unzip -y
yum install gcc git autoconf -y
unzip codis-3.2.2.zip -d $GOPATH/src/github.com/CodisLabs
cd $GOPATH/src/github.com/CodisLabs
mv codis-3.2.2/ codis
cd codis/
make MALLOC=libc
cd /usr/local/codis/src/github.com/CodisLabs/codis/config/
vim redis.conf
61 bind 0.0.0.0
cd /usr/local/codis/src/github.com/CodisLabs/codis/admin
./codis-server-admin.sh start
netstat -antuple #6379端口
在客户端(真机)上:
点击codis-demo,查看codis信息
添加组信息,将server1加入组:
将server2添加到组:
将server3添加到组: