Redis 运维与故障排查

📌 本文由 7 篇相关文章智能合并整理而成

Redis 客户端连接异常处理

Redis 客户端连接异常处理

常见连接异常类型

1. 连接超时

# 错误信息
# redis.exceptions.TimeoutError: Timeout connecting to server

# 或
# Cannot assign requested address (Java: ConnectException)

# 原因
# - Redis 负载过高,无法接受新连接
# - 网络延迟过高
# - 防火墙阻止了连接
# - 连接数超过 maxclients

2. 连接被拒绝

# 错误信息
# redis.exceptions.ConnectionError: Error 111 connecting to host:6379. Connection refused.

# 原因
# - Redis 服务未启动
# - Redis 绑定到错误的 IP/端口
# - 防火墙拦截
# - maxclients 达到上限

3. 连接中断

# 错误信息
# redis.exceptions.ConnectionError: Connection closed by server.

# 原因
# - Redis 超时关闭空闲连接(timeout 配置)
# - Redis 关闭连接(client-output-buffer-limit 触发)
# - 网络断开
# - Redis 重启

4. 读取超时

# 错误信息
# redis.exceptions.TimeoutError: Timeout reading from socket

# 原因
# - 慢查询导致响应延迟
# - 网络延迟波动
# - Redis CPU 过高
# - 返回大量数据

排查步骤

第一步:检查 Redis 服务状态

# 1. 检查 Redis 是否运行
systemctl status redis-server
# Active: active (running) since ...

# 2. 检查 Redis 是否可访问
redis-cli PING
# PONG

# 3. 检查绑定配置
redis-cli CONFIG GET bind
# 1) "bind"
# 2) "0.0.0.0"  ← 监听所有接口
# 或 "127.0.0.1" ← 只能本机访问

# 4. 检查保护模式
redis-clI CONFIG GET protected-mode
# "yes"  ← 保护模式下有限制

第二步:检查 Redis 统计

# 1. 连接信息
redis-cli INFO Clients
# connected_clients:5000
# client_longest_output_list:0
# client_biggest_input_buf:0
# blocked_clients:0

# 2. 拒绝连接统计
redis-cli INFO Stats | grep rejected_connections
# rejected_connections:100  ← 时间窗口内拒绝了 100 次连接

# 3. 最大连接数
redis-cli CONFIG GET maxclients
# "10000"

第三步:检查客户端连接

# 查看所有客户端连接
redis-cli CLIENT LIST | head -20

# 统计各 IP 连接数
redis-cli CLIENT LIST | awk '{print $2}' | cut -d= -f2 | cut -d: -f1 | sort | uniq -c | sort -nr

第四步:检查应用日志

# Java 应用日志
tail -100 /var/log/app/application.log | grep -i redis
# Redis connection error - Max retries reached

# Python 应用日志
tail -100 /var/log/app/app.log | grep -i redis
# Connection to Redis lost, reconnecting...

常见原因及解决方案

原因 1:连接池耗尽

# Java Jedis 连接池配置检查
# 问题配置
maxTotal: 10        # 池子很小
maxWaitMillis: 100  # 等待时间很短

# 症状
# - 高峰期大量连接超时
# - 应用日志出现 "Could not get a resource from the pool"

# 解决方案
maxTotal: 50        # 根据 QPS 和响应时间计算
maxIdle: 20         # 合适空闲连接数
minIdle: 5          # 保持基本连接
maxWaitMillis: 3000 # 合理的等待时长
testOnBorrow: true  # 获取时验证连接有效性

原因 2:Redis 连接空闲超时

# redis.conf
timeout 300  # 默认 300 秒,连接空闲 5 分钟关闭

# 如果应用使用长连接,但长时间没有请求
# Redis 会关闭连接

# 解决方案
# 1. 增大 timeou(或设为 0 禁止超时)
timeout 0

# 2. 应用侧使用心跳(保持活跃)
# Jedis 示例:测试连接是否有效
jedis.ping()

原因 3:输出缓冲区溢出

# redis.conf
client-output-buffer-limit normal 0 0 0          # 普通客户端
client-output-buffer-limit pubsub 32mb 8mb 60    # 发布订阅客户端
client-output-buffer-limit slave 256mb 64mb 60   # 从库客户端

场景:订阅者消费速度跟不上,输出缓冲区满,连接被强制关闭。

# 解决方案
# 1. 增加缓冲区限制
client-output-buffer-limit pubsub 64mb 16mb 120

# 2. 优化消费者逻辑(消费速度 > 生产速度)
# 3. 使用 Stream 替代 Pub/Sub(支持消费者组、ACK)

原因 4:DNS 解析异常

# 症状
# 连接 Redis 时出现 "Name or service not known"

# 原因
# - DNS 记录变更
# - /etc/hosts 配置错误
# - DNS 服务器不可用

# 解决方案
# 1. 使用 IP 地址代替域名
# 2. 设置合理的 DNS 缓存
# 3. 配置 /etc/hosts 静态解析
echo "10.0.0.1 redis-master" >> /etc/hosts

原因 5:TCP 连接未释放

# 症状
# 应用端出现 "Too many open files"

# 检查
lsof -p $(pgrep -f java | head -1) | wc -l
# 5000  ← 文件描述符过多

# 解决方案
# 1. 使用连接池(避免短连接)
# 2. 设置 TCP keepalive
sysctl -w net.ipv4.tcp_keepalive_time=120
sysctl -w net.ipv4.tcp_keepalive_intvl=30
sysctl -w net.ipv4.tcp_keepalive_probes=3

# 3. 增大 ulimit
ulimit -n 65535

客户端最佳实践

Java (Jedis/Lettuce)

// 1. 使用连接池
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(50);
poolConfig.setMaxIdle(20);
poolConfig.setMinIdle(5);
poolConfig.setTestOnBorrow(true);
poolConfig.setTestOnReturn(true);
poolConfig.setTestWhileIdle(true);

// 2. 设置合理的超时
JedisPool pool = new JedisPool(poolConfig, "localhost", 6379, 
                                2000,  // connectionTimeout
                                2000,  // soTimeout
                                null); // password

// 3. 使用 try-with-resources 确保归还连接
try (Jedis jedis = pool.getResource()) {
    jedis.get("key");
}

