编辑
2024-06-06
💌中间键
00
请注意,本文编写于 377 天前,最后修改于 225 天前,其中某些信息可能已经过时。

目录

4.4 客户端管理
4.4.1 客户端API
1.client list
2.client setName和client getName
3.client kill
4.client pause
5.monitor
4.4.2 客户端相关配置
timeout
maxclients
tcp-keepalive
tcp-backlog
4.4.3 客户端统计片段
4.5 客户端常见异常
1.无法从连接池获取到连接
2.客户端读写超时
3.客户端连接超时
4.客户端缓冲区异常
5.Lua脚本正在执行
6.Redis正在加载持久化文件
7.Redis使用的内存超过maxmemory配置
8.客户端连接数过大
4.6 客户端案例分析
4.6.1 Redis内存陡增
1.现象
2.分析原因
3.处理方法和后期处理
4.6.2 客户端周期性的超时
1.现象
2.分析
3.处理方法和后期处理

4.4 客户端管理

4.4.1 客户端API

1.client list

client list命令能列出与Redis服务端相连的所有客户端连接信息,例如下 面代码是在一个Redis实例上执行client list的结果:

127.0.0.1:6379> client list id=254487 addr=10.2.xx.234:60240 fd=1311 name= age=8888581 idle=8888581 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get id=300210 addr=10.2.xx.215:61972 fd=3342 name= age=8054103 idle=8054103 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get id=5448879 addr=10.16.xx.105:51157 fd=233 name= age=411281 idle=331077 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ttl id=2232080 addr=10.16.xx.55:32886 fd=946 name= age=603382 idle=331060 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get id=7125108 addr=10.10.xx.103:33403 fd=139 name= age=241 idle=1 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=del id=7125109 addr=10.10.xx.101:58658 fd=140 name= age=241 idle=1 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=del ...

输出结果的每一行代表一个客户端的信息,可以看到每行包含了十几个属性,它们是每个客户端的一些执行状态,

理解这些属性对于Redis的开发和运维人员非常有帮助。

下面将选择几个重要的属性进行说明,其余通过表格的形式进行展示。

(1)标识:id、addr、fd、name

这四个属性属于客户端的标识:

  • id:客户端连接的唯一标识,这个id是随着Redis的连接自增的,重启Redis后会重置为0。
  • addr:客户端连接的ip和端口。
  • fd:socket的文件描述符,与lsof命令结果中的fd是同一个,如果fd=-1代表当前客户端不是外部客户端,而是Redis内部的伪装客户端。
  • name:客户端的名字,后面的client setName和client getName两个命令会对其进行说明。

(2)输入缓冲区:qbuf、qbuf-free

Redis为每个客户端分配了输入缓冲区,它的作用是将客户端发送的命令临时保存,

同时Redis从会输入缓冲区拉取命令并执行,输入缓冲区为客户端发送命令到Redis执行命令提供了缓冲功能,如图4-5所示。

client list中qbuf和qbuf-free分别代表这个缓冲区的总容量和剩余容量,

Redis没有提供相应的配置来规定每个缓冲区的大小,输入缓冲区会根据输入内容大小的不同动态调整,只是要求每个客户端缓冲区的大小不能超过1G,超过后客户端将被关闭。

下面是Redis源码中对于输入缓冲区的硬编码:

image.png

/* Protocol and I/O related defines */ #define REDIS_MAX_QUERYBUF_LEN (1024*1024*1024) /* 1GB max query buffer. */

输入缓冲使用不当会产生两个问题:

  • 一旦某个客户端的输入缓冲区超过1G,客户端将会被关闭。
  • 输入缓冲区不受maxmemory控制,假设一个Redis实例设置了maxmemory为4G,已经存储了2G数据,但是如果此时输入缓冲区使用了3G,已经超过maxmemory限制,可能会产生数据丢失、键值淘汰、OOM等情况(如图4-6所示)。

image.png 执行效果如下:

127.0.0.1:6390> info memory # Memory used_memory_human:5.00G ... maxmemory_human:4.00G ....

上面已经看到,输入缓冲区使用不当造成的危害非常大,那么造成输入缓冲区过大的原因有哪些?

输入缓冲区过大主要是因为Redis的处理速度跟不上输入缓冲区的输入速度,并且每次进入输入缓冲区的命令包含了大量bigkey,从而造成了输入缓冲区过大的情况。

还有一种情况就是Redis发生了阻塞,短期内不能处理命令,造成客户端输入的命令积压在了输入缓冲区,造成了输入缓冲区过大。

那么如何快速发现和监控呢?监控输入缓冲区异常的方法有两种:

  • 通过定期执行client list命令,收集qbuf和qbuf-free找到异常的连接记录并分析,最终找到可能出问题的客户端。
  • 通过info命令的info clients模块,找到最大的输入缓冲区,例如下面命令中的其中client_biggest_input_buf代表最大的输入缓冲区,例如可以设置超过10M就进行报警:
127.0.0.1:6379> info clients # Clients connected_clients:1414 client_longest_output_list:0 client_biggest_input_buf:2097152 blocked_clients:0

这两种方法各有自己的优劣势,表4-3对两种方法进行了对比。

image.png

运维提示

输入缓冲区问题出现概率比较低,但是也要做好防范,在开发中要减少bigkey、减少Redis阻塞、合理的监控报警。

(3)输出缓冲区:obl、oll、omem

Redis为每个客户端分配了输出缓冲区,它的作用是保存命令执行的结果返回给客户端,为Redis和客户端交互返回结果提供缓冲,如图4-7所示。

与输入缓冲区不同的是,输出缓冲区的容量可以通过参数client-output￾buffer-limit来进行设置,并且输出缓冲区做得更加细致,按照客户端的不同分为三种:普通客户端、发布订阅客户端、slave客户端,如图4-8所示。

image.png

image.png

对应的配置规则是:

client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
  • class:客户端类型,分为三种。a)normal:普通客户端;b)slave:slave客户端,用于复制;c)pubsub:发布订阅客户端。
  • </hard limit>/:如果客户端使用的输出缓冲区大于</hard limit>/,客户端会被立即关闭。
  • </soft limit>/和</soft seconds>/:如果客户端使用的输出缓冲区超过了 softlimit 并且持续了</soft limit>秒,客户端会被立即关闭。

Redis的默认配置是:

client-output-buffer-limit normal 0 0 0 client-output-buffer-limit slave 256mb 64mb 60 client-output-buffer-limit pubsub 32mb 8mb 60

和输入缓冲区相同的是,输出缓冲区也不会受到maxmemory的限制,如果使用不当同样会造成maxmemory用满产生的数据丢失、键值淘汰、OOM等情况。

实际上输出缓冲区由两部分组成:固定缓冲区(16KB)和动态缓冲区,其中固定缓冲区返回比较小的执行结果,而动态缓冲区返回比较大的结果,

例如大的字符串、hgetall、smembers命令的结果等,通过Redis源码中redis.h的redisClient结构体(Redis3.2版本变为Client)可以看到两个缓冲区的实现细节:

typedef struct redisClient { // 动态缓冲区列表 list *reply; // 动态缓冲区列表的长度(对象个数) unsigned long reply_bytes; // 固定缓冲区已经使用的字节数 int bufpos; // 字节数组作为固定缓冲区 char buf[REDIS_REPLY_CHUNK_BYTES]; } redisClient;

固定缓冲区使用的是字节数组,动态缓冲区使用的是列表。

当固定缓冲区存满后会将Redis新的返回结果存放在动态缓冲区的队列中,队列中的每个对象就是每个返回结果,如图4-9所示。

image.png

client list中的obl代表固定缓冲区的长度,oll代表动态缓冲区列表的长度,omem代表使用的字节数。

例如下面代表当前客户端的固定缓冲区的长度为0,动态缓冲区有4869个对象,两个部分共使用了133081288字节=126M内存:

id=7 addr=127.0.0.1:56358 fd=6 name= age=91 idle=0 flags=O db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=4869 omem=133081288 events=rw cmd=monitor

监控输出缓冲区的方法依然有两种:

  • 通过定期执行client list命令,收集obl、oll、omem找到异常的连接记录并分析,最终找到可能出问题的客户端。
  • 通过info命令的info clients模块,找到输出缓冲区列表最大对象数,例如:
127.0.0.1:6379> info clients # Clients connected_clients:502 client_longest_output_list:4869 client_biggest_input_buf:0 blocked_clients:0

其中,client_longest_output_list代表输出缓冲区列表最大对象数,这两种统计方法的优劣势和输入缓冲区是一样的,这里就不再赘述了。

相比于输入缓冲区,输出缓冲区出现异常的概率相对会比较大,那么如何预防呢?方法如下:

  • 进行上述监控,设置阀值,超过阀值及时处理。
  • 限制普通客户端输出缓冲区的,把错误扼杀在摇篮中,例如可以进行如下设置:
    client-output-buffer-limit normal 20mb 10mb 120
  • 适当增大slave的输出缓冲区的,如果master节点写入较大,slave客户端的输出缓冲区可能会比较大,一旦slave客户端连接因为输出缓冲区溢出被kill,会造成复制重连。
  • 限制容易让输出缓冲区增大的命令,例如,高并发下的monitor命令就是一个危险的命令。
  • 及时监控内存,一旦发现内存抖动频繁,可能就是输出缓冲区过大。

(4)客户端的存活状态

client list中的age和idle分别代表当前客户端已经连接的时间和最近一次的空闲时间:

id=2232080 addr=10.16.xx.55:32886 fd=946 name= age=603382 idle=331060 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get

例如上面这条记录代表当期客户端连接Redis的时间为603382秒,其中空闲了331060秒:

id=254487 addr=10.2.xx.234:60240 fd=1311 name= age=8888581 idle=8888581 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get

例如上面这条记录代表当期客户端连接Redis的时间为8888581秒,其中空闲了8888581秒,实际上这种就属于不太正常的情况,当age等于idle时,说明连接一直处于空闲状态。

为了更加直观地描述age和idle,下面用一个例子进行说明:

String key = "hello"; // 1) 生成jedis,并执行get操作 Jedis jedis = new Jedis("127.0.0.1", 6379); System.out.println(jedis.get(key)); // 2) 休息10秒 TimeUnit.SECONDS.sleep(10); // 3) 执行新的操作ping System.out.println(jedis.ping()); // 4) 休息5秒 TimeUnit.SECONDS.sleep(5); // 5) 关闭jedis连接 jedis.close();

下面对代码中的每一步进行分析,用client list命令来观察age和idle参数的相应变化。 注意: 为了与redis-cli的客户端区分,本次测试客户端IP地址:10.7.40.98。

  1. 在执行代码之前,client list只有一个客户端,也就是当前的redis￾cli,下面为了节省篇幅忽略掉这个客户端。
127.0.0.1:6379> client list id=45 addr=127.0.0.1:55171 fd=6 name= age=2 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
  1. 使用Jedis生成了一个新的连接,并执行get操作,可以看到IP地址为10.7.40.98的客户端,最后执行的命令是get,age和idle分别是1秒和0秒:
127.0.0.1:6379> client list id=46 addr=10.7.40.98:62908 fd=7 name= age=1 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
  1. 休息10秒,此时Jedis客户端并没有关闭,所以age和idle一直在递增:
127.0.0.1:6379> client list id=46 addr=10.7.40.98:62908 fd=7 name= age=9 idle=9 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
  1. 执行新的操作ping,发现执行后age依然在增加,而idle从0计算,也就是不再闲置:
127.0.0.1:6379> client list id=46 addr=10.7.40.98:62908 fd=7 name= age=11 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ping
  1. 休息5秒,观察age和idle增加:
127.0.0.1:6379> client list id=46 addr=10.7.40.98:62908 fd=7 name= age=15 idle=5 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ping
  1. 关闭Jedis,Jedis连接已经消失:
redis-cli client list | grep "10.7.40.98”为空

(5)客户端的限制maxclients和timeout

Redis提供了maxclients参数来限制最大客户端连接数,一旦连接数超过maxclients,新的连接将被拒绝。maxclients默认值是10000,

可以通过infoclients来查询当前Redis的连接数:

127.0.0.1:6379> info clients # Clients connected_clients:1414 ...

可以通过config set maxclients对最大客户端连接数进行动态设置:

127.0.0.1:6379> config get maxclients 1) "maxclients" 2) "10000" 127.0.0.1:6379> config set maxclients 50 OK 127.0.0.1:6379> config get maxclients 1) "maxclients" 2) "50"

