如果你维护过 Redis 实例,大概率经历过这样的场景:凌晨监控告警炸了,maxclients被顶满,新连接根本进不来,业务侧全是连接超时。这时候我脑子里第一个冒出来的命令就是CLIENT LIST。它能把当前实例上每一个客户端连接的"身份信息、行为状态、内存占用"全部摊开给你看,是整个连接排障体系的起点。这篇文章不绕弯子,就把CLIENT LIST的用法、字段含义、排查套路、配套命令完整讲透,适合正在被连接问题折磨的后端开发、负责缓存运维的同学,以及刚学 Redis 想弄明白"连接"到底是怎么回事的新手。
1. 连接数告警时,为什么第一件事是敲 CLIENT LIST
1.1 一条命令拿到全量连接快照
CLIENT LIST的用法极其简单,在redis-cli里直接执行即可:
redis-cli client list输出没有表头,一行就是一条连接,每行由一堆字段=值组成,大概长这样:
id=5 addr=127.0.0.1:51234 laddr=127.0.0.1:6379 fd=7 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=20448 argv-mem=0 multi-mem=0 tot-mem=21056 rbs=1024 rbp=0 obl=0 oll=0 omem=0 events=r cmd=client|list user=default redir=-1 resp=2 id=8 addr=192.168.10.5:39012 laddr=127.0.0.1:6379 fd=12 name=order-service-3 age=120 idle=3 flags=S db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=20448 argv-mem=0 multi-mem=0 tot-mem=21408 rbs=1024 rbp=0 obl=0 oll=0 omem=0 events=r cmd=replconf user=default redir=-1 resp=2 id=11 addr=10.20.1.2:50210 laddr=127.0.0.1:6379 fd=19 name= age=3600 idle=1800 flags=N db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=20448 argv-mem=0 multi-mem=0 tot-mem=21088 rbs=1024 rbp=0 obl=0 oll=0 omem=0 events=r cmd=get user=default redir=-1 resp=2连接数怎么数?因为输出没有表头,直接wc -l就是连接数:
redis-cli client list | wc -l这个数字和INFO clients里的connected_clients理论上一致,可能略多一两行,因为执行CLIENT LIST的这个连接本身也会被列出来。需要注意,线上如果接入了堡垒机或代理,你看到的实际上可能是代理与 Redis 之间的连接,而不是业务应用的原始 IP,这种情况后面会专门讲。
1.2 CLIENT LIST 和 INFO clients 的分工
很多新手会问:INFO clients里不是也能看到连接数吗,为什么要用CLIENT LIST?答案是粒度完全不同。
INFO clients返回的是一组汇总指标:
# Clients connected_clients:32 blocked_clients:3 tracking_clients:0 clients_in_timeout_table:0它能告诉你"有多少连接、有多少阻塞中的连接",但给不出任何细节。而CLIENT LIST是逐连接的明细数据,每个连接几十个字段,能回答的问题包括:是哪台机器连过来的、这个连接空闲多久了、它在跑什么命令、它占了多少内存、它的输出缓冲区是不是已经膨胀到要撑爆内存了。
做个类比:INFO clients是体检报告的封面,只给总检结论;CLIENT LIST是血液化验单的每一项指标。真要定位问题,只靠封面是不够的,必须翻到每一项。
2. 输出字段逐个拆解:看懂每一列在说什么
很多人在CLIENT LIST面前发怵,是因为一行太长,字段太多。其实这些字段可以分成三组:身份识别、活动状态、缓冲区与内存。分组记忆会轻松很多。
2.1 身份识别类字段:这个连接到底是谁
先看一组描述"连接本体"的字段:
| 字段 | 示例 | 含义 |
|---|---|---|
| id | 11 | 连接的唯一 ID,全局递增,排障时用于精准定位 |
| addr | 10.20.1.2:50210 | 客户端地址,格式是 IP:端口 |
| laddr | 127.0.0.1:6379 | 服务端本地的监听地址,多网卡、多 IP 部署时有参考价值 |
| fd | 19 | 该连接对应的文件描述符编号,可以和系统层的排查结合 |
| name | order-service-3 | 客户端名称,由CLIENT SETNAME设置,不设置则为空 |
| db | 0 | 该连接当前选中的逻辑数据库编号 |
| user | default | ACL 认证用户名,Redis 6.0 之后存在 |
| resp | 2 | 客户端使用的 RESP 协议版本,RESP2 还是 RESP3 |
addr是初看最直观的字段,但注意这里的 IP 是客户端到 Redis 的实际 TCP 对端地址。如果业务应用通过负载均衡、网关或者云数据库代理访问 Redis,这里看到的是代理节点 IP,真正的应用 IP 需要结合代理日志再查一层。name字段在排障时价值极高,后面专门讲,如果团队没有统一设置连接名的习惯,这个字段通常空着,你也就失去了最快定位业务归属的抓手。
2.2 活动状态类字段:这个连接正在做什么
第二组字段反映连接的"实时行为",也是最需要结合业务语义理解的:
| 字段 | 示例 | 含义 |
|---|---|---|
| age | 3600 | 连接已经存活的秒数 |
| idle | 1800 | 距离上一次有命令交互的秒数 |
| flags | N / S / b / x | 连接状态标志,可以组合出现 |
| sub | 0 | 常规频道订阅数 |
| psub | 0 | 模式订阅数 |
| ssub | 0 | shard 频道订阅数,Redis 7.0 后才有 |
| multi | -1 | 事务中已排队的命令数,-1 表示当前没有事务 |
| watch | 0 | 该连接 WATCH 的键数量 |
| events | r / w | 文件事件,r 表示连接可读,w 表示可写 |
| cmd | get | 当前正在执行的命令 |
flags是最需要展开的字段。常见的标志有这么几个:
N:无特殊标志,就是一个普通连接。S:从节点(副本)连接到主节点的连接,M相反,是主节点连接到从节点的连接。看到S别急着杀,那是复制链路的一部分。O:MONITOR 模式的客户端。生产环境开着 MONITOR 的客户端会让 Redis 处理速度显著下降,如果发现大量O标志连接,要考虑是不是有人在进行诊断操作忘了关。b:连接正处于阻塞状态,比如正在执行BLPOP、BRPOP、XREAD BLOCK等命令。看到flags=b且cmd=blpop是正常的,说明有消费者在等待队列消息。x:连接处于 MULTI/EXEC 事务上下文中,已经发了 MULTI,但还没有 EXEC。u:通过 Unix socket 连接,不是 TCP。A:连接已经被标记为 ASAP 关闭,通常是输出缓冲区超过限制,Redis 准备强制断开,这种连接往往还伴有巨大的omem。c:连接会在这个请求回复写完后关闭。d:当前这个正在执行CLIENT LIST的连接本身。
排查时扫一眼flags就能大致给连接分类。比如某台 redis 实例连接数几千,你发现其中关键业务的连接 flags 都是b,cmd 都是blpop,那说明消费者都在正常等消息;如果有一批连接flags=N、idle=0、cmd 都是get,但业务却报超时,那问题可能不在 Redis 而在应用侧,或者连接根本没有被有效复用。
2.3 缓冲区与内存字段:连接吃掉的资源
第三组字段是很多资深运维最爱看的,因为它们和内存、稳定性直接相关:
| 字段 | 示例 | 含义 |
|---|---|---|
| qbuf | 0 | 查询缓冲区当前已使用的字节数 |
| qbuf-free | 20448 | 查询缓冲区剩余可用空间 |
| argv-mem | 0 | 解析后的命令参数占用的内存 |
| multi-mem | 0 | 事务中排队命令占用的内存 |
| tot-mem | 21056 | 该连接整体消耗的内存 |
| rbs | 1024 | 固定回复缓冲区大小 |
| rbp | 0 | 固定回复缓冲区已使用量 |
| obl | 0 | 输出缓冲区固定部分长度 |
| oll | 0 | 输出缓冲区链表部分长度 |
| omem | 0 | 输出缓冲区总内存占用 |
简单理解:客户端发命令过来,字节先堆积在qbuf,Redis 解析后放进argv-mem,执行完的返回结果先写进输出缓冲区,用obl、oll、omem统计。tot-mem就是这条连接吃掉的综合内存。
我把这三块打个比方:qbuf是餐厅门口等待叫号的排队区,客人还没进去;argv-mem是已经拿到菜单开始点菜的客人;输出缓冲区是后厨做好但外卖小哥一直没来取的餐,越堆越多,omem就是那堆没人取的餐量。正常情况下omem应该很小(几 KB 到几十 KB),如果某个连接的omem涨到几 MB、几十 MB,说明它"点了大量餐却不取走"——典型情况是 Pub/Sub 消费者消费速度跟不上生产速度,或者应用代码长时间不读取 socket 数据。
3. 用 CLIENT LIST 定位线上问题的三段排查链
命令会用了,字段能看懂了,接下来聊实战。下面是我实际处理过的高频问题,完整的排查思路和操作命令都在这。
3.1 连接数打满:先按 IP 聚合再精准击杀
场景:业务侧批量报错ERR max number of clients reached,INFO clients显示connected_clients已经接近maxclients。
第一步别急着杀连接,先看连接是哪里来的。执行:
redis-cli client list | awk '{$0=substr($0, index($0,$2)); split($2,a,":"); print a[1]}' | sort | uniq -c | sort -rn稍微解释下这个命令的思路:addr=在连接行里基本是第二个字段(老版本没有laddr时是第二个,新版本有laddr时addr依然排第二),取出来按冒号切分,只留 IP 部分,然后按 IP 统计连接数,从大到小排序。
如果嫌上面太长,也可以用更稳妥的匹配写法:
redis-cli client list | awk '{ for(i=1;i<=NF;i++){ if($i ~ /^addr=/){ split($i,a,":"); cnt[a[2]]++ } } } END{for(v in cnt) print cnt[v], v}' | sort -rn这里为了避免误踩字段顺序的坑,主动去匹配以addr=开头的字段,只要 Redis 输出格式不大改,这段脚本是健壮的。
聚合结果一看,某个 IP 的连接数占了一半。这个 IP 通常就是没写连接池的应用主机、或者配置了错误重试策略的服务。接下来做两件事:
- 确认业务归属,联系负责团队检查连接池配置(最大连接数、空闲回收时间等)。
- 临时止血,用
CLIENT KILL清理该 IP 的多余连接。
redis-cli client kill addr 192.168.1.23:52345CLIENT KILL一次只能按一个精确的IP:端口杀,如果想批量清某个 IP 的所有连接,可以用脚本遍历:
redis-cli client list | grep "addr=192.168.1.23:" | awk '{for(i=1;i<=NF;i++) if($i ~ /^id=/) print $i}' | cut -d= -f2 | while read id; do redis-cli client kill id "$id" done这里从CLIENT LIST里筛出目标 IP 的连接,提取id字段,逐个调用CLIENT KILL ID。为什么不直接用ADDR?因为 IP 固定但端口不固定,需要一条条处理。
注意:杀连接前务必确认目标不是你自己的管理通道、不是主从复制连接。复制连接在
flags里能看到S或M,这种连接如果被误杀,主从复制会中断重建,产生不必要的全量同步开销。稳妥做法是先CLIENT LIST TYPE normal只看普通连接再动手。
3.2 阻塞和事务:从 flags=b 看排队真相
另一个常见场景是延迟突增、业务超时,直觉上怀疑 Redis 被阻塞了。这时候INFO clients里的blocked_clients指标会跳出来,但先别急着下结论。
执行CLIENT LIST,重点筛出 flags 含b的连接:
redis-cli client list | grep " flags=b " | awk '{print $2, $9, $NF}'flags=b表示该连接正在阻塞等待某个事件,比如BLPOP、BRPOPLPUSH、XREAD BLOCK。这类连接多,只说明有很多消费者在"等着新数据进来"。这个状态对队列、延迟任务、消息通知类场景是常态,不代表 Redis 卡住了。
真正需要警惕的是另一种阻塞:flags=N但cmd显示的是GET、SET这类普通命令,同时age很小、idle=0,说明命令发进去后 Redis 没有立刻返回。这种一般是 Redis 内部在跑慢命令,比如KEYS、大SORT、XREVRANGE拉取超大范围的数据,或者宿主机 CPU 被打满、内存 swap 严重。
当时我排查某个报表服务的毛刺问题,CLIENT LIST里发现一个cmd=keys的连接,argv-mem只有几十字节,但 Redis 整体延迟高了几十毫秒。顺着cmd找到应用调用方,把KEYS换成了分页SCAN,问题立刻消失。这个排查链路的关键点在于:CLIENT LIST能告诉你每一条连接"正在执行什么",而慢命令监控只能告诉你"有过哪些慢命令",两者结合才是完整拼图。
还有一类比较隐蔽的情况:连接显示flags=x,处于 MULTI 事务中,multi字段大于等于 0,说明事务里已经塞了多条命令但还没 EXEC。如果事务里混入了大命令,整个事务的执行时间会被拉长,期间该连接的事务锁会一直持有。定位方法同样是看CLIENT LIST里的flags=x和multi字段。
3.3 输出缓冲区异常膨胀:用 omem 揪出慢消费者
实例内存莫名上涨,INFO memory里used_memory一直爬,但BIGKEYS扫描也没发现大 key。这种情况我会直接看CLIENT LIST里omem和tot-mem最大的几个连接。
列出omem最大的 20 条连接:
redis-cli client list | awk '{ for(i=1;i<=NF;i++){ if($i ~ /^omem=/) {split($i,a,"="); omem=a[2]} if($i ~ /^addr=/) {addr=$i} if($i ~ /^cmd=/) {cmd=$i} } print omem, addr, cmd }' | sort -rn | head -20如果前排连接omem动辄几十 MB,而且cmd是subscribe或psubscribe,基本可以断定是 Pub/Sub 消费者消费太慢。Redis 会把发布的消息推给所有订阅者,只要有一个订阅者不读数据,它的输出缓冲区就会持续膨胀,直到触发client-output-buffer-limit pubsub的限制被强制断开,然后重新连接、再次膨胀,形成内存抖动。
处理思路分两步:立即止血,把异常消费者踢掉:
redis-cli client kill addr 10.20.1.5:33021长期治理,则要考虑架构层面是否适合 Pub/Sub。Pub/Sub 是广播语义,消费者掉队了不会重放消息,慢消费者会拖累整个实例,这时候改用 Stream 配合消费者组通常更合适,Stream 能把消息暂存在服务端,消费者恢复后可以继续消费。
如果膨胀的是普通normal类型连接,多半是应用代码没有及时读取响应,比如用了同步客户端却在一个线程里发了大量请求不读结果,或者大 key 查询返回体积巨大。这两种情况的共同点,都能在omem上暴露出来。所以我把omem列为连接排查里性价比最高的排查字段之一,因为它直指问题本质:有数据在堆积,你还没读。
4. 过滤参数和组合命令:把 CLIENT LIST 变成管理工具
光看不够,还得能处理。Redis 在CLIENT命令族里提供了不少配套能力,组合起来才是完整的连接治理方案。
4.1 TYPE 和 ID 过滤:只看你想看的连接
连接多的时候,CLIENT LIST全量输出一次可能非常大,靠grep过滤不够优雅。Redis 提供了内置过滤参数。
从 Redis 5.0 开始支持按类型过滤:
# 只看普通客户端连接 redis-cli client list type normal # 只看主从复制中从节点建立的连接 redis-cli client list type replica # 只看 Pub/Sub 连接 redis-cli client list type pubsub从 Redis 6.2 开始支持按 ID 过滤:
redis-cli client list id 5 8 11我在实际中用得最多的是type normal。排查业务连接问题时,从库连接、复制心跳这类连接往往会干扰视线,过滤掉之后输出更干净。比如上面 3.1 节的按 IP 聚合脚本,如果先加上type normal,就不会把复制链路误统计进去,脚本可以简化成:
redis-cli client list type normal | awk '{split($2,a,":"); print a[2]}' | sort | uniq -c | sort -rn4.2 配合 CLIENT KILL 精准断开异常连接
CLIENT KILL是处理问题连接的"手术刀",不同版本支持的过滤条件不同。常用的几种形态:
# 按连接 ID 断开(最精准) redis-cli client kill id 1234 # 按客户端地址断开 redis-cli client kill addr 192.168.1.23:52345 # 按客户端名称断开 redis-cli client kill name order-service-3 # 按 ACL 用户断开 redis-cli client kill user legacy-user # 断开空闲时间超过 3600 秒的连接(较新版本支持) redis-cli client kill maxage 3600执行后返回OK表示至少杀掉了匹配的一个连接,如果没有匹配会返回一条错误信息。MAXAGE这个参数在处理连接泄漏时很实用,一条命令就能清掉所有闲置超过一小时的空连接,不用再写循环脚本。
使用
CLIENT KILL有几个细节要注意:一是当心杀掉自己的连接,如果你的 redis-cli 来源 IP 和被杀条件匹配,会把自己断开;二是不要在从库上按TYPE replica去杀连接,这会破坏主从关系;三是按name杀连接时,如果有两个服务用了同一个连接名,会一起被断开,所以连接命名必须唯一。
4.3 给连接起名字:CLIENT SETNAME 让排查效率翻倍
CLIENT SETNAME是容易被忽视但回报极高的命令。它给当前连接设置一个名字,之后这个名字会出现在CLIENT LIST的name字段里,也会出现在慢日志的客户端信息中。
命令行手动设置:
redis-cli client setname my-ops-conn redis-cli client getname业务代码里也建议统一配置。以 Python 的 redis-py 为例:
r = redis.Redis( host="10.0.0.8", port=6379, client_name="order-service-prod-01", )Java 的 Lettuce、Jedis 也都有对应的 client name 配置。建议命名规范是项目名-环境-实例序号,比如order-service-prod-01、pay-service-gray-02。有了连接名之后,CLIENT LIST的输出里name字段一目了然,按业务聚合连接就非常简单:
redis-cli client list type normal | grep "name=order-service-" | wc -l我曾经接手过一套混乱的 Redis 环境,所有业务方都没有设置连接名,出问题时只能靠 IP 反查。后来推动团队把连接名纳入上线 checklist,之后再看CLIENT LIST,每条连接的归属瞬间清楚,排障时间从小时级降到分钟级。这一步投入极小,收益却是长期的。
5. 版本差异与生产环境使用建议
5.1 不同版本的输出字段与过滤参数差异
CLIENT LIST不是一成不变的,不同 Redis 版本输出的字段和可用参数差异明显。我整理了一份简表供参考:
| Redis 版本 | 主要变化 |
|---|---|
| 2.8 | 基础字段完整,CLIENT KILL仅支持按 ADDR 断开 |
| 4.0 | 输出调整,新增部分内存字段,连接 ID 引入 |
| 5.0 | 支持CLIENT LIST TYPE过滤;重写客户端内存管理,新增qbuf-free、argv-mem、tot-mem字段 |
| 6.0 | ACL 正式推出,输出新增user字段;新增laddr;支持CLIENT TRACKING相关字段redir |
| 6.2 | 支持CLIENT LIST ID;CLIENT KILL支持更多过滤条件;新增CLIENT INFO |
| 7.0 | 新增ssub(shard 订阅)、resp字段 |
这意味着,如果你用脚本解析CLIENT LIST,不能假设字段顺序永远不变。上面的 awk 脚本里主动匹配addr=、omem=而不是依赖固定列位置,正是为了兼容不同版本。生产环境的 Redis 升级之后,原先跑得好好的分析脚本可能因为多了一个字段而索引用错,这个坑我在实际中踩过。
5.2 CLIENT LIST 本身也有代价
CLIENT LIST的复杂度是 O(N),N 是当前连接数。连接数几千时执行毫无压力,但连接数上了五位数,并且在高并发时段频繁执行,它对主线程还是会造成可见的 CPU 开销,因为 Redis 需要遍历所有客户端连接并格式化字符串。输出也可能一次拉到几 MB。
所以我建议:监控告警尽量用INFO clients里的汇总指标,CLIENT LIST作为人工排障手段,不要做成每秒拉一次的监控脚本。如果确实需要周期性采集连接明细,频率控制在分钟级,并且加上type normal过滤减少数据量,尽量避开业务高峰的每秒级别采样。
另外,CLIENT LIST输出格式在不同版本可能有细微变化,脚本解析时不要依赖固定列位置,像前面示例那样用字段匹配的方式更稳妥。
5.3 团队规范与止血预案
结合多年踩坑经验,我建议在团队层面把下面几条做成规范:
第一,应用必须统一设置CLIENT SETNAME,连接名包含项目和实例信息。这条看起来简单,但执行到位能省掉大量跨团队沟通成本。
第二,合理配置maxclients和timeout。maxclients不要盲目调大,过大容易把 Redis 宿主机的文件描述符和内存耗尽,反而引发雪崩。timeout参数决定空闲连接多少秒后被服务端回收,默认 0 表示不回收,这个要根据业务特点设置,比如普通的缓存连接设 300 秒左右比较安全,但要小心长连接业务和主从复制连接不受该参数影响。
第三,定期巡检CLIENT LIST。我用一个简单的 shell 脚本每小时检查一次最大omem连接和连接数 TOP 10 的 IP,写成告警项:
#!/bin/bash redis-cli client list type normal | awk '{ for(i=1;i<=NF;i++){ if($i ~ /^omem=/) {split($i,a,"="); m=a[2]} if($i ~ /^addr=/) {addr=$i} } if(m > 10*1024*1024) print "big-omem", m, addr }'超过阈值就报警。把omem纳入日常巡检后,很多内存抖动问题能在业务感知前就被发现。
第四,准备一套连接止血预案:连接数打满时先按 IP 聚合、确认归属、CLIENT KILL临时清理;omem异常时判断是 Pub/Sub 慢消费者还是大 key 返回,分别采用踢连接和优化查询的方式处理;阻塞类问题通过flags区分是预期阻塞还是慢命令导致的非预期阻塞,后者顺着cmd定位到具体调用方。
我个人在实际操作中的体会是,CLIENT LIST用得越多,越觉得它像一张连接世界的"实时地图"——每次线上连接出问题,先不急着重启服务、扩容机器,而是静下心看看这张地图上的每一个连接在哪、在干什么、占了多少资源,往往问题答案就藏在那几行不起眼的字段里。最后分享一个小技巧:排查时千万别忘了CLIENT LIST里那个cmd字段,它记录的是连接此刻正在执行或最后执行过的命令,有时候一个诡异的cmd=keys比任何监控图表都更早暴露真相。