// 4. 配置重试机制(Lettuce 支持自动重连)
// 5. 监控连接池

Python (redis-py)

# 1. 连接池配置
pool = redis.ConnectionPool(
    host='localhost',
    port=6379,
    db=0,
    max_connections=50,
    socket_connect_timeout=2,
    socket_timeout=2,
    retry_on_timeout=True,
    health_check_interval=30,  # 定期健康检查
)

# 2. 使用连接池
r = redis.Redis(connection_pool=pool)

# 3. 重试装饰器
from retry import retry

@retry(redis.exceptions.RedisError, tries=3, delay=0.1)
def get_redis_data(key):
    return r.get(key)

监控与告警

# Prometheus 监控指标
- metrics:
  - redis_connected_clients          # 连接数
  - redis_rejected_connections_total  # 拒绝连接数
  - redis_uptime_in_seconds          # 运行时间
  - redis_total_connections_received  # 累计连接数
  - redis_mem_fragmentation_ratio     # 内存碎片

# 告警规则
- alert: RedisConnectionRejected
  expr: rate(redis_rejected_connections_total[1m]) > 0
  for: 1m
  annotations:
    summary: "Redis 拒绝连接,可能是连接数或内存已满"

面试要点

  • 连接异常三大类:超时、拒绝、中断
  • 排查四步走:服务状态 → 连接统计 → 客户端列表 → 应用日志
  • 连接池是根因:配置不当是大部分连接异常的源头
  • 超时设置:connectTimeout、socketTimeout 必须合理设置
  • 重试机制:必须有但不可无限重试(防雪崩)
  • 监控指标:connected_clients、rejected_connections
  • 资源管理:连接使用完毕及时归还,避免泄漏

Redis OOM command not allowed 错误排查

Redis OOM command not allowed 错误排查

错误现象

redis-cli SET order:10001 "new order"
# (error) OOM command not allowed when used memory > 'maxmemory'.

这是 Redis 内存达到 maxmemory 上限时,禁止写入操作的经典报错。

错误触发条件

# 触发条件:
used_memory >= maxmemory   AND   maxmemory-policy = noeviction

# 查看当前状态
redis-cli INFO Memory | grep -E "used_memory:|maxmemory:|maxmemory_policy"
# used_memory_human:7.50G
# maxmemory_human:7.50G       ← 内存已满
# maxmemory_policy:noeviction  ← 不淘汰策略

排查步骤

第一步:确认内存使用情况

# 详细内存信息
redis-cli INFO Memory
# used_memory:8053063680       # 8GB
# used_memory_rss:10066329600  # RSS 10GB
# used_memory_peak:8589934592  # 峰值 8.5GB
# maxmemory:8053063680         # 最大 8GB
# maxmemory_policy:noeviction  # 不淘汰
# mem_fragmentation_ratio:1.25 # 碎片率

# 内存使用率 = 8GB / 8GB = 100%

第二步:检查淘汰策略

# 当前淘汰策略
redis-cli CONFIG GET maxmemory-policy
# "noeviction"

# 可选策略对比
# noeviction      → 内存满时写入报错(默认,安全但不实用)
# allkeys-lru     → 淘汰最近最少使用的 key(缓存场景推荐)
# volatile-lru    → 只淘汰有 TTL 的 key(适用于混合存储)
# allkeys-lfu     → 淘汰使用频率最低的 key
# volatile-ttl    → 淘汰最快过期的 key
# allkeys-random  → 随机淘汰

第三步:查找内存消耗来源

# 1. 查看 keyspace 统计
redis-cli INFO Keyspace
# db0:keys=500000,expires=300000,avg_ttl=7200
# 50 万 key,其中 30 万有 TTL

# 2. 查找大 key
redis-cli --bigkeys
# Biggest string found 'session:10001' has 10485761 bytes

# 3. 按前缀统计内存
# 使用 redis-rdb-tools (rdb -c memory) 或开发模式扫描

第四步:检查业务代码

// 常见场景:业务逻辑中大量写入但没有过期时间
public void saveSession(String sessionId, Object data) {
    String key = "session:" + sessionId;
    // ❌ 没有设置过期时间
    redisTemplate.opsForValue().set(key, data);

    // ✅ 应该设置过期时间
    redisTemplate.opsForValue().set(key, data, 30, TimeUnit.MINUTES);
}

紧急处理

方案 1:临时扩容

# 运行时增大 maxmemory
redis-cli CONFIG SET maxmemory 12G

# 注意:需要有足够的物理内存
free -h
# total=16G, available>12G

方案 2:修改淘汰策略

# 临时改为淘汰策略,允许写入
redis-cli CONFIG SET maxmemory-policy allkeys-lru

# 检查淘汰是否生效
redis-cli INFO Stats | grep evicted_keys

# 注意:
# - 这会淘汰一些 key,可能导致缓存失效
# - 生产环境谨慎操作,评估影响

方案 3:立即清理

# 删除无用的 key
redis-cli DEL temp:batch_001
redis-cli UNLINK temp:batch_002  # 异步删除(推荐)

# 批量清理特定前缀(小心使用)
redis-cli SCAN 0 MATCH "temp:*" COUNT 1000 | while read key; do
    redis-cli UNLINK "$key"
done

# 批量设置过期时间
redis-cli SCAN 0 MATCH "session:*" COUNT 1000 | while read key; do
    redis-cli EXPIRE "$key" 3600  # 1 小时过期
done

方案 4:在从库执行强制操作

# 如果主库不能写,但在从库可以读
# 检查从库配置
redis-cli -p 6380 CONFIG GET maxmemory
redis-cli -p 6380 CONFIG GET maxmemory-policy

# 从库通常也有内存限制
# 可以考虑从从库读取数据评估影响

长期预防

1. 配置合理的淘汰策略

# redis.conf
# 缓存场景推荐
maxmemory 8G
maxmemory-policy allkeys-lru
maxmemory-samples 10              # LRU 采样数

# 混合场景(部分数据不能丢)
# 设置 volatile-lru,并确保所有 key 都已设置 TTL
maxmemory-policy volatile-lru

2. 监控预警

# 内存预警阈值
# - WARNING: 80% maxmemory
# - CRITICAL: 95% maxmemory

# 可以用 redis_exporter + Prometheus 监控

3. 业务规范

# 业务代码规范
# 1. 所有缓存 key 必须设置过期时间
def set_cache(key, value, ttl_seconds=3600):
    redis.set(key, value, ex=ttl_seconds)

# 2. 定期清理垃圾 key
def cleanup_temp_keys():
    cursor = 0
    while True:
        cursor, keys = redis.scan(cursor, match="temp:*", count=100)
        if keys:
            redis.unlink(*keys)
        if cursor == 0:
            break

# 3. 使用最大内存限制的本地缓存
from functools import lru_cache
@lru_cache(maxsize=10000)
def get_user_profile(user_id):
    return redis.get(f"user:{user_id}")

4. 容量规划

# 增长率监控
# 如果每天增长 500MB
# maxmemory = 8G
# 剩余容量 = 8G - 7.5G = 0.5G
# 可支撑天数 = 0.5G / 0.5GB/天 = 1 天

# 需要 3 天内扩容或优化

与淘汰策略的配合

# 不同策略下的 OOM 行为

# noeviction(默认)
# → 内存满时 IO 写命令全部返回 OOM
# → 读命令正常

# allkeys-lru / volatile-lru
# → 内存满时自动淘汰旧 key
# → 淘汰量突然增大 → 缓存命中率下降

# allkeys-lfu
# → 按照使用频率淘汰
# → 适合数据访问频率差异大的场景

面试要点

  • OOM 只影响写:读操作不受影响
  • 根本原因:maxmemory 满了 + noeviction 策略
  • 紧急处理:扩容 / 改策略 / 清理
  • 长期方案:合理设置淘汰策略 + 业务代码规范
  • 关键监控指标:内存使用率、淘汰速率、maxmemory
  • 与操作系统 OOM 不同:Redis OOM 是写保护,不是进程被 kill
  • 碎片坑:RSS 可能比 used_memory 大很多,maxmemory 基于 used_memory

Redis 集群通信超时排查

Redis 集群通信超时排查

集群通信机制

Redis Cluster 中各节点通过 gossip 协议进行通信。每个节点定时向其他节点发送 PING 消息,并等待 PONG 响应。

集群节点 A ── PING ──→ 集群节点 B
           └── PONG ←─┘

关键配置

# redis.conf(集群相关)
cluster-enabled yes                    # 启用集群模式
cluster-node-timeout 15000             # 节点超时时间(15 秒)
cluster-require-full-coverage no       # 部分节点故障时继续服务
cluster-migration-barrier 1            # 从库迁移门槛

通信超时的常见原因

原因 1:网络问题

# 检查节点间延迟
redis-cli -h node1 -p 6379 PING
# PONG
# 返回时间 < 1ms → 正常
# 返回时间 > 100ms → 有问题

# 检查节点间连通性
nmap -p 6379,16379 node1 node2 node3
# PORT     STATE SERVICE
# 6379/tcp open  redis
# 16379/tcp open  redis-cluster-bus

集群总线端口:每个节点需要两个端口
6379:客户端连接端口
16379:集群总线端口(= 客户端端口 + 10000)

原因 2:节点负载过高

# 检查节点负载
redis-cli INFO CPU
# used_cpu_sys:1200.50          # 内核态 CPU 时间
# used_cpu_user:3500.30         # 用户态 CPU 时间
# used_cpu_sys_children:100.20  # 子进程 CPU
# used_cpu_user_children:200.10

# 节点负载过高导致:
# - 无法及时响应集群总线的 PING
# - gossip 消息处理被延迟
# - 其他节点判定该节点超时

原因 3:节点间时钟不同步

# 时钟不同步的影响
# Redis Cluster 使用时间戳判断消息的新旧

# 检查节点时间
date
# 节点 A: Mon May 18 16:00:00 CST 2026
# 节点 B: Mon May 18 15:59:30 CST 2026  ← 慢了 30 秒

# 时间差 > cluster-node-timeout 时,节点可能被误判为故障

# 解决方案:配置 NTP 时间同步
ntpdate -u ntp.aliyun.com
systemctl enable ntpd

原因 4:集群规模过大

# 集群节点数与 gossip 流量关系
N 个节点:每个节点需要维护 N-1 个连接
gossip 消息量  # 节点数  gossip PING 频率
# 3        低
# 10       中等
# 50       较高
# 100+     很高,网络开销显著

# 大集群的优化
cluster-node-timeout 30000     # 增大超时
# 或者拆分为多个小集群

原因 5:槽位迁移期间

# 槽位迁移过程中:
# - 源节点和目标节点的数据在同步
# - 这期间节点可能响应变慢

# 查看槽位迁移状态
redis-cli CLUSTER INFO
# cluster_state:ok
# cluster_slots_assigned:16384
# cluster_slots_ok:16384
# cluster_slots_pfail:0
# cluster_slots_fail:0
# cluster_known_nodes:6
# cluster_size:3

# 如果 cluster_slots_pfail > 0,说明部分节点正在迁移

原因 6:节点故障

# 查看节点状态
redis-cli CLUSTER NODES
# 节点 ID                           IP:端口  角色  状态
# 7e5e8289f1c1cf2b0d08c5dfb6e0c22522f77f88 10.0.0.1:6379@16379 master - 0 1700000000000 0 connected
# 8f6f9390a2d2df3c1d09c6dfb7f1d33633g88e99 10.0.0.2:6379@16379 slave 7e5e8289... 0 1700000000001 3 connected
# 9g7g0401b3e3eg4d2e10c7dfb8g2d44744h99f00 10.0.0.3:6379@16379 master - 0 1700000000002 2 connected

# 状态说明
# connected:正常连
# disconnected:断开
# fail:故障
# handshake:正在握手中

排查步骤

第一步:确认超时范围

# 查看所有节点状态
for node in node1 node2 node3; do
    echo "=== $node ==="
    redis-cli -h $node CLUSTER INFO | grep -E "cluster_state|cluster_slots"
    redis-cli -h $node CLUSTER NODES | awk '{print $1, $2, $3, $8}'
done

第二步:检查节点日志

# 查看 Redis 日志
tail -100 /var/log/redis/redis-server.log | grep -E "timeout|fail|error|cluster"

# 关键日志
# "*** FAIL ***" → 节点被标记为故障
# "Connecting to Node" → 节点重连
# "Unable to connect" → 连接失败
# "hours without contact" → 长时间失联

第三步:网络诊断

# 1. 节点间延迟
mtr -r node1 node2

# 2. 检查集群总线端口
ss -tlnp | grep 16379

# 3. 查看 TCP 连接状态
netstat -ant | grep 16379 | awk '{print $6}' | sort | uniq -c
# 10 ESTABLISHED  ← 正常
# 3 TIME_WAIT     ← 可能有抖动

第四步:检查资源使用

# CPU 使用率
top -bn1 | grep redis

# 内存使用
redis-cli INFO memory | grep used_memory_human

# 网络带宽
sar -n DEV 1 5 | grep eth0

# 磁盘 I/O(持久化导致)
iostat -x 1 5 | grep sda

解决方案

1. 调整超时配置

# 根据网络环境调整
cluster-node-timeout 30000      # 从 15 秒增大到 30 秒

2. 优化网络

# 同机房部署(推荐)
# 集群节点间的延迟应 < 1ms

# 跨机房部署时
# - 使用专线连接
# - 增大 cluster-node-timeout

3. 均衡负载

# 热点 key 分布不均导致单个节点负载过高

# 查看槽位分布
redis-cli CLUSTER KEYSLOT hot_key
# (integer) 12345  ← 该 key 在 12345 号槽

redis-cli CLUSTER SLOTS | grep "12345"
# 找到该槽位所属节点

# 解决方案
# - 业务上拆分热点 key
# - 使用 hash tag 做数据分片

4. 踢出故障节点

# 如果节点确实无法恢复

# Step 1: 在主库迁移槽位
redis-cli CLUSTER SETSLOT slot IMPORTING node-id
redis-cli CLUSTER SETSLOT slot MIGRATING node-id
redis-cli MIGRATE target-host target-port key 0 5000

# Step 2: 移除故障节点
redis-cli CLUSTER FORGET node-id

# Step 3: 添加新节点
redis-cli CLUSTER MEET new-host new-port

面试要点

  • 集群总线端口:客户端端口 + 10000(常被防火墙忽略)
  • gossip 协议:节点间通过 PING/PONG 维护状态
  • 超时机制:cluster-node-timeout 内无响应 → PFAIL → FAIL
  • 时钟同步:NTP 时间同步对集群稳定至关重要
  • 大集群问题:节点越多,gossip 流量约大
  • 槽位迁移影响:迁移期间节点负载上升
  • 防火墙常见坑:漏开放 16379 端口导致集群通信失败

Redis 主从复制断开恢复

Redis 主从复制断开恢复

主从复制断开的常见原因

网络问题

Redis 主库 ──── 网络中断 ──── 从库
                    ↓
              复制连接断开
              从库进入断线重连状态
  • 网络抖动、防火墙中断
  • 跨机房专线故障
  • 带宽打满导致超时

主库问题

主库
  ├── BGSAVE 期间响应变慢 → 从库心跳超时
  ├── 内存满导致写阻塞 → 从库无法同步
  ├── 主库负载过高 → 处理复制请求延迟
  └── 主库宕机 → 从库完全断开

从库问题

从库
  ├── 从库 BGSAVE/AOF 重写 → 同步延迟
  ├── 从库内存满 → 无法写入新数据
  └── 从库配置问题 → repl-timeout 设置不合理

断开后的行为

从库的重连机制

复制断开
  ↓
从库进入重连状态(reconnecting)
  ↓
从库定时尝试重连(repl-timeout / 2 间隔)
  ↓
连接恢复?
  ├── Yes → 检查复制积压缓冲区
  │   ├── 数据还在积压缓冲区 → 部分同步(PSYNC)
  │   └── 数据已过期 → 全量同步(FULL RESYNC)
  └── No → 继续重试

查看复制状态

# 检查复制状态
redis-cli INFO replication
# role:slave
# master_host:10.0.0.1
# master_port:6379
# master_link_status:down  ← 关键指标
# master_last_io_seconds_ago:120  ← 上次通信时间
# master_sync_in_progress:0
# slave_read_repl_offset:100000
# slave_repl_offset:100000
# master_link_down_since_seconds:3600  ← 断开已 1 小时

# 主库侧查看
redis-clI INFO replication
# role:master
# connected_slaves:2
# slave0:ip=10.0.0.2,port=6380,state=online,offset=100000,lag=0
# slave1:ip=10.0.0.3,port=6381,state=offline  ← 从库离线

恢复方式

自动恢复

# 网络恢复后,从库会自动重连
# 大部分情况下不需要人工干预

# 监控重连状态
watch -n 5 "redis-cli INFO replication | grep -E 'master_link_status|master_last_io'"

手动强制重连

# 如果自动重连失败,手动触发
redis-cli SLAVEOF NO ONE          # 断开当前主库
redis-cli SLAVEOF master-ip 6379  # 重新建立主从关系

全量同步触发

# 如果复制积压缓冲区中的数据已过期,会触发全量同步
# 全量同步过程:
# 1. 主库 BGSAVE 生成 RDB
# 2. 传输 RDB 到从库
# 3. 从库加载 RDB
# 4. 从库追赶增量数据

# 触发全量同步(清空从库数据)
redis-cli SLAVEOF NO ONE
redis-cli FLUSHALL
redis-cli SLAVEOF master-ip 6379

复制积压缓冲区(repl-backlog)

什么是积压缓冲区

复制积压缓冲区是主库维护的一个环形缓冲区,存最近N字节的写命令。

主库 repl-backlog 缓冲区(环形)
  ┌─────────────────────────────────┐
  │ offset=100  offset=200  offset=300   │
  │ [SET key1 v1][SET key2 v2][...]   │
  └─────────────────────────────────┘
                       ↑
                从库当前读取位置

缓冲区配置

# redis.conf
repl-backlog-size 1mb       # 缓冲区大小,默认 1MB
repl-backlog-ttl 3600       # 没有从库时保留 N 秒后释放

缓冲区大小计算

# 估算需要的缓冲区大小
# 高峰期复制速率:10MB/s
# 最大容忍中断时间:60 秒
# 所需缓冲区 = 10MB/s × 60s = 600MB

# 配置建议
repl-backlog-size 512mb     # 给个余量

缓冲区不足的后果

# 当从库的断线时间超过了缓冲区能容纳的数据量
# 自动转为全量同步(FULL RESYNC)

# 从库日志
# "MASTER <-> SLAVE sync: Starting partial resynchronization"
# "MASTER <-> SLAVE sync: Full resync requested"

# 全量同步的代价:
# - 主库需要 BGSAVE(大内存时很慢)
# - 传输 RDB 文件(大带宽消耗)
# - 从库加载 RDB(期间不可服务)

优化复制稳定性

1. 增大超时设置

# redis.conf
repl-timeout 120              # 默认 60 秒,网络差时增大
repl-ping-slave-period 10     # 心跳间隔,默认 10 秒

2. 增大积压缓冲区

# redis.conf
repl-backlog-size 512mb       # 增大缓冲区减少全量同步
repl-backlog-ttl 7200         # 从库断开后保持缓冲区更久

3. 磁盘不阻塞复制

# redis.conf
repl-diskless-sync yes        # 无盘同步(直接 socket 传输)
repl-diskless-sync-delay 5    # 延迟 5 秒等待更多从库

4. 从库只读配置

# redis.conf
replica-read-only yes         # 从库只读,避免写入不一致
replica-serve-stale-data no   # 复制中断时不返回旧数据

故障恢复场景实战

场景 1:网络抖动导致断开

# 现象:master_link_status:down
# 原因:短暂网络抖动
# 恢复:自动恢复,几分钟内自动重连

# 检查
redis-cli INFO replication | grep master_link_status
# 几分钟后变为 master_link_status:up

场景 2:从库宕机重启

# Step 1: 从库启动后,自动向主库发送 PSYNC 请求
# Step 2: 主库检查 repl-backlog
#   - 数据在缓冲区内 → 部分同步(快)
#   - 数据不在缓冲区内 → 全量同步(慢)

# 从库日志
# Connecting to MASTER 10.0.0.1:6379
# MASTER <-> SLAVE sync started
# Non blocking connect for SYNC fired the event.
# Full resync from master: 7e5e8289f1c1cf2b0d08c5dfb6e0c22522f77f88

场景 3:主库宕机

# 从库断开,等待新主库
# 如果有哨兵:哨兵自动切换,从库自动重新指向新主库
# 无哨兵:手动将某个从库提升为主库

# 手动提升
redis-cli -p 6380 SLAVEOF NO ONE   # 从库 6380 变为主库
# 其他从库重新指向新主库
redis-cli -p 6381 SLAVEOF 10.0.0.2 6380

监控指标

# 关键监控指标
redis-cli INFO replication | grep -E \
  "master_link_status|master_last_io|slave_repl_offset|master_repl_offset"

# 复制延迟 = master_repl_offset - slave_repl_offset
# 延迟 > repl-backlog-size 时必然触发全量同步

# 全量同步次数监控(过高说明配置有问题)
redis-cli INFO Stats | grep sync_full

面试要点

  • 断开自动重连:从库有自动重连机制
  • PSYNC vs 全量同步:积压缓冲区决定是部分还是全量同步
  • repl-backlog 关键:大小决定能容忍多长的断开时间
  • 全量同步代价:占用主库 CPU、带宽、磁盘,要尽量避免
  • repl-timeout 配置:跨机房场景需要增大超时
  • 监控指标:master_link_status、复制延迟、全量同步次数
  • 恢复策略:自动重连 ➝ 手动重连 ➝ SLAVEOF NO ONE 提升

Redis 哨兵频繁切换主节点排查

Redis 哨兵频繁切换主节点排查

问题现象

哨兵频繁切换主节点(也称哨兵漂移或主从颠簸),表现为:

现象 1:短时间内主节点多次变化
现象 2:应用频繁断连,需要不断重连到新主库
现象 3:哨兵日志显示多次 failover

排查步骤

第一步:查看哨兵日志

# 查看哨兵日志
tail -100 /var/log/redis/sentinel.log

# 关注关键日志
# +sdown:主观下线(哨兵认为主库挂了)
# +odown:客观下线(多个哨兵确认主库挂了)
# +try-failover:开始故障转移
# +failover-end:故障转移结束
# +switch-master:切换主库
# -sdown:主观下线恢复

第二步:检查故障原因

# 查看哨兵配置
redis-cli -p 26379 SENTINEL MASTER mymaster
# 1) "name"
# 2) "mymaster"
# 3) "ip"
# 4) "10.0.0.1"
# 5) "port"
# 6) "6379"
# 7) "quorum"
# 8) "2"            ← 判定客观下线的票数要求
# 9) "failover-timeout"
# 10) "180000"      ← 超时时间 3 分钟
# 11) "down-after-milliseconds"
# 12) "30000"       ← 主观下线判定时间

常见原因

原因 1:哨兵配置过于敏感

# 问题配置
down-after-milliseconds: 1000     # 1 秒没响应就认为下线(太敏感!)
quorum: 1                          # 1 个哨兵就判定客观下线(太容易触发)

# 推荐配置
down-after-milliseconds: 30000     # 30 秒(需要 3 个心跳周期)
quorum: 2                          # 至少 2 个哨兵同意

原因 2:网络抖动

主库 → 哨兵 A:心跳正常
主库 → 哨兵 B:心跳超时(网络抖动)
哨兵 B 向群组宣布:"主库挂了"
→ 如果 quorum=1,直接触发切换!

原因 3:CPU 毛刺

# 主库 CPU 瞬间飙高导致心跳超时
# 但 1 秒后恢复,哨兵却已经触发了切换

# 检查 CPU 峰值
sar -u -f /var/log/sysstat/saXX | grep -E "max|avg"

原因 4:GC 暂停(Java 应用 + 共享主机)

JVM Full GC 暂停了 10 秒
→ 应用无法访问主库 → 但实际上 Redis 正常
→ 哨兵监控的是 "可连接性" 而不是 GC 暂停

原因 5:哨兵节点分布不合理

❌ 错误:3 个哨兵全部在同一个机房
↓
该机房网络故障 → 所有哨兵同时失联 → 集体触发切换
↓
另一个机房以为主库挂了 → 切换成功
↓
原机房恢复 → 又发现"主库"不在原位置 → 再次切换

✅ 正确:哨兵分布在 3 个不同机房/可用区

解决方案

1. 调整哨兵配置

# sentinel.conf 优化配置

# 延长下线判定时间
sentinel down-after-milliseconds mymaster 30000
# 合理设置:30 秒 = 3 × 哨兵检查周期

# 增加 quorum
sentinel quorum mymaster 2
# 至少 2 个哨兵同意才能判定客观下线

# 设置故障转移超时
sentinel failover-timeout mymaster 180000
# 3 分钟:防止短时间内重复切换

2. 部署策略优化

# 节点部署建议
# 3 个哨兵分布在 3 个不同的可用区
哨兵 A: 可用区 1
哨兵 B: 可用区 2
哨兵 C: 可用区 3

# 主从节点
主库: 可用区 1
从库 A: 可用区 2
从库 B: 可用区 3

3. 监控告警调优

# 避免误告警

# ❌ 太敏感:CPU 超过 50% 就告警
# ✅ 合理:CPU 持续 90% 超过 5 分钟

# ❌ 太敏感:PING 延迟 10ms 就告警
# ✅ 合理:PING 失败超过 30 秒

4. 升级到 Redis Cluster(推荐长期方案)

# Redis Cluster 的分片机制
# - 单个节点故障不影响全局
# - 自动 failover 比哨兵更稳定
# - 无哨兵组件,减少故障点

# 迁移方式:
# redis-trib.rb 在线迁移
# 或使用 Redis-Shake 逐步迁移

预防措施

配置检查列表

# 哨兵配置检查清单
✅ down-after-milliseconds: 30000     # 不能太短
✅ quorum: (N/2 + 1)                  # 超过半数的哨兵同意
✅ failover-timeout: 180000           # 切换超时
✅ sentinel runid 唯一                # 每个哨兵唯一 ID
✅ 哨兵数量 >= 3                       # 奇数部署
✅ 哨兵分布在不同故障域               # 避免单点
✅ sentinel notification-script 配置   # 切换通知

监控哨兵事件

import redis

def monitor_sentinel_events():
    sentinel = redis.Sentinel([('localhost', 26379)])
    master = sentinel.discover_master('mymaster')
    previous_master = master

    while True:
        current_master = sentinel.discover_master('mymaster')
        if current_master != previous_master:
            print(f"ALERT: 主节点切换 {previous_master}{current_master}")
            previous_master = current_master
        time.sleep(10)

面试要点

  • 频繁切换 = 哨兵太敏感:down-after-milliseconds 设置不当是主因
  • 网络抖动诱发:短暂的网络延迟不应该触发切换
  • quorum 配置原则:quorum = N/2 + 1(超过半数)
  • 奇数哨兵节点:3 个哨兵容忍 1 个故障,5 个容忍 2 个
  • 跨可用区部署:避免单机房故障集体失联
  • failover-timeout:足够长的超时防止震荡切换
  • 长期方案:考虑迁移到 Redis Cluster

Redis 内存用满的表现及处理

Redis 内存用满的表现及处理

内存用满的典型场景

Redis 作为内存数据库,当内存使用达到配置的 maxmemory 上限时,会触发特定的行为。生产环境中,内存用满是最常见的故障之一。

内存满时的表现

1. 写入操作失败

# 表现:写命令返回 OOM 错误
redis-cli SET key value
# (error) OOM command not allowed when used memory > 'maxmemory'.

# 但读操作不受影响
redis-cli GET existing_key
# "value"  ← 读操作正常

2. 淘汰策略触发

# 如果配置了淘汰策略,Redis 会自动删除 key 释放内存
# 这可能导致缓存命中率下降

# 检查当前淘汰策略
redis-cli CONFIG GET maxmemory-policy
# "allkeys-lru"  ← 从所有 key 中淘汰最近最少使用的

# 查看淘汰统计
redis-cli INFO Stats | grep evicted_keys
# evicted_keys:150000  ← 已淘汰 15 万个 key

3. 性能下降

# 淘汰策略执行本身消耗 CPU
# 特别是在 volatile-ttl 策略下,需要遍历 key 检查过期时间

# 大量淘汰导致:
# - 请求延迟增加
# - 缓存命中率急剧下降
# - 数据库负载飙升(缓存失效后请求打到数据库)

4. 内存碎片化

# 内存满 + 频繁淘汰 = 内存碎片
redis-cli INFO Memory | grep mem_fragmentation_ratio
# mem_fragmentation_ratio:2.5  ← 严重碎片化
# 实际使用 4GB,但 RSS 占用了 10GB

5. 触发 Swap

# 系统内存不足时,部分 Redis 数据被换到磁盘

# 检查 swap 使用
redis-cli INFO Memory | grep used_memory
# used_memory_human:10.00G  ← 声称用了 10G
redis-cli INFO Memory | grep maxmemory
# maxmemory_human:8.00G     ← 只配了 8G

# 系统层面检查
cat /proc/$(pgrep -f redis-server)/status | grep VmSwap
# VmSwap: 2097152 kB  ← 使用了 2GB swap!

# swap 导致性能断崖式下降(访问时间从微妙级变成毫秒级)

监控与预警

关键监控指标

# 1. 内存使用率
redis-cli INFO Memory | grep -E "used_memory|maxmemory|mem_fragmentation"
# used_memory_human:7.50G     # 当前使用
# used_memory_rss_human:10G   # 实际 RSS 内存
# maxmemory_human:8.00G       # 最大限制
# mem_fragmentation_ratio:1.33  # 碎片率
# 使用率 = 7.5G / 8G = 93.75%

# 2. 淘汰统计
redis-cli INFO Stats | grep evicted_keys
# evicted_keys:50000

# 3. 过期 key 统计
redis-cli INFO Stats | grep expired_keys
# expired_keys:100000

告警阈值

# Prometheus 告警规则
groups:
- name: redis_memory
  rules:
  - alert: RedisMemoryHigh
    expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.8
    for: 5m
    annotations:
      summary: "Redis 内存使用超过 80%"

  - alert: RedisMemoryCritical
    expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.95
    for: 1m
    annotations:
      summary: "Redis 内存使用超过 95%,即将 OOM"

  - alert: RedisEvictionsHigh
    expr: rate(redis_evicted_keys_total[5m]) > 100
    annotations:
      summary: "Redis 淘汰速率超过 100/s"