一般来说maxclients=10000在大部分场景下已经绝对够用,但是某些情况由于业务方使用不当(例如没有主动关闭连接)可能存在大量idle连接,

无论是从网络连接的成本还是超过maxclients的后果来说都不是什么好事,因此Redis提供了timeout(单位为秒)参数来限制连接的最大空闲时间,

一旦客户端连接的idle时间超过了timeout,连接将会被关闭,例如设置timeout为30秒:

#Redis默认的timeout是0,也就是不会检测客户端的空闲 127.0.0.1:6379> config set timeout 30 OK

下面继续使用Jedis进行模拟,整个代码和上面是一样的,只不过第2)步骤休息了31秒:

String key = "hello"; // 1) 生成jedis,并执行get操作 Jedis jedis = new Jedis("127.0.0.1", 6379); System.out.println(jedis.get(key)); // 2) 休息31秒 TimeUnit.SECONDS.sleep(31); // 3) 执行get操作 System.out.println(jedis.get(key)); // 4) 休息5秒 TimeUnit.SECONDS.sleep(5); // 5) 关闭jedis连接 jedis.close();

执行上述代码可以发现在执行完第2)步之后,client list中已经没有了Jedis的连接,也就是说timeout已经生效,将超过30秒空闲的连接关闭掉:

127.0.0.1:6379> client list id=16 addr=10.7.40.98:63892 fd=6 name= age=19 idle=19 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get # 超过timeout后,Jedis连接被关闭 redis-cli client list | grep “10.7.40.98”为空

同时可以看到,在Jedis代码中的第3)步抛出了异常,因为此时客户端已经被关闭,所以抛出的异常是JedisConnectionException,并且提示

Unexpected end of stream: stream: world Exception in thread "main" redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.

如果将Redis的loglevel设置成debug级别,可以看到如下日志,也就是客户端被Redis关闭的日志:

12885:M 26 Aug 08:46:40.085 - Closing idle client

Redis源码中redis.c文件中clientsCronHandleTimeout函数就是针对timeout参数进行检验的,只不过在源码中timeout被赋值给了server.maxidletime:

int clientsCronHandleTimeout(redisClient *c) { // 当前时间 time_t now = server.unixtime; // server.maxidletime就是参数timeout if (server.maxidletime && // 很多客户端验证,这里就不占用篇幅,最重要的验证是下面空闲时间超过了maxidletime就会 // 被关闭掉客户端 (now - c->lastinteraction > server.maxidletime)) { redisLog(REDIS_VERBOSE,"Closing idle client"); // 关闭客户端 freeClient(c); } }

Redis的默认配置给出的timeout=0,在这种情况下客户端基本不会出现上面的异常,这是基于对客户端开发的一种保护。

例如很多开发人员在使用 JedisPool时不会对连接池对象做空闲检测和验证,如果设置了timeout>0,可 能就会出现上面的异常,对应用业务造成一定影响,但是如果Redis的客户 端使用不当或者客户端本身的一些问题,造成没有及时释放客户端连接,可 能会造成大量的idle连接占据着很多连接资源,一旦超过maxclients;后果也 是不堪设想。

所在在实际开发和运维中,需要将timeout设置成大于0,例如可以设置为300秒,同时在客户端使用上添加空闲检测和验证等等措施,例如JedisPool使用common-pool提供的三个属性:minEvictableIdleTimeMillis、 testWhileIdle、timeBetweenEvictionRunsMillis,4.2节已经进行了说明,这里 就不再赘述。

(6)客户端类型

client list中的flag是用于标识当前客户端的类型,例如:

  • flag=S代表当前客户端是slave客户端、
  • flag=N代表当前是普通客户端,
  • flag=O代表当前客户端

正在执行monitor命令,表4-4列出了11种客户端类型。

image.png

(7)其他

上面已经将client list中重要的属性进行了说明,表4-5列出之前介绍过以及一些比较简单或者不太重要的属性。

image.png

2.client setName和client getName

client setName xx client getName

client setName用于给客户端设置名字,这样比较容易标识出客户端的来源,例如将当前客户端命名为test_client,可以执行如下操作:

127.0.0.1:6379> client setName test_client OK

此时再执行client list命令,就可以看到当前客户端的name属性为test_client:

127.0.0.1:6379> client list id=55 addr=127.0.0.1:55604 fd=7 name=test_client age=23 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client

如果想直接查看当前客户端的name,可以使用client getName命令,例如下面的操作:

127.0.0.1:6379> client getName "test_client"

client getName和setName命令可以做为标识客户端来源的一种方式,但是通常来讲,在Redis只有一个应用方使用的情况下,IP和端口作为标识会更加清晰。

当多个应用方共同使用一个Redis,那么此时client setName可以作为标识客户端的一个依据。

3.client kill

client kill ip:port

此命令用于杀掉指定IP地址和端口的客户端,例如当前客户端列表为:

127.0.0.1:6379> client list id=49 addr=127.0.0.1:55593 fd=6 name= age=9 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client id=50 addr=127.0.0.1:52343 fd=7 name= age=4 idle=4 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get

如果想杀掉127.0.0.1:52343的客户端,可以执行:

127.0.0.1:6379> client kill 127.0.0.1:52343 OK

执行命令后,client list结果只剩下了127.0.0.1:55593这个客户端:

127.0.0.1:6379> client list id=49 addr=127.0.0.1:55593 fd=6 name= age=9 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client

由于一些原因(例如设置timeout=0时产生的长时间idle的客户端),需要手动杀掉客户端连接时,可以使用client kill命令。

4.client pause

client pause timeout(毫秒)

如图4-10所示,client pause命令用于阻塞客户端timeout毫秒数,在此期间客户端连接将被阻塞。

image.png

例如在一个客户端执行:

127.0.0.1:6379> client pause 10000 OK

在另一个客户端执行ping命令,发现整个ping命令执行了9.72秒(手动执行redis-cli,只为了演示,不代表真实执行时间):

127.0.0.1:6379> ping PONG (9.72s)

该命令可以在如下场景起到作用:

  • client pause只对普通和发布订阅客户端有效,对于主从复制(从节点内部伪装了一个客户端)是无效的,也就是此期间主从复制是正常进行的,所以此命令可以用来让主从复制保持一致。
  • client pause可以用一种可控的方式将客户端连接从一个Redis节点切换到另一个Redis节点。

需要注意的是在生产环境中,暂停客户端成本非常高。

5.monitor

monitor命令用于监控Redis正在执行的命令,如图4-11所示,我们打开了两个redis-cli,一个执行set get ping命令,另一个执行monitor命令。

可以看到monitor命令能够监听其他客户端正在执行的命令,并记录了详细的时间戳。

image.png

monitor的作用很明显,如果开发和运维人员想监听Redis正在执行的命令,就可以用monitor命令,

但事实并非如此美好,每个客户端都有自己的输出缓冲区,既然monitor能监听到所有的命令,一旦Redis的并发量过大,monitor客户端的输出缓冲会暴涨,可能瞬间会占用大量内存,图4-12展示了 monitor命令造成大量内存使用。

image.png

4.4.2 客户端相关配置

timeout

检测客户端空闲连接的超时时间,一旦idle时间达到了timeout,客户端将会被关闭,如果设置为0就不进行检测。

maxclients

客户端最大连接数,4.4.1节中的客户端存活状态部分已经进行分析,这里不再赘述,但是这个参数会受到操作系统设置的限制,第12章Linux相关配置小节还会对这个参数进行介绍。

tcp-keepalive

检测TCP连接活性的周期,默认值为0,也就是不进行检测,如果需要设置,建议为60,那么Redis会每隔60秒对它创建的TCP连接进行活性检测,防止大量死连接占用系统资源。

tcp-backlog

TCP三次握手后,会将接受的连接放入队列中,tcpbacklog就是队列的大小,它在Redis中的默认值是511。通常来讲这个参数不需要调整,但是这个参数会受到操作系统的影响,例如在Linux操作系统 中,如果/proc/sys/net/core/somaxconn小于tcp-backlog,那么在Redis启动时会 看到如下日志,并建议将/proc/sys/net/core/somaxconn设置更大。

# WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/ sys/net/core/somaxconn is set to the lower value of 128.

修改方法也非常简单,只需要执行如下命令:

echo 511 > /proc/sys/net/core/somaxconn

4.4.3 客户端统计片段

例如下面就是一次info clients的执行结果:

127.0.0.1:6379> info clients # Clients connected_clients:1414 client_longest_output_list:0 client_biggest_input_buf:2097152 blocked_clients:0

说明如下:

  1. connected_clients:代表当前Redis节点的客户端连接数,需要重点监控,一旦超过maxclients,新的客户端连接将被拒绝。
  2. client_longest_output_list:当前所有输出缓冲区中队列对象个数的最大值。
  3. client_biggest_input_buf:当前所有输入缓冲区中占用的最大容量。
  4. blocked_clients:正在执行阻塞命令(例如blpop、brpop、brpoplpush)的客户端个数。

除此之外info stats中还包含了两个客户端相关的统计指标,如下:

127.0.0.1:6379> info stats # Stats total_connections_received:80 ... rejected_connections:0

参数说明:

  • total_connections_received:Redis自启动以来处理的客户端连接数总数。
  • rejected_connections:Redis自启动以来拒绝的客户端连接数,需要重点监控

4.5 客户端常见异常

1.无法从连接池获取到连接

JedisPool中的Jedis对象个数是有限的,默认是8个。

这里假设使用的默认配置,如果有8个Jedis对象被占用,并且没有归还,此时调用者还要从 JedisPool中借用Jedis,就需要进行等待(例如设置了maxWaitMillis>0),

如果在maxWaitMillis时间内仍然无法获取到Jedis对象就会抛出如下异常:

redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool … Caused by: java.util.NoSuchElementException: Timeout waiting for idle object at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool. java:449)

