代理与路由方案:客户端直连之外的另一种选择

Redis 代理与路由方案实战:客户端直连 Cluster 的痛点、Twemproxy 与 Codis 及 Predixy 与 Envoy Redis Proxy 对比、Envoy redis_proxy 过滤器配置与集群定义、读写分离与多集群路由、代理的一跳延迟与单点风险、MOVED 与 ASK 重定向处理、代理层鉴权限流、与客户端分片方案的取舍

接入 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客户端一致性哈希不支持基本停更历史方案,慎选
CodisGo自有 Proxy + ZooKeeper自有模型,非原生社区维护老牌分片方案
PredixyC++多线程支持活跃度低高性能多线程代理
Envoy Redis ProxyC++无分片,单后端集群需额外配置活跃(CNCF)通用数据面,读写分离
HAProxyCTCP 层转发不感知协议活跃纯 TCP 负载均衡
redis-cluster-proxyC官方实验性支持实验性官方但未 GA
云厂商 Proxy(如阿里云)闭源托管支持商用托管集群自带

选型的关键判断点有三个:

  1. 是否需要 Cluster 原生支持。需要就用 Predixy 或云厂商 Proxy;不需要分片、只要读写分离,Envoy 是更好的选择(配置即代码、可观测性好)。
  2. 是否要求「应用零改造」。所有代理都能做到,因为它们对客户端呈现的就是一个普通单机 Redis。
  3. 团队是否有能力维护代理。代理是数据面组件,挂了就是全站故障,需要单独的高可用与升级流程。

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 分片与扩容 。

代理的实现质量差异就体现在这里:

代理MOVEDASK多键跨槽
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 的网络 RTT0.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 ms0.8 ms可接受
大 Value(10KB)读0.5 ms1.5 ms需评估
Pipeline 批量 100 条1.2 ms1.8 ms可接受
高并发(5 万 QPS)0.6 ms2.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 延迟和一个新的故障点。

真正的决策依据不是技术优劣,而是客户端可改造性:能改就用直连,不能改就用代理。需要跨集群路由、读写分离、集中鉴权时,代理是唯一能做到应用无感的位置——这时它的代价是值得付的。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「redis」更多文章

  1. 多租户隔离与资源配额:共享 Redis 的边界设计
  2. Key 设计与命名规范:Redis 里唯一的结构约束
  3. Kubernetes Operator 运维:Redis 集群的声明式管理