codis介绍

分片介绍

介绍codis前,先介绍下分片知识

分片(partitioning)就是将你的数据拆分到多个 Redis 实例的过程,这样每个实例将只包含所有键的子集。

分片能做什么

Redis 的分片承担着两个主要目标:

  • 允许使用很多电脑的内存总和来支持更大的数据库。没有分片,你就被局限于单机能支持的内存容量。
  • 允许伸缩计算能力到多核或多服务器,伸缩网络带宽到多服务器或多网络适配器。

分片基础

有很多不同的分片标准(criteria)。假想我们有 4 个 Redis 实例 R0,R1,R2,R3,还有很多表示用户的键,像 user:1,user:2,… 等等,我们能找到不同的方式来选择一个指定的键存储在哪个实例中。换句话说,有许多不同的办法来映射一个键到一个指定的 Redis 服务器。

范围分片

最简单的执行分片的方式之一是范围分片(range partitioning),通过映射对象的范围到指定的 Redis 实例来完成分片。例如,我可以假设用户从 ID 0 到 ID 10000 进入实例 R0,用户从 ID 10001 到 ID 20000 进入实例 R1,等等。

这套办法行得通,并且事实上在实践中被采用,然而,这有一个缺点,就是需要一个映射范围到实例的表格。这张表需要管理,不同类型的对象都需要一个表,所以范围分片在 Redis 中常常并不可取,因为这要比替他分片可选方案低效得多。

哈希分片(hash partitioning)

一种范围分片的替代方案是哈希分片(hash partitioning)。这种模式适用于任何键,不需要键像 object_name: 这样的饿形式,就像这样简单:

  • 使用一个哈希函数(例如,crc32 哈希函数) 将键名转换为一个数字。例如,如果键是 foobar,crc32(foobar)将会输出类似于 93024922 的东西。
  • 对这个数据进行取模运算,以将其转换为一个 0 到 3 之间的数字,这样这个数字就可以映射到我的 4 台 Redis 实例之一。93024922 模 4 等于 2,所以我知道我的键 foobar 应当存储到 R2 实例。注意:取模操作返回除法操作的余数,在许多编程语言总实现为%操作符。

一致性哈希(consistent hashing)

有许多其他的方式可以分片,从这两个例子中你就可以知道了。一种哈希分片的高级形式称为一致性哈希(consistent hashing),被一些 Redis 客户端和代理实现。

 

分片的不同实现

分片可由软件栈中的不同部分来承担。

  • 客户端分片(Client side partitioning)意味着,客户端直接选择正确的节点来写入和读取指定键。许多 Redis 客户端实现了客户端分片。
  • 代理协助分片(Proxy assisted partitioning)意味着,我们的客户端发送请求到一个可以理解 Redis 协议的代理上,而不是直接发送请求到 Redis 实例上。代理会根据配置好的分片模式,来保证转发我们的请求到正确的 Redis 实例,并返回响应给客户端。Redis 和 Memcached 的代理 Twemproxy 实现了代理协助的分片。
  • 查询路由(Query routing)意味着,你可以发送你的查询到一个随机实例,这个实例会保证转发你的查询到正确的节点。Redis 集群在客户端的帮助下,实现了查询路由的一种混合形式 (请求不是直接从 Redis 实例转发到另一个,而是客户端收到重定向到正确的节点)。

分片的缺点

Redis 的一些特性与分片在一起时玩转的不是很好:

  • 涉及多个键的操作通常不支持。例如,你不能对映射在两个不同 Redis 实例上的键执行交集(事实上有办法做到,但不是直接这么干)。
  • 涉及多个键的事务不能使用。
  • 分片的粒度(granularity)是键,所以不能使用一个很大的键来分片数据集,例如一个很大的有序集合。
  • 当使用了分片,数据处理变得更复杂,例如,你需要处理多个 RDB/AOF 文件,备份数据时你需要聚合多个实例和主机的持久化文件。
  • 添加和删除容量也很复杂。例如,Redis 集群具有运行时动态添加和删除节点的能力来支持透明地再均衡数据,但是其他方式,像客户端分片和代理都不支持这个特性。但是,有一种称为预分片(Presharding)的技术在这一点上能帮上忙。

数据存储还是缓存

尽管无论是将 Redis 作为数据存储还是缓存,Redis 的分片概念上都是一样的,但是作为数据存储时有一个重要的局限。当 Redis 作为数据存储时,一个给定的键总是映射到相同的 Redis 实例。当 Redis 作为缓存时,如果一个节点不可用而使用另一个节点,这并不是一个什么大问题,按照我们的愿望来改变键和实例的映射来改进系统的可用性(就是系统回复我们查询的能力)。

一致性哈希实现常常能够在指定键的首选节点不可用时切换到其他节点。类似的,如果你添加一个新节点,部分数据就会开始被存储到这个新节点上。

