☰
Codis深度解析:Redis切片集群的代理分片与数据迁移实践
2026/10/22 4:43:40 网站建设 项目流程

我们先把背景说清楚:单机 Redis 撑不住的时候,你要么升级机器,要么拆数据。升级机器总有天花板,拆数据这件事才是真正的分水岭。这也就是切片集群要解决的命题。而谈到切片集群,绕不开一个老牌方案:Codis。它是一个把 Redis 改造成分布式集群的中间件方案,核心思路是“代理层 + 分片 + 无状态路由”,曾经在很多公司的生产环境里跑了好多年。哪怕官方 Redis Cluster 后来成熟了,Codis 依然在存量系统里服役,很多从 Codis 迁移到 Cluster 的团队,也都经历过从“陌生”到“摸透”的过程。

这篇文章写给三类人:一是 Redis 数据量已经打到单机上限、正在做架构选型的开发者;二是接手了 Codis 老集群、需要在运维上不出错的工程师;三是单纯想理解“中间件分片”和“官方集群”差异、补齐分布式缓存视野的同学。我会把 Codis 的组件分工、槽位分配、数据迁移、部署步骤、问题排查这些全过一遍,尽量讲得细致,能直接拿去用。

1. 从单机到切片集群:为什么偏偏是 Codis

1.1 单机 Redis 的瓶颈到底在哪

我在实际项目里观察到的规律是,单机 Redis 的问题通常不是先出现在 CPU 上,而是出现在内存容量和网络带宽上。假设你一台机器买了 64G 内存,Redis 自身数据加复制缓冲加持久化开销,实际能安全用到的也就 40G 左右。业务一涨,缓存命中率稍微掉一点,热点数据多塞几个大 key,容量立刻报警。

第二个瓶颈是 fork。Redis 做 RDB 持久化要 fork 子进程,虽然操作系统有写时复制,但内存越大,fork 瞬间页表复制就越慢,主线程卡顿越明显。实测过 30G 数据量的实例,fork 一次就把延迟从 1ms 拉到几十毫秒,这对线上交易链路来说就是事故。

第三个是单线程模型的约束。Redis 6 之前指令执行是单线程的,你加再多的核,单个实例能跑到的 QPS 也就在 8 万到 15 万之间,视读写比例而定。当你的客户端连接数、key 访问频率、热 key 带宽同时上来,单机在劫难逃。

这个时候自然想到:把数据水平拆开。每个节点管一部分 key,请求按规则路由。这就是切片集群。但切片有两个问题需要解决:一是数据到底按什么规则切,二是客户端怎么知道去哪个节点拿数据。

1.2 为什么当初大家不直接用官方集群

官方 Redis Cluster 在 3.0 版本就发布了,按理说一出来就该解决切片问题。但在它刚问世的头几年,生产环境真正敢大规模用的团队并不多。原因有几个:早期版本的稳定性口碑一般,节点 Gossip 通信在节点多的时候可能出现心跳风暴;客户端必须支持 Cluster 协议,很多老代码用的连接池没有 cluster 能力,改造成本不低;而且官方 Cluster 没有一个像样的可视化运维面板,监控、扩缩容、手动迁移槽位都靠命令行一行一行敲,对运维团队很不友好。

就在这个窗口期,Codis 出现了。它的思路和官方 Cluster 截然不同:官方 Cluster 是“去中心化”,每个 Redis 节点自己知道集群拓扑;Codis 是“中心化代理”,所有客户端只连代理,代理来实现路由和转发。客户端不需要知道数据在哪个节点,也不需要关心扩容缩容。Redis 协议保持原样,旧代码几乎零改动就能接入。

这个设计让 Codis 在很长一段时间的国内互联网公司里成了默认选项。它的好处是“业务无感”,代价是“架构多了一层”。今天回头看,这个“多一层”恰恰是理解 Codis 全部机制的关键。

2. Codis 架构拆解:六个组件各管一段

2.1 组件地图与职责边界

Codis 不是单一个程序,而是一套组件体系,彼此配合。我第一次接触的时候被一堆名词搞晕了,后来自己画了张职责表才彻底理清:

组件职责关键特征
codis-proxy客户端入口,解析 Redis 协议,计算槽位,转发请求无状态,可横向扩展,不持久化任何数据
codis-server真正存数据的 Redis 节点,基于 Redis 3.2 分支改造支持 slot 迁移指令,能按 slot 粒度搬数据
codis-dashboard控制中心,负责编排槽位、协调迁移、维护集群拓扑一个集群一个,类似集群的“大脑”
codis-fe可视化 Web 管理界面人机交互,查看集群状态、执行管理操作
codis-admin命令行工具用来初始化集群、添加组、调用 dashboard 操作
ZooKeeper/etcd元数据注册中心保存 slot 和 server group 的映射关系、proxy 列表等

一句话记忆法:proxy 是手,dashboard 是脑,server 是仓库,zk 是记事本,fe 是仪表盘,admin 是遥控器。

2.2 一个读写请求的完整旅程

假设客户端执行GET name:1001。在 codis-proxy 收到这条命令后,流程是这样的:

先解析 key 字符串,计算 key 的 crc32 校验和,对 1024 取模,得到槽位编号。然后从本地缓存的路由表里查到该槽位属于哪个 server group,找到对应的 codis-server 地址,把命令转发过去。codis-server 执行完,结果原路返回,proxy 再把结果回给客户端。

整个过程对客户端完全透明,客户端始终只跟一个 Proxy 的地址通信。这里有个非常关键的细节:proxy 本地会缓存路由表,并且监听 ZooKeeper 上的变更通知。一旦路由表变化(比如某个槽的归属变了),proxy 会立刻更新本地缓存。这个设计保证了请求不会每次都去查 ZooKeeper,否则性能早就崩了。

2.3 两个关键决策:ZooKeeper 与 1024 个槽

为什么用 ZooKeeper 而不是让 proxy 之间互相通信?因为 Codis 选的是中心化架构,zk 的作用就是提供“稳定的事实来源”。无论集群里有多少个 proxy,它们都从同一个地方拿到路由信息,不存在各说各话的脑裂问题。Dashboard 做任何变更,也是先写 zk,再通知 proxy 刷新。

为什么是 1024 个槽而不是官方 Cluster 的 16384 个?我个人的理解是,1024 在当年已经足够做到比较好的均匀性,而更少的槽位让迁移管理更简单——你迁移一个槽就是一次操作,槽太少则单个槽数据量过大,均衡性差。Codis 没有用虚拟节点的一致性哈希,而是固定哈希槽,就是为了让迁移可以“按槽搬迁”,带有明确的进度感。

注意:Codis 的老版本还支持一种“按表分片”的玩法,就是给 key 加前缀,同一前缀强制落到同一槽。实际生产我建议优先靠公共前缀加哈希标签,而不是过度设计分片规则,否则后续槽位调整会变得很痛苦。

3. 数据分片、路由规则与数据迁移

3.1 哈希算法与哈希标签

Codis 默认的 slot 计算方式是crc32(key) % 1024。直接对 key 做 CRC32 取模,最直观,但有一个衍生问题:它不支持多 key 操作。比如MGET key1 key2,这两个 key 大概率落在不同的槽位,proxy 就不知道该把命令转给谁了。

这就引出了 hash tag 机制。规则是:如果 key 里有{...}花括号,Codis 只对花括号内的部分做哈希计算。比如user:{1001}:profile和order:{1001}:list,花括号里的内容都是1001,这两个 key 的槽位一定相同。

这个特性非常实用。一旦你需要在同一个节点上操作关联数据,就给它们设计一个公共的业务 ID 塞进花括号。我在生产里用{订单号}:detail、{订单号}:log这类命名规范,效果很好。注意,花括号必须成对,且花括号中内容不能为空;如果出现多层花括号,Codis 只取最外层第一对。

3.2 同一槽的数据是可以平滑迁移的

槽位分配和迁移是切片集群最热闹的部分。Codis 的迁移单位是槽,它支持两种方式:同步迁移和异步迁移。

先说同步迁移:dashboard 发现要把某个槽从 A 组挪到 B 组,会先执行SLOTSMGRT系列的指令,把 A 组中这个槽的数据一个个搬到 B 组。搬完后再把路由表切过去。这个过程中,如果 A 组在搬数据时收到了对这个槽的读写请求,proxy 要么转发到 A,等迁移完成后再转到 B,要么让请求等待。在同步迁移实现里,为了保证一致性,迁移期间对槽的访问会被挡住,可能出现短时间延迟抖动。