处理步骤

第一步:紧急扩容

# 方案 A:修改 maxmemory(运行时生效)
redis-cli CONFIG SET maxmemory 12G

# 方案 B:扩容物理内存 + 重启(需要停机)
# 1. 增大云主机规格
# 2. 启动 Redis 时指定更大的 maxmemory
# 3. 从旧节点迁移数据

第二步:清理非必要数据

# 1. 查找并删除大 Key
redis-cli --bigkeys

# 2. 清理特定模式的 key
redis-cli SCAN 0 MATCH "temp:*" COUNT 1000 | while read key; do
    redis-cli UNLINK "$key"  # 异步删除
done

# 3. 清理 TTL 过长的 key
redis-cli SCAN 0 | while read key; do
    ttl=$(redis-cli TTL "$key")
    if [ "$ttl" = "-1" ]; then  # 没有过期时间
        echo "Key $key has no TTL, consider adding one"
    fi
done

第三步:优化淘汰策略

# 查看当前策略
redis-cli CONFIG GET maxmemory-policy
# 可能的值:
# noeviction      - 不淘汰,写入返回 OOM(默认)
# allkeys-lru     - 所有 key,LRU 淘汰(推荐)
# volatile-lru    - 有 TTL 的 key,LRU 淘汰
# allkeys-random  - 全部 key,随机淘汰
# volatile-ttl    - 有 TTL 的 key,快过期的淘汰
# allkeys-lfu     - 所有 key,LFU 淘汰(Redis 4.0+)
# volatile-lfu    - 有 TTL 的 key,LFU 淘汰

# 生产推荐:allkeys-lru 或 allkeys-lfu
redis-cli CONFIG SET maxmemory-policy allkeys-lru

第四步:分析内存使用

# 按数据类型统计内存
redis-cli INFO Keyspace
# db0:keys=500000,expires=300000,avg_ttl=7200

# 使用 redis-rdb-tools 分析 RDB 文件
rdb -c memory /var/lib/redis/dump.rdb --bytes 100000
# database,type,key,size_in_bytes,encoding,num_elements,len_largest_element
# db0,string,session:10001,10485761,string,10485761,10485761
# db0,hash,user:info,5242880,hashtable,10000,100

预防措施

1. 合理设置 maxmemory

# redis.conf
maxmemory 10G                    # 设置内存上限
maxmemory-policy allkeys-lru     # 淘汰策略
maxmemory-samples 10             # LRU 抽样数(越大越精确)

2. 设置合理的 TTL

// 业务代码中设置合理过期时间
// 缓存场景:过期时间与业务容忍度相关
redisTemplate.opsForValue().set(
    "user:profile:" + userId, 
    userProfile, 
    1, TimeUnit.HOURS  // 1小时过期
);

// 会话场景:根据业务设置
redisTemplate.opsForValue().set(
    "session:" + sessionId,
    sessionData,
    30, TimeUnit.MINUTES  // 30分钟过期
);

3. 定期巡检

#!/bin/bash
# 每日内存巡检脚本

MAXMEM=$(redis-cli CONFIG GET maxmemory | tail -1)
USEDMEM=$(redis-cli INFO Memory | grep used_memory_human | cut -d: -f2 | sed 's/M//')
RATIO=$((USEDMEM * 100 / MAXMEM))

echo "最大内存: ${MAXMEM}MB"
echo "已用内存: ${USEDMEM}MB"
echo "使用率: ${RATIO}%"

if [ $RATIO -gt 80 ]; then
    echo "WARNING: 内存使用超过 80%"
fi

if [ $RATIO -gt 95 ]; then
    echo "CRITICAL: 内存使用超过 95%"
fi

# 检查淘汰速率
EVICTED=$(redis-cli INFO Stats | grep evicted_keys | cut -d: -f2)
echo "已淘汰 key 数: ${EVICTED}"

4. 容量规划

# 内存增长的预估
# 当前增长率 ≈ 100MB/天
# 当前 maxmemory = 8GB
# 当前使用 = 6GB

# 剩余天数 = (8G - 6G) / (100MB/天) = 20 天
# 建议:15 天内完成扩容或优化

面试要点

  • OOM 写保护:内存满时,写操作报错,读操作正常
  • 淘汰策略:默认 noeviction(写失败),推荐 allkeys-lru
  • Swap 是性能杀手:内存不足触发 swap 导致性能断崖
  • 碎片率:mem_fragmentation_ratio > 1.5 需要考虑优化
  • 淘汰 ≠ 释放内存:淘汰 key 后碎片还在,RSS 不一定会减少
  • 扩容不能掉链子:提前规划,设置 80% 告警、95% 紧急
  • 监控是关键:内存使用率、淘汰速率、碎片率都要监控

Redis 响应变慢原因排查

Redis 响应变慢原因排查

响应慢的定位方法

Redis 响应变慢是一个典型的面试和运维高频问题。排查需要从多个维度入手。

快速诊断命令

# 第一梯队:查看 Redis 自身统计
redis-cli INFO Commandstats
redis-cli INFO Stats
redis-cli SLOWLOG GET 10

# 第二梯队:系统层面
top -p $(pgrep -f redis-server)
iostat -x 1

# 第三梯队:网络
netstat -ant | grep 6379 | wc -l
ping redis-host

常见原因及排查

原因 1:慢查询(慢命令)

# 查看慢查询
redis-cli SLOWLOG GET 10
# 1) 1) (integer) 1              # ID
#    2) (integer) 1700000000     # 时间戳
#    3) (integer) 50000          # 耗时(微秒)= 50ms
#    4) 1) "KEYS"                # 命令
#       2) "user:*"

# 查看慢查询阈值
redis-cli CONFIG GET slowlog-log-slower-than
# "10000"  # 默认 10 毫秒

# 优化:避免 O(N) 命令
# ❌ KEYS user:*          → O(N) 扫描所有 key
# ✅ SCAN 0 MATCH user:* → 游标分批扫描
# ❌ SMEMBERS big_set     → 大集合全部返回
# ✅ SSCAN big_set 0      → 分批扫描
# ❌ DEL big_key          → 删除大 key 阻塞
# ✅ UNLINK big_key       → 异步删除