这里的主要概念如下:

  • 如果 Redis 用作缓存,使用一致性哈希来来实现伸缩扩展(scaling up and down)是很容易的。
  • 如果 Redis 用作存储,使用固定的键到节点的映射,所以节点的数量必须固定不能改变。否则,当增删节点时,就需要一个支持再平衡节点间键的系统,当前只有 Redis 集群可以做到这一点,但是 Redis 集群现在还处在 beta 阶段,尚未考虑再生产环境中使用。

预分片

我们已经知道分片存在的一个问题,除非我们使用 Redis 作为缓存,增加和删除节点是一件很棘手的事情,使用固定的键和实例映射要简单得多。

然而,数据存储的需求可能一直在变化。今天我可以接受 10 个 Redis 节点(实例),但是明天我可能就需要 50 个节点。

因为 Redis 只有相当少的内存占用(footprint)而且轻量级(一个空闲的实例只是用 1MB 内存),一个简单的解决办法是一开始就开启很多的实例。即使你一开始只有一台服务器,你也可以在第一天就决定生活在分布式的世界里,使用分片来运行多个 Redis 实例在一台服务器上。

你一开始就可以选择很多数量的实例。例如,32 或者 64 个实例能满足大多数的用户,并且为未来的增长提供足够的空间。

这样,当你的数据存储需要增长,你需要更多的 Redis 服务器,你要做的就是简单地将实例从一台服务器移动到另外一台。当你新添加了第一台服务器,你就需要把一半的 Redis 实例从第一台服务器搬到第二台,如此等等。

使用 Redis 复制,你就可以在很小或者根本不需要停机时间内完成移动数据:

  • 在你的新服务器上启动一个空实例。
  • 移动数据,配置新实例为源实例的从服务。
  • 停止你的客户端。
  • 更新被移动实例的服务器 IP 地址配置。
  • 向新服务器上的从节点发送 SLAVEOF NO ONE 命令。
  • 以新的更新配置启动你的客户端。
  • 最后关闭掉旧服务器上不再使用的实例。

Redis 分片的实现

Redis 集群是自动分片和高可用的首选方式。当前还不能完全用于生产环境,但是已经进入了 beta 阶段。

一旦 Redis 集群可用,以及支持 Redis 集群的客户端可用,Redis 集群将会成为 Redis 分片的事实标准。

Redis 集群是查询路由和客户端分片的混合模式。

Twemproxy 是 Twitter 开发的一个支持 Memcached ASCII 和 Redis 协议的代理。它是单线程的,由 C 语言编写,运行非常的快。他是基于 Apache 2.0 许可的开源项目。

Twemproxy 支持自动在多个 Redis 实例间分片,如果节点不可用时,还有可选的节点排除支持(这会改变键和实例的映射,所以你应该只在将 Redis 作为缓存是才使用这个特性)。

这并不是单点故障(single point of failure),因为你可以启动多个代理,并且让你的客户端连接到第一个接受连接的代理。

Twemproxy 之外的可选方案,是使用实现了客户端分片的客户端,通过一致性哈希或者别的类似算法。有多个支持一致性哈希的 Redis 客户端,例如 Redis-rb 和 Predis。

 

Codis设计

既然重新设计,那么Codis首先必须满足自动扩容和缩容的需求,其次则是必须避免单点故障和单点带宽不足,做一个高可用的系统。在这之后,基于原有的遗留系统,还必须可以轻松地将数据从Twemproxy迁移到Codis,并实现良好的运维和监控。基于这些,Codis的设计跃然纸面:

然而,一个新系统的开发并不是件容易的事情,特别是一个复杂的分布式系统。刘奇表示,虽然当时团队只有3个人,但是他们几乎考量了可以考量的各种细节:

  • 尽量拆分,简化每个模块,同时易于升级
  • 每个组件只负责自己的事情
  • Redis只作为存储引擎
  • Proxy的状态
  • Redis故障判定是否放到外部,因为分布式系统存活的判定异常复杂
  • 提供API让外部调用,当Redis Master丢失时,提升Slave为Master
  • 图形化监控一切:slot状态、Proxy状态、group状态、lock、action等等

而在考量了一切事情后,另一个争论摆在了眼前——Proxy或者是Smart Client:Proxy拥有更好的监控和控制,同时其后端信息亦不易暴露,易于升级;而Smart Client拥有更好的性能,及更低的延时,但是升级起来却比较麻烦。对比种种优劣,他们最终选择了Proxy,无独有偶,在codis开源后,twitter的一个分享提到他们也是基于proxy的设计。

Codis主要包含Codis Proxy(codis-proxy)、Codis Manager(codis-config)、Codis Redis(codis-server)和ZooKeeper四大组件,每个部分都可动态扩容。

codis-proxy 客户端连接的Redis代理服务,本身实现了Redis协议,表现很像原生的Redis (就像 Twemproxy)。一个业务可以部署多个 codis-proxy,其本身是无状态的。

codis-config。Codis 的管理工具,支持添加/删除Redis节点、添加/删除Proxy节点、发起数据迁移等操作。codis-config自带了一个http server,会启动一个dashboard,用户可以在浏览器上观察 Codis 集群的运行状态。

