丨 导语 在云数据库的性能竞赛中,Redis 凭借单线程事件驱动和内存常驻的架构优势,一直占据着"高性能"的制高点。然而,当我们用火焰图深入剖析生产环境时,却发现了一个令人意外的真相:高达 74.5% 的 CPU 时间被 sys_write 和 sys_read 系统调用所消耗,频繁的网络往返成为了制约吞吐的隐形瓶颈。更棘手的是,业务高峰期"Redis 连接数被打爆"的生产事故时有发生,运维团队在"扩分片迁移数据"与"暂时硬撑"之间左右为难。 如何在不改一行业务代码的前提下,既解决连接数爆炸问题,又让性能翻倍?腾讯云 Redis 团队从 Proxy 层架构出发,创新性地设计了"共享连接 Pipeline 优化"方案:通过精准的命令分类决策体系,让 99% 的普通读写命令自动走共享通道批量发送,而事务、阻塞等特殊命令则独享专属连接保障语义安全。这个看似简单的优化,背后却是对 Redis 命令语义的深刻理解、对异常场景的周密设计,以及对系统调用开销的极致压榨。 接下来,让我们一起揭开这套透明优化技术的幕后故事。

一、背景

Redis 一直被视为高性能存储的代名词:在常见的读写场景中,单节点能轻松实现每秒十万级请求,单次操作延迟也常落在亚毫秒范围。Redis 的单线程事件驱动模型规避了多线程调度和锁的开销,再辅以 epoll/kqueue 等多路复用机制,使请求路径异常紧凑。

核心数据结构常驻内存且针对热门命令做了细粒度优化,RESP 协议也刻意保持简洁,尽量降低网络和解析成本,这些设计叠加起来,奠定了它“快“的口碑。

不过,真实生产环境远比内核理想。客户端与服务器之间的往返次数、批量操作的串行化、网络抖动乃至业务侧的串联调用,都可能抵消 Redis 的原生优势。要继续逼近系统性能的上限,就必须在这些外围因素上做文章。

二、腾讯云Redis架构

腾讯云 Redis 采用前后双层组件的架构设计:前端是 Proxy 层,后端是 Redis 集群层。

请在此添加图片描述

Proxy 负责集中接入所有客户端连接,统一处理鉴权、连接复用、命令路由与流量调度,并在高并发场景下承担请求拆分、聚合、限流与监控等工作,让上层业务无需关心底层节点拓扑。

后端的 Redis 集群层则由多个分片节点构成,每个分片提供主从复制和故障切换能力,通过槽位映射存储数据,实现容量扩展与高可用。两者协同后,业务侧看到的是一个逻辑上统一的 Redis 服务,而实际的数据读写路径已经被 Proxy 层优化过;这也为我们利用 Proxy 挖掘性能冗余奠定了基础。

三、性能提升的思考

在性能调优之前,我们先对 Redis 做了一次性能摸底测试,期望找到在实际场景中 Redis 的瓶颈到底在哪里。

3.1 测试环境

  • 硬件和软件信息
    • CPU: Intel(R) Xeon(R) Platinum 8255C CPU @ 2.50GHz
    • Memory: DDR4 32GB
    • 操作系统: Linux VM-74-156-tlinux 3.10.107-1-tlinux2_kvm_guest-0054
    • Redis版本: Redis 7.1.0 (c497aaf3/0) 64 bit

3.2 测试结果

我们在上述环境中使用 RedisLabs 开源工具 memtier_benchmark 直接压测 Redis 节点(压测时启用 300个 客户端,只发送 SET 请求,将 Value 大小固定为 32 Bytes,关闭 pipeline),同一时间,我们还采集了系统级监控数据,便于后续绘制火焰图、定位性能瓶颈。

请在此添加图片描述

可以看到,Redis 在单节点上就能轻松承受每秒近 10 万个请求,平均延迟不到 1ms。不过我们希望进一步挖掘性能潜力,于是接下来通过火焰图分析当前瓶颈所在。

3.3 瓶颈分析

请在此添加图片描述

火焰图显示 sys_writesys_read 两个系统调用合计占用了 74.5% 的 CPU 时间(其中 sys_write 占 64.3%,sys_read 占 10.2%),远高于命令解析或数据结构操作。这说明在当前架构下 Redis 内核本身已经很高效,真正拖慢响应的是频繁的内核网络收发——也就是每条请求都要经历的网络往返与协议处理。

