搞Redis的人早晚都会遇到一个问题单机内存不够用了怎么办你在Redis里存的数据越来越多8G内存不够加到16G、32G甚至64G但机器内存总有天花板而且大内存服务器的成本非常吓人。更难受的是就算内存够单实例的QPS、网络带宽、fork时的毛刺都会成为你上量的瓶颈。这时候就要考虑切片集群把数据打散到多台Redis实例上。业界比较成熟的老牌方案里Codis是绕不开的一个。Codis本质上是一个分布式Redis中间层方案在Redis 3.0时代之后很长一段时间里是很多公司做大容量Redis的首选。它的核心思路很简单前端放一堆无状态的代理层后端挂多组Redis实例数据按槽位均匀分布在各个实例上应用层看到的仍然是一个Redis但实际上数据早就被切片了。这篇文章我会从为什么需要切片集群开始完整拆解Codis的架构、槽位机制、部署流程、常见坑以及它和官方Redis Cluster的对比。无论你是刚从单机Redis转过来还是已经在用Codis但没真正搞懂它的细节这篇文章都能帮你建立完整的认知。1. 为什么要做切片集群先得理解“分片”这件事1.1 单机Redis的瓶颈内存、CPU、网络都是天花板单机Redis的瓶颈往往不是Redis本身而是它运行在物理世界里的各种约束。第一是内存。一台物理机的内存是有限的就算你能装1TB内存价格也足够让你放弃。更别说Redis是纯内存数据库内存就是它的全部。如果你的业务数据有200GB单机Redis基本没戏。第二是CPU。Redis虽然是单线程模型但处理大量请求时CPU单核的极限就是瓶颈。你可以跑很多个Redis实例在同一台机器上但那样又引入管理成本和资源竞争问题。第三是网络与带宽。数据要经过网卡单机网卡带宽有限大流量场景下单机带宽跑满也是常事。还有人会问为什么不用主从加哨兵主从解决的是高可用不是容量和写性能。主从模式下主节点和从节点保存的是同一份数据内存总量并没有增加。哨兵只是负责故障切换数据量该多大还是多大。所以当你遇到下面任何一种情况基本就要考虑切片集群了单机内存不足以容纳全部数据且无法通过清理无用数据解决。写入QPS超过单实例能力比如超过10万当然取决于机器性能。需要横向扩展希望通过加机器来提升总容量和吞吐。1.2 分片的关键数据怎么均匀地拆到多台机器上切片集群的核心问题只有一个数据如何分配常见的分配方式有两种。一种是一致性哈希。把key哈希到一个2^32的数轴上每个节点负责一段区间节点变化时只影响相邻节点。缺点是容易出现数据倾斜需要引入虚拟节点来均衡但实现复杂度高。另一种是固定槽位。把整个哈希空间均匀切成固定数量的槽比如1024或16384然后每个节点负责若干个槽。数据属于哪个槽通过哈希函数计算出来节点个数变化只需要把槽重新分配。Codis用的就是固定槽位共1024个槽Codis的具体槽位数是1024不是Redis Cluster的16384。从工程角度看固定槽位更可控、更直白。这也是Codis和Redis Cluster都选择它的原因。你不需要理解复杂的哈希环只记住“key通过CRC32算出槽号槽通过配置映射到某个分片组”就够了。这里补一句单机Redis没有分片概念它天然适合做缓存而不适合做大容量存储。一旦你要把Redis做成能承载全量数据的存储层分片就是绕不开的功课。2. Codis架构与核心组件代理转发模式是怎么工作的2.1 整体架构Proxy Group 元数据中心Codis的架构可以用“中间层代理 后端分片组”来概括整体包含以下组件codis-proxy无状态的轻量代理负责接收客户端请求、计算槽位、把请求转发到对应的codis-server分片组并返回结果。客户端只和proxy通信完全不感知背后有多少个分片。codis-server基于Redis改造的实例本质上还是Redis但增加了slot迁移相关的命令和内部状态用来配合prox和dashboard完成数据搬迁。codis-dashboard集群控制中心负责维护槽位与分片组的映射关系触发数据迁移、节点上下线等运维操作。可以理解成“调度器”。codis-feWeb管理界面方便你查看集群状态、节点状态、槽位分布、迁移进度。codis-admin命令行运维工具用于增删节点、配置proxy、调整拓扑。元数据存储早期版本依赖ZooKeeper后来版本支持etcd。它保存了集群拓扑、槽位分配、proxy列表等信息。dashboard、proxy、fe都通过它来感知集群状态。一句话概括数据均匀切片在多个Redis分片组上proxy负责转发dashboard负责管槽位zookeeper/etcd负责记状态。架构逻辑图可以这样理解非严谨图仅为思维模型客户端 → codis-proxy多个 → 按槽位路由 → codis-server group 1主从 → codis-server group 2主从 → codis-server group 3主从 ↕ dashboard / zookeeperetcd2.2 为什么选Proxy模式而不是客户端直连有人会问为什么不直接让客户端连到各个真实Redis实例像很多客户端库比如Redis Cluster的官方客户端那样自动路由。Proxy模式的核心优势是屏蔽复杂度。你的应用不需要关心数据落在哪个节点不需要内置槽位计算逻辑不需要维护节点列表。所有路由逻辑收敛到proxy层客户端用最普通的Redis客户端即可甚至连接串都不用改只需要指向proxy端口。对于已有大量业务方、不愿意改造客户端语言版本的公司来说这个“透明接入”价值非常大。你只需要把Redis地址从原来的单机IP改成proxy地址就能在不改业务代码的情况下完成扩容。缺点也很明显多一层转发意味着多一跳网络延迟局域网环境通常增加零点几毫秒可接受proxy本身可能成为瓶颈所以要部署多台proxy并做LB以及某些Redis特性无法透传比如需要跨slot的复杂事务、pipeline。作为对比Redis Cluster走的是客户端直连模式集群的槽位信息由客户端自己缓存和更新。这要求客户端SDK实现cluster协议旧的不支持cluster协议的Redis客户端基本没法用。所以Codis能火很大程度是因为它“代理层透明”的设计让接入成本降到了最低。3. 槽位映射与请求转发Codis内部是怎么工作的3.1 槽位计算key如何映射到分片Codis把整个keyspace划分成1024个槽位早期是1024部分版本可通过参数配置但默认就是1024。计算某个key属于哪个槽公式大致如下slot_id crc32(key_without_hashtag) % 1024注意这里的两个细节。第一个细节是如果key带有哈希标签在Codis中默认使用{...}即key形如foo{bar}baz那么加标签的目的是让包含同一标签的一组key落在同一个槽上从而支持在同一个分片内执行多key操作。Codis会提取花括号内的子串来做CRC32计算。这一点对业务设计非常重要比如你希望同一用户的多个维度数据能批量读写那就必须让这些key共享同一个标签。第二个细节是Codis使用CRC32而不是简单的取模虽然最终还是取模1024。选CRC32是因为它的分布相对均匀而且计算速度快。我自己在实际测过的一些数据里CRC32 1024槽位的批量key分布一般都比较均匀极少出现单个分片数据量翻倍的情况。3.2 请求转发proxy收到命令后做什么当客户端向codis-proxy发送一个命令时proxy的处理流程大致如下解析命令拿到key参数。根据key计算slot_id。查询本地缓存的slot→group映射表这个表来自dashboard/zookeeperproxy启动时拉取并在变更时更新。与目标group的主节点或从节点取决于读策略建立连接池发送命令。将结果返回给客户端。如果目标group处于迁移状态proxy会先尝试从原节点读发现key已被迁移走再从新节点读写入类似旧节点会返回特殊错误proxy自动重发到新节点。在多proxy场景下每台proxy都保存一份槽位映射缓存所以互相独立。当dashboard调整了槽位分配后会通过zookeeper/etcd推送变更给各proxy让它们更新本地缓存。3.3 多key命令、事务与pipeline的天然限制这是新手最容易踩坑的地方。因为数据分布在多个分片组上跨槽位的多key操作比如MGET、MSET、DEL多个非同一标签的key无法在一个redis实例上原子完成。Codis是怎么处理的答案是“尽力而为”。如果多key经过哈希后落在同一个slot或者通过hashtag强制落在同一slot那proxy可以直接透传到对应分片执行。如果落在不同slotproxy会拆分为多个单key命令分别发给不同分片再合并结果。注意这不再是原子操作某个key失败不影响其他key返回值顺序也可能有变化。事务MULTI/EXEC如果涉及多个槽位Codis无法保证原子性甚至会直接禁止执行或者仅当所有key都在同一槽位时才允许。SCAN命令在Codis中会在每个分片上执行然后由proxy做合并去重结果不保证全局一致如果你在迁移过程中做scan可能有重复数据。所以在业务设计阶段就要尽量把需要批量操作的key放到同一个hashtag下。这算是使用Codis最重要的一条军规。4. 部署与初始化实操从零搭建一套Codis集群4.1 环境准备与组件获取Codis的发布包可以从官方GitHub仓库获取编译好的release二进制。你需要准备至少一台Linux服务器当然可以多台。元数据组件ZooKeeper3.4或etcd用于存储集群元数据。最小配置一个节点即可生产至少三个节点做高可用。若干台用于运行codis-server的节点每台可以跑多个实例。用于运行codis-proxy的节点一般至少2台配合负载均衡器对外提供统一入口。我的建议是把dashboard和fe部署在独立机器上不要和proxy挤在一起避免互相影响。元数据节点也尽量分开。如果预算紧张可以混合部署但生产环境千万不要把所有角色放在一台机器上。假设我们用ZooKeeper先启动ZooKeeper。常规操作不再赘述。然后准备好codis-server的二进制。Codis的codis-server是基于Redis 3.2改造的所以配置文件和基本操作和Redis类似。你需要为每个分片实例准备独立的Redis配置设置好端口、持久化路径、日志路径等。4.2 初始化流程dashboard、proxy、group、slot我这里以一个简化的三group集群为例讲讲初始化顺序。假设三组实例分别为group 1: 10.0.0.1:6379主从节点等会再加。group 2: 10.0.0.2:6379主group 3: 10.0.0.3:6379主启动dashboard之前要准备好dashboard的配置文件其中指定zookeeper地址和集群名。# 启动dashboard ./bin/codis-dashboard --ncpu4 --configdashboard.conf --logdashboard.log --log-levelWARN # 启动codis-fe用于WEB管理 ./bin/codis-fe --ncpu4 --configfe.conf --logfe.log --log-levelWARN # 启动第一个proxy ./bin/codis-proxy --ncpu4 --configproxy.conf --logproxy.log --log-levelWARN proxy启动后默认不在线需要通过admin接口将它标记为online并加入集群。可以通过codis-fe界面操作也可以用命令行工具。接着把后端的codis-server注册到指定group中./bin/codis-admin --dashboard10.0.0.4:18080 --create-group --gid1 ./bin/codis-admin --dashboard10.0.0.4:18080 --create-group --gid2 ./bin/codis-admin --dashboard10.0.0.4:18080 --create-group --gid3 ./bin/codis-admin --dashboard10.0.0.4:18080 --add-server --gid1 --addr10.0.0.1:6379 ./bin/codis-admin --dashboard10.0.0.4:18080 --add-server --gid2 --addr10.0.0.2:6379 ./bin/codis-admin --dashboard10.0.0.4:18080 --add-server --gid3 --addr10.0.0.3:6379注意gid就是分片组的编号。创建组后先不加slot让集群暂时处于无数据状态。之后需要为每个group分配槽位。比如平均分配1024个槽位到三个组./bin/codis-admin --dashboard10.0.0.4:18080 --slot-initslot-init会自动把全部槽位平均分配到已创建的所有group上。如果某个group是后来加的它还不会有槽需要手动迁移一些槽过去。最后把proxy设为online./bin/codis-admin --dashboard10.0.0.4:18080 --proxy-online --proxy10.0.0.5:19000到这里一个简单的Codis集群就初始化完成了。业务Redis连接串指向proxy地址10.0.0.5:19000即可。4.3 为分片组增加从节点与读负载高可用是生产环境必须考虑的事情。单分片只有主节点一旦宕机这组数据就全挂了即使有别的分片也没用。所以每个group至少要有一个从节点。增加从节点用codis-admin./bin/codis-admin --dashboard10.0.0.4:18080 --add-server --gid1 --addr10.0.0.11:6379 --roleslavedashboard会自动让它执行REPLICAOF成为主节点的副本。注意从节点的数据是异步同步的不是强一致在主节点崩溃时可能出现少量数据丢失。这是Redis主从同步机制决定的也不是Codis能改变的。Codis的proxy支持读请求分流到从节点在fe或配置里可以开启“读从节点”策略让某些读请求打到group内的从节点。这是个不错的功能但你要接受数据有可能读到旧值。对一致性要求高的读请求还是走主节点更稳。4.4 扩容新加一个group并迁移槽位假设业务涨了三组不够要扩容到四组。创建group 4并添加codis-server。手动指定要把哪些槽迁移到group 4./bin/codis-admin --dashboard10.0.0.4:18080 --slot-migrate --gid4 --slot300这样就把slot 300迁移到group 4。如果槽很多可以写脚本循环执行。观察迁移进度确认完成后再迁移其他槽。在迁移过程中该槽的数据还是可以正常读写因为Codis支持动态迁移。迁移耗时取决于槽内key数量和大小大key会导致迁移很慢甚至卡住。所以如果遇到迁移一直“假死”先查有没有大key。4.5 Dashboard高可用的补充说明很多人在部署时会忽略dashboard自身的可用性。dashboard挂掉集群还能正常运行吗答案是能业务请求依然走proxy但管理操作和自动容错会失效。也就是说节点宕机后dashboard无法触发自动主从切换slot迁移也无法进行。所以生产环境建议给dashboard做高可用比如用进程守护、运行多实例投票选择等方式。具体方案可以根据你自己运维体系来定但绝不要让它裸奔。5. 常见问题与避坑经验从实践中趟出来的要点5.1 槽位迁移对读写的影响迁移是重操作尤其是迁移大key时会对源实例造成很大的阻塞。我遇到过一台机器在迁移过程中Redis的响应突然从2ms变成200ms。查了半天发现某个槽里有一个几十MB的hash结构在迁移时被做序列化传输主线程卡了好一会儿。解决办法有几种上线前做数据清理控制单key大小。一般建议单key不要超过1MB对待大型缓存数据要做拆分。迁移过程尽量放在业务低峰期比如凌晨。如果key太大且无法拆分考虑跳过迁移只迁移其他槽把大key留在原节点。或者先手动将该key拆成多个小key再执行迁移。总的来说大key在Codis里非常要命。除了迁移问题大key还会拖垮整个分片的吞吐和持久化所以必须提前治理。5.2 Proxy层面的限流、连接池和超时Codis的proxy虽然无状态但连接管理、请求排队、内存占用都值得注意。客户端连接数不要无脑调大。每个codis-proxy默认支持的连接数有上限超过后会拒绝新连接。如果业务方连接激增先看是不是连接没释放。需要设置合理的超时参数比如dial timeout、read timeout避免某个后端Redis出现阻塞时proxy的所有请求都卡在那里。多proxy模式下不要自己造负载均衡规则建议用LVS、Nginx TCP转发或云LB对client连接做分发。我自己踩过的一个坑是某次后端一台恢复节点hung住proxy的“读写超时”配得太长导致故障恢复时长拖了很久。后来把超时值调低故障影响面缩小很多。5.3 Codis不支持或支持不好的命令Codis虽然大多数时候让你感觉像Redis但它毕竟是个代理某些Redis能力会被削弱或去掉。以下命令或行为需要特别注意命令/能力Codis表现KEYS无法直接支持命令会在proxy层被禁用或报错需要用SCAN代替SCAN在proxy层会被合并执行但迁移期间可能出现重复key不保证一致性MGET/MSET 跨slot会拆分成多命令执行不再原子返回结果顺序不保证MULTI/EXEC仅当事务内所有key都在同一个slot时才支持否则会报错阻塞命令 BLPOP/BRPOP支持但行为受限阻塞等待可能因为迁移导致异常PUBSUBCodis早期版本对发布订阅支持不完整跨分片消息可能漏掉Lua脚本仅支持同一slot内的脚本跨slot脚本无法执行所以在设计业务时一定要先评估自己用到的Redis功能在Codis下是否可用。建议把所有包含多key的命令都收敛到hashtag或单key场景减少对复杂事务的依赖。5.4 与Redis官方Cluster的异同与选择Codis和Redis Cluster的定位高度重合但设计决策不同。简单对比维度CodisRedis Cluster路由模式Proxy转发透明客户端直连SDK路由槽位数102416384多key操作限制可用hashtag强制同slot支持hash tag但slot粒度更细迁移能力dashboard触发动态迁移在线动态迁移需要客户端配合依赖组件zookeeper/etcd proxy dashboard无需外部依赖运维复杂度组件多部署运维相对重相对简单原生集成社区与维护项目已停止维护多年官方持续迭代如果你是一个新项目没有历史包袱我建议直接用Redis Cluster毕竟官方驱动和工具链最成熟社区问题也容易搜到。但如果你已经有大量业务使用普通Redis客户端且无法升级到支持Cluster协议的客户端Codis的透明代理模式仍然是迁移成本最低的方案。不过需要知道Codis已经很久没有大版本更新了新的Redis版本特性比如Redis 6/7的客户端缓存、多线程IO、新数据类型都无法享受。长期看还是要规划把业务迁到Redis Cluster或自研的Proxy方案上。5.5 持久化与数据安全Codis底层是Redis所以持久化能力完全取决于你对codis-server的配置。每台codis-server要分别配置RDB/AOF。要注意的是Codis不提供跨分片的持久化一致性各分片持久化进度独立。在做主从切换时如果主节点和从节点数据不同步未落AOF的数据可能丢失。备份时不要直接对生产实例启动BGSAVE因为fork会占用大量内存。可以用低峰期或者先从实例同步数据再备份。我的做法是每个分片同时开启AOF和RDBAOF做崩溃恢复RDB做定期备份。从节点关闭持久化避免造成不必要的IO波动。6. 经验总结运维Codis这些年的一些体会6.1 数据设计远比架构重要Codis解决了“数据多了怎么放”但没解决“数据怎么放才合理”。如果你把业务key设计得乱七八糟不加hashtag那无论用Codis还是Cluster多key操作和扩展都会很难受。在设计key时应尽早统一规范。比如所有要关联的key都遵循业务:用户ID:类型并且让用户ID进入hashtag{user:1001}xxx。这样同一用户的数据都落在同一分片跨分片操作基本不会出现。6.2 监控一定要完善Codis的组件多每个环节都可能出问题。我至少要监控这些指标每台codis-server的CPU、内存、网络、持久化RDB状态。proxy的QPS、连接数、延迟、错误率。zookeeper/etcd节点状态。槽位分布均衡度各分片的内存占用量差异。迁移状态与是否有异常迁移进程。缺少监控的情况下出问题你基本只能靠猜。被坑过几次后我把所有组件都接上了指标采集和告警。这里就不展开具体到底用哪套监控工具了按你们公司现有的基础设施接就行。6.3 迁移计划永远是“先小批、再大批、再验证”我在初期扩容时曾一次性把几百个槽同时迁移结果整个集群在几分钟内持续高延迟。后来养成习惯先迁移1个槽观察指标再尝试10个、100个分别评估影响。批量迁移的过程中还要随时监控源和目标实例的内存、CPU、网络及时暂停迁移。另外迁移结束后一定要验证数据抽查几个槽的key数量、key抽样值与迁移前一致业务流量跑一轮看有没有报错。很多坑都是“没验证就上生产”才冒出来的。6.4 迁移结束语虽然Codis已经被官方看作是过气技术但在真实环境中仍有大量遗留系统在用它。如果你是维护这些系统的工程师希望这篇文章能帮你更懂它的原理和运维细节如果你是在做新架构选型那么也不必排斥它的设计思想——代理层透明、固定槽位、动态迁移这些思想至今仍然应用在很多数据库中间件中。理解Codis对理解分布式存储系统的核心难点会有很大帮助。个人实际体验下来要在Codis里长期安稳运行最核心的事就三件控制好key大小、设计好hashtag、管理好迁移节奏。做到这三点你基本能避开大部分生产事故。
阅读完成 · 觉得有帮助?