codis-reidis。Codis 项目维护的一个Redis分支,加入了slot的支持和原子的数据迁移指令。

ZooKeeper。Codis依赖ZooKeeper来存放数据路由表和codis-proxy节点的元信息,codis-config发起的命令会通过 ZooKeeper同步到各个存活的codis-proxy。

 

Q&A

Why proxy?

 

Codis 的架构采用了 Proxy-based 的设计,没有走官方 Cluster 的路,官方的 Cluster 实现是 P2P 的模型,依靠 Gossip 协议进行消息同步和将数据分若干个 Slot 作为管理的单位,客户端需要更改。这个模型的好处是:

  • 真正的无中心节点
  • 对于客户端来说请求的性能不会损失太多

但是缺点同样明显:

  • 状态很难明确,你很难清楚的知道集群现在处于什么状态
  • 对与redis来说,集群的升级困难,运维困难,因为它将分布式的逻辑和存储引擎绑定在了一起。
  • 需要依赖 smart client

这两个缺点几乎在任何 p2p 模型的分布式系统中都存在,由于第一个问题,导致开发和调试的过程也异常艰辛(官方的 cluster 几乎写了 3 年才比较稳定)

而 Proxy-based 的方式的好处就比较明显了:

  • 开发成本低
  • 业务的切换成本低
  • Proxy 的逻辑和存储的逻辑是隔离的

所以,在 Codis 之前,Twemproxy 是这个方案的最优的选择,应用也非常广泛,许多大型的互联网公司都在使用它,但是 Twemproxy 也有它的问题, 最大的问题是:Twemproxy 真的就只是一个 Proxy 而已, 集群的功能完全没有。而且看上去 Twitter 也不打算继续维护它了。
Twitter 最近的一个 Talk,提到了Twitter 内部对于 Scaling Redis 的一些做法和想法,其中对于 Proxy 的方案是比较推崇的(同时也提到了他们内部也不再使用 Twemproxy 了….),里面理由写得比较清楚了,有兴趣的可以去看看。同样的, Facebook 之前那篇关于扩展 memcached 的论文也提到了类似的方案 (mcrouter)。其中我觉得这个方案背后最重要的思想就是:存储和分布式逻辑分离。至于因为转发请求而损失的性能,可以通过其他的方式补回来, 比如水平扩展 Proxy。带来的好处是整个系统的状态非常清晰,几乎所有组件都可以独立的部署和升级,程序还比较好写,所以 Codis 从一开始就坚定的走 Proxy 这条路。

但是相对于 Twemproxy, Codis 又有一些改进,首先集成了集群的功能,使用 Presharding 对分散数据的存储。所有的集群状态信息依赖 ZooKeeper 进行同步, 所有的 Proxy 是无状态的。这样就可以实现多 Proxy 的水平扩展。
另外一个比较重要的决定是使用 Go 作为主要开发语言,抛掉信仰问题不谈 (我和 @goroutine 都是 go 的脑残粉), go 几乎就是针对这种后端的网络程序而发明出来的语言,这给开发工作带来了极大的效率提升,从写下第一行代码,到第一个可用版本,几乎才用了不到一个月的时间。

 

数据如何迁移?

在 Codis 的设计中, Proxy 被设计成无状态的,客户端连接任何一个 Proxy 都是一样的。而且每个 Proxy 启动的时候,会在 Zookeeper 上注册一个临时节点, 所以客户端甚至可以根据这个特性实现 HA 

当然,这个设计带来的好处是,请求可以被负载均衡,而且在整个系统中不会出现单点。 但是,问题来了,由于 Codis 是动态扩缩容的功能的, 当 Codis 在进行数据迁移的过程中,如何保证任意一个 Proxy 都不会读到老的或者错误的数据?

解释这个问题之前,我想先介绍一下 Codis 的数据存储方式和关于数据迁移的一些前置知识:

  • 数据被根据key,分布在 1024 个 slot 中, slot 是一个虚拟的概念,数据存储在实际的多个 codis-server (codis 修改版的 redis-server) 中,每个 codis-server 负责一部分key-value数据, 哈希算法是 crc32(key) % 1024
  • 数据迁移是由 codis-config 发起的,在 codis-config 看来,数据迁移的最小单位是 slot
  • 对于 codis-server 来说,没有任何分布式逻辑在其中, 只是实现了几个关于数据传输的指令: slotsmgrtone, slotsmgrt…. 其主要的作用是:随机选取特定 slot 中的一个 key-value pair, 传输给另外一个 codis-server, 传输成功后,把本地的这个 key-value pair 删除, 注意, 这个整个操作是原子的。

所以,这就决定了 Codis 并不太适合 key 少,但是 value 特别大的应用, 而且你的 key 越少, value 越大,最后就会退化成单个 redis 的模型 (性能还不如 raw redis),所以 Codis 更适合海量 Key, value比较小 (<= 1 MB) 的应用。

为什么 codis-server 的数据迁移是一个个key的,而不是类似很多其他分布式系统,采用 replication 的方式? 我认为,对于 redis 这种系统来说,实现 replication 不太经济, 首先,你需要 rdb dump 吧? 在 redis 里面所有的操作严格来说都是串行的(单线程模型决定),所以dump数据是需要 fork 一个新进程的, 否则如果直接 SAVE 会阻塞唯一的主线程,同时还得考虑dump过程和传输过程中产生的新数据的同步的问题, 实现起来比较复杂。所以我们每次只原子的迁走一个 key,不会把主线程 block 住, redis 操作的是内存,批量的一次性写入和分多次set几乎没有区别(对于单机而言), 而且这个模型还避免了迁移过程中的数据更新同步的问题,因为由于迁移一个 key 的操作是原子的, 对于这个 redis-server 来说, 在完成这次迁移指令之前,是不会响应其他请求的。 所以保证了数据的安全。

一次典型的迁移流程:

  1. codis-config 发起迁移指令如 pre_migrate slot_1 to group 2

  2. codis-config 等待所有的 proxy 回复收到迁移指令, 如果某台 proxy 没有响应, 则标记其下线 (由于proxy启动时会在zk上注册一个临时节点, 如果这个proxy挂了, 正常来说, 这个临时节点也会删除, 在codis-config发现无响应后, codis-config会等待30s, 等待其下线, 如果还没下线或者仍然没有响应, 则codis-config 将不会释放锁, 通知管理员出问题了) 相当于一个2阶段提交

  3. codis-config 标记slot_1的状态为 migrate, 服务该slot的server group改为group2, 同时codis-config向group1的redis机器不断发送 SLOTSMGRT 命令, target参数是group2的机器, 直到group1中没有剩余的属于slot_1的key

  4. 迁移过程中, 如果客户端请求 slot_1 的 key 数据, proxy 会将请求转发到group2上, proxy会先在group1上强行执行一次 MIGRATE key 将这个键值提前迁移过来. 然后再到group2上正常读取

  5. 迁移完成, 标记slot_1状态为online

关键点:

  1. 所有的操作命令,都通过 Zookeeper 中转,所有的路由表,都放置在 ZooKeeper 中,确保任意一个 proxy 的视图都是一样的。

  2. codis-config 在实际修改slot状态之前,会确保所有的 proxy 收到这个迁移请求。

  3. 在客户端读取正在迁移的slot内的数据之前, 会强制在源redis是执行一下迁移这个key的操作。

这两点保证了,proxy 在读取数据的时候,总是能在迁移的目标机上命中这个 key。

这就是 Codis 如何进行安全的数据迁移的过程。

 

性能如何?

Codis 采用了 Proxy 的方案,所以必然会带来单机性能的损失,经测试,在不开 pipeline 的情况下,大概会损失 40% 左右的性能,但是 Redis 本身是一个快得吓人的东西,即使单机损失了 40% 仍然是一个很大的数字。
另外一个比较好的地方是,Codis 本身是可以充分的利用多核的(Thanks to golang),在多线程客户端的环境下,不会像 Twemproxy 那样,撑死就跑满一个 CPU(当然你可以部署 Twemproxy 的多实例,但不就又增加运维成本了嘛。。。)。而且不要忘了,
Codis 可以通过平行扩展多个 Proxy 实现性能成倍的提升,一台机器 CPU 满了?没事,再另一台机器起一个 proxy 就是了。某个 Redis 跑满了?没事,把一部分数据迁移到另外一个 Redis 实例好了。所以,我们从来不认为单机性能能说明什么。 使用 Codis 带来的最大的好处,是为你的缓存提供了弹性扩缩容的能力, 而不是追求榨干底下 Redis 的性能。。。这也是我们选择了 go 而不是 c 来开发的原因。

我们进行了一个详细的性能测试,测试的结果如下:Benchmark , 2 proxy 的情况下,单机可以到达 20w 的 QPS.

 

 

HA 介绍

对于Redis HA, 我个人是蛮纠结的,仔细用过的 Codis 的同学会发现,在某个 server group 的 master 死掉的时候,虽然 server group 里可以有多个 slave,但是这些 slave 并不会自动的提升为 master。
当然实现这个功能并不困难,但是我认为这种情况应该让管理员清楚,并手动的操作,因为如果自动的切到 slave 上,这段时间原 master 还没同步到 slave 上的数据有可能就会冲突,如果 master 又复活了,解决数据的冲突是一个麻烦的问题,与其自动的操作,不如给客户端返回失败 (也只是这个机器负责的slots 会失败,如果实例足够多,不会出现致命的单点故障), 交给管理员去处理。

Proxy HA, 在决定使用无状态的 proxy 的方案时,就自动带来了高可用性的保证,这个不多说,有很多种做法,比如智能DNS,HAProxy,客户端连接 ZooKeeper 做 Proxy 的连接池等。

 

如何运维?