再说异步迁移:这是 Codis 后期主推的方式。它先把“槽归属”标记为迁移中,A 和 B 都能服务这个槽的请求。迁移指令在 A 组把数据搬走的同时,A 组会记录一个“迁移进度”的游标,proxy 根据游标判断某个 key 是否已经搬到了 B 组。如果还没搬到,请求落到 A 组;如果已经搬到,就路由到 B 组。这个设计把迁移对线上的毛刺降到了最低,也是 Codis 在当时显得“能打”的核心原因之一。

从运维角度,我建议把迁移窗口选在业务低峰期,并且一次只迁移少数几个槽。你手动操作的时候,如果看过 dashboard 上每秒迁移 key 数的指标,你会明白:大 key 迁移一个就够受的了。

3.3 数据热点与倾斜问题

切片解决了容量,但解决不了热点。比如一个千万 QPS 的热 key,你再怎么切,它也只会存在于一个槽、一个节点里。Codis 社区针对这个问题有一些技巧,最常见的便是把 key 拆成多个带后缀的子 key,比如把hot:item拆成hot:item:0到hot:item:99,再用客户端做随机读写。这招不需要改 Codis 任何配置,纯业务层面解决,我实测可以把单 key 带宽压力摊到几十个 key 上。

还有一种倾斜是数据量倾斜:某个用户的数据特别大,占了几个 G,它所在的槽就会比其他槽重很多。这种只能靠拆 key 或者按业务维度重设计数据模型,单纯的 rehash 救不了。

重要经验:不要等集群倾斜了再来调槽。我见过有的团队一次性把 1024 个槽全部重新分配,结果迁移风暴把带宽打满,业务直接不可用。合理做法是每次只迁 20~50 个槽,观察一段时间再继续。

4. 与 Redis Cluster 对比:选型必须知道的五件事

4.1 架构哲学的差异

这两种方案最本质的区别在于控制面和数据面是否耦合。Redis Cluster 让每个节点都参与 Gossip 通信,节点之间互相交换槽位信息,客户端也自己维护槽位缓存。Codis 则把所有集群元数据抽到了 ZooKeeper 和 dashboard 上,proxy 统一代理。

用生活类比:Redis Cluster 是“人人都有通讯录”,每个员工都认识全公司的人;Codis 是“只有前台有通讯录”,员工有事都找前台。前者架构更扁平,后者更容易管理。

这个差异直接影响了客户端依赖。Redis Cluster 要求客户端实现 Cluster 协议,解析MOVED和ASK重定向;Codis 只需要普通 Redis 客户端,任何语言、任何老版本连接池都能直接连 proxy。如果你的团队技术栈里有一堆老系统,不想逐个改客户端,Codis 这种“协议透明”就有巨大吸引力。

4.2 功能边界与运维细节的差异

多 key 操作是个典型差异点。在 Redis Cluster 里,如果你用了 hash tag,也可以把多个 key 绑到同一 slot,但官方对跨 slot 的 multi-key 指令是直接拒绝的。Codis 的 proxy 层也会尽量用 hash tag 支持更多场景,但整体上两者都不鼓励跨节点事务。

再看运维模型。Redis Cluster 的节点数是自发现的,你加一个节点,它会通过 Gossip 慢慢被所有客户端知道。Codis 加节点、加 proxy、迁移槽位,都能在 fe 面板上点出来,操作路径清晰,这对不熟悉底层协议的人来说友好得多。

但论生态和发展趋势,官方 Cluster 是正路。Codis 的 GitHub 仓库早已归档,不再有新特性,新项目如果从零开始选型,我不推荐再选 Codis。只有当你是在维护存量系统,或者你的网络环境里确实需要中心化代理带来的管控便利,Codis 才有继续存在的理由。

对比项CodisRedis Cluster
客户端要求标准 Redis 协议即可需支持 Cluster 协议
架构角色中心化代理 + 元数据中心去中心化节点自管理
扩容操作dashboard/fe 可视化迁移槽位命令行手动迁移槽位
多 key 操作hash tag 内支持hash tag 内支持
运维面板自带 fe Web 界面依赖第三方工具
GIS 维护状态已归档持续演进

5. 从零部署一套 Codis 集群:完整实操过程

5.1 规划与环境准备

