这一篇讲什么
这一篇讲三件经常被搅在一起的事:
- 连接数:满了以后谁报错、报什么?服务端会不会留下记录?
- 超时:「连接数没涨,命令却在超时」是怎么回事?命令超时和连接池耗尽,在 Java 里怎么区分?
- 大 key:怎么找出来?删一个、读一个,会让其他请求等多久?
实测环境 :CentOS 7.9,Redis 6.2.4,实验实例端口 6380。Java 端 Spring Boot 4.1.1 + Lettuce 7.5.2,配置:
yaml
spring:
data:
redis:
timeout: 5s
lettuce:
pool:
max-active: 8
max-wait: 3s
1. 连接数满了
1.1 客户端看到什么,服务端记了什么
maxclients 默认 10000。为了好测,改成 20,然后开 30 个连接,每个发一条 PING:
20 个连接收到 +PONG (第 1~20 个)
10 个连接收到 -ERR max number of clients reached (第 21~30 个)
服务端日志里一行相关记录都没有。唯一留下痕迹的地方是:
127.0.0.1:6380> info stats
...
rejected_connections:10
所以「连接数满了服务端会有错误信息吗」的答案是:没有日志,只有 INFO stats 里 rejected_connections 这个计数器在涨。 要监控连接数打满,盯的是它。
启动日志里那句
Increased maximum number of open files to 10032 (it was originally set to 1024).也和连接数有关:每个连接占一个文件描述符,Redis 启动时会按maxclients去调高自己的上限。
1.2 Java 应用看到的
连接被占满时,Spring Boot(Lettuce)里:
org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis
org.springframework.data.redis.connection.PoolException: Could not get a resource from the pool
io.lettuce.core.RedisConnectionException: Unable to connect to 192.168.3.111/<unresolved>:6380
java.io.IOException: 你的主机中的软件中止了一个已建立的连接。
max number of clients reached 这句话在应用里看不到 ,只有一句「连接被中止」------ 和 protected-mode 拒绝远程连接时的报错一模一样(那次的原文同样是这一句)。看到它,要同时想到这两种可能,去服务端看 rejected_connections 或者 CONFIG GET protected-mode。
2. 连接数没涨,命令却在超时
2.1 Spring 默认只用一条连接
先看一个很多人没意识到的事实。50 个线程同时疯狂 GET 3 秒,期间在服务端 CLIENT LIST 看来自应用的连接:
并发中服务端看到的连接数 = 1
3 秒共完成 GET 166309 次
只有 1 条。 Spring Data Redis 用 Lettuce 时,默认让普通命令共用一条连接(Lettuce 的连接是线程安全的,命令在一条连接上排队发出)。配置的连接池 max-active: 8,这时候根本没用上。
2.2 服务端卡住时:所有请求一起超时
让服务端卡 8 秒(DEBUG SLEEP 8,模拟一条慢命令或者一次长时间的阻塞),这期间 20 个线程各发一次 GET:
20 个请求总耗时 5017ms,结果:
20 个: QueryTimeoutException / RedisCommandTimeoutException: Command timed out after 5 second(s)
之后服务端看到的连接数 = 1
20 个请求同时超时,连接数一直是 1。 这就是「连接数不增加,命令超时」:问题不在连接,在于那唯一的一条连接上,前面有东西卡着。去服务端看 slowlog get,看有没有执行很久的命令(第 4 节的大 key 就是典型)。
2.3 连接池耗尽:报的是另一句
连接池什么时候才用上?实测里是阻塞命令 (BLPOP):它要独占一条连接,这时才从池子里借。8 个线程各发一个等 10 秒的 BLPOP,1 秒后在服务端看:
连接 8 条:blpop 7 条,client 1 条(执行 CLIENT LIST 的那条)
这时再发第 9 个 BLPOP:
耗时 3007ms:
org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis
org.springframework.data.redis.connection.PoolException: Could not get a resource from the pool
java.util.NoSuchElementException: Timeout waiting for idle object, maxWaitDuration=PT3S
同一时刻普通 GET 完全正常(0ms)------ 它走的是那条共享连接,不经过池子。
2.4 两种超时怎么分
| 报错 | 等的是哪个配置 | 说明 |
|---|---|---|
RedisCommandTimeoutException: Command timed out after 5 second(s) |
spring.data.redis.timeout |
命令发出去了,Redis 没在时限内回。看服务端慢命令、阻塞 |
NoSuchElementException: Timeout waiting for idle object, maxWaitDuration=PT3S |
lettuce.pool.max-wait |
连接池里没有空闲连接。看是不是阻塞命令占着连接不还 |
前者调大连接池没有用;后者调大连接池才有用。
3. 大 key 怎么找
造了一组数据:一个 100 万字段的 hash、一个 100 万元素的 list、一个 10MB 的 string,外加 10 万个普通 key。
3.1 --bigkeys:按「个数」排,不是按内存
$ redis-cli -p 6380 --bigkeys
...
Biggest list found '"biglist"' has 1000000 items
Biggest hash found '"bighash"' has 1000000 fields
Biggest string found '"bigstr"' has 10485760 bytes
扫完整个库用了 1.47 秒。输出开头写着它是用 SCAN 遍历的(You can use -i 0.1 to sleep 0.1 sec per 100 SCAN commands)。注意它的口径:list、hash 按元素个数,string 按字节。「100 万个字段」和「10MB」没法放在一起比,它也不会告诉你哪个更占内存。
3.2 --memkeys:按内存排
$ redis-cli -p 6380 --memkeys
...
Biggest list found '"biglist"' has 12957989 bytes
Biggest hash found '"bighash"' has 64388760 bytes
Biggest string found '"bigstr"' has 12582960 bytes
用了 0.59 秒。这样就能看出 hash 占 64MB,是 list 和 string 的 5 倍。另外,10MB 的 string 实际占 12.58MB ------ 分配器按块分配,会有多出来的部分。
3.3 MEMORY USAGE:单个 key 的内存
| key | 默认 | SAMPLES 0 |
|---|---|---|
| bighash | 64,388,760 字节(3ms) | 63,588,768 字节(105ms) |
| biglist | 12,957,989 字节(5ms) | 12,956,803 字节(3ms) |
| bigstr | 12,582,960 字节 | 12,582,960 字节 |
默认是抽样估算 ,对 100 万字段的 hash 误差约 1%,3 毫秒;SAMPLES 0 是逐个元素精确计算,要 105 毫秒 ------ 这 105 毫秒里 Redis 什么别的都不做。在线上排查时,默认值通常就够了。
4. 删一个大 key、读一个大 key,别人要等多久
测法:一个后台客户端每 10 毫秒发一次 PING,记每次的往返时间;同时对那个 100 万字段的 hash 做操作。
| 操作 | 命令本身 | 同时 PING 最长等了 | PING 中位数 |
|---|---|---|---|
DEL bighash |
212ms | 198.9ms | 0.16ms |
UNLINK bighash |
3ms | 1.3ms | 0.18ms |
HGETALL bighash |
581ms | 324.9ms | 0.18ms |
slowlog(默认阈值 10 毫秒)里记下了:
hgetall bighash 332512 微秒
del bighash 208795 微秒
读法:
DEL一个 100 万字段的 hash,整个 Redis 卡住约 0.2 秒 。释放这么多元素的内存是在主线程上做的,这期间所有客户端的所有命令都在等。默认lazyfree-lazy-user-del no,也就是DEL就是同步删。UNLINK只要 3 毫秒 ,PING 几乎没受影响 ------ 它先把 key 从库里摘掉,内存交给后台线程慢慢释放。之后INFO memory的lazyfree_pending_objects回到 0,说明后台已经释放完。删大 key 用UNLINK。HGETALL更慢 :服务端执行 332 毫秒(slowlog),客户端拿到全部结果花了 581 毫秒(多出来的是传输)。大 key 不光删的时候慢,读整个的时候一样会卡住别人 。读大 hash 用HSCAN分批,或者只取需要的字段(HGET/HMGET)。
这也就是第 2.2 节「所有请求一起超时」的一种来源:某个请求对大 key 执行了 HGETALL 或 DEL,共享连接上排在它后面的、以及所有其他客户端的请求,都得等它。
5. 速查
| 现象 | 看哪里 | 说明 |
|---|---|---|
客户端 ERR max number of clients reached |
INFO stats 的 rejected_connections;CONFIG GET maxclients |
服务端日志不记 |
Java IOException: 你的主机中的软件中止了一个已建立的连接 |
rejected_connections、protected-mode |
两种原因报错一样 |
| 并发高,连接数却只有 1 | --- | 正常,Lettuce 共享连接 |
一批请求同时 Command timed out after N second(s) |
SLOWLOG GET |
服务端被慢命令卡住;调大连接池没用 |
Timeout waiting for idle object |
阻塞命令 | 连接池被独占连接占满 |
| 想知道哪个 key 最大 | --memkeys(按内存);--bigkeys(按个数) |
跨类型比较用 --memkeys |
| 单个 key 多大 | MEMORY USAGE key |
默认抽样;SAMPLES 0 精确但慢 |
| 删大 key | UNLINK |
DEL 会卡住主线程 |
| 读大 key | HSCAN / SSCAN / ZSCAN / LRANGE 分段 |
HGETALL 一次读完同样卡 |
下一篇:主从与哨兵 ------ 主从数据不一致、从库连不上主库、主从切换期间应用能不能写,以及哨兵设了 ACL 以后为什么不切换。