说到可运维性,Codis 几乎一切的操作都是通过 codis-config 发起的,codis-config 在做任何操作的时候,都会到 ZooKeeper 拿一个锁,以保证是唯一的操作实例,这也是防止路由表被改坏的一个措施,尤其是设置迁移这样比较敏感的操作,必须保证不能同时有多个 slots 处于迁移状态,所以,在整个迁移的过程中,是不释放锁的。

那么如果在迁移一个 slot 的过程中,我强行杀死 (kill -9) codis-config 不让它释放迁移的锁会怎么样?会死锁吗?

答案是:不会的。

为什么?首先,在迁移过程中的任意阶段打断,都是没有问题的,因为对底层的 redis 来说,迁移的只是一个个原子的 key,我杀掉了 codis-config ,只是停止了发指令的人,导致这个 slot 没有全部迁移干净而已, 在 Zk 上看到的也只是一个长期处于迁移状态的 slot (由于 codis-config 被杀了,没人去发迁移命令,也不会在迁移完成的时候修改slot状态了). 此时如果客户端有请求,proxy 也会主动的发一个 migratekey 先强行的把这个key迁移过来,所以对客户端也是没有影响的。

而且 codis-config 还有一个特性,每次启动的时候,都会在 Zk 上注册一个临时节点, 记录自己的 pid 和机器名,而且上的所有的锁,都会带上机器名和pid的签名,每次启动的时候,会扫描一下所有未释放的锁,如果这个锁的所属进程的临时节点已经不存在了,就会直接把这个锁释放掉,于是避免了死锁的状态。

只不过在下次开始新的迁移任务之前,需要先将这个未迁移完成的slot迁移掉,方法是发起一个迁移任务,from slot 和 to slot 都写成这个slot id, new group id设成之前未完成的那个任务的group id, 这个是为了保证系统能重新回到一个干净的状态,再进行下一个新的迁移任务。这是一个人为加的限制。

在实际使用 Codis 的过程中,我们为一些特别懒的业务 (懒得重建缓存),开发了叫做redis-port的工具,它的作用是,作为一个假的 slave,挂在一个redis后面,然后将master的数据同步回来,sync 到 codis 集群上,所以,业务方根本无需重建缓存,直接同步完后,换个地址重启服务就OK了, 这也是问哦们在公司内部推动项目特别轻松的一个杀手锏。 :P

除此之外,Codis 还有一个和其他后端中间件不太一样的地方:它不仅提供了完整的 Unix CLI interface, 居然有一个炫酷的 dashboard 和完整的 RESTful API ! 恩,没错,而且在实际的生产环境中,我们发现使用 dashboard 更安全,更少误操作,更方便,而且系统的实时状态都非常清晰。

 

为什么不支持读写分离

很多朋友问我,为什么不支持读写分离,其实这个事情的原因很简单,因为我们当时的业务场景不能容忍数据不一致,由于Redis本身的replication模型是主从异步复制,在master上写成功后,在slave上是否能读到这个数据是没有保证的,而让业务方处理一致性的问题还是蛮麻烦的。而且Redis单点的性能还是蛮高的,不像mysql之类的真正的数据库,没有必要为了提升一点点读QPS而让业务方困惑。这和数据库的角色不太一样。所以,你可能看出来了,其实Codis的HA,并不能保证数据完全不丢失,因为是异步复制,所以master挂掉后,如果有没有同步到slave上的数据,此时将slave提升成master后,刚刚写入的还没来得及同步的数据就会丢失。不过在RebornDB中我们会尝试对持久化存储引擎(qdb)可能会支持同步复制(syncreplication),让一些对数据一致性和安全性有更强要求的服务可以使用。

 

一致性问题

说到一致性,这也是Codis支持的MGET/MSET无法保证原本单点时的原子语义的原因。 因为MSET所参与的key可能分不在不同的机器上,如果需要保证原来的语义,也就是要么一起成功,要么一起失败,这样就是一个分布式事务的问题,对于Redis来说,并没有WAL或者回滚这么一说,所以即使是一个最简单的二阶段提交的策略都很难实现,而且即使实现了,性能也没有保证。所以在Codis中使用MSET/MGET其实和你本地开个多线程SET/GET效果一样,只不过是由服务端打包返回罢了,我们加上这个命令的支持只是为了更好的支持以前用Twemproxy的业务。

  在实际场景中,很多朋友使用了lua脚本以扩展Redis的功能,其实Codis这边是支持的,但记住,Codis在涉及这种场景的时候,仅仅是转发而已,它并不保证你的脚本操作的数据是否在正确的节点上。比如,你的脚本里涉及操作多个key,Codis能做的就是将这个脚本分配到参数列表中的第一个key的机器上执行。所以这种场景下,你需要自己保证你的脚本所用到的key分布在同一个机器上,这里可以采用hashtag的方式。

  比如你有一个脚本是操作某个用户的多个信息,如uid1age,uid1sex,uid1name形如此类的key,如果你不用hashtag的话,这些key可能会分散在不同的机器上,如果使用了hashtag(用花括号扩住计算hash的区域):{uid1}age,{uid1}sex,{uid1}name,这样就保证这些key分布在同一个机器上。这个是twemproxy引入的一个语法,我们这边也支持了。

 

