• Redis客户端管理


    1.客户端管理

      Redis提供了客户端相关API对其状态进行监控和管理,本节将深入介绍各个API的使用方法以及在开发运维中可能遇到的问题。

    1.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.x x.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  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=-l 代表当前客户端不是外部客户端,而是Redis内部的伪装客户端。

    □ name: 客户端的名字,后面的client setName和 client getName两个命令会对其进行说明。

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

    Redis为每个客户端分配了输入缓冲区,它的作用是将客户端发送的命令临时保存,同时Redis从会输入缓冲区拉取命令并执行,输入缓冲区为客户端发送命令到Redis执行命令提供了缓冲功能,如图4-5所示。

    client list中qbuf和 qbuf-free分别代表这个缓冲区的总容量和剩余容量,Redis没有提供相应的配置来规定每个缓冲区的大小,输入缓冲区会根据输入内容大小的不同动态调整,只是要求每个客户端缓冲区的大小不能超过1G,超过后客户端将被关闭。下面是Redis源码中对于输入缓冲区的硬编码:

    /* 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所示)。

    执行效果如下:

    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对两种方法进行了对比

    表 4-3 对比 client list 和 info clients 监控输入缓冲区的优劣势

    命令

    优点

    缺点

    client list

    能精准分析每个客户端来定位

    问题

    执行速度较慢(尤其在连接数较多的情况下),频繁执行存在阻塞 Redis 的可能

    info clients

    执行速度比 client list快,分析过程较为简单

    不能精准定位到客户端

    不能显示所有输入缓冲区的总量,只能显示最大量

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

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

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

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

     

    对应的配置规则是:

    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> :如果客户端使用的输出缓冲区超过了〈soft limit〉并且持续了<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 redis Client {
             //动态缓冲区列表
             list *reply;
             //动态缓冲区列表的长度(对象个数)
             unsigned long reply_bytes;
            //固定缓冲区已经使用的字节数
            int bufpos ;
            //字节数组作为固定缓冲区
            char buf[REDIS_REPLY_CHUNK_BYTES] ;
    } redis Client ;

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

    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=0 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代表输出缓冲区列表最大对象数,这两种统计方法的优劣势和输入缓冲区是一样的,这里就不再赞述了。相比于输入缓冲区,输出缓冲区出现异常的概率相对会比较大,那么如何预防呢?方法如下:

    □ 进行上述监控,设置阀值,超过阀值及时处理。

    □ 限制普通客户端输出缓冲区的〈hard limit>〈soft limit〉〈soft seconds〉,把错误扼杀在摇篮中,例如可以进行如下设置:

    client-output-buffer-limit normal 20mb 10mb 120

    □ 适当增大slave的输出缓冲区的 <hard limit>< soft limit>< soft seconds〉,如果master节点写人较大,slave客户端的输出缓冲区可能会比较大,一旦slave客户端连接因为输出缓冲区溢出被kill,会造成复制重连。

    □ 限制容易让输出缓冲区增大的命令,例如,高并发下的monitor命令就是一个危险的命令。

    □ 及时监控内存,一旦发现内存抖动频繁,可能就是输出缓冲区过大。

    (4) 客户端的存活状态

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

    id=2232080 addr=10.16.x x.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,下面用一个例子进行说明:

    Stringkey = "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

    2) 使用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

    3) 休息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

    4) 执行新的操作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

    5) 休息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

    6) 关闭Jedis, Jedis 连接已经消失:

    redis-cli client list | grep "10.7.40.98”  为空

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

    Redis提供了maxclients 参数来限制最大客户端连接数,一旦连接数超过 maxclients,新的连接将被拒绝。 maxclients 默认值是10000,可以通过info clients来查询当前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.printIn (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 I grep “ 10.7.40.98”  为空

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

    world
    Exception in thread "main" redis.clients .jedis.exceptions.JedisConnectionException:
    Unexpected end o f 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 -> lastinter action > server.max idle time))
         {
               redisLog(REDIS_VERBOSE,"Closing idle client" ) ;
                //关闭客户端
               freedient (c);
          }
    }

    Redis的默认配置给出的timeout=0,在这种情况下客户端基本不会出现上面的异常,这是基于对客户端开发的一种保护。例如很多开发人员在使用 JedisPool 时不会对连接池对象做空闲检测和验证,如果设置了 timeout>0, 可能就会出现上面的异常,对应用业务造成一定影响,但是如果 Redis 的客户端使用不当或者客户端本身的一些问题,造成没有及时释放客户端连接,可能会造成大量的idle连接占据着很多连接资源 , 一旦超过maxclients;后果也是不堪设想。所在在实际开发和运维中,需要将tim eout设置成大于0,例如可以设置为300秒,同时在客户端使用上添加空闲检测和验证等等措施,例如 JedisPool 使用common-pool 提供的三个属性:minE victableldleT imeMillis、testWhileldle、timeBetweenEvictionRunsMillis.

    (6) 客户端类型

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

    序号 客户端类型 说明
    1 N 普通客户端
    2 M 当前客户端是master节点
    3 S 当前客户端是slave节点
    4 O 当前客户端正在执行 monitor 命令
    5 x 当前客户端正在执行事务
    6 b 当前客户端正在等待阻塞事件
    7 i 当前客户端正在等待 VMI/O,但是此状态目前已经废弃不用
    8 d 一个受监视的键已被修改,exec命令将失败
    9 u 客户端未被阻塞
    10 c 回复完整输出后,关闭连接
    11 A 尽可能快地关闭连接

     

    (7) 其他

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

    序号 客户端类型 说明
    1 id 客户端连接id
    2 addr 客户端连接IP和端口
    3 fd socket 的文件描述符
    4 name 客户端连接名
    5 age 客户端连接存活时间
    6 idle 客户端连接空闲时间
    7 flags 客户端类型标识
    8 db 当前客户端正在使用的数据库索引下标
    9 sub/psub 当前客户端订阅的频道或者模式数
    10 multi 当前事务中已执行命令个数
    11 qbuf 输入缓冲区总容量
    12 qbuf-free 输入缓冲区剩余容量
    13 obl 固定缓冲区的长度
    14 oll 动态缓冲区列表的长度
    15 omem 固定缓冲区和动态缓冲区使用的容量
    16 events 文件描述符事作件(r/w):r和w分别代表客户端套接字可读和可写
    17 cmd 当前客户端最后一次执行的命令,不包含参数

     

      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  ool=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  ool=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  ool=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 毫秒数,在此期间客户端连接将被阻塞。

      例如在一个客户端执行:

    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命令能够监听其他客户端正在执行的命令,并记录了详细的时间戳。

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

     

    1.2 客户端的相关配置

      本节将对剩余配置进行介绍:

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

    □ maxclients:客户端最大连接数,这个参数会受到操作系统设置的限制。

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

    □ tcp-backlog: TCP三次握手后,会将接受的连接放入队列中,tcp-backlog就是队列的大小,它在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 en forced because /proc/sys/net/core/somaxconn is set to the lower value of 128.

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

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

     

    1.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

      说明如下:

    l) 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自启动以来拒绝的客户端连接数,需要重点监控。

     

    2.客户端常见异常

      在客户端的使用过程中,无论是客户端使用不当还是 Redis 服务端出现问题,客户端会反应出一些异常。本小节将分析一下 Jedis 使用过程中常见的异常情况。

      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.borrov/Object (GenericObjectPool.java:449)

      还有一种情况,就是设置了blockWhenExhausted=false,那么调用者发现池子中没有资源时,会立即抛出异常不进行等待,下面的异常就是blockVJhenExhausted=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.borrovyObject (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 (Exceptione)  {
               e.printStackTrace();
           }
    }

      当调用者再向连接池借用Jedis时(如下操作),就会抛出异常:

    jedisPool.getResource().ping();

    □ 客户端:存在慢查询操作,这些慢查询持有的Jedis对象归还速度会比较慢,造成池子满了。

    □ 服务端:客户端是正常的,但是Redis服务端由于一些原因造成了客户端命令执行过程的阻塞,也会使得客户端抛出这种异常。可以看到造成这个异常的原因是多个方面的,不要被异常的表象所迷惑,而且并不存在万能钥&解决所有问题,开发和运维只能不断加强对于Redis的理解,顺藤摸瓜逐渐找到问题所在。

     

      2.客户端读写超时

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

    redis.clients.jedis.exceptions.JedisConnectionException:
    java.net.SocketTimeout Exception: 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 o f stream.

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

    1) 输出缓冲区满。例如将普通客户端的输出缓冲区设置为1M 1M 60:

    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时,会收到下面的异常。对于如何处理这类问题,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做故障转移。

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

     

    3.客户端案例分析

      到目前为止,有关Redis客户端的相关知识基本已经介绍完毕,本节将通过Redis开发运维中遇到的两个案例分析,让读者加深对于Redis客户端相关知识的理解。

    3.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(因为处理速度会足够快),于是执行如下代码,找到mem非零的客户端连接:

    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运维工具。例如CacheCloud,上述问题在CacheCloud中会收到相应的报警,快速发现和定位问题。

     

    3.2 客户端周期性超时

      1.现象

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

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

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

     

      2.分析

    □ 网络原因:服务端和客户端之间的网络出现周期性问题,经过观察网络是正常的。

    □ Redis本身:经过观察Redis日志统计,并没有发现异常。

    □ 客户端:由于是周期性出现问题,就和慢查询日志的历史记录对应了一下时间,发现只要慢查询出现,客户端就会产生大量连接超时,两个时间点基本一致(如表4-6和图4-14所示)。

    表4-6慢查询

    序号

    耗时(单位:微秒)

    命令

    时间点

    1

    1 913 525

    HGETALL_user_fan_hset_sort

    2016-08-25 03:00:00

    2

    1 932 363

    HGETALL_user_fan_hset_sort

    2016-08-25 03:05:00

    3

    1 961 714

    HGETALL_user_fan_hset_sort

    2016-08-25 03:10:00

    4

    1 977 355

    HGETALL_user_fan_hset_sort

    2016-08-25 03:15:00

    5

    1 961 355

    HGETALL_user_fan_hset_sort

    2016-08-25 03:20:00

    6

    1 909 158

    HGETALL_user_fan_hset_sort

    2016-08-25 03:25:00

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

    127.0.0.1:6399> hlen user_fan_hset_sort
    (integer) 2883279

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

     

      3.处理方法和后期处理

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

    □从运维层面,监控慢查询,一旦超过阀值,就发出报警。

    □从开发层面,加强对于Redis的理解,避免不正确的使用方式。

    □使用专业的Redis运维工具,CacheCloud上述问题在CacheCloud中会收到相应的报警,快速发现和定位问题。

     

    4.总结

      1. 客户端输入缓冲区不能配置,强制限制在1G之内,但是不会受到maxmemory限制。 

      2. 客户端输出缓冲区支持普通客户端、发布订阅客户端、复制客户端配置,同样会受到maxmemory限制 0。

      3. Redis的timeout配置可以自动关闭闲置客户端,tcp-keepalive参数可以周期性检査关闭无效TCP连接。

      4. monitor命令虽然好用,但是在大并发下存在输出缓冲区暴涨的可能性。

      5. info clients帮助开发和运维人员找到客户端可能存在的问题。

    资料来自《redis开发与运维》

    作者:小家电维修

    相见有时,后会无期。

  • 相关阅读:
    All consistent reads within the same transaction read the snapshot established by the first read.
    Mojo 分析日志接口
    Mojo 分析日志接口
    Mojo Mysql utf-8字符集 需要{mysql_enable_utf8 => 1}
    Mojo Mysql utf-8字符集 需要{mysql_enable_utf8 => 1}
    MySQL 关闭FOREIGN_KEY_CHECKS检查
    MySQL 关闭FOREIGN_KEY_CHECKS检查
    14.3.2.4 Locking Reads 锁定读
    14.3.2.3 Consistent Nonlocking Reads 一致性非锁定读
    14.3.2.3 Consistent Nonlocking Reads 一致性非锁定读
  • 原文地址:https://www.cnblogs.com/lizexiong/p/14702731.html
Copyright © 2020-2023  润新知