部署前先想清楚规模。我以三台机器为例:两台跑 codis-server 组成两个 server group,一台跑 ZooKeeper、dashboard、proxy 和 fe。生产环境当然要把这些组件分散开,但测试环境这样摆足够验证整个链路。

机器要求:64 位 Linux,内存和磁盘按数据量规划,至少需要能装下目标数据量的两倍磁盘;安装 Go 1.9+ 环境,因为 Codis 的源码编译依赖 Go;ZooKeeper 版本选 3.4 以上即可。

Codis 源码从 GitHub 上克隆,注意它已经不再更新,但源码仍然完整可编译。克隆之后进入目录,用 make 命令编译。make 产物会在 bin 目录生成 codis-server、codis-proxy、codis-dashboard、codis-admin、codis-fe 这些可执行文件。

5.2 配置文件的编写要点

codis-dashboard 需要一个 dashboard.toml,核心参数是产品名称、对外监听地址、ZooKeeper 地址。里面有一个容易被忽略的字段:hostname,它决定了 dashboard 注册进 zk 的地址。如果配成 127.0.0.1,后续 proxy 和 admin 连不上 dashboard,这个坑我踩过一次。

codis-proxy 的 proxy.toml 也比较关键。product_name要和 dashboard 保持一致,proxy_addr是客户端连接的入口,jodis_addr是注册到 Jodis 的地址。Jodis 是 Codis 提供的服务发现组件,客户端通过它从 zk 里拿可用 proxy 列表,实现连接的高可用。

codis-server 的配置基本沿用 Redis 的 redis.conf,但要注意两个点:关闭 RDB 自动保存或改成策略性触发,因为切片集群本来就有多个节点,某个节点挂了直接从副本或迁移链路恢复,没必要依赖单机 RDB;开启 AOF 并设置为 everysec,兼顾恢复速度和数据安全。

5.3 启动与初始化命令序列

整个过程我总结成一串顺序,按这个步骤走基本不会乱:

第一步,先启动 ZooKeeper。Codis 对 zk 的依赖是写路由元数据,zk 挂了集群不会立刻崩,但变更操作全部失灵,所以你还得把它当关键依赖来监控。

第二步,启动 dashboard:./bin/codis-dashboard --config dashboard.toml --log-level WARN。日志级别先调成 WARN,避免刷屏,出问题时再临时调 DEBUG。

第三步,初始化集群。用 codis-admin 创建 product,并指定 zk 地址。然后添加 group,每个 group 里添加 server。注意:codis-server 的启动命令需要指定集群角色--product,不然它只能当单机 Redis 用,不能参与槽位分配。

第四步,设置槽位数量。如果只有两个 server group,通常会直接把 1024 个槽全部给 group_1,然后再慢慢迁到 group_2,或者一开始就平均分配。用 codis-admin 的slot-init指令,指定--from和--to槽位范围,把范围分配给某个 group。

第五步,启动 proxy,注册到 zk。等状态变成 online 后,用redis-cli -h 127.0.0.1 -p 19000测试set/get,如果命令能通,说明代理转发链路已经建立。

第六步,启动 fe Web 面板,浏览器打开端口,就能看到集群拓扑、每个 group 的节点状态、每个槽的分布情况。到这里一套基础设施就起来了。

5.4 数据验证与容量扩张流程

数据验证别只测 set/get,要重点测跨槽位表现和哈希标签。比如写入key1和{tag}:1,观察它们在 dashboard 上分别落在哪个组。再用mget测试多个带相同 tag 的 key,确认能同时返回。这一步验证的是你业务的 key 设计能不能在 Codis 上工作。

扩容流程我建议按这个节奏走:新加一个 server group,把若干空闲槽分配给它,然后逐个把旧组的槽迁移过去。迁移指令用 codis-admin 的slot-migrate,指定源组、目标组、槽位范围,dashboard 会协调 proxy 执行迁移。每次迁移完,去看 fe 面板上的槽位统计,确认两边数据量差距缩小。

补充一个细节:Codis 支持SLOTSRESTORE这类专门的迁移命令,但你是通过 dashboard 操作的,不需要直接对 server 发。直接对 server 发容易把槽状态搞乱,非必要不做。

6. 运行中最常见的五个问题与排查经验

6.1 问题现象与根因速查