后续要解决的问题

在开源Codis后,我们收到了很多社区的反馈,大多数的意见是集中在Zookeeper的依赖,Redis的修改,还有为啥需要Proxy上面,我们也在思考,这几个东西是不是必须的。当然这几个部件带来的好处毋庸置疑,上面也阐述过了,但是有没有办法能做得更漂亮。于是,我们在下一阶段会再往前走一步,实现以下几个设计:

  使用proxy内置的Raft来代替外部的Zookeeper,zk对于我们来说,其实只是一个强一致性存储而已,我们其实可以使用Raft来做到同样的事情。将raft嵌入proxy,来同步路由信息。达到减少依赖的效果。

  抽象存储引擎层,由proxy或者第三方的agent来负责启动和管理存储引擎的生命周期。具体来说,就是现在codis还需要手动的去部署底层的Redis或者qdb,自己配置主从关系什么的,但是未来我们会把这个事情交给一个自动化的agent或者甚至在proxy内部集成存储引擎。这样的好处是我们可以最大程度上的减小Proxy转发的损耗(比如proxy会在本地启动Redis instance)和人工误操作,提升了整个系统的自动化程度。

  还有replication based migration。众所周知,现在Codis的数据迁移方式是通过修改底层Redis,加入单key的原子迁移命令实现的。这样的好处是实现简单、迁移过程对业务无感知。但是坏处也是很明显,首先就是速度比较慢,而且对Redis有侵入性,还有维护slot信息给Redis带来额外的内存开销。大概对于小key-value为主业务和原生Redis是1:1.5的比例,所以还是比较费内存的。

  在RebornDB中我们会尝试提供基于复制的迁移方式,也就是开始迁移时,记录某slot的操作,然后在后台开始同步到slave,当slave同步完后,开始将记录的操作回放,回放差不多后,将master的写入停止,追平后修改路由表,将需要迁移的slot切换成新的master,主从(半)同步复制,这个之前提到过。

 

经验分享

