Skip to content

Instantly share code, notes, and snippets.

@c4pt0r
Created June 3, 2015 11:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save c4pt0r/90e3f9553fbba40cac1f to your computer and use it in GitHub Desktop.
Save c4pt0r/90e3f9553fbba40cac1f to your computer and use it in GitHub Desktop.
RebornDB (Codis) 的设计和实现及我眼中未来的分布式存储
你好, 我是 开源项目 Codis 的 co-author 黄东旭, 之前在豌豆荚做 infrastructure 相关的事情, 现在在创业 公司是 PingCAP, 方向也是分布式存储方向(NewSQL). Codis 是一个分布式 Redis 解决方案, 和官方的纯 P2P 的模式不同, Codis 走了一个 Proxy-based 的方案. 今天我们介绍一下 Codis 及下一个大版本 RebornDB 的设计, 同时会介绍一些 Codis 在实际应用场景中的 Best Practices. 最后抛砖引玉, 我会介绍一下我对分布式存储的一些观点和看法, 望各位首席们雅正.
1. Redis, Redis Cluster 和 Codis
想必大家的架构中, Redis 已经是一个必不可少的部件, 丰富的数据结构和超高的性能以及简单的协议, 让 Redis 能够很好的作为数据库的上游缓存层, 但是我们总是会比较担心 Redis 的单点问题, 单点 Redis 容量大小总是受限于内存, 在业务对性能要求比较高的情况下, 我们理想上希望所有的数据都能在内存里面, 不要打到数据库上, 所以很自然的就会寻求其他方案, 比如, SSDB 将内存换成了磁盘, 以换取更大的容量. 更自然的想法是将 Redis 变成一个可以水平扩展的分布式缓存服务, 在 Codis 之前, 业界只有 Twemproxy, 但是 Twemproxy 本身是一个静态的分布式 Redis 方案, 进行扩容/缩容时候对运维要求非常高, 而且很难做到平滑的扩缩容. Codis 的目标其实就是尽量兼容 Twemproxy 的基础上, 加上数据迁移的功能以实现扩容和缩容.
与 Codis 同期发布正式版的官方 cluster, 我认为有优点也有缺点, 作为架构师, 我并不会在生产环境中使用, 原因有几个:
1. cluster 的数据存储模块和分布式的逻辑模块是耦合在一起的, 这个带来的好处是部署异常简单, all-in-the-box, 没有像 Codis 那么多概念, 组件和依赖. 但是带来的缺点是, 你很难进行对业务无痛的升级. 比如哪天 redis cluster 的分布式逻辑出现了比较严重的 bug, 你该如何升级? 除了重启整个集群, 没什么好办法。
2. 对协议进行了较大的修改, 对客户端不太友好, 目前很多客户端已经成为事实标准, 而且很多程序已经写好了, 让业务方去更换 redis client, 是不太现实的, 而且目前很难说有哪个 redis cluster 客户端经过了大规模生产环境的验证, 从 HunanTV 开源的 redis cluster proxy 上可以看得出这个影响还是蛮大的, 否则就会支持使用cluster 的client了.
和 Redis cluster 不同的是, Codis 采用一层无状态的 proxy 层, 将分布式逻辑写在 proxy 上, 底层的存储引擎还是 redis 本身(尽管基于 redis 2.8.13 上做了一些小 patch..) , 数据的分布状态存储于 zookeeper (etcd) 中, 底层的数据存储变成了可插拔的部件.
这个事情的好处其实不用多说, 就是各个部件是可以动态水平扩展的, 尤其无状态的 proxy 对于动态的负载均衡, 还是意义很大的, 而且还可以做一些有意思的事情, 比如发现一些 slot的数据比较冷, 可以专门用一个支持持久化存储的 server group 来负责这部分 slot, 以节省内存, 当这部分数据变热起来时, 可以再动态的迁移到内存的 server group 上,一切对业务透明. 这个事情也是 RebornDB 在做的一件事情. btw RebornDB 和 它的持久化引擎都是完全开源的, 见 github.com/reborndb/reborn 和 github.com/reborndb/qdb
这样的设计的坏处是,经过了proxy,多了一次网络交互,看上去性能下降了一些,但是记住,我们的 proxy 是可以动态扩展的,整个服务的 QPS 并不由单个 proxy 的性能决定(所以生产环境中我建议使用 LVS/HAProxy 或者 Jodis )
2. 「我们更爱一致性」
很多朋友问我,为什么不支持读写分离,其实这个事情的原因很简单,因为我们当时的业务场景不能容忍数据不一致,由于 redis 本身的 replication 模型是主从异步复制,在 master 上写成功后,在 slave 上是否能读到这个数据是没有保证的,而让业务方处理一致性的问题还是蛮麻烦的。而且 redis 单点的性能还是蛮高的,不像 mysql 什么的, 没有必要为了提升一点点 QPS 而让业务方困惑。
所以,你可能看出来了,其实 Codis 的 HA,并不能保证数据完全不丢失,因为是异步复制,所以 master 挂掉后,如果有没有同步到 slave 上的数据,此时将 slave 提升成 master 后,刚刚写入的还没来得及同步的数据就会丢失。
在 RebornDB 中我们会尝试对持久化存储引擎(qdb)可能会支持同步复制 (sync replication),让一些对数据一致性和安全性有更强要求的服务可以使用。
所以说到一致性,这也是我们支持的 MGET/MSET 无法保证原本单点时的原子语义的原因,因为 MSET 所参与的 key 可能分不在不同的机器上,如果需要保证原来的语义,也就是要么一起成功,要么一起失败,这样就是一个分布式事务的问题,对于 Redis 来说,并没有 WAL 或者回滚这么一说, 所以即使是一个最简单的二阶段提交的策略都很难实现,而且即使实现了,性能也没有保证。所以在 codis 中使用 MSET/MGET 其实和你本地开个多线程 SET/GET 效果一样,只不过是由服务端打包返回罢了, 我们加上这个命令的支持只是为了更好的支持以前用 Twemproxy 的业务。
3. 「codis 的 lua 和 hashtag 」
在实际场景中,很多朋友使用了 lua 脚本以扩展 redis 的功能,其实 codis 这边是支持的,但记住,codis 在涉及这种场景的时候,仅仅是转发而已,它并不保证你的脚本操作的数据是否在正确的节点上。比如,你的脚本里涉及操作多个 key,codis 能做的就是将这个脚本分配到参数列表中的第一个 key 的机器上执行。所以这种场景下,你需要自己保证你的脚本所用到的 key 分布在同一个机器上,这里可以采用 hashtag 的方式:比如你有一个脚本是操作某个用户的多个信息,如 uid_1_age, uid_1_sex, uid_1_name 形如此类的key, 如果你不用 hashtag 的话,这些 key 可能会分散在不同的机器上,如果使用了 hashtag(用花括号扩住计算hash的区域) : {uid_1}_age, {uid_1}_sex, {uid_1}_name,这样就保证这些 key 分布在同一个机器上。
4. Proxy 到 P2P 之路
在开源 Codis 后,我们收到了很多社区的反馈,大多数的意见是集中在 Zookeeper 的依赖,Redis 的修改,还有为啥需要 Proxy 上面,我们也在思考,这几个东西是不是必须的。当然这几个部件带来的好处毋庸置疑,上面也阐述过了,但是有没有办法能做得更漂亮。
于是,我们在 RebornDB 上会再往前走一步,实现以下几个设计:
使用 proxy 内置的 Raft 来代替外部的 Zookeeper,zk 对于我们来说,其实只是一个强一致性存储而已,我们其实可以使用 Raft 来做到同样的事情。将 raft 嵌入 proxy ,来同步路由信息. 达到减少依赖的效果。
抽象存储引擎层,由 proxy 或者第三方的 agent 来负责启动和管理存储引擎的生命周期。具体来说,就是现在 codis 还需要手动的去部署底层的 redis 或者 qdb,自己配置主从关系什么的,但是未来我们会把这个事情交给一个自动化的 agent 或者甚至在 proxy 内部集成存储引擎。这样的好处是我们可以最大程度上的减小 Proxy 转发的损耗(比如 proxy 会在本地启动 redis instance)和人工误操作,提升了整个系统的自动化程度。在未来可能启动多个 reborn proxy 就能自动的部署好整个集群。
replication based migration。总所周知,现在 codis 的数据迁移方式是通过修改底层redis,加入单key的原子迁移命令实现的。这样的好处是:1. 实现简单, 2. 迁移过程对业务无感知。但是坏处也是很明显,首先就是速度比较慢,而且对 redis 有侵入性,还有维护slot信息给redis带来的额外的内存开销。在 RebornDB 中我们会尝试提供基于复制的迁移方式,也就是开始迁移时,记录某 slot 的操作,然后在后台开始同步到 slave,当 slave 同步完后,开始将记录的操作回放,回放差不多后,将 master 的写入停止,追平后修改路由表,将需要迁移的 slot 切换成新的 master。
主从(半)同步复制,这个之前提到过。
更好的 dashboard 管理工具
这几个事情做完以后,相信 RebornDB 的易用性和成熟度会再上一个台阶。在六月末,RebornDB 的第一个版本会发布出来,迈出第一步。
5. Codis 在生产环境中的使用的经验和坑们
关于多产品线部署:
很多朋友问我们如果有多个项目时,codis 如何部署比较好,我们当时在豌豆荚的时候,一个产品线会部署一整套 codis,但是 zk 共用一个,不同的 codis 集群拥有不同的 product name 来区分,codis 本身的设计没有命名空间那么一说,一个 codis 只能对应一个 product name。不同 product name 的 codis 集群在同一个 zk 上不会相互干扰。
关于 zk:
由于 Codis 是一个强依赖的 zk 的项目,而且在 proxy 和 zk 的连接发生抖动造成 session expired 的时候,proxy 是不能对外提供服务的,所以尽量保证 proxy 和 zk 部署在同一个机房。生产环境中 zk 一定要是 >= 3 台的奇数台机器
关于 HA:
这里的 HA 分成两部分,一个是 proxy 层的 ha,还有底层 redis 的 HA。先说 proxy 层的 HA: 之前提到过 proxy 本身是无状态的,所以 proxy 本身的 ha 是比较好做的,因为连接到任何一个或者的 proxy 上都是一样的,在生产环境中,我们使用的是 jodis,这个是我们开发的一个 jedis 连接池,很简单,就是监听 zk 上面的存活 proxy 列表,挨个返回 jedis 对象,达到负载均衡和 HA 的效果。也有朋友在生产环境中使用 LVS 和 HAProxy 来做负载均衡,这也是可以的。
redis 本身的 HA,这里的 redis 指的是 codis 底层的各个 server group 的 master,在一开始的时候 codis 本来就没有将这部分的 ha 设计进去,因为 redis 在挂掉后,如果直接将 slave 提升上来的话,可能会造成数据不一致的情况,因为有新的修改可能在 master 中还没有同步到 slave 上,这种情况下需要管理员手动的操作修复数据。后来我们发现这个需求确实比较多的朋友反映,于是我们开发了一个简单的 ha 工具: codis-ha ,用于监控各个 server group 的 master 的存活情况,如果某个 master 挂掉了,会直接提升该 group 的一个 slave 成为新的 master。
关于 dashboard:
dashboard 在 codis 中是一个很重要的角色,所有的集群信息变更操作都是通过 dashboard 发起的(这个设计有点像 docker ), dashboard 对外暴露了一系列 RESTful API 接口,不管是 web 管理工具,还是命令行工具都是通过访问这些 http api 来进行操作的,所以请保证 dashboard 和其他各个组件的网络连通性。比如,经常发现有用户的 dashboard 中集群的 ops 为 0,就是因为 dashboard 无法连接到 proxy 的机器的缘故
关于 go 环境:
在生产环境中尽量使用 go 1.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 的位置来选择位置最佳的实例,需要修改。
6. 「闲聊分布式存储」
架构师们是如此贪心,有单点就一定要变成分布式,同时还希望尽可能的透明 :P 。就 MySQL 来看, 从最早的单点到主从读写分离,再到后来阿里的类似 Cobar 和 TDDL ,分布式和可扩展性是达到了,但是牺牲了事务支持,于是有了后来的 OceanBase。Redis 从单点到 Twemproxy,再到 Codis,再到 Reborn。到最后的存储早已和最初的面目全非,但协议和接口永存,比如 SQL 和 Redis Protocol。
NoSQL 来了一茬又一茬,从 HBase 到 Cassandra 到 MongoDB,解决的是数据的扩展性问题,通过裁剪业务的存储和查询的模型来在 CAP 上平衡。但是几乎还是都丢掉了跨行事务(插一句,小米上在 HBase 上加入了跨行事务,不错的工作)。
我认为,抛开底层存储的细节,对于业务来说,KV,SQL查询(关系型数据库支持)和事务,可以说是构成业务系统的存储原语。为什么 memcached/redis + mysql 的组合如此的受欢迎,正是因为这个组合,几个原语都能用上,对于业务来说,可以很方便的实现各种业务的存储需求,能轻易的写出「正确」的程序。但是,现在的问题是数据大到一定程度上时,从单机向分布式进化的过程中,最难搞定的就是事务,SQL 支持什么的还可以通过各种 mysql proxy 搞定,KV 就不用说了,天生对分布式友好。
于是这样,我们就默认进入了一个没有(跨行)事务支持的世界里,很多业务场景我们只能牺牲业务的正确性来在实现的复杂度上平衡。
比如一个很简单的需求:微博关注数的变化,最直白,最正常的写法应该是,将被关注者的被关注数的修改和关注者的关注数修改放到同一个事务里,一起提交,要么一起成功,要么一起失败。但是现在为了考虑性能,为了考虑实现复杂度,一般来说的做法可能是队列辅助异步的修改,或者通过 cache 先暂存等等方式绕开事务。但是在一些需要强事务支持的场景就没有那么好绕过去了(目前我们只讨论开源的架构方案),比如支付/积分变更业务,常见的搞法是关键路径根据用户特征 sharding 到单点 MySQL,或者 MySQL XA,但是性能下降得太厉害。
后来 Google 在他们的广告业务中遇到这个问题,既需要高性能,又需要分布式事务,还必须保证一致性 :) ,Google 在此之前是通过一个大规模的 MySQL 集群通过 sharding 苦苦支撑,这个架构的可运维/扩展性实在太差。这要是在一般公司,估计也就忍了,但是 Google 可不是一般公司,用原子钟搞定 Spanner,然后再 Spanner 上构建了 SQL 查询层 F1。我在第一次看到这个系统的时候,感觉简直惊艳,应该是第一个可以真正称为 NewSQL 的公开设计的系统。所以,BigTable (KV) + F1 (SQL) + Spanner (高性能分布式事务支持),同时 Spanner 还有一个非常重要的特性是跨数据中心的复制和一致性保证(通过 Paxos 实现),多数据中心,刚好补全了整个 Google 的基础设施的数据库栈,使得 Google 对于几乎任何类型的业务系统开发都非常方便。我想,这就是未来的方向吧,一个可扩展的KV数据库(作为缓存和简单对象存储),一个高性能支持分布式事务和 SQL 查询接口的分布式关系型数据库,提供表支持。目前第一个KV系统有很多了,其实各种 NoSQL 包括分布式 Redis (Codis/Reborn)都可以归纳到第一个系统中,第二个分布式关系型数据库,比如阿里内部的 OB,Google 的 Spanner+F1,被 Apple收购的 FoundationDB都属于这个类型。但是目前开源社区里面还没有成熟的解决方案,但我认为,如果社区有一个成熟的方案出来,相信会成为一个很有前途的项目 :) , 很高兴我们的创业项目正在往这方面努力。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment