Redis05-客户端

1、客户端通信协议

  • 几乎所有的主流编程语言都有Redis的客户端(http://redis.io/clients),不考虑Redis非常流行的原因,如果站在技术的角度看原因还有两个:
    • 第一,客户端与服务端之间的通信协议是在TCP协议之上构建的。
    • 第二,Redis制定了RESP(REdis Serialization Protocol,Redis序列化协议)实现客户端与服务端的正常交互,这种协议简单高效,既能够被机器解析,又容易被人类识别。

1.1、发送命令格式

  • RESP的规定一条命令的格式如下,CRLF代表"\r\n"。
*<参数数量> CRLF
$<参数1的字节数量> CRLF
<参数1> CRLF
...
$<参数N的字节数量> CRLF
<参数N> CRLF

示例:

  • 客户端发送一条set hello world命令给服务端,按照RESP的标准,客户端需要将其封装为如下格式(每行用\r\n分隔)。
//参数数量是3个,因此第一行为
*3
//参数字节数分别是355,因此后面几行为
$3
SET
$5
hello
$5
world
  • 注意,上面只是格式化显示的结果,实际传输格式为如下代码,整个过程如图4-1所示。
*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n

1.2、返回结果格式

  • Redis的返回结果类型分为以下五种,如图4-2所示:
    • 状态回复:在RESP中第一个字节为"+"。
    • 错误回复:在RESP中第一个字节为"-"。
    • 整数回复:在RESP中第一个字节为":"。
    • 字符串回复:在RESP中第一个字节为"$"。
    • 多条字符串回复:在RESP中第一个字节为"*"。

  • 我们知道redis-cli只能看到最终的执行结果,那是因为redis-cli本身就是按照RESP进行结果解析的,所以看不到中间结果,如果想要看到Redis服务端返回的“真正”结果,可以使用nc命令、telnet命令、甚至写一个socket程序进行模拟。
]# nc 10.1.1.11 6379
auth hengha123
+OK
set hello world
+OK
get hello
$5
world

2、客户端管理

  • Redis提供了客户端相关API对其状态进行监控和管理。

2.1、客户端API

2.1.1、client list

  • client list命令会列出与Redis服务端相连的所有客户端连接信息。
CLIENT LIST [options ...]    #Options:TYPE (normal|master|replica|pubsub)

示例:

//输出结果的每一行代表一个客户端的信息
10.1.1.11:6379> client list
id=22 addr=10.1.1.12:59720 fd=8 name= age=9666 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742
    argv-mem=10 obl=0 oll=0 omem=0 tot-mem=61466 events=r cmd=client user=default

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

  • qbuf:输入缓冲区的总容量
  • qbuf-free:输入缓冲区的剩余容量
  • Redis为每个客户端分配了输入缓冲区,它的作用是将客户端发送的命令临时保存,同时Redis从会输入缓冲区拉取命令并执行,输入缓冲区为客户端发送命令到Redis执行命令提供了缓冲功能。
  • Redis没有提供相应的配置来规定每个缓冲区的大小,输入缓冲区会根据输入内容大小的不同动态调整,只是要求每个客户端缓冲区的大小不能超过1G,超过后客户端将被关闭。
  • 输入缓冲使用不当会产生两个问题:
    • 一旦某个客户端的输入缓冲区超过1G,客户端将会被关闭。
    • 输入缓冲区不受maxmemory控制,假设一个Redis实例设置了maxmemory为4G,已经存储了2G数据,但是如果此时输入缓冲区使用了3G,已经超过maxmemory限制,可能会产生数据丢失、键值淘汰、OOM等情况(如图4-6所示)。
10.1.1.11:6379> info memory
used_memory_human:867.83K
maxmemory_human:0B
...

  • 输入缓冲区使用不当造成的危害非常大,那么造成输入缓冲区过大的原因有哪些?
    • (1)主要是因为Redis的处理速度跟不上输入缓冲区的输入速度,并且每次进入输入缓冲区的命令包含了大量bigkey,从而造成了输入缓冲区过大的情况。
    • (2)Redis发生了阻塞,短期内不能处理命令,造成客户端输入的命令积压在了输入缓冲区,造成了输入缓冲区过大。
  • 那么如何快速发现和监控呢?监控输入缓冲区异常的方法有两种:
    • 通过定期执行client list命令,收集qbuf和qbuf-free找到异常的连接记录并分析,最终找到可能出问题的客户端。
    • 通过info命令的info clients模块,找到最大的输入缓冲区,例如client_recent_max_input_buffer超过10M就进行报警。
  • 这两种方法各有自己的优劣势,表4-3对两种方法进行了对比。

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

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

  • obl代表固定缓冲区的长度。
  • oll代表动态缓冲区列表的长度。
  • omem代表使用的字节数。
  • Redis为每个客户端分配了输出缓冲区,它的作用是保存命令执行的结果返回给客户端,为Redis和客户端交互返回结果提供缓冲。
  • 输出缓冲区按照客户端的不同分为三种:普通客户端、发布订阅客户端、slave客户端。
  • 可以通过配置文件对其进行配置:
client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
    <class>:客户端类型,分为三种。
        normal:普通客户端;
        slave:slave客户端,用于复制;
        pubsub:发布订阅客户端。
    <hard limit>:如果客户端使用的输出缓冲区大于<hard limit>,客户端会被立即关闭。
    <soft limit>和<soft seconds>:如果客户端使用的输出缓冲区超过了<soft limit>并且持续了<soft limit>秒,客户端会被立即关闭。

//Redis的默认配置
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
  • 和输入缓冲区相同的是,输出缓冲区也不会受到maxmemory的限制,如果使用不当同样会造成maxmemory用满产生的数据丢失、键值淘汰、OOM等情况。
  • 实际上输出缓冲区由两部分组成:固定缓冲区(16KB)和动态缓冲区。
    • 固定缓冲区返回比较小的执行结果。
    • 动态缓冲区返回比较大的结果。
  • 固定缓冲区使用的是字节数组,动态缓冲区使用的是列表。当固定缓冲区存满后会将Redis新的返回结果存放在动态缓冲区的队列中,队列中的每个对象就是每个返回结果,如图4-9所示。

  • 监控输出缓冲区的方法依然有两种:
    • 通过定期执行client list命令,收集obl、oll、omem找到异常的连接记录并分析,最终找到可能出问题的客户端。
    • 通过info命令的info clients模块,找到输出缓冲区列表最大对象数。client_recent_max_output_buffer代表输出缓冲区列表最大对象数,这两种统计方法的优劣势和输入缓冲区是一样的,这里就不再赘述了。
  • 相比于输入缓冲区,输出缓冲区出现异常的概率相对会比较大,那么如何预防呢?
    • 进行上述监控,设置阀值,超过阀值及时处理。
    • 限制普通客户端输出缓冲区的,把错误扼杀在摇篮中,例如可以进行如下设置:
      • client-output-buffer-limit normal 20mb 10mb 120
    • 适当增大slave的输出缓冲区的,如果master节点写入较大,slave客户端的输出缓冲区可能会比较大,一旦slave客户端连接因为输出缓冲区溢出被kill,会造成复制重连。
    • 限制容易让输出缓冲区增大的命令,例如,高并发下的monitor命令就是一个危险的命令。
    • 及时监控内存,一旦发现内存抖动频繁,可能就是输出缓冲区过大。

4、客户端的存活状态:age、idle

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

示例:

  • 这条记录代表当前客户端连接Redis的时间为341秒,其中空闲了341秒,实际上这种就属于不太正常的情况,当age等于idle时,说明连接一直处于空闲状态。
10.1.1.11:6379> client list
id=25 addr=10.1.1.12:59726 fd=7 name= age=341 idle=341 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0
    argv-mem=0 obl=0 oll=0 omem=0 tot-mem=20488 events=r cmd=NULL user=default