还有一种情况,就是设置了blockWhenExhausted=false,那么调用者发现池子中没有资源时,会立即抛出异常不进行等待,下面的异常就是

blockWhenExhausted=false时的效果:

redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool … Caused by: java.util.NoSuchElementException: Pool exhausted at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool. java:464)

对于这个问题,需要重点讨论的是为什么连接池没有资源了,造成没有资源的原因非常多,可能如下:

  • 客户端:高并发下连接池设置过小,出现供不应求,所以会出现上面的错误,但是正常情况下只要比默认的最大连接数(8个)多一些即可,因为正常情况下JedisPool以及Jedis的处理效率足够高。
  • 客户端:没有正确使用连接池,比如没有进行释放,例如下面代码所示。定义JedisPool,使用默认的连接池配置:
    GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);
    像JedisPool借用8次连接,但是没有执行归还操作:
    for (int i = 0; i < 8; i++) { Jedis jedis = null; try { jedis = jedisPool.getResource(); jedis.ping(); } catch (Exception e) { e.printStackTrace(); } }
    当调用者再向连接池借用Jedis时(如下操作),就会抛出异常:
    jedisPool.getResource().ping();
  • 客户端:存在慢查询操作,这些慢查询持有的Jedis对象归还速度会比较慢,造成池子满了。
  • 服务端:客户端是正常的,但是Redis服务端由于一些原因造成了客户端命令执行过程的阻塞,也会使得客户端抛出这种异常。

可以看到造成这个异常的原因是多个方面的,不要被异常的表象所迷惑,而且并不存在万能钥匙解决所有问题,开发和运维只能不断加强对于Redis的理解,顺藤摸瓜逐渐找到问题所在。

