跳转到内容
seren-spark

11 性能优化与排障

优化从测量开始,排障从分层开始

Section titled “优化从测量开始,排障从分层开始”

学习目标 · 面对 Redis 变慢、内存上涨或命中率下降,能提出有顺序的检查和可验证的改进。

平均值容易掩盖尾部请求。P99 = 99% 的观测请求在该时间内完成,它不是最大值。要区分接口耗时、客户端 Redis 调用耗时与服务端命令执行耗时。一次 300ms 的 GET 可能只在 Redis 执行了 50 微秒,其余时间在网络、排队、连接等待或应用 GC。

看到什么先找哪些证据常见下一步
接口慢,Redis 服务端慢日志空调用链、连接等待、网络 RTT、JVM GC调整调用次数、连接管理、跨区访问与超时预算
SLOWLOG 出现 HGETALL / 大 Lua参数规模、Value 字节、执行时间缩小单次工作量、改结构、分页、缩短脚本
命中率下降且 evicted_keys 增长内存、Key 增长、TTL、淘汰策略容量和模型优化,识别低价值缓存
大量超时和数据库负载飙升Redis 可用性、回源并发与实例数先限流降级保护 DB,再恢复 Redis
单分片很忙,其余空闲槽分布、热门 Key、hash tag处理热点或调整分片分布
INFO stats
INFO memory
INFO clients
INFO commandstats
INFO replication
INFO persistence
SLOWLOG GET 10
SCAN 0 MATCH learn:* COUNT 100
MEMORY USAGE learn:encoding:hash

这些诊断仍有成本,应按权限和实例负载使用。SLOWLOG 时间单位为微秒,主要记录命令执行,排除与客户端通信的 I/O 等部分;没有慢日志不证明端到端没有问题。默认阈值可能不适合你的观察目标,但修改配置应走正常变更流程。

指标正确解读
keyspace_hits / keyspace_missesRedis 查 Key 的累计命中/未命中;选同一时间窗口求增量再计算比例
evicted_keys / expired_keys因内存淘汰 / 到期清理;上涨原因不同
used_memory / used_memory_rssRedis 分配器使用量 / 进程驻留内存;两者差值要结合碎片、缓冲和系统判断
connected_clients / blocked_clients连接数 / 阻塞式命令的客户端数;blocked 不等于服务器彻底卡住
rejected_connections达到连接等限制而被拒绝,需要结合配置与趋势
commandstats命令调用量和耗时概况;平均耗时不能替代尾延迟

命中率用 Δhits / (Δhits + Δmisses);分母为 0 时不要报告 100%。Redis 统计可能包含非业务缓存命令,应用也应埋点区分“命中正常值、命中空值、回源、读取失败、写缓存失败”。

11.3 扫描与删除:控制每次操作的工作量

Section titled “11.3 扫描与删除:控制每次操作的工作量”

SCAN 首次传 0,以后把上次返回的游标传回,直到游标为 0 才结束。COUNT 是工作量提示,不保证每次恰好返回 100 条;一次可能返回空列表但游标非 0,也可能重复,遍历期间数据还会变化。Cluster 需要按节点遍历;一次 SCAN 不自动扫全局。

生产避免 KEYS 全量扫描和 HGETALL 大集合。定位一个明确属于自己的大 Key 后,可考虑 UNLINK 让值的主要释放工作异步进行,而不是同步 DEL 一个巨大集合;仍要评估后台释放压力和数据归属,不把整个实例当成练习场。

11.4 批量操作:先减少次数,再调整批大小

Section titled “11.4 批量操作:先减少次数,再调整批大小”

查 100 个独立 String,先考虑受控大小的 MGET;复杂的不同命令可以 Pipeline。一个简化模型:串行时间约 N × (RTT + 每条服务时间),批量后约 批次数 × RTT + 总服务时间 + 传输成本。这说明收益来源,不意味着 Pipeline 把 100 条命令变成一条命令。

// Java · 方法片段;适合独立写操作,不能依据前条结果做判断
// import java.nio.charset.StandardCharsets;
// import org.springframework.data.redis.core.RedisCallback;
redis.executePipelined((RedisCallback<Object>) connection -> {
    for (int i = 0; i < 100; i++) {
        byte[] key = ("learn:batch:" + i).getBytes(StandardCharsets.UTF_8);
        byte[] value = ("value-" + i).getBytes(StandardCharsets.UTF_8);
        connection.stringCommands().setEx(key, 60L, value);
    }
    return null; // 结果来自管线响应,回调按约定返回 null
});

不要无限堆批量数据:响应缓冲占内存,超大批次也会增加尾延迟。用 100、500 等小规模做自己的测试,逐渐调整;在 Cluster 中按槽/节点和客户端支持处理。结果部分失败或调用超时时可能已有部分命令执行,重试仍要考虑幂等。

11.5 容量、超时与压测:三个常用计算

Section titled “11.5 容量、超时与压测:三个常用计算”

容量粗算:100 万个 Key,若采样得到每个 Key 含结构开销约 1.5KiB,则数据集约 1.43GiB。再加复制、客户端缓冲、碎片、增长和持久化峰值余量。副本也要相应内存;maxmemory 不是容器总内存上限。采样不同类型、长度、元素数,不能只测最小对象。

超时预算:接口目标 200ms,却配置每次 Redis 超时 2 秒并重试 3 次,预算已经失效。连接超时、命令超时、重试次数和后续 DB 查询要合起来算。读请求可有界重试;INCR、扣减等写操作超时要先面对“结果未知”,不要盲重试。

优化验证:保持数据量、Value 大小、并发、网络位置和预热方式一致,比较改前/改后的吞吐、P95/P99、错误率、CPU、内存、网络与回源量。下面只用于专用学习实例,禁止直接对共享生产实例压测。

# PowerShell · 小规模本地练习,端到端数字仅适用于当前机器
docker exec redis-learning redis-benchmark -t set,get -n 10000 -c 20 -d 128 -q
docker exec redis-learning redis-benchmark -t set,get -n 10000 -c 20 -d 128 -P 16 -q

两次分别不加 Pipeline 与每批 16 条。内置压测工具并不等于应用性能:实际 JSON、对象创建、DB 回源和热点分布都没有完整覆盖。它可能创建自己的测试 Key,因此只在专用实例运行。

  • “所有优化先加连接池”:Lettuce 可复用连接,多开连接不一定更快;阻塞操作与事务连接需求要分开看。
  • “只调大 timeout”:可能把更长的等待堆到应用线程里;先确定瓶颈与业务预算。
  • “删除缓存马上解决内存”:如果热点重新回填,还可能引起回源风暴。

采集两次 INFO stats,手算期间命中率;用 SCAN 找自己的学习 Key。比较一个小 Hash 和一个较长 JSON 的 MEMORY USAGE,再解释为什么编码和数据量都影响容量。

自测与答案 · GET 在应用里花了 200ms,SLOWLOG 没记录,接下来查什么?

检查慢日志阈值和采样窗口,同时查客户端连接/请求队列、网络往返、重试、线程阻塞与 GC。SLOWLOG 不包含端到端所有时间。先定位耗时在哪一层,再决定是批量请求、减小 Value、修复热点,还是调整客户端和网络;不能直接推出“Redis 必须扩容”。

依据:INFO · SLOWLOG · SCAN · MEMORY USAGE · 延迟诊断 · 连接管理