现象常见根因排查方向
客户端连接超时proxy 带宽打满或 CPU 跑满看 proxy 的 QPS、连接数、网卡流量
特定 key 延迟突然变高该 key 是热 key,或所在槽正在迁移用热key统计或 dashboard 迁移状态确认
迁移完成后数据不一致迁移窗口内业务持续写入检查新主库的数据条数和抽查 key
proxy 频繁重连zk 会话超时看 zk 日志、网络抖动、proxy 与 zk 的连通性
所有读写都失败一段时间路由表刷新失败proxy 日志里搜路由相关异常

6.2 热 key 问题的排查实录

有段时间我们某个活动中奖查询接口出现了严重的整体超时,而且故障节点非常集中。第一反应不是重启,而是去看 proxy 的 per-key 请求统计。Codis 的 dashboard 没有内置热 key 排序,我们通过 proxy 的日志采样,筛出访问次数最高的几个 key,发现是一个活动头图的 JSON 被反复从 Redis 读取,而且这个 key 恰好是一个大 key,单次传输就得几十毫秒。

处理方式分三步:先让客户端做本地缓存,把访问频率降下来;再把这个 JSON 拆成几十个小份 key,客户端按需读取;最后观察节点带宽恢复正常。整个过程没有动集群配置,就把问题解决了。这也再次说明:切片集群承载的是容量,不承载“单个 key 无限大”的逻辑。

6.3 迁移过程中“读了不到新数据”的坑

异步迁移有一个特性容易让运维误判:迁移过程中,旧节点可能仍有部分 key 没搬走,你的读写请求如果命中了旧节点,读到的是旧数据;命中了新节点,读到的是新数据。对于缓存场景问题不大,但如果你把 Redis 当数据库用,并且业务强一致,这个窗口是不可接受的。

规避办法有两个:一是把迁移放在低峰期,二是如果业务允许,在迁移期间对该槽位执行只读降级,等迁移完成再恢复读写。Codis 本身支持迁移期间选择“拒绝写”等策略,但在 dashboard 的默认配置里不强制,你要自己评估业务可接受程度。

6.4 proxy 的无状态性怎么用起来

我这个经验要着重分享:很多团队用 Codis 几年了,都还是只开一个 proxy,这完全没把 Codis 的中心化优势发挥出来。proxy 是无状态的,它的所有路由数据都来自 zk,所以你可以开多个 proxy 共享同一个集群地址,客户端随便连哪个都行。

多开 proxy 的好处不只是吞吐量翻倍,更重要的是故障隔离。假设单台 proxy 资源耗尽,它只会影响连到它身上的那部分客户端,其他 proxy 不受影响。搭配 Jodis 做服务发现,客户端在 proxy 不可用时能自动切换到可用 proxy,这一套才是 Codis 在生产环境的完整姿势。

6.5 ZooKeeper 抖动引发的连锁反应

另一个容易忽略的点是:dashboard 的元数据变更通知,最终要广播到所有 proxy。如果 zk 节点出现抖动,proxy 会不断重连 zk,这个过程中的路由更新可能出现窗口期的“路由暂时不存在”错误。解决方向不是改 Codis,而是把 zk 当核心组件运维:独立机器、独立磁盘、监控会话超时指标。

我在实践中观察到,zk 抖动对 Codis 的影响和对 Kafka 的影响很类似,都是“控制面失灵,数据面还能撑一会”。所以应急预案不要只盯 Redis 节点状态,还要盯 zk 集群的健康度。

7. 写在最后的维护心得

其实我现在手头还在维护着几套 Codis 老集群,都是历史业务留下来的。它们能跑这么多年,靠的不是某个精巧的配置,而是一套稳定的运维纪律:key 命名统一带哈希标签、热点 key 及时拆分、迁移永远分批做、zk 和 dashboard 监控到位。Codis 在新项目里不再是我的首选,但理解这套架构的收益是长期的——它把“分片路由”和“数据搬迁”这两件事拆得特别清楚,你看懂了 Codis,再去理解任何分布式存储系统,都会觉得骨架是相似的。

最后再分享一个小技巧:Codis 归入归档状态后,网上能查到的文档大多是零散的,如果你要维护存量集群,最靠谱的资料就是源码目录里的 README 和 example 配置文件。读一遍配置文件,比翻十篇博客都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询