redis:客户端client

发布时间 2023-08-30 14:49:15作者: ShineLe

学习自:《Redis开发与运维》pdf 247页

简写

C:client,客户端

S:server,服务端

ibuf:输入缓冲区

obuf:输出缓冲区

几个网站:

[1] http://redis.io
[2] http://antirez.com
[3] https://github.com/antirez/redis 源码

1、客户端通信协议

C与S的通信(网络传输)是在TCP协议之上构建的。

C与S端的正常交互协议RESP(REdis Serialization Protocal,Redis序列化协议),这种协议简单高效,既能被机器解析,又容易被人类识别。例如C端发一条set hello world给S,按照RESP的标准,C端会将其封装为以下格式(每行用\r\n分割):

*3
$3
SET
$5
hello
$5
world  

S端收到后按照RESP将其解析为set hello world,执行,回复格式:

+OK  

可以看到除了命令和结果外,还包含了一些特殊字符及数字

下面对这些进行解释

1)发送命令的格式

RESP规定一条命令的格式如下,其中CRLF代表"\r\n"

*<参数数量> CRLF
$<参数1字节数> CRLF
<参数1> CRLF
$<参数2字节数> CRLF
<参数2> CRLF
...
$<参数N字节数> CRLF
<参数N> CRLF  

以之前的set hello world为例:

*3 #3个参数
$3 #第一个参数的字节数
SET#第一个参数 SET
$5 #第二个参数字节数
hello#第二个参数 hello
$5 #第三个参数的字节数
world#第三个参数 world  

需要注意的是,以上只是格式化显示的结果,实际的传输格式为:

*3\r\n$3\r\nSET\r\n$5\r\nhello\r\n$5\r\nworld\r\n
2.

2)返回结果的格式

回复

回复结果的首字节

适用命令

状态 + set
错误 - 错误命令
整数 : incr
字符串 $ get
多条字符串 * mget

redis-cli只能看到最终的执行结果,因为redis-cli本身就是按照RESP进行结果解析的,所以看不到中间结果。

如果想S端返回真正的结果,可以用nc命令、telnet命令、写一个socket程序进行模拟。例如在用nc时,首先连接到redis:

nc 127.0.0.1 6379  

状态回复

set hello world
+OK  

错误回复:某条指令不存在时

sethx
-ERR unknown command 'sethx'  

整数回复

incr counter
:1  

字符串回复

get hello
$5
world  

多条字符串回复

mget java python
*2
$5
jedis
$8
redis-py  

在回复字符串或多字符串时,如果有nil,那么会返回$-1。

get not_exist_key
$-1

mget hello not_exist_key java
*3
$5
world
$-1
$5
jedis  

有了RESP提供的发送命令、返回结果的协议格式,各种编程语言就能利用其来实现相应的Redis客户端:

Java有许多优秀的Redis客户端,详见:http://redis.io/clients#java,使用比较广泛的是Jedis;

Python的Redis客户端叫做redis-py。

2、客户端管理

1)客户端API

命令

命令

说明

client list 列出所有C端信息

client setName xx

clent getName

给C端设置名字(会改变client list中的name属性)

查看当前C端的名字

client kill ip:port kill指定ip和port对应的C端
client pause t 阻塞C端t ms,在此期间C端连接将被阻塞
monitor

监控Redis正在执行的命令

可以监听到其他C端正在执行的命令,并记录详细时间戳

详细介绍

①client list

client list命令可以列出与Redis S端相连的所有C端信息,例如下面代码就是在一个Redis实例上执行client list的结果:

127.0.0.1:6379> client list
id=3 addr=127.0.0.1:47538 laddr=127.0.0.1:6379 fd=9 name= age=21 idle=0
flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=40928 argv-mem=10
obl=0 oll=0 omem=0 tot-mem=61466 events=r cmd=client user=default redir=-1
id=4 addr=192.168.10.20:58090 laddr=192.168.10.20:6379 fd=10 name= age=3
idle=3 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=20496 events=r cmd=command user=default redir=-1  

每行输出代表一个C端信息,下面对这些属性进行说明:

属性

说明

C端标识

id

客户端连接的唯一标识;

随着Redis的连接自增,当Redis重启后置0。

addr C端IP和端口
fd

Socket文件描述符;

与lsof命令中的fd相同,如果fd=-1代表当前C端不是外部C端,而是Redis内部的伪装C端

name C端名字
flags 客户端类型:N M S O x b i d u c A

C端存活状态  