5、客户端的限制maxclients和timeout

  • maxclients参数来限制最大客户端连接数,一旦连接数超过maxclients,新的连接将被拒绝。maxclients默认值是10000,可以通过info clients来查询当前Redis的连接数。
  • timeout(单位为秒)参数来限制连接的最大空闲时间,一旦客户端连接的idle时间超过了timeout,连接将会被关闭。timeout默认是0,也就是不会检测客户端的空闲。
  • 可以通过配置文件设置maxclients和timeout
10.1.1.11:6379> config get maxclients
1) "maxclients"
2) "10000"

10.1.1.11:6379> config get timeout
1) "timeout"
2) "0"

6、客户端类型:flag

  • flag是用于标识当前客户端的类型,例如flag=S代表当前客户端是slave客户端、flag=N代表当前是普通客户端,flag=O代表当前客户端正在执行monitor命令,表4-4列出了11种客户端类型。

7、其他

  • 表4-5列出client list命令结果的全部属性。

2.1.2、client setName和client getName

CLIENT SETNAME <name>    #将名称<name>分配给当前连接。
CLIENT GETNAME           #返回当前连接的名称。
  • client setName用于给客户端设置名字,这样比较容易标识出客户端的来源。
  • client getName和setName命令可以做为标识客户端来源的一种方式,但是通常来讲,在Redis只有一个应用方使用的情况下,IP和端口作为标识会更加清晰。当多个应用方共同使用一个Redis,那么此时client setName可以作为标识客户端的一个依据。

2.1.3、client kill

CLIENT KILL <ip:port>                              #终止<ip:port>连接
CLIENT KILL <option> <value> [option value ...]    #终止连接。选项有:
    ADDR <ip:port>                         #终止<ip:port>连接
    TYPE (normal|master|replica|pubsub)    #按类型终止连接。
    USER <username>                        #终止使用该用户验证的连接。
    SKIPME (yes|no)                        #Skip killing current connection (default: yes).

2.1.4、client pause

  • client pause命令用于阻塞客户端timeout毫秒数,在此期间客户端连接将被阻塞。
CLIENT PAUSE <timeout>    #暂停所有Redis客户端毫秒。
  • 该命令可以在如下场景起到作用:
    • client pause对普通和发布订阅客户端有效,对于主从复制(从节点内部伪装了一个客户端)是无效的,也就是此期间主从复制是正常进行的,所以此命令可以用来让主从复制保持一致。
    • client pause可以用一种可控的方式将客户端连接从一个Redis节点切换到另一个Redis节点。
  • 如图4-10所示,client pause命令用于阻塞客户端timeout毫秒数,在此期间客户端连接将被阻塞。

2.1.5、monitor

  • monitor命令用于监控Redis正在执行的命令。
MONITOR
  • monitor的作用很明显,如果开发和运维人员想监听Redis正在执行的命令,就可以用monitor命令,但事实并非如此美好,每个客户端都有自己的输出缓冲区,既然monitor能监听到所有的命令,一旦Redis的并发量过大,monitor客户端的输出缓冲会暴涨,可能瞬间会占用大量内存。

示例:

  • 打开两个redis-cli,一个执行set get ping命令,另一个执行monitor命令。可以看到monitor命令能够监听其他客户端正在执行的命令,并记录了详细的时间戳。
10.1.1.11:6379> set get ping
OK

10.1.1.11:6379> MONITOR
OK
1660649995.300768 [0 10.1.1.12:59752] "set" "get" "ping"

2.2、客户端相关配置

  • timeout:检测客户端空闲连接的超时时间,一旦idle时间达到了timeout,客户端将会被关闭,如果设置为0就不进行检测。
  • maxclients:客户端最大连接数,但是这个参数会受到操作系统设置的限制。
  • 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

2.3、客户端统计片段

10.1.1.11:6379> info clients
connected_clients:1                  #代表当前Redis节点的客户端连接数,需要重点监控,一旦超过maxclients,新的客户端连接将被拒绝。
client_recent_max_input_buffer:8     #当前所有输入缓冲区中占用的最大容量。
client_recent_max_output_buffer:0    #当前所有输出缓冲区中占用的最大容量。
blocked_clients:0                    #正在执行阻塞命令(例如blpop、brpop、brpoplpush)的客户端个数。
...