2.客户端读写超时

Jedis在调用Redis时,如果出现了读写超时后,会出现下面的异常:

redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out

造成该异常的原因也有以下几种:

  • 读写超时间设置得过短。
  • 命令本身就比较慢。
  • 客户端与服务端网络不正常。
  • Redis自身发生阻塞。

3.客户端连接超时

Jedis在调用Redis时,如果出现了连接超时后,会出现下面的异常:

redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: connect timed out

造成该异常的原因也有以下几种:

  1. 连接超时设置得过短,可以通过下面代码进行设置:
    // 毫秒 jedis.getClient().setConnectionTimeout(time);
  2. Redis发生阻塞,造成tcp-backlog已满,造成新的连接失败。
  3. 客户端与服务端网络不正常。

4.客户端缓冲区异常

Jedis在调用Redis时,如果出现客户端数据流异常,会出现下面的异常:

redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.

造成这个异常的原因可能有如下几种:

  1. 输出缓冲区满。例如将普通客户端的输出缓冲区设置为1M1M60:
    config set client-output-buffer-limit "normal 1048576 1048576 60 slave 268435456 67108864 60 pubsub 33554432 8388608 60"
    如果使用get命令获取一个bigkey(例如3M),就会出现这个异常。
  2. 长时间闲置连接被服务端主动断开,上节已经详细分析了这个问题。
  3. 不正常并发读写:Jedis对象同时被多个线程并发操作,可能会出现上述异常。

5.Lua脚本正在执行

如果Redis当前正在执行Lua脚本,并且超过了lua-time-limit,此时Jedis 调用Redis时,会收到下面的异常。对于如何处理这类问题,在第3章Lua的 小节已经进行了介绍,这里就不再赘述。

redis.clients.jedis.exceptions.JedisDataException: BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.

6.Redis正在加载持久化文件

Jedis调用Redis时,如果Redis正在加载持久化文件,那么会收到下面的异常:

redis.clients.jedis.exceptions.JedisDataException: LOADING Redis is loading the dataset in memory

7.Redis使用的内存超过maxmemory配置

Jedis执行写操作时,如果Redis的使用内存大于maxmemory的设置,会收到下面的异常,此时应该调整maxmemory并找到造成内存增长的原因:

redis.clients.jedis.exceptions.JedisDataException: OOM command not allowed when used memory > 'maxmemory'.

8.客户端连接数过大

如果客户端连接数超过了maxclients,新申请的连接就会出现如下异常:

redis.clients.jedis.exceptions.JedisDataException: ERR max number of clients reached

此时新的客户端连接执行任何命令,返回结果都是如下:

127.0.0.1:6379> get hello (error) ERR max number of clients reached

这个问题可能会比较棘手,因为此时无法执行Redis命令进行问题修复,一般来说可以从两个方面进行着手解决:

  • 客户端:如果maxclients参数不是很小的话,应用方的客户端连接数基本不会超过maxclients,通常来看是由于应用方对于Redis客户端使用不当造成的。此时如果应用方是分布式结构的话,可以通过下线部分应用节点(例如占用连接较多的节点),使得Redis的连接数先降下来。从而让绝大部分节点可以正常运行,此时再通过查找程序bug或者调整maxclients进行问题的修复。
  • 服务端:如果此时客户端无法处理,而当前Redis为高可用模式(例如Redis Sentinel和Redis Cluster),可以考虑将当前Redis做故障转移。

此问题不存在确定的解决方式,但是无论从哪个方面进行处理,故障的快速恢复极为重要,当然更为重要的是找到问题的所在,否则一段时间后客户端连接数依然会超过maxclients。

4.6 客户端案例分析

4.6.1 Redis内存陡增

1.现象

服务端现象:Redis主节点内存陡增,几乎用满maxmemory,而从节点内存并没有变化(第5章将介绍Redis复制的相关知识,这里只需要知道正常情况下主从节点内存使用量基本相同),如图4-13所示。

image.png

客户端现象:客户端产生了OOM异常,也就是Redis主节点使用的内存已经超过了maxmemory的设置,无法写入新的数据:

redis.clients.jedis.exceptions.JedisDataException: OOM command not allowed when used memory > 'maxmemory'

2.分析原因

从现象看,可能的原因有两个。

  1. 确实有大量写入,但是主从复制出现问题:查询了Redis复制的相关信息,复制是正常的,主从数据基本一致。主节点的键个数:
    127.0.0.1:6379> dbsize (integer) 2126870 从节点的键个数: 127.0.0.1:6380> dbsize (integer) 2126870
  2. 其他原因造成主节点内存使用过大:排查是否由客户端缓冲区造成主节点内存陡增,使用info clients命令查询相关信息如下:
127.0.0.1:6379> info clients # Clients connected_clients:1891 client_longest_output_list:225698 client_biggest_input_buf:0 blocked_clients:0

很明显输出缓冲区不太正常,最大的客户端输出缓冲区队列已经超过了20万个对象,于是需要通过client list命令找到omem不正常的连接,

一般来说大部分客户端的omem为0(因为处理速度会足够快),于是执行如下代码,找到omem非零的客户端连接:

redis-cli client list | grep -v "omem=0"

找到了如下一条记录:

id=7 addr=10.10.xx.78:56358 fd=6 name= age=91 idle=0 flags=O db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=224869 omem=2129300608 events=rw cmd=monitor

已经很明显是因为有客户端在执行monitor命令造成的。

3.处理方法和后期处理

对这个问题处理的方法相对简单,只要使用client kill命令杀掉这个连接,让其他客户端恢复正常写数据即可。

但是更为重要的是在日后如何及时发现和避免这种问题的发生,基本有三点:

  • 从运维层面禁止monitor命令,例如使用rename-command命令重置monitor命令为一个随机字符串,除此之外,如果monitor没有做rename￾command,也可以对monitor命令进行相应的监控(例如client list)。
  • 从开发层面进行培训,禁止在生产环境中使用monitor命令,因为有时候monitor命令在测试的时候还是比较有用的,完全禁止也不太现实。
  • 限制输出缓冲区的大小。
  • 使用专业的Redis运维工具,例如13章会介绍CacheCloud,上述问题在Cachecloud中会收到相应的报警,快速发现和定位问题。

4.6.2 客户端周期性的超时

1.现象

客户端现象:客户端出现大量超时,经过分析发现超时是周期性出现的,这为问题的查找提供了重要依据:

Caused by: redis.clients.jedis.exceptions.JedisConnectionException: java.net. SocketTimeoutException: connect timed out

服务端现象:服务端并没有明显的异常,只是有一些慢查询操作。

2.分析

  • 网络原因:服务端和客户端之间的网络出现周期性问题,经过观察网络是正常的。
  • Redis本身:经过观察Redis日志统计,并没有发现异常。
  • 客户端:由于是周期性出现问题,就和慢查询日志的历史记录对应了一下时间,发现只要慢查询出现,客户端就会产生大量连接超时,两个时间点基本一致(如表4-6和图4-14所示)。

image.png

最终找到问题是慢查询操作造成的,通过执行hlen发现有200万个元素,这种操作必然会造成Redis阻塞,通过与应用方沟通了解到他们有个定时任务,每5分钟执行一次hgetall操作。

127.0.0.1:6399> hlen user_fan_hset_sort (integer) 2883279

以上问题之所以能够快速定位,得益于使用客户端监控工具把一些统计数据收集上来,这样能更加直观地发现问题,如果Redis是黑盒运行,相信很难快速找到这个问题。处理线上问题的速度非常重要。

3.处理方法和后期处理

这个问题处理方法相对简单,只需要业务方及时处理自己的慢查询即可,但是更为重要的是在日后如何及时发现和避免这种问题的发生,基本有三点:

  • 从运维层面,监控慢查询,一旦超过阀值,就发出报警。
  • 从开发层面,加强对于Redis的理解,避免不正确的使用方式。
  • 使用专业的Redis运维工具,例如13章会介绍CacheCloud,上述问题在CacheCloud中会收到相应的报警,快速发现和定位问题。

本文作者:Eric

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!