age 当前C端已经连接的时间(s) 
idle 最近一次的空闲时间(s)

输入缓冲区 input_buf

qbuf 缓冲区总容量
qbuf-free 缓冲区剩余容量

输出缓冲区 obuf

obl 固定缓冲区的长度
oll 动态缓冲区的当前长度(动态缓冲区的长度是实时变化的) 
omem 两个缓冲区已使用的字节数 

其他

db 当前C端正在使用的数据库索引
sub/psub 当前C端订阅的频道或者模式数 
events 文件描述符(r/w):C端的Socket可读、可写 
cmd C端最后一次执行的命令,不含参数 

一些项的具体介绍

a、输入缓冲区input_buf

Redis为每个C端分配了输入缓冲区,其作用是将C端命令临时保存,同时Redis会从输入缓冲区拉取命令并执行输入缓冲区C端发送命令到Redis执行命令提供了缓冲功能

可以通过qbuf、qbuf-free查询缓冲区的总容量剩余容量conf没有提供相应配置来规定每个缓冲区大小,它会根据输入内容大小不同动态调整,只是要求每个C端input_buf大小<1GB,超过之后C端将会关闭(这点只能从源码中看到):

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

输入缓冲设置不当会引发两个问题:

  • 一旦某个C端的input_buf>1G,这个C端将被关闭;
  • input_buf不受maxmemory控制,假设某个input_buf使用量+已经存储的数据>maxmemory,可能会产生数据丢失、键值淘汰、OOM等情况(如下图)。

此时再用info memory查看,会出现如下情况:

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

这里展示了input_buf使用不当造成的危害,那么造成input_buf过大的原因有哪些?

  • 主要原因在于Redis处理速度<input_buf的输入速度,并且每次进入input_buf的命令包含了大量bigkey,从而造成了input_buf过大。
  • Redis发生了阻塞,短期内不能处理命令——C端输入的命令积压在了input_buf,造成input_buf过大

input_buf异常监控

  • 定期执行client list命令,收集qbuf、qbuf-free找到异常连接并分析,最终找到可能出问题的C端;
  • 通过info clients模块,找到最大input_buf,这个命令中的client_biggest_input_buf代表最大的input_buf为10MB,超过这个值就报警:
    127.0.0.1:6379> info clients
    # Clients
    connected_clients:1414
    client_longest_output_list:0
    client_biggest_input_buf:2097152 #11MB
    blocked_clients:0  

这两种方式有各自的优劣势:

命令

优点

缺点

client list 精准分析每个C端来定位问题 执行速度慢(特别是连接较多时),频繁执行存在阻塞Redis的可能
info clients 执行速度比client list快,过程简单

不能精确定位到哪个C端

不能显示所有input_buf的总量,只能显示最大量

input_buf出问题的情况比较少,但也要做好防范,在开发中减少bigkey、减少redis阻塞、合理的监控报警

b、输出缓冲区 obuf

Reids为每个C端分配了obuf,其作用是保存命令的执行结果返回给C端,为Redis与C端的交互返回提供了buffer。

 

其容量通过配置项client-output-buffer-limit设置,并且obuf做得更加细致,按照C端不同可以分为3种:普通C端、发布订阅C端、slave C端

用法:client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>

各项

说明

class

客户端类型:

normal:普通C端

slave:slave C端,用于复制

pubsub:发布订阅客户端

hard limit 如果buffer大于hard limit,C端会立刻关闭

soft limit

soft seconds

如果C端使用的obuf超过了soft limit并且持续了soft second秒,C端会立刻关闭

和ibuf类似,obuf也不会受到maxmemory限制,如果使用不当同样会造成maxmemory用满,产生数据丢失、KV淘汰、OOM等问题。

实际上的obuf由两部分组成:固定缓冲区(16KB)、动态缓冲区

固定缓冲区:存储较小的执行结果

动态缓冲区:存储较大的执行结果,如大字符串、hgetall、smembers命令的结果。

redis.h(4.0版本后现在在server.h)中的结构体Client可以看到两个buffer的实现细节:

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

 正如上边源码所写,固定缓冲区采用字节数组动态缓冲区采用list。当固定缓冲区了之后,会将Redis新返回结果存放在动态缓冲区队列中,队列中的每个对象就是每个返回结果:

obuf的信息在client list指令中的obl、oll、omem中可以查看。

obl=0 oll=4869 omem=133081288代表固定缓冲区长0、动态缓冲区有4869个对象,两个缓冲区共用了133081288B=126M内存