原因 2:大 Key 问题

# 查找大 Key
redis-cli --bigkeys
# Biggest string found 'session:10001' has 10485761 bytes
# Biggest list  found 'log:2024'     has 1000000 items

# 大 key 的影响:
# 1. GET/SET 延迟高——传输大量数据
# 2. DEL/EXPIRE 阻塞——操作大 key 耗时
# 3. 内存碎片——频繁变更大数据
# 4. 网络带宽——每次操作都传输大量数据

# 解决方案:
# 拆分大 key:hash → 多个分片字段
# 压缩存储:String 数据使用压缩
# 限制大小:业务上控制单个 value 不超过 10KB

原因 3:CPU 瓶颈

# Redis 是单线程模型,CPU 是核心瓶颈

# 检查 CPU 使用率
top -p $(pgrep -f redis-server)
# %CPU 150%  ← Redis 超过 100% 说明有持久化子进程

# CPU 过高时的表现
# - 请求响应时间普遍上升
# - 所有命令都变慢(不是特定命令)
# - 网络吞吐量达到极限

# 解决方案:
# - 拆分 Redis 实例(使用多实例 + 集群)
# - 改用 Redis Cluster 分摊负载
# - 业务上减少不必要的命令调用

原因 4:内存问题

内存碎片

# 查看内存碎片率
redis-cli INFO Memory | grep mem_fragmentation_ratio
# mem_fragmentation_ratio:1.5  ← 超过 1.5 说明碎片严重

# 碎片率指标
# < 1.0:swap 使用了虚拟内存
# 1.0-1.5:正常范围
# > 1.5:碎片严重,需要整理

# 解决方案
# - 重启 Redis(内存清零重新加载)
# - 使用 MEMORY PURGE 命令(整理碎片)
# - 调整 jemalloc 配置

内存达到 maxmemory

# 检查是否达到内存上限
redis-cli INFO Memory | grep -E "used_memory|maxmemory"
# used_memory_human:8.00G
# maxmemory_human:8.00G  ← 已用满!

# 内存满时
# - 写操作可能失败或触发淘汰策略
# - 淘汰策略执行时可能阻塞
# - 读取大 key 可能触发 swap

# 解决方案
# 增大 maxmemory
# 检查淘汰策略配置
redis-cli CONFIG GET maxmemory-policy

原因 5:网络问题

# 网络延迟检查
# 从应用服务器 ping Redis 服务器
ping redis-server
# 64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.5 ms  ← 同机房
# 64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=50 ms  ← 跨机房

# 网络带宽监控
sar -n DEV 1
# 检查网络吞吐是否接近带宽上限

# TCP 连接问题
netstat -s | grep -E "retransmit|timeout"
# 如果重传率高,说明网络不稳定

# 解决方案
# - 同机房部署应用和 Redis
# - 使用连接池复用连接
# - 使用 Pipeline 减少网络往返

原因 6:持久化导致的延迟

# Fork 延迟
redis-cli INFO Persistence | grep latest_fork_usec
# latest_fork_usec:500000  ← fork 耗时 500ms

# AOF fsync 延迟
# 如果 appendfsync=always,每次写入都 fsync
# 磁盘 I/O 慢会导致写入变慢

# BGSAVE/AOF 重写期间
# - 子进程写 RDB 占用磁盘 I/O
# - COW 机制导致缺页中断
# - 父进程间因内存页争用而变慢

# 解决方案
# - 从库执行持久化
# - appendfsync 设为 everysec
# - 使用 SSD 替代 HDD
# - 将 AOF 放在不同磁盘

原因 7:连接数过多

# 检查连接数
redis-cli INFO Clients
# connected_clients:5000

# 连接过多导致的问题
# - CPU 上下文切换频繁
# - 内核维护大量 TCP 连接的开销
# - Redis 事件循环处理大量 I/O

# 解决方案
# - 合理设置连接池大小
# - 使用连接池(不要短连接)
# - 考虑使用 Redis 代理(Twemproxy/Redis Proxy)

系统级排查

检查慢查询日志

# 配置慢查询
redis-cli CONFIG SET slowlog-log-slower-than 5000  # 记录 >5ms 的查询
redis-cli CONFIG SET slowlog-max-len 1000           # 保留 1000 条

# 分析慢查询
SLOWLOG GET | python3 -c "
import sys, json
for line in sys.stdin:
    data = json.loads(line.strip())
    print(f'耗时: {data[\"duration\"]/1000:.1f}ms | 命令: {\" \".join(data[\"args\"])}')"

性能基线

# 用 redis-benchmark 测试性能基线
redis-benchmark -h 127.0.0.1 -p 6379 -c 50 -n 10000
# ====== SET ======
# 100000 requests completed in 0.86 seconds
# 116279.06 requests per second

# 对比变慢后的性能
redis-benchmark -h 127.0.0.1 -p 6379 -c 50 -n 10000
# ====== SET ======
# 100000 requests completed in 2.50 seconds
# 40000.00 requests per second  ← 性能下降 65%

排查流程总结

症状:Redis 响应变慢
  ↓
第一步:Info 统计检查
  ├── CPU 高?→ 是否持久化子进程?→ 从库备份
  ├── 内存满?→ maxmemory 策略?→ 扩容/优化
  └── 连接多?→ 空闲连接?→ 清理/池化
  ↓
第二步:Slowlog 检查
  ├── 有慢查询?→ KEYS/DEL 大 key → 优化命令
  └── 无慢查询?→ 检查网络/系统
  ↓
第三步:系统层面
  ├── 网络延迟?→ ping、带宽
  ├── 磁盘 I/O?→ iostat
  └── 系统负载?→ top、vmstat
  ↓
第四步:大 Key 检查
  └── redis-cli --bigkeys

面试要点

  • 慢查询是排查入口:先看 SLOWLOG
  • 单线程模型:CPU 瓶颈常见的瓶颈
  • 大 Key 是万恶之源:导致慢查询、阻塞、内存碎片
  • 网络不可忽略:跨机房部署时网络延迟可能是主因
  • 持久化影响:AOF fsync、BGSAVE fork、子进程 CPU 消耗
  • 内存碎片:mem_fragmentation_ratio > 1.5 需要处理
  • 性能基线:提前做好基线,变慢时才有对比依据
© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容