来说一些 tips,作为开发工程师,一线的操作经验肯定没有运维的同学多,大家一会可以一起再深度讨论。

  关于多产品线部署:很多朋友问我们如果有多个项目时,codis如何部署比较好,我们当时在豌豆荚的时候,一个产品线会部署一整套codis,但是zk共用一个,不同的codis集群拥有不同的product name来区分,codis本身的设计没有命名空间那么一说,一个codis只能对应一个product name。不同product name的codis集群在同一个zk上不会相互干扰。

  关于zk:由于Codis是一个强依赖的zk的项目,而且在proxy和zk的连接发生抖动造成sessionexpired的时候,proxy是不能对外提供服务的,所以尽量保证proxy和zk部署在同一个机房。生产环境中zk一定要是>=3台的奇数台机器,建议5台物理机。

  关于HA:这里的HA分成两部分,一个是proxy层的HA,还有底层Redis的HA。先说proxy层的HA。之前提到过proxy本身是无状态的,所以proxy本身的HA是比较好做的,因为连接到任何一个活着的proxy上都是一样的,在生产环境中,我们使用的是jodis,这个是我们开发的一个jedis连接池,很简单,就是**zk上面的存活proxy列表,挨个返回jedis对象,达到负载均衡和HA的效果。也有朋友在生产环境中使用LVS和HA Proxy来做负载均衡,这也是可以的。 Redis本身的HA,这里的Redis指的是codis底层的各个server group的master,在一开始的时候codis本来就没有将这部分的HA设计进去,因为Redis在挂掉后,如果直接将slave提升上来的话,可能会造成数据不一致的情况,因为有新的修改可能在master中还没有同步到slave上,这种情况下需要管理员手动的操作修复数据。后来我们发现这个需求确实比较多的朋友反映,于是我们开发了一个简单的ha工具:codis-ha,用于监控各个server group的master的存活情况,如果某个master挂掉了,会直接提升该group的一个slave成为新的master。 项目的地址是:https://github.com/ngaut/codis-ha。

  关于dashboard:dashboard在codis中是一个很重要的角色,所有的集群信息变更操作都是通过dashboard发起的(这个设计有点像docker),dashboard对外暴露了一系列RESTfulAPI接口,不管是web管理工具,还是命令行工具都是通过访问这些httpapi来进行操作的,所以请保证dashboard和其他各个组件的网络连通性。比如,经常发现有用户的dashboard中集群的ops为0,就是因为dashboard无法连接到proxy的机器的缘故。

  关于go环境:在生产环境中尽量使用go1.3.x的版本,go的1.4的性能很差,更像是一个中间版本,还没有达到production ready的状态就发布了。很多朋友对go的gc颇有微词,这里我们不讨论哲学问题,选择go是多方面因素权衡后的结果,而且codis是一个中间件类型的产品,并不会有太多小对象常驻内存,所以对于gc来说基本毫无压力,所以不用考虑gc的问题。

  关于队列的设计:其实简单来说,就是「不要把鸡蛋放在一个篮子」的道理,尽量不要把数据都往一个key里放,因为codis是一个分布式的集群,如果你永远只操作一个key,就相当于退化成单个Redis实例了。很多朋友将Redis用来做队列,但是Codis并没有提供BLPOP/BLPUSH的接口,这没问题,可以将列表在逻辑上拆成多个LIST的key,在业务端通过定时轮询来实现(除非你的队列需要严格的时序要求),这样就可以让不同的Redis来分担这个同一个列表的访问压力。而且单key过大可能会造成迁移时的阻塞,由于Redis是一个单线程的程序,所以迁移的时候会阻塞正常的访问。

  关于主从和bgsave:codis本身并不负责维护Redis的主从关系,在codis里面的master和slave只是概念上的:proxy会将请求打到「master」上,master挂了codis-ha会将某一个「slave」提升成master。而真正的主从复制,需要在启动底层的Redis时手动的配置。在生产环境中,我建议master的机器不要开bgsave,也不要轻易的执行save命令,数据的备份尽量放在slave上操作。

  关于跨机房/多活:想都别想。。。codis没有多副本的概念,而且codis多用于缓存的业务场景,业务的压力是直接打到缓存上的,在这层做跨机房架构的话,性能和一致性是很难得到保证的

  关于proxy的部署:其实可以将proxy部署在client很近的地方,比如同一个物理机上,这样有利于减少延迟,但是需要注意的是,目前jodis并不会根据proxy的位置来选择位置最佳的实例,需要修改。

  四、对于分布式数据库和分布式架构的一些看法(one more Thing)

  Codis相关的内容告一段落。接下来我想聊聊我对于分布式数据库和分布式架构的一些看法。 架构师们是如此贪心,有单点就一定要变成分布式,同时还希望尽可能的透明:P。就MySQL来看,从最早的单点到主从读写分离,再到后来阿里的类似Cobar和TDDL,分布式和可扩展性是达到了,但是牺牲了事务支持,于是有了后来的OceanBase。Redis从单点到Twemproxy,再到Codis,再到Reborn。到最后的存储早已和最初的面目全非,但协议和接口永存,比如SQL和Redis Protocol。

  NoSQL来了一茬又一茬,从HBase到Cassandra到MongoDB,解决的是数据的扩展性问题,通过裁剪业务的存储和查询的模型来在CAP上平衡。但是几乎还是都丢掉了跨行事务(插一句,小米上在HBase上加入了跨行事务,不错的工作)。

  我认为,抛开底层存储的细节,对于业务来说,KV,SQL查询(关系型数据库支持)和事务,可以说是构成业务系统的存储原语。为什么memcached/Redis+mysql的组合如此的受欢迎,正是因为这个组合,几个原语都能用上,对于业务来说,可以很方便的实现各种业务的存储需求,能轻易的写出「正确」的程序。但是,现在的问题是数据大到一定程度上时,从单机向分布式进化的过程中,最难搞定的就是事务,SQL支持什么的还可以通过各种mysqlproxy搞定,KV就不用说了,天生对分布式友好。

  于是这样,我们就默认进入了一个没有(跨行)事务支持的世界里,很多业务场景我们只能牺牲业务的正确性来在实现的复杂度上平衡。比如一个很简单的需求:微博关注数的变化,最直白,最正常的写法应该是,将被关注者的被关注数的修改和关注者的关注数修改放到同一个事务里,一起提交,要么一起成功,要么一起失败。但是现在为了考虑性能,为了考虑实现复杂度,一般来说的做法可能是队列辅助异步的修改,或者通过cache先暂存等等方式绕开事务。

  但是在一些需要强事务支持的场景就没有那么好绕过去了(目前我们只讨论开源的架构方案),比如支付/积分变更业务,常见的搞法是关键路径根据用户特征sharding到单点MySQL,或者MySQLXA,但是性能下降得太厉害。

  后来Google在他们的广告业务中遇到这个问题,既需要高性能,又需要分布式事务,还必须保证一致性:),Google在此之前是通过一个大规模的MySQL集群通过sharding苦苦支撑,这个架构的可运维/扩展性实在太差。这要是在一般公司,估计也就忍了,但是Google可不是一般公司,用原子钟搞定Spanner,然后再Spanner上构建了SQL查询层F1。我在第一次看到这个系统的时候,感觉简直惊艳,应该是第一个可以真正称为NewSQL的公开设计的系统。所以,BigTable(KV)+F1(SQL)+Spanner(高性能分布式事务支持),同时Spanner还有一个非常重要的特性是跨数据中心的复制和一致性保证(通过Paxos实现),多数据中心,刚好补全了整个Google的基础设施的数据库栈,使得Google对于几乎任何类型的业务系统开发都非常方便。我想,这就是未来的方向吧,一个可扩展的KV数据库(作为缓存和简单对象存储),一个高性能支持分布式事务和SQL查询接口的分布式关系型数据库,提供表支持。

  Q & A

  Q1:我没看过Codis,您说Codis没有多副本概念,请问是什么意思?

  A1:Codis是一个分布式Redis解决方案,是通过presharding把数据在概念上分成1024个slot,然后通过proxy将不同的key的请求转发到不同的机器上,数据的副本还是通过Redis本身保证

  Q2:Codis的信息在一个zk里面存储着,zk在Codis中还有别的作用吗?主从切换为何不用sentinel

  A2:Codis的特点是动态的扩容缩容,对业务透明;zk除了存储路由信息,同时还作为一个事件同步的媒介服务,比如变更master或者数据迁移这样的事情,需要所有的proxy通过**特定zk事件来实现 可以说zk被我们当做了一个可靠的rpc的信道来使用。因为只有集群变更的admin时候会往zk上发事件,proxy**到以后,回复在zk上,admin收到各个proxy的回复后才继续。本身集群变更的事情不会经常发生,所以数据量不大。Redis的主从切换是通过codis-ha在zk上遍历各个server group的master判断存活情况,来决定是否发起提升新master的命令。

  Q3:数据分片,是用的一致性hash吗?请具体介绍下,谢谢。

  A3:不是,是通过presharding,hash算法是crc32(key)%1024

  Q4:怎么进行权限管理?

  A4:Codis中没有鉴权相关的命令,在reborndb中加入了auth指令。

  Q5:怎么禁止普通用户链接Redis破坏数据?

  A5:同上,目前Codis没有auth,接下来的版本会加入。

  Q6:Redis跨机房有什么方案?

  A6:目前没有好的办法,我们的Codis定位是同一个机房内部的缓存服务,跨机房复制对于Redis这样的服务来说,一是延迟较大,二是一致性难以保证,对于性能要求比较高的缓存服务,我觉得跨机房不是好的选择。

  Q7:集群的主从怎么做(比如集群S是集群M的从,S和M的节点数可能不一样,S和M可能不在一个机房)?

  A7:Codis只是一个proxy-based的中间件,并不负责数据副本相关的工作。也就是数据只有一份,在Redis内部。

  Q8:根据你介绍了这么多,我可以下一个结论,你们没有多租户的概念,也没有做到高可用。可以这么说吧?你们更多的是把Redis当做一个cache来设计。

  A8:对,其实我们内部多租户是通过多Codis集群解决的,Codis更多的是为了替换twemproxy的一个项目。高可用是通过第三方工具实现。Redis是cache,Codis主要解决的是Redis单点、水平扩展的问题。把codis的介绍贴一下: Auto rebalance Extremely simple to use Support both Redis or rocksdb transparently. GUI dashboard & admin tools Supports most of Redis commands. Fully compatible with twemproxy(https://github.com/twitter/twemproxy). Native Redis clients are supported Safe and transparent data migration, Easily add or remove nodes on-demand.解决的问题是这些。业务不停的情况下,怎么动态的扩展缓存层,这个是codis关注的。

  Q9:对于Redis冷备的数据库的迁移,您有啥经验没有?对于Redis热数据,可以通过migrate命令实现两个Redis进程间的数据转移,当然如果对端有密码,migrate就玩完了(这个我已经给Redis官方提交了patch)。

  A9:冷数据我们现在是实现了完整的Redissync协议,同时实现了一个基于rocksdb的磁盘存储引擎,备机的冷数据,全部是存在磁盘上的,直接作为一个从挂在master上的。实际使用时,3个group,keys数量一致,但其中一个的ops是另外两个的两倍,有可能是什么原因造成的?key的数量一致并不代表实际请求是均匀分布的,不如你可能某几个key特别热,它一定是会落在实际存储这个key的机器上的。刚才说的rocksdb的存储引擎:https://github.com/reborndb/qdb,其实启动后就是个Redis-server,支持了PSYNC协议,所以可以直接当成Redis从来用。是一个节省从库内存的好方法。

  Q10:Redis实例内存占比超过50%,此时执行bgsave,开了虚拟内存支持的会阻塞,不开虚拟内存支持的会直接返回err,对吗?

  A10:不一定,这个要看写数据(开启bgsave后修改的数据)的频繁程度,在Redis内部执行bgsave,其实是通过操作系统COW机制来实现复制,如果你这段时间的把几乎所有的数据都修改了,这样操作系统只能全部完整的复制出来,这样就爆了。

  Q11:刚读完,赞一个。可否介绍下codis的autorebalance实现。

  A11:算法比较简单,https://github.com/wandoulabs/codis/blob/master/cmd/cconfig/rebalancer.go#L104。代码比较清楚,code talks:)。其实就是根据各个实例的内存比例,分配slot好的。

  Q12:主要想了解对降低数据迁移对线上服务的影响,有没有什么经验介绍?

  A12:其实现在codis数据迁移的方式已经很温和了,是一个个key的原子迁移,如果怕抖动甚至可以加上每个key的延迟时间。这个好处就是对业务基本没感知,但是缺点就是慢。

参考 https://www.cnblogs.com/houziwty/p/5167075.html

codis安装部署可参考 http://blog.51cto.com/quenlang/1636441

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值