既然问题出在来回跑的包上,下一步自然是设法减少网络往返。常见做法有两类:

  • 批量化(Batch)处理: 客户端把多个GET/SET请求合并成一次 MGET/MSET 等多键命令,以一趟往返完成多次读写。
  • 管道传输(Pipeline): 客户端保持每条命令的原子语义不变,只是把多条命令成批发送,然后按顺序收回结果,用更少的往返处理更多请求。

在腾讯云 Redis 的双层架构里,Proxy 本身就扮演着后端 Redis 的“客户端”角色,因此在减少 Proxy 到 Redis 这条链路上的包数量并不要求业务方改代码。我们在 Proxy 内部掌控命令调度与转发逻辑,可以像真实客户端那样主动把请求批量化或串成 Pipeline 然后发送给后端 Redis,这对业务来说是完全透明的。

基于这一前提,上述 BatchPipeline 优化都能在 Proxy 层落地,这两个优化方向本质上都是把多次交互并入更少的网络往返,达到削减 RTT 的目的,但仍有如下差异:

  • Batch 优化: 需要 Proxy 侧显式改写请求组合、易受命令语义限制(如原子性、键分布、对应数据结构等),并不是所有命令都可以进行 Batch 优化,并且将客户端原始请求改写之后,请求对应的回复也无法一一对应。
  • Pipeline 优化: 保留单条命令的语义与原子性边界,只是改变 Proxy 向后端 Redis 发送、接收数据包的时机,更易适配已有逻辑。

综合权衡后,我们在 Proxy 层选择以 Pipeline 为主要优化手段:它无需强行改写业务命令,能在代理处对不同客户端的请求做透明合并,通用性更高,同时实现复杂度可控。

四、工程落地

在介绍实际的工程落地之前,我们先用两幅插图来说明一下优化前后的客户端请求的访问路径

4.1 优化前(使用私有连接)

当前Proxy 内部维护着到每个后端 Redis 节点的私有连接池。当客户端发送请求时,Proxy 会从连接池里取出对应分片的空闲连接,把这条命令塞进去转发;待 Redis 处理完成,连接恢复空闲后,再把它放回连接池中等待下次复用。

请在此添加图片描述

这种模式实现简单,命令语义天然隔离,但缺点也显而易见:同一批请求会触发多次 sys_write/sys_read,网络往返次数与客户端数量一一对应,CPU 时间大部分消耗在系统调用上;同时,在客户端连接数或请求峰值较大的情况下,Proxy 需要频繁从私有池中取出连接,如果池子撑不开就不得不新建连接,后端 Redis 的连接数存在被打满的风险。

4.2 优化后(采用共享连接)

在启用共享连接优化后,在满足顺序语义的前提下,Proxy 会把多个客户端的普通命令排入同一条共享链路,成批推送给后端,再按顺序拆分回复。这样一来,sys_write/sys_read 的调用次数和报文数量都明显减少,系统调用占用的 CPU 比例随之下降,Proxy 与 Redis 能把更多时间片留给真正的业务逻辑,整体吞吐也水涨船高。

请在此添加图片描述

同时,后端 Redis 只需要维持少量共享连接,即使客户端数量飙升,Proxy 也能有效挡住连接放大的压力。

4.3 面临的挑战

共享连接优化看似只是在 Proxy 侧将不同客户端命令打包后通过共享连接一次性发送给后端 Redis,但是当我们真正在 Proxy 上落地共享连接优化时却要补齐大量工程细节,核心是在能共享不能共享之间拿捏得足够精准,同时保证任何异常都会安全落地。我们最终做了几件关键的事:

  • 区分可共享与独占命令: Proxy 会在调度阶段识别出事务类(MULTI/EXEC)、阻塞类(BLPOP/WAIT等)、订阅类(SUBSCRIBE/PSUBSCRIBE/SHARDSUBSCRIBE)等特殊命令, 这些命令会被判定为必须独占连接,直接走私有链路保持语义一致。而除上述特例外的大多数普通读写指令(GET/SETHGET/HSETINCR/DECRZADD等)则自动落在共享连接上排队发送,Pipeline 让这一批请求共用更少的网络往返,从而显著降低收发包开销。

维护会话级顺序和回放: 即便客户端本身已经以 pipeline 形式发送了一串命令,Proxy 仍会逐条按到达顺序判断是否可以走共享连接。当前面没有未完成请求时,就会立即把可共享的普通指令压到共享连接上。一旦遇到必须独占的命令,Proxy 会等待之前那些共享命令全部回包,再切换到私有连接执行,随后继续处理 pipeline 中的后续命令。整个流程确保共享链路和私有链路交替使用时仍保持先到先处理,不会破坏客户端原本的调用顺序和语义。

请在此添加图片描述

客户端异常断连场景: 无论客户端是否使用 pipeline,只要它断开时仍有自己的请求挂在共享连接上,Proxy 都会把这些未完成的命令单独标记为游离态,与原会话解绑但继续等待后端回复;其他会话的命令保持原状。待相关回复返回后,Proxy 再统一丢弃结果并释放资源,确保共享连接在交给后续会话前已经“排空”,从根本上杜绝串包风险。

请在此添加图片描述

4.4 命令分类具体实现

命令分类:精准识别的艺术

要实现透明的连接复用,第一道关卡就是精准判断:哪些命令可以“拼车“走共享连接,哪些必须“专车“独享连接?这个看似简单的问题,背后却隐藏着对Redis 命令语义的深刻理解。

构建命令“身份证“系统

Proxy 在启动时会加载一份完整的 Redis 命令列表——涵盖 Redis 4.0、5.0、7.0 等各个版本。每个命令都拥有一张“身份证“,记录着它的关键特征:是读还是写?是否涉及发布订阅?最重要的是,是否需要独占连接?

请在此添加图片描述

这张“身份证“的核心信息用一个 CMD_EXCLUSIVE 标志位表达。在命令表初始化阶段,我们给那些“特立独行“的命令打上这个标记:事务类命令MULTI/EXEC 需要保证命令序列的完整性,阻塞类命令 BLPOP/BRPOP 可能长时间占用连接,订阅类命令 SUBSCRIBE/PSUBSCRIBE 会改变连接的工作模式——这些命令天生就不适合与他人共享连接。

请在此添加图片描述

从命令名到决策判断

当客户端发来的请求到达 Proxy 后,我们首先解析出命令名称(比如 GETBLPOP),然后在命令表中快速查找对应的元数据。这个过程就像海关验证护照——通过命令名在哈希表中定位,瞬间就能知道这个命令的“身份属性“。

找到命令元数据后,首先会通过简单的位运算检查 flags 中是否携带CMD_EXCLUSIVE 标记。但判断工作远未结束,因为即使命令本身看起来“无害“,客户端的上下文状态也可能改变决策结果。

综合研判:不只看命令,更要看状态

真正的决策逻辑综合了多个维度:命令本身是否独占、客户端是否处于事务中、系统是否在热升级、共享连接配置开关是否开启等等,任何一个条件触发,都会让命令走私有连接通道。

事务状态的特殊考量尤其值得一提。当客户端发送 MULTI 命令进入事务模式后,即使后续发送的是普通的 GET 或 SET,也必须在同一条私有连接上排队执行。因为 Redis 的事务机制要求所有命令在同一个连接上按顺序缓存,直到 EXEC 时才批量提交。如果中间某个命令“开小差“跑到共享连接上,就会破坏事务的原子性保障。

想象这样的场景:客户端发送 MULTI 后,Proxy 为其分配了私有连接 A;接下来的 SET key1 value1GET key2 虽然看起来是普通命令,但因为当前客户端处于事务上下文中,所以后续的普通命令依然在私有连接 A 上排队;直到 EXEC 执行完毕,才可以解除客户端的事务上下文,后续的 GET key3 这样的命令才恢复使用共享连接。

这套机制就像城市交通的公交专用道应急车道并行运作:绝大多数通勤乘客通过公交车(共享连接)集中运输,一辆车承载数十人效率极高;而急救车、消防车等特殊车辆(独占命令)则走应急车道专线直达,确保任务不受干扰——既提升了道路整体通行能力,又保障了特殊任务的可靠完成。

一次决策,精准分发

当决策结果出炉后,Proxy 会根据判断结果选择相应的连接池:需要独占的命令从私有连接池中获取(或临时创建)一条专属连接;普通命令则直接加入共享连接的发送队列,等待批量发送。整个过程对客户端完全透明——它感知不到背后连接的切换,只知道请求发出去了,回复按时回来了。

通过这套精细化的分类决策体系,我们在保证语义正确性的前提下,让绝大多数命令享受到了共享连接带来的性能红利。这就是“精准识别“的价值:不是一刀切地全部共享或全部独占,而是基于对每个命令特性的深刻理解,做出最优的资源调度决策。

4.5 性能收益

在我们将共享连接优化落地之后,我们使用腾讯云 Redis 7.0 标准版实例(该实例前端通过 3 个单线程 Proxy 进程对外提供访问入口(CPU 上限可以达300%), 后端是1个Redis节点负责(CPU 上限可达100%)所有读写请求的处理)进行了性能对比测试(压测时启用300 个客户端,只发送 SET 请求,将Value 大小固定为 32 Bytes,关闭 pipeline)。

请在此添加图片描述

测试结果显示,常规版本实例吞吐量只能维持在约 17.2万QPS,平均时延与 p50 均在1.5ms上下,长尾 p99.9 甚至拉到 6.69ms,这是由于 Proxy 需要频繁以单请求形式触发网络往返,连接复用度低,使得网络 IO 成为主要的性能开销。

共享连接优化版本,由于 Proxy 会把常规读写命令集中到共享链路,用Pipeline成批推送到Redis,单次批量能覆盖多条指令。Redis 侧的收发包开销因此骤降,更多 CPU 周期腾出来处理真正的业务操作,于是整体吞吐直接跃升到 37.5 万QPS;平均时延与 p50 分别压缩到 0.70ms、0.66ms,长尾 p99.9 降至3.15 ms,带宽利用率也同步提升到28.9 MB/s,实现更高吞吐的同时更加节省 CPU。

在开启共享连接优化之前,整套系统的瓶颈压在 Redis 身上,Proxy 组件才消耗 258% 的 CPU,Redis 负载就已经被打满,启用共享连接后,系统的瓶颈从 Redis 转移到了 Proxy 上,Proxy 组件CPU利用率达到 300%,而Redis 只消耗 75% 的 CPU。

试想在没有共享连接优化之前,如果恰逢业务流量冲高,Redis 负载被打满。如果要靠扩分片提吞吐,这时不仅要调度额外算力,还得迁移数据、重新平衡槽位;迁移过程会进一步拉高 CPU 占用,性能波动难以避免,运维团队必须在“先挨一波性能抖动”与“暂时撑着”之间取舍,常常陷入进退两难。在我们拥有了共享连接优化之后,我们只需横向补充更多无状态 Proxy,就能把剩余的后端算力继续转化成更高的吞吐量,提升性能的路径也变得更轻量。

五、总结

从系统调用开销的瓶颈发现,到双模式连接池的设计落地,从“连接数被打爆“的生产问题到“用更少资源支撑更高吞吐“的降本增效——这场性能优化,本质上是对 Proxy-Redis 双层架构潜力的一次深度挖掘。

我们没有改动内核,也没有要求业务方修改代码,而是在 Proxy 中间层做文章:通过对 Redis 命令语义的理解,构建精准的分类决策体系;通过对异常场景的设计,用游离态机制保障连接复用的安全;通过对系统调用的优化,把CPU 周期从内核空间节省下来用于业务处理。实测结果显示,吞吐量翻倍提升,延迟显著下降,这验证了一个工程原则:找到系统的关键约束点,用最小的改动撬动最大的收益。

更重要的是,这套优化改变了性能扩展的方式。过去 Redis 负载打满时,需要扩分片、迁移数据,操作重、风险高;现在瓶颈转移到无状态的 Proxy 层,横向扩容 Proxy 即可释放后端算力,过程轻量、平滑。这不仅是技术优化,更是成本结构的改善——云厂商能在相同硬件上承载更多实例,客户能以更低成本应对业务增长。

当然,方案也有局限。事务密集场景下优化效果会打折扣,极端异常场景需要更完善的监控。但共享连接优化已在腾讯云 Redis 全面落地,承载大量生产实例稳定运行。未来我们还将探索更智能的负载均衡、更精细的批量策略,持续挖掘云数据库的性能潜力。

文章来源于腾讯云开发者社区,点击查看原文