接入 Redis 有两条路:客户端直连,或者中间加一层代理。前者是主流——所有 Redis 客户端库都内置了 Cluster 拓扑感知与槽位路由;后者看起来是多余的中间层,增加一跳延迟、多一个故障点。
但在几种场景下代理不是可选项:老应用用的是不支持 Cluster 的客户端(比如某些语言的旧驱动、只认单机的框架组件);需要做读写分离但对客户端透明;多集群/多活需要统一入口;或者要在代理层统一做鉴权、限流、大 Key 拦截。这时候代理就是唯一能同时满足「不改应用」和「集中管控」的位置。
本文对比主流 Redis 代理的定位与能力边界,给出 Envoy Redis Proxy 的完整配置,再算清代理的真实代价。
一、代理层解决什么问题
先把需求分类,不同需求指向不同的代理形态:
| 需求 | 说明 | 是否需要代理 |
|---|---|---|
| 老客户端接入 Cluster | 客户端不支持 MOVED 重定向 | 需要 |
| 读写分离 | 写走主、读走从,应用无感 | 需要 |
| 多集群统一入口 | 按 key 前缀路由到不同集群 | 需要 |
| 集中鉴权限流 | 代理层校验 ACL、限 QPS | 可选(客户端也能做) |
| 大 Key / 危险命令拦截 | 拦截 KEYS、FLUSHALL | 可选 |
| 纯 Cluster 访问 | 客户端已支持槽位路由 | 不需要 |
| 减少连接数 | 客户端连接池管理 | 不需要 |
一个常见的误判是「Cluster 必须配代理」。事实正相反:现代客户端库(Jedis、Lettuce、go-redis、redis-py)都原生支持 Cluster 槽位路由,直连性能更好、故障面更小。代理的价值集中在「客户端无法改造」和「需要集中管控」这两类。
二、主流代理方案对比
| 方案 | 语言 | 分片模型 | Cluster 支持 | 维护状态 | 定位 |
|---|---|---|---|---|---|
| Twemproxy(nutcracker) | C | 客户端一致性哈希 | 不支持 | 基本停更 | 历史方案,慎选 |
| Codis | Go | 自有 Proxy + ZooKeeper | 自有模型,非原生 | 社区维护 | 老牌分片方案 |
| Predixy | C++ | 多线程 | 支持 | 活跃度低 | 高性能多线程代理 |
| Envoy Redis Proxy | C++ | 无分片,单后端集群 | 需额外配置 | 活跃(CNCF) | 通用数据面,读写分离 |
| HAProxy | C | TCP 层转发 | 不感知协议 | 活跃 | 纯 TCP 负载均衡 |
| redis-cluster-proxy | C | 官方实验性 | 支持 | 实验性 | 官方但未 GA |
| 云厂商 Proxy(如阿里云) | 闭源 | 托管 | 支持 | 商用 | 托管集群自带 |
选型的关键判断点有三个:
- 是否需要 Cluster 原生支持。需要就用 Predixy 或云厂商 Proxy;不需要分片、只要读写分离,Envoy 是更好的选择(配置即代码、可观测性好)。
- 是否要求「应用零改造」。所有代理都能做到,因为它们对客户端呈现的就是一个普通单机 Redis。
- 团队是否有能力维护代理。代理是数据面组件,挂了就是全站故障,需要单独的高可用与升级流程。
Twemproxy 之所以要慎选:它不支持 Cluster、不支持 MOVED、很多命令(如 SCAN、事务、多键操作)需要额外改造,且社区基本停止维护。新项目不应该再基于它设计。
2.1 Codis 的架构与历史定位
Codis 是豌豆荚在 2014 年开源的方案,它比原生 Cluster 早两年解决了分片问题。架构由三部分组成:
- Codis Proxy:对客户端呈现为单机 Redis,负责路由。
- Codis Dashboard:管理界面与配置中心,槽位映射存在 ZooKeeper / etcd 里。
- Codis Server:基于 Redis 分支改造的存储节点,增加了槽位同步命令。
它有自己的槽位模型(默认 1024 个槽,而非原生 Cluster 的 16384),槽位映射集中在 Dashboard,因此扩缩容由 Dashboard 统一协调,不需要节点间 Gossip。这曾经是它的优势:迁移过程可控、可视化。
但代价也很明显:Codis Server 是 Redis 的 fork,版本长期落后于官方(停在 Redis 3.2 附近),无法使用后续版本的新命令与新特性。原生 Cluster 在 Redis 3.0 稳定、5.0 增强后,Codis 的生存空间被大幅压缩。今天除非是存量系统,否则不应新上 Codis。
2.2 Predixy 的多线程模型
Predixy 是 C++ 编写的代理,最大的技术特点是多线程 + 每线程独立事件循环,因此可以吃满多核。相比之下 Twemproxy 是单线程模型,单实例吞吐存在天花板。
它的另一个优势是原生支持 Cluster 语义:内置槽位缓存,能正确处理 MOVED/ASK,也支持 MGET 等命令的跨槽拆分(拆成多个子请求再合并)。配置片段:
ClusterServerPool {
MasterReadPriority 60
StaticSlaveReadPriority 50
DynamicSlaveReadPriority 50
RefreshInterval 1
ServerTimeout 1
ServerFailureLimit 10
ServerRetryTimeout 1
Servers {
+ 10.0.0.1:6379
+ 10.0.0.2:6379
+ 10.0.0.3:6379
}
}
MasterReadPriority、StaticSlaveReadPriority 这组参数控制读写分离的权重——与 Envoy 的 read_policy 是同一个意图,但粒度更细(可以按主/从/静态从节点分别设权重)。
三、Envoy Redis Proxy 实战
Envoy 的 redis_proxy 是一个 L7 网络过滤器,能解析 RESP 协议、按 key 前缀路由、做读写分离。它不参与分片,所以典型拓扑是「Envoy 后面挂一个 Redis 集群」,用于统一入口与读写分离。
3.1 最小可用配置
static_resources:
listeners:
- name: redis_listener
address:
socket_address:
address: 0.0.0.0
port_value: 6379
filter_chains:
- filters:
- name: envoy.filters.network.redis_proxy
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.redis_proxy.v3.RedisProxy
stat_prefix: redis
settings:
op_timeout: 5s
enable_redirection: true
prefix_routes:
catch_all_route:
cluster: redis_primary
clusters:
- name: redis_primary
connect_timeout: 1s
type: STRICT_DNS
lb_policy: MAGLEV
load_assignment:
cluster_name: redis_primary
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: redis-primary.cache.svc
port_value: 6379
关键字段说明:
| 字段 | 作用 | 建议值 |
|---|---|---|
op_timeout | 单条命令超时 | 5s(与客户端超时对齐) |
enable_redirection | 是否跟随 MOVED/ASK 重定向 | 后端是 Cluster 时开 |
prefix_routes | 按 key 前缀路由到不同 cluster | 多集群场景必配 |
lb_policy | 负载均衡策略 | MAGLEV 或 RING_HASH |
downstream_auth_password | 代理层鉴权 | 生产必须设 |
downstream_auth_password 是容易被漏掉的一环——不设的话,代理会把所有请求原样转发,客户端不发 AUTH 也能通过:
settings:
downstream_auth_password:
inline_string: "your-strong-password"
3.2 读写分离配置
Envoy 的读写分离靠 read_policy 实现:
settings:
read_policy: PREFER_REPLICA
op_timeout: 5s
可选值:
| 值 | 行为 |
|---|---|
PREFER_MASTER(默认) | 所有命令都发往主节点 |
PREFER_REPLICA | 读命令发往从节点,写命令发往主节点 |
REPLICA_ONLY | 所有命令发往从节点(只读场景) |
Envoy 通过解析命令名判断读写:GET、MGET、HGET、LRANGE 等归类为读,SET、DEL、EXPIRE 等归类为写。但这份清单是硬编码在白名单里的,自定义命令或模块命令可能被误判为写而全部走主节点。
读写分离带来的一致性风险必须明确:主从复制是异步的,刚写入的值立刻读从节点可能读不到。如果业务不能容忍,就不要开 PREFER_REPLICA——这是运维决策,不是技术决策。
3.3 按前缀路由到多集群
prefix_routes 让代理根据 key 前缀选择后端:
prefix_routes:
routes:
- prefix: "session:"
cluster: redis_session
- prefix: "cache:"
cluster: redis_cache
catch_all_route:
cluster: redis_default
匹配规则是最长前缀优先,未命中则走 catch_all_route。注意 Envoy 需要解析出 key 才能匹配前缀,因此多键命令(MGET、MSET、DEL 多参数)的行为需要确认——不同版本的实现可能只取第一个 key,或者直接拒绝。上线前务必用真实命令压测验证。
Envoy 的完整过滤器链、集群发现(STRICT_DNS vs EDS)与热更新机制见 Envoy 代理高级配置
,Redis 过滤器只是其中一种网络过滤器,配置框架与 HTTP 过滤器一致。
四、Cluster 模式下的代理
如果后端是原生 Redis Cluster,代理必须处理 MOVED 与 ASK:
MOVED 3999 127.0.0.1:6381:槽位永久迁移到了另一个节点,代理应更新自己的路由表并重试。ASK 3999 127.0.0.1:6381:槽位正在迁移中,本次请求需先向目标节点发送ASKING再重发命令,但不更新路由表。
这两个语义的区别是 Cluster 协议的核心,处理错误会导致「迁移期间数据读不到」。槽位迁移的完整流程见 Cluster 分片与扩容 。
代理的实现质量差异就体现在这里:
| 代理 | MOVED | ASK | 多键跨槽 |
|---|---|---|---|
| Predixy | 支持 | 支持 | 部分支持(MGET 拆分) |
| redis-cluster-proxy | 支持 | 支持 | 实验性 |
Envoy(enable_redirection) | 支持 | 支持 | 不支持拆分 |
| Twemproxy | 不支持 | 不支持 | 不支持 |
跨槽多键操作是代理的普遍短板。MGET k1 k2 k3 如果三个 key 落在不同槽,标准 Cluster 客户端会报 CROSSSLOT,代理同样无法拆分(拆分需要理解命令语义,且破坏原子性)。业务上应该用哈希标签 {user}:1、{user}:2 把相关键强制同槽。
五、代理的真实代价
5.1 延迟:多一跳不是「多 0.1ms」
代理引入的额外开销包括:
客户端 ──> 代理(解析 RESP + 路由决策)──> Redis
<── <──
| 环节 | 典型耗时 |
|---|---|
| 客户端到代理的网络 RTT(同机房) | 0.1~0.3 ms |
| RESP 协议解析 | 0.01~0.05 ms |
| 路由决策与连接复用 | 0.01~0.1 ms |
| 代理到 Redis 的网络 RTT | 0.1~0.3 ms |
| 合计额外延迟 | 约 0.3~0.8 ms |
对比直连 Redis 的 0.10.3 ms,代理让单次调用延迟**增加约 23 倍**。对于 P99 要求 1ms 以内的场景,这是致命的。相关的主线程与网络模型分析见 网络模型与高性能 IO
。
缓解手段:代理与 Redis 部署在同一可用区、开启连接池复用(避免每请求建连)、用 MAGLEV 而非轮询减少长尾。
5.2 单点与容量
代理是无状态组件,可以水平扩容,但有两个约束:
- 连接数放大:N 个代理实例 × 每实例到后端的连接池,可能让后端连接数翻几倍。Redis 的
maxclients默认 10000,要提前核算。 - 故障域扩大:所有代理实例挂掉,全站不可用。必须多副本 + 反亲和 + 健康检查。
5.3 命令兼容性
代理需要解析 RESP 才能路由,因此对「不认识」的命令通常有三种处理:透传、拒绝、或错误路由。生产前必须验证清单:
# 逐个验证关键命令是否被代理正确转发
redis-cli -h proxy-host -p 6379 PING
redis-cli -h proxy-host -p 6379 SET k v
redis-cli -h proxy-host -p 6379 MGET k1 k2
redis-cli -h proxy-host -p 6379 EVAL "return 1" 0
redis-cli -h proxy-host -p 6379 SUBSCRIBE ch # Pub/Sub 常被代理拒绝
redis-cli -h proxy-host -p 6379 MULTI # 事务常被代理拒绝
redis-cli -h proxy-host -p 6379 SCAN 0 # 大范围扫描常被限制
SUBSCRIBE、MULTI、SCAN、BLPOP 是代理兼容性最差的四类命令。如果业务重度依赖它们,代理方案要重新评估。
六、代理层的限流与鉴权
代理位于所有请求的必经之路上,是做集中管控的天然位置。相比在每个应用里配一套限流,代理层只需配一次。
6.1 连接级鉴权
最小要求是「客户端必须发 AUTH」。Envoy 的 downstream_auth_password 就干这个。如果后端启用了 ACL,代理还需要用具备相应权限的用户连接后端:
# 代理到后端的认证(Envoy 通过 cluster 的 auth 或自定义 filter 实现)
# 常见做法是给代理分配一个专用的 ACL 用户
# 后端为代理创建专用用户,限制命令范围
ACL SETUSER proxy_user on >strongpass ~* &* +@all -@dangerous -flushall -keys
-@dangerous 一次性屏蔽了 FLUSHALL、FLUSHDB、KEYS、CONFIG、DEBUG 等危险命令——这是代理层最有价值的管控点:应用即使被注入,也无法通过代理执行破坏性命令。
6.2 请求级限流
Envoy 可以用 redis_proxy 配合速率限制过滤器实现按 key 前缀的限流。更简单的做法是在代理前置一个令牌桶(如基于 Redis 自身的 限流器
实现),但要注意:限流器自己用 Redis,会造成递归依赖,通常要指向一个独立的限流集群。
| 限流维度 | 实现位置 | 说明 |
|---|---|---|
| 连接数 | 代理 max_connections | 防止连接耗尽 |
| QPS(全局) | 代理速率限制过滤器 | 保护后端 |
| QPS(按业务前缀) | prefix_routes + 独立限流器 | 租户级配额 |
| 大 Key 拦截 | 代理解析命令后按参数长度判断 | 需自定义 filter |
6.3 危险命令与慢命令拦截
KEYS *、FLUSHALL、HGETALL(大 Hash)是线上事故的三大来源。代理可以在解析 RESP 后直接拒绝:
# 伪代码:在代理的请求处理路径上
if cmd in ["KEYS", "FLUSHALL", "FLUSHDB", "DEBUG"]:
return "-ERR command disabled by proxy\r\n"
这一层的价值在于它是应用改不掉的:即使某个业务方在代码里写了 KEYS *,也会被代理拦下。这是纯客户端方案做不到的。
七、可观测性
代理是观测 Redis 调用的最佳位置,因为所有请求都经过它。
Envoy 暴露的关键指标:
| 指标 | 含义 | 告警建议 |
|---|---|---|
redis.<prefix>.downstream_cx_active | 活跃客户端连接数 | 突增说明连接泄漏 |
redis.<prefix>.upstream_cx_active | 到后端的连接数 | 接近 maxclients 时告警 |
redis.<prefix>.command.<cmd>.total | 各命令调用量 | 观察命令分布 |
redis.<prefix>.command.<cmd>.error | 命令错误数 | 错误率突增 |
redis.<prefix>.command.<cmd>.latency | 命令延迟直方图 | P99 超阈值 |
redis.<prefix>.downstream_cx_drain_close | 被排空的连接 | 滚动升级时的信号 |
这份指标比 Redis 自身的 INFO commandstats 更细,因为它能按客户端来源、按代理实例维度拆分。结合 Prometheus 可以回答「是哪个业务方在打 HGETALL 大 Key」这类问题,而 Redis 自身只能告诉你「HGETALL 调用量大」。
代理延迟与后端延迟要分开看:
代理总延迟 = 代理处理开销 + 到后端 RTT + 后端执行时间
如果代理总延迟远高于后端执行时间,说明瓶颈在代理自身(CPU 打满、连接池不足),需要扩容代理而不是优化 Redis。
八、压测对比方法
代理方案上线前,必须做「直连 vs 代理」的对照压测。方法:
# 直连压测:10 个并发连接,各 10000 次 GET/SET 混合
redis-benchmark -h redis-primary.cache.svc -p 6379 -c 10 -n 10000 -t get,set -q
# 代理压测:同样的参数打到代理
redis-benchmark -h envoy-proxy.cache.svc -p 6379 -c 10 -n 10000 -t get,set -q
# 大 Value 场景(100 字节 vs 10KB 差异巨大)
redis-benchmark -h envoy-proxy.cache.svc -p 6379 -c 10 -n 10000 -t set -d 10240 -q
关注三个数字:QPS 下降幅度(代理通常损失 20%~50% 峰值吞吐)、P99 延迟增量(应控制在 1 ms 以内)、代理实例 CPU 使用率(决定需要几个代理副本)。
| 场景 | 直连 P99 | 代理 P99 | 可接受性 |
|---|---|---|---|
| 小 Value(< 100B)读 | 0.3 ms | 0.8 ms | 可接受 |
| 大 Value(10KB)读 | 0.5 ms | 1.5 ms | 需评估 |
| Pipeline 批量 100 条 | 1.2 ms | 1.8 ms | 可接受 |
| 高并发(5 万 QPS) | 0.6 ms | 2.5 ms | 代理需扩容 |
Pipeline 场景代理表现较好,因为一次网络往返承载了多条命令,代理的解析开销被摊薄;而单命令高频场景代理开销占比最高。
九、代理 vs 客户端分片
| 维度 | 代理层 | 客户端分片 |
|---|---|---|
| 应用改造 | 无 | 需换客户端/改配置 |
| 额外延迟 | +0.3~0.8 ms | 无 |
| 故障面 | 增加一层 | 无 |
| 集中管控 | 强(鉴权/限流/审计) | 弱(每客户端各自配置) |
| 多语言支持 | 天然统一 | 依赖各语言客户端质量 |
| 扩缩容 | 代理无感 | 需客户端感知拓扑 |
| 运维成本 | 高 | 低 |
判断标准可以归纳成一句话:如果所有客户端都支持 Cluster 且团队能管住客户端配置,就用直连;如果有任何一个客户端改不动,或者需要在数据面前做统一管控,就用代理。
现实中常见的折中是「混合模式」:核心业务用直连(性能优先),老旧系统与运维工具走代理(兼容优先),两者指向同一套 Cluster。这样既不牺牲主链路性能,又能收编历史包袱。
十、生产实践清单
- 新项目优先客户端直连 Cluster,代理只用于「客户端改不动」或「需要集中管控」的场景。
- 选代理时先确认是否支持
MOVED/ASK,否则无法对接原生 Cluster。 - Envoy 方案必须配置
downstream_auth_password,否则鉴权形同虚设。 PREFER_REPLICA读写分离要评估主从延迟,不能容忍脏读就不开。- 多键操作一律用哈希标签强制同槽,不要指望代理拆分。
- 代理与 Redis 同可用区部署,用连接池复用把额外延迟压到 0.5 ms 以内。
- 上线前逐条验证
SUBSCRIBE/MULTI/SCAN/BLPOP的兼容性。 - 代理自身要多副本 + 反亲和 + 健康检查,并纳入与 Redis 同等级的监控。
小结
代理与路由方案的取舍,本质是「把复杂度放在应用侧还是放在中间层」。客户端直连 Cluster 的性能最优、故障面最小,代价是每个客户端都要正确配置并感知拓扑;代理把这份复杂度集中到一个可统一管控的组件上,代价是增加 0.3~0.8 ms 延迟和一个新的故障点。
真正的决策依据不是技术优劣,而是客户端可改造性:能改就用直连,不能改就用代理。需要跨集群路由、读写分离、集中鉴权时,代理是唯一能做到应用无感的位置——这时它的代价是值得付的。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。