10.1.1.11:6379> info stats
total_connections_received:37        #Redis自启动以来处理的客户端连接数总数。
rejected_connections:0               #Redis自启动以来拒绝的客户端连接数,需要重点监控。
...

3、客户端常见异常

  • 在客户端的使用过程中,无论是客户端使用不当还是Redis服务端出现问题,客户端会反应出一些异常。

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的处理效率足够高。
    • 客户端:没有正确使用连接池,比如没有进行释放。
    • 客户端:存在慢查询操作,这些慢查询持有的Jedis对象归还速度会比较慢,造成池子满了。
    • 服务端:客户端是正常的,但是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
  • 造成该异常的原因也有以下几种:
    • 连接超时设置得过短。
    • Redis发生阻塞,造成tcp-backlog已满,造成新的连接失败。
    • 客户端与服务端网络不正常。

4、客户端缓冲区异常

  • Jedis在调用Redis时,如果出现客户端数据流异常,会出现下面的异常:
redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
  • 造成这个异常的原因可能有如下几种:
    • 输出缓冲区满。
    • 长时间闲置连接被服务端主动断开,上节已经详细分析了这个问题。
    • 不正常并发读写:Jedis对象同时被多个线程并发操作,可能会出现上述异常。

5、Lua脚本正在执行

  • 如果Redis当前正在执行Lua脚本,并且超过了lua-time-limit,此时Jedis调用Redis时,会收到下面的异常。
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、客户端案例分析

4.1、Redis内存陡增

1、现象

  • 服务端现象:Redis主节点内存陡增,几乎用满maxmemory,而从节点内存并没有变化(正常情况下主从节点内存使用量基本相同),如图4-13所示。

  • 客户端现象:客户端产生了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非零的客户端连接:
//已经很明显是因为有客户端在执行monitor命令造成的
redis-cli client list | grep -v "omem=0"

3、处理方法和后期处理

  • 对这个问题处理的方法相对简单,只要使用client kill命令杀掉这个连接,让其他客户端恢复正常写数据即可。但是更为重要的是在日后如何及时发现和避免这种问题的发生,基本有三点:
    • 运维层面禁止monitor命令,例如使用rename-command命令重置monitor命令为一个随机字符串,除此之外,如果monitor没有做renamecommand,也可以对monitor命令进行相应的监控(例如client list)。从开发层面进行培训,禁止在生产环境中使用monitor命令,因为有时候monitor命令在测试的时候还是比较有用的,完全禁止也不太现实。
    • 限制输出缓冲区的大小。
    • 使用专业的Redis运维工具,上述问题在Cachecloud中会收到相应的报警,快速发现和定位问题。

4.2、客户端周期性的超时

1、现象

  • 客户端现象:客户端出现大量超时,经过分析发现超时是周期性出现的,这为问题的查找提供了重要依据:
Caused by: redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: connect timed out
  • 服务端现象:服务端并没有明显的异常,只是有一些慢查询操作。

2、分析

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

  • 最终找到问题是慢查询操作造成的,通过执行hlen发现有200万个元素,这种操作必然会造成Redis阻塞,通过与应用方沟通了解到他们有个定时任务,每5分钟执行一次hgetall操作。
127.0.0.1:6399> hlen user_fan_hset_sort
(integer) 2883279
  • 以上问题之所以能够快速定位,得益于使用客户端监控工具把一些统计数据收集上来,这样能更加直观地发现问题,如果Redis是黑盒运行,相信很难快速找到这个问题。处理线上问题的速度非常重要。

3、处理方法和后期处理

  • 这个问题处理方法相对简单,只需要业务方及时处理自己的慢查询即可,但是更为重要的是在日后如何及时发现和避免这种问题的发生,基本有三点:
    • 从运维层面,监控慢查询,一旦超过阀值,就发出报警。
    • 从开发层面,加强对于Redis的理解,避免不正确的使用方式。
    • 使用专业的Redis运维工具,在CacheCloud中会收到相应的报警,快速发现和定位问题。
#                                                                                                                         #
posted @ 2022-08-16 19:43  麦恒  阅读(38)  评论(0编辑  收藏  举报