obuf监控

  • 定期执行client list,记录obl、oll、omem找到异常的连接记录并分析,最终找到可能出问题的C端;
  • 通过info clients,找到obuf的最大对象数
    connected_clients:502
    client_longest_output_list:4869
    client_biggest_input_buf:0
    blocked_clients:0  

其中,client_longest_output_list代表了输出缓冲区list最大对象数

  • 这两种方式优劣势和input buffer相同,这里不再多说。

相比ibuf,obuf出现异常的概率更大,如何预防:

  • 进行上述监控,设置阈值,超过阈值及时处理
  • 普通C端的obuf作出限制:
    client-output-buffer-limit normal 20mb 10mb 120  
  • 适当增大slave的obuf,如果m节点写入较大,那么slave C端的obuf可能会比较大,一旦slave C端连接因为obuf溢出被kill,会造成复制重连
  • 限制容易让obuf增大的命令,如高并发下的monitor;
  • 及时监控内存,一旦发生内存抖动,可能就是obuf过大。

c、客户端存活状态

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

... age=603382 idle=331060 ...  

该记录代表:当前C端连接Redis的时间为603382s,空闲了331060s。

Redis提供了maxclients参数来限制最大客户端连接数,一旦连接数超过maxclients,新的连接将被拒绝,最大连接和当前连接数都可以通过info clients查看。

在实际开发运维时,为了避免C端连接未被及时释放,造成大量idle连接占据着很多连接资源,通常要设置timeout。

d、客户端类型

标识:flag

类型:

②client setName、getName

用法

  • client setName xx
  • client getName

用途:给C端命名、获取C端名字(都是当前)

说明

  • 会使client list中的name项发生变化。

③client kill

用法:client kill ip:port

作用:kill指定ip和port的C端

说明

  • 常用于手动kill一些timeout为0产生的长期idle C端

④client pause

用法:client pause t

用途:阻塞C端t ms

说明

  • 一个C端阻塞示意图:

 

  •  在一个C端上开启两个连接,其中一个执行pause阻塞10s:
192.168.10.20:6379> client pause 10000
OK  

 

另一个连接执行ping命令,会发现一共执行了6.98s(从开始执行到获得响应)

127.0.0.1:6379> ping
PONG
(6.98s)  

使用场景

  • pause只对普通、发布订阅C端有效,对于主从复制无效,在此期间主从复制仍会正常进行,因此该命令可以用来让主从复制保持一致;
  • client pause以一种可控的方式将C端连接一个Redis节点切换到另一个Redis节点
  • 需要注意的是,在生产环境下,暂停C端的成本很高

⑤monitor

用法:直接用,无参数,连client前缀都没有

用途:监控Redis正在执行的命令,可以监听到其他C端

例子

开两个连接(一个连接就是一个C端),其中一个执行monitor,另一个执行set hello world,此时运行了monitor的连接会看到另一个连接运行set指令的情况

192.168.10.20:6379> monitor
OK
1693364802.987665 [0 127.0.0.1:58692] "set" "hello" "world"  
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379>   

说明

虽然看起来开发和运维人员可以用monitor监听Redis正在执行的命令,但由于每个C端都有自己的obuf,既然monitor能监听到全部命令,一旦Redis并发量过大,monitor所在的C端的obuf会暴涨,瞬间占用大量内存,下图就展示了这一情况:

2)C端相关配置

上一节介绍了部分关于C端的配置,本节将介绍剩余配置

配置

说明

timeout

C端空闲的超时时间,一旦idle达到了timeout,C端将被关闭;

如果设置为0就不进行检测

maxclients C端最大连接数
tcp-keepalive

检测TCP连接活性的周期,默认为0,即不进行检测。

如果设置为60,就意味着每60s对它建立的TCP连接进行活性检测,防止大量死连接占用系统资源

 tcp-backlog TCP三次握手后,会将接受的连接放入队列中,tcp-backlog就是队列的大小,它在Redis中的默认值为511。

3)客户端统计片段 

在info clients中有几项:

说明

connected_clients 当前连接的客户端数量
maxclients 最大允许连接的客户端数量(在conf中设置)
client_recent_max_output_buffer 最近output buffer最大占用量
client_recent_max_input_buffer 最近input buffer最大占用量
blocked_clients 正在执行阻塞命令(如blpop、brpop)的客户端个数

在info stats中也有两项:

应用环境

说明

total_connections_received C端指标 Redis启动以来处理的C端连接总数
rejected_connections Redis启动以来拒绝的客户端连接总数,需要重点监控

4)客户端常见异常