redis-py服务控制与状态监控实战:从shutdown到智能巡检

发布时间:2026/9/16 9:07:50
redis-py服务控制与状态监控实战:从shutdown到智能巡检 做 Redis 运维和排查线上问题的时候最头疼的往往不是增删改查这些日常操作而是服务突然异常时连个下手的工具都没有。连接数暴涨、内存飙升、慢查询刷屏、主从切换出岔子这些场景下真正救命的是 redis-py 里那一批服务控制与状态监控的辅助函数。这篇是 redis-py 系列第六篇聚焦 shutdown、bgsave、replicaof、config、info、slowlog、scan_iter 这一组日常存在感不高、但排查问题时极度好用的函数逐个讲清楚调用方式、参数含义和实际坑位最后给出一个可以直接拿来改造的巡检脚本。适合谁看已经能用 redis-py 做基本读写、但对服务控制这块还没有系统梳理过的开发者以及需要负责 Redis 实例日常巡检和故障排查的运维同学。不涉及 Redis 源码层面纯客户端使用视角别担心门槛。1. 服务控制函数不只是 shutdown 那么简单1.1 SHUTDOWN 与数据落盘策略平时业务代码里很少主动调 shutdown但在灰度发布、机器维护、自动化重置数据节点这些场景下shutdown 是绕不开的。redis-py 的 shutdown 方法对应 Redis 的 SHUTDOWN 命令最需要理解的是它和数据落盘之间的关系。默认情况下shutdown 会先执行一次 SAVE把内存里的数据完整写到 RDB 文件中再关闭进程。如果开启了 AOFRedis 也会保证 AOF 文件处于正常可恢复状态这样实例重启之后数据不丢。所以要安全关停一个实例直接r.shutdown()就行别的不用管。但有个参数叫 nosave很多人不知道什么时候该用。Redis 里 SHUTDOWN NOSAVE 的含义是跳过持久化直接退出进程。什么场景会用到最典型的是临时测试节点或者业务流量已经切走、纯下线一个没价值的实例时你不想让它在关停前花几秒钟去写 RDB就直接跳过。另一个场景是 RDB 文件已经损坏、或者你明确知道当前实例里的数据不需要保留那么加 nosave 能避免一些不可控的覆盖行为。实际操作里有个非常容易踩的坑对主从架构中的主节点直接执行 shutdown如果没有哨兵、没有管理平台自动切换业务会瞬间全断。而且 shutdown 之后连接会立刻被服务端关闭同一个 redis 连接对象再调 ping 会抛 ConnectionError。所以脚本里如果有 shutdown 后面的操作一定要重新创建连接别复用旧对象。注意shutdown 是个“有去无回”的命令。确认要关时再调关之前先确认故障切换机制就位、或者业务流量已经摘除否则就是事故现场。1.2 SAVE / BGSAVE / BGREWRITEAOF 的调用时机save 是同步落盘执行期间 Redis 会阻塞住所有读写请求都卡住。生产环境绝对不要用。bgsave 是 fork 子进程后台落盘不会阻塞主进程需要触发一次即时备份时用它。redis-py 里对应就是r.save()和r.bgsave()调起来很简单坑在后面。先说一个我踩过的误区bgsave 方法返回 True 不代表持久化一定成功。它只是表示 Redis 已经接受了 BGSAVE 命令后台子进程可能还在跑也可能 fork 时因为内存不足直接失败了。要确认 bgsave 到底成没成功必须去 INFO persistence 里看rdb_last_bgsave_status字段值是 ok 才是真的成功。我见过不止一次这样的场景运维脚本里调了 bgsave看到返回 True 就去云控制台删磁盘了结果实际 RDB 没生成数据直接没了。所以正确的用法是调完 bgsave 之后轮询 INFO直到rdb_bgsave_in_progress变为 0再检查 rdb_last_bgsave_status 是否为 ok这套确认流程一步都不能省。bgrewriteaof 是压缩 AOF 文件的命令只有 appendonly 开启时才有意义。当 AOF 文件增长到很大、或者你刚通过 config_set 把 appendonly 从 no 改成 yes 时建议主动调用一次r.bgrewriteaof()让 AOF 从全量快照重新开始记录体积会明显缩小。它同样是后台执行不会阻塞主进程。1.3 REPLICAOF 与主从切换控制replicaof 是 Redis 5.0 之后取代 slaveof 的命令用于把当前实例设置为某个主节点的从节点。redis-py 里对应的方法是r.replicaof(host, port)不传参数调用r.replicaof()则执行 REPLICAOF NO ONE取消当前实例的从属关系把它提升为主节点。这个函数在手动主从切换时是核心。一次典型的切换流程大概是这样的假设 A 是主节点、B 是从节点要让 B 接管。第一步对 B 执行B.replicaof()让它解除从属状态变成独立主节点第二步把业务连接切到 B第三步等 A 恢复后对 A 执行A.replicaof(B_host, B_port)让 A 重新挂到 B 下面做从节点。整个过程用 redis-py 脚本化可以做到分钟级完成。有几个关键点必须提醒。第一执行replicaof(host, port)后从节点会尝试与新主节点做全量同步同步期间按照 repl-diskless-sync 等配置从节点可能会先清空本节点已有数据再接收主节点快照。如果新主节点上的 RDB 数据不完整从节点数据会被清空到一个很旧的状态。第二REPLICAOF 命令本身只改运行状态不改配置文件所以手工切换完成后要记得把 redis.conf 里的 replicaof 配置也改掉否则实例重启后又会恢复到配置里的旧主从关系这是我们线上真实遇到过的回切事故。1.4 CONFIG 系列在线修改配置config_set 和 config_get 是运维必用的两个函数。读取配置用r.config_get(maxmemory)返回的是一个字典比如{maxmemory: 4294967296}。修改配置用r.config_set(maxmemory, 4gb)成功返回 True。在线改配置要分清楚两类参数。一类是改完即时生效、但不持久化的比如 maxmemory、maxmemory-policy、requirepass 这些运行时参数通过 config_set 调整后如果没写入配置文件重启就还原。另一类是危险操作型参数比如config_set(appendonly, yes)可以热开启 AOF但开启瞬间 Redis 会去做一次类似 BGSAVE 的全量写盘性能抖动是实打实的。我的习惯是每次手动 config_set 完成、验证无误后立即执行r.config_rewrite()把当前运行配置写回配置文件。这样既保证重启后配置不丢也避免下次改配置时搞不清配置文件里的旧值是什么。config_rewrite 在 redis-py 里对应r.config_rewrite()在 Redis 7.0 之后它还可以安全地处理那些原本不能重写的配置项更值得当作常规操作。1.5 CLIENT 系列连接管理排查连接数暴涨问题时r.client_list()是第一个要用的函数。它返回当前所有客户端连接的列表每个连接信息包含 id、addr、fd、name、db、cmd 等字段。你可以很直观地看到是哪个 IP、哪个客户端库、在哪个数据库上、正在执行什么命令。r.client_kill()可以按地址杀连接。比如定位到某个连接一直在跑 MONITOR或者某个应用实例占着连接不释放直接r.client_kill(addr)断开它。较新的 redis-py 版本还支持按 ID 或过滤条件杀比如client_kill(filter_id12345)。老版本只有 addr 一个参数写脚本时要注意 Redis 服务端版本和 redis-py 版本的兼容性。r.client_setname(app-xxx)给当前连接设置名称这个建议在创建连接池后统一做掉。线上几百个连接混在一起时连接名能帮你直接从 client_list 里分辨出是哪个服务、哪个功能模块发起的连接排查效率完全不一样。2. 状态监控INFO 是核心但远不止 INFO2.1 INFO 分节读取别用 info() 一把梭r.info()不带参数返回的是全量 INFO里面的 section 非常多信息量大到普通排查根本用不上。更合理的做法是按 section 精确读取r.info(memory)只拿内存相关r.info(clients)只拿连接相关r.info(stats)拿命令处理统计。分节读取不只是为了减少数据量更重要的是语义清晰方便把巡检字段映射到监控平台。比如内存告警看 memory 节里的 used_memory、used_memory_rss、maxmemory_policy连接异常看 clients 节的 connected_clients 和 blocked_clients持久化异常看 persistence 节的 rdb_last_bgsave_status、aof_last_write_status主从状态看 replication 节的 role 和 master_link_status。有个坑需要提醒不同 Redis 版本的 INFO 字段名不是完全一致的。Redis 6.0 之后新增了 errorstats 节Redis 6.2 之后部分字段做了重命名比如rdb_changes_since_last_save在不同版本里一直有小改动。巡检代码里建议对关键字段做一层变量映射或者用 try-except 兜底不要硬取字段否则升级 Redis 之后巡检脚本直接崩掉。2.2 内存监控MEMORY STATS 与 OBJECT ENCODING 组合used_memory 只是 Redis 分配器视角的内存使用量并不完全等于操作系统看到的 RSS。要真正定位内存问题建议用r.memory_stats()拿更细的分配器数据它会返回一个 JSON里面把 peak.allocated、total.allocated、startup.allocated、overhead.hashtable.main、overhead.hashtable.expires 等拆得非常清楚。定位内存碎片、哈希表开销过大的场景时这些字段非常有用。定位某个 key 占多少内存用r.memory_usage(key, samples5)。这是 MEMORY USAGE 命令的封装samples 参数表示采样的元素个数取值越大估算越准确但耗时越长默认 5 是效率和精度的平衡点。它返回的是字节数在巡检脚本里统一按字节比较判断大 key 时非常方便。排查大 value 的 key 时r.object(encoding, key)也值得配合使用。它返回的是 key 底层使用的编码方式比如 embstr、raw、hashtable、ziplist、quicklist 等。如果发现某个 key 编码是 raw 或 hashtable说明它已经脱离了小对象优化区间大概率是一个值得警惕的大 key 候选。2.3 SLOWLOG 慢查询排查r.slowlog_get(n)拉取最近 n 条慢查询返回的对象里包含 entry_id、duration 微秒耗时、command 数组、timestamp 时间戳。命令数组里每个元素是命令名和参数。一个流程很清晰的慢查询排查步骤是先r.slowlog_len()看慢查询总量然后r.slowlog_get(30)拉最近 30 条看一看高频慢命令集中在哪些指令上。慢查询的阈值由 slowlog-log-slower-than 配置控制默认是 10000 微秒也就是 10ms。注意这个单位是微秒不少人会把 1000 当成 1000ms实际只是 1ms判断阈值时容易产生偏差。如果你只关心更慢的请求可以r.config_set(slowlog-log-slower-than, 50000)把它调到 50ms。压测前后做对比时r.slowlog_reset()很有用。压测前清一次历史记录压测后再拉慢查询能精确拿到本轮压测产生的慢命令不会被历史数据干扰。我说一个真实经历某业务 Redis 偶发卡顿INFO stats 里 ops 完全正常找了很久都没头绪最后是 slowlog 抓到一个大 list 的 LTRIM 操作耗时 300 多毫秒才把元凶定位到。慢查询往往是隐性问题的先兆建议纳入日常巡检。2.4 LATENCY 与 DEBUG OBJECT 的补充Redis 2.8.13 之后引入了 LATENCY 命令用r.execute_command(LATENCY, LATEST)可以查看最近的事件延迟采样。这个功能比 slowlog 更前置因为它记录的是事件循环的等待延迟一旦事件循环被阻塞Redis 是全服务停顿慢查询可能都来不及记录下来而 latency 能抓到这个证据。r.debug_object(key)返回 key 底层对象的细节其中 serializedlength 给出序列化后的字节长度。定位大 key 时的路径通常是scan_iter 把所有 key 扫一遍配合 memory_usage 按大小排序找出可疑目标之后再用 debug_object 确认序列化长度判断是否值得拆 key 或者换更紧凑的数据结构。需要注意 DEBUG 系列命令在云 Redis 上可能被禁掉或者在安全策略里被限制使用前先确认服务端允许 DEBUG 命令执行否则脚本里加个 try-except 兜底。3. 辅助函数批量、扫描、订阅场景实战3.1 SCAN_ITER 与大 Key 定位keys 命令在生产环境绝对不要用这个是 Redis 运维的第一条铁律因为 keys 全量匹配时会把整个 Redis 卡死。r.scan_iter(matchuser:*, count1000)内部走的是 SCAN 游标协议每次迭代只扫一部分数据不会阻塞主进程是遍历大量 key 的唯一安全姿势。count 参数控制的是每次迭代的桶数不是返回的 key 数量。1000 是实际使用中比较均衡的选择太小扫描速度慢、需要更多轮次太大会拉长单次命令响应时间有可能触发慢查询。需要遍历所有 key 时直接r.scan_iter()不传 match 即可。扫描结果配合 memory_usage可以做一个快速的大 key 检测器。下面的代码在 4.2 节会完整给出思路就是遍历所有 key用 memory_usage 检查每个 key 的字节数超过阈值就记录到列表里。注意 SCAN 游标扫描期间key 可能新产生也可能被删除所以结果只是一个近似快照别把它当成精确计数。3.2 Pipeline 不要只当批量操作工具r.pipeline(transactionFalse)可以把一批命令打包发送减少网络往返。很多人只把它当批量写入工具其实在监控辅助场景下它同样好使。举个例子巡检时你要检查一批 key 的剩余 TTL假设有 5000 个 key逐个执行 TTL 命令要发 5000 次请求几百毫秒都未必跑完。改用 pipeline 把 5000 个 TTL 命令一次性发出去一次 execute() 就拿到全部结果耗时可以缩小一个数量级。批量检查 key 是否存在、批量获取多个哈希字段也是同样的逻辑。注意 pipeline(transactionTrue) 是 MULTI/EXEC 事务模式所有命令会打包在一个事务里执行存在事务开销和原子性语义。如果只是批量读取建议用 transactionFalse性能更好语义也更纯粹。3.3 PubSub 做实时监控告警Redis 的 keyspace notification 可以让你在 key 变化时收到通知。redis-py 里先创建发布订阅对象ps r.pubsub()然后ps.subscribe(__keyevent0__:set)订阅某个数据库的 SET 事件处理完后在ps.listen()循环里消费消息。这个能力可以做成一个极简的实时缓存变更监听器。业务层需要刷新本地缓存时订阅 key 前缀对应的更新事件和过期事件收到消息后按 key 增量刷新比定时全量轮询优雅得多。前提是 redis.conf 里开启了 notify-keyspace-events比如设置为 Ex 表示只暴露 key 过期事件KEA 表示全部事件。默认是关闭的没开启时订阅后一条消息都收不到排查时会很困惑。PubSub 还有一个坑Redis 的 PubSub 是即发即弃的消费者不在线期间的消息全部丢失。比如订阅过期事件做缓存清理如果监控程序正好重启了几秒钟这期间的过期事件就漏掉了。所以 PubSub 适合做实时提示型监控不适合做可靠消息传递。3.4 连接池与超时参数监控redis-py 的持续运行依赖连接池但连接池本身的状态也值得关注。r.connection_pool内部有连接数量统计不过更标准的做法是用 INFO clients 里的 connected_clients 判断总连接数。连接池耗尽时redis-py 默认会等待 pool_timeout默认值不算小如果业务并发高大量请求会积压在这个等待上表面现象就是接口变慢但 Redis 本身没异常。建议根据业务场景显式设置连接池参数。一个实际用的配置是max_connections50socket_timeout3socket_connect_timeout3retry_on_timeoutFalse。socket_timeout 尤其重要默认 None 会导致请求永远等待一旦 Redis 实例异常卡住所有客户端请求都会跟着挂起没有超时机制的话只能重启应用进程。连接池的监控还有一个实用技巧定期统计 connection_pool 的 _created_connections 和 _available_connections结合 INFO clients 的 connected_clients能判断出连接池缩扩容是否符合预期。不过这些下划线字段在不同 redis-py 版本里变化比较频繁生产代码还是优先以 INFO 输出为准。4. 组合实战写一个 Redis 实例巡检脚本4.1 巡检项设计把前面讲的函数组合起来可以封装成一个能定时跑的巡检脚本。巡检项设计如下基础连通性ping、运行版本与时长、客户端连接数、内存使用量、持久化最近状态、慢查询条数、大 key 线索、主从角色状态。每一项都能从前面详解的函数里直接拿到数据脚本整体没有太复杂的逻辑。阈值建议单独抽出来配置方便不同环境调参。比如内存使用率超过 80% 就报警大 key 超过 10MB 就告警慢查询超过 20ms 且数量超过 10 条就提示风险。这里我给出一个可以改造成独立模块的报警阈值结构。巡检执行完毕后建议统一输出成 JSON 结构方便对接监控平台或者直接落日志。巡检本身也有频率建议常规巡检 1 分钟一次足够大 key 扫描类操作更重建议 10 分钟到 30 分钟一次避免 SCAN 对高负载实例产生额外压力。4.2 关键代码实现import redis import json import time class RedisInspector: def __init__(self, redis_client, configNone): self.client redis_client self.config config or { memory_max_ratio: 0.8, big_key_threshold: 10 * 1024 * 1024, slowlog_threshold_us: 20000, slowlog_warn_count: 10, } def _check_connectivity(self): try: self.client.ping() return {connected: True} except redis.exceptions.RedisError as e: return {connected: False, error: str(e)} def _collect_basic_info(self): info self.client.info() return { redis_version: info.get(redis_version), uptime_in_seconds: info.get(uptime_in_seconds), role: info.get(role), } def _collect_memory(self): info self.client.info(memory) used_memory info.get(used_memory, 0) maxmemory info.get(maxmemory, 0) rss info.get(used_memory_rss, 0) return { used_memory: used_memory, used_memory_human: info.get(used_memory_human), rss: rss, maxmemory: maxmemory, ratio: used_memory / maxmemory if maxmemory else 0.0, } def _collect_clients(self): info self.client.info(clients) return { connected_clients: info.get(connected_clients), blocked_clients: info.get(blocked_clients), } def _collect_persistence(self): info self.client.info(persistence) return { rdb_last_bgsave_status: info.get(rdb_last_bgsave_status), aof_enabled: info.get(aof_enabled), aof_last_write_status: info.get(aof_last_write_status), } def _collect_slowlog(self): slowlogs self.client.slowlog_get(20) slow_count 0 slow_commands [] for log in slowlogs: if log.duration self.config[slowlog_threshold_us]: slow_count 1 slow_commands.append({ id: log.entry_id, duration_us: log.duration, command: log.command, }) return { slowlog_total: self.client.slowlog_len(), slow_count_above_threshold: slow_count, slow_commands: slow_commands[:5], } def _scan_big_keys(self, limit20): big_keys [] for key in self.client.scan_iter(count500): try: size self.client.memory_usage(key, samples5) if size and size self.config[big_key_threshold]: big_keys.append({key: key, bytes: size}) except redis.exceptions.ResponseError: continue big_keys.sort(keylambda x: x[bytes], reverseTrue) return big_keys[:limit] def inspect(self): result { timestamp: int(time.time()), connectivity: self._check_connectivity(), } if not result[connectivity][connected]: return result result[basic] self._collect_basic_info() result[memory] self._collect_memory() result[clients] self._collect_clients() result[persistence] self._collect_persistence() result[slowlog] self._collect_slowlog() result[big_keys] self._scan_big_keys() warnings [] if result[memory][maxmemory] 0 and result[memory][ratio] self.config[memory_max_ratio]: warnings.append(内存使用率超过阈值) if result[persistence][rdb_last_bgsave_status] ! ok: warnings.append(RDB 最近一次 bgsave 未成功) if result[slowlog][slow_count_above_threshold] self.config[slowlog_warn_count]: warnings.append(慢查询数量异常) if result[big_keys]: warnings.append(检测到大 key) result[warnings] warnings return result if __name__ __main__: r redis.Redis( host127.0.0.1, port6379, db0, socket_timeout3, socket_connect_timeout3, ) inspector RedisInspector(r) report inspector.inspect() print(json.dumps(report, indent2, ensure_asciiFalse, defaultstr))这个脚本运行一次就能输出实例的整体健康度快照。注意 big_keys 扫描部分是唯一比较重的操作如果实例的 key 数量非常庞大建议把 scan_iter 的 count 调小把扫描频率降到 30 分钟一次或者直接放到后台任务调度。4.3 输出与告警巡检脚本的输出是一个 JSON里面每一项都对应一个监控指标。接入监控平台时可以直接把这个 JSON 解析后映射到已有的监控指标如果是自建告警只需要对 warnings 列表做非空判断即可。告警的阈值不要拍脑袋定。内存使用率、慢查询次数、大 key 大小这些阈值建议先跑两周历史数据观察正常波动范围后再定。我见过团队把大 key 阈值设成 1MB结果每天告警几百条告警疲劳之后真正的大 key 反而没人看了。巡检脚本的调度用 cron 或者系统定时任务都可以。一个经验是时间点错开整点比如每小时的 3 分、18 分、33 分、48 分跑一次避免和大量定时任务挤在一起。5. 常见坑位与排查实录5.1 shutdown 执行后连接池失效shutdown 会关闭当前 redis 连接但连接池里的其他连接在服务端关闭后处于半开状态第一次使用时会抛出 ConnectionErrorredis-py 会自动重连重连前可能有一到两次报错。所以巡检脚本或者管理平台里如果要连续执行多个控制命令shutdown 应该是最后一个动作或者执行完直接重建连接对象。5.2 bgsave 失败排查顺序bgsave 失败后先看 INFO persistence 里的 rdb_last_bgsave_status 给出的具体失败原因。最常见的几个fork 子进程时内存不足、磁盘空间不足、repl-diskless-sync配置冲突。排查顺序建议是先 df -h 看磁盘再 free -m 看内存有没有给 fork 留够余量最后看日志里有没有 fork 失败的具体报错。不要一上来就重启实例重启只能解决一时的内存碎片问题治标不治本。5.3 memory 统计口径的偏差used_memory 是 Redis 分配器统计的内存和 used_memory_rss 经常有偏差特别是碎片率高的时候。判断内存告警两个值都要看used_memory 反映的是逻辑使用量used_memory_rss 反映的是实际占用的物理内存。如果 rss 比 used_memory 大很多说明碎片率很高可以用r.execute_command(MEMORY, PURGE)尝试整理 jemalloc 的 arena但注意这只是释放可回收的碎片并不能解决整体增长的问题。5.4 ACL 权限问题Redis 6.0 之后启用 ACL 后普通业务账号可能没有执行 CONFIG、SLOWLOG、MEMORY 这些管理命令的权限。巡检脚本如果用业务账号跑会疯狂抛 PermissionError。建议为巡检创建专用账号只授予 CONFIG GET、SLOWLOG、INFO、MEMORY、CLIENT 等巡检所需命令的权限不要直接给 allcommands。5.5 monitor 生产慎用r.execute_command(MONITOR)能实时打印所有命令听起来很适合排查问题但 MONITOR 在高并发实例上会消耗大量 CPU拖垮 Redis 本身。能用 slowlog 和 INFO 解决的就别用 MONITOR。真要抓某一波突发流量时限时开启、用完立即关闭别让它一直运行。另外一个容易被忽略的问题是redis-py 的 pubsub 连接是独占连接的如果复用同一个连接池里的连接做订阅操作可能把连接池的连接占满影响普通读写。订阅专用连接应该单独创建不放入业务连接池。我个人在实际操作中的体会是服务控制和状态监控这一块真正拉开运维效率差距的往往不是某个函数本身而是你把它组合进巡检流程的完整度。shutdown、bgsave、replicaof 这类控制操作平时用得少关键时刻一旦用错就是事故info、slowlog、scan_iter 这类监控手段平时不起眼但能帮你把大多数隐患消灭在告警之前。建议把这套巡检脚本在自己的环境里跑起来先看几天输出你会对自己管理的 Redis 实例有完全不一样的理解。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询