最近在使用 redis-benchmark 测试时,发现偶尔连接超时。这个问题让人匪夷所思,因为所有 Redis  服务都是正常的,在测试的过程中,也没有出现重启现象,主从切换也没有发生。这里记录一下问题排查的过程。
 
 
背景
 
先说一下 Redis 运行的环境,我搭建的是一套 Redis 集群,运行在 k8s 中。Redis 3 主 3 从分布在 3 个 StatefulSet 中,连接用的 Service,绑定对象为这 6 个 Pod。在使用 redis-benchmark 测试的时候,就是用的 Service 的 IP 来连接的。
redis-benchmark 报错:
bash-5.1# redis-benchmark -h 10.4.119.162 -p 6379 -c 50 --cluster -n 1000000 -r 1000000 -d 512 --threads 8  --csv -t set,incr,hset
Cluster has 3 master nodes:
Master 0: 2c424d84214aa03cf402c9a886dd8a1d1487a344 10.3.0.115:6379
Master 1: c54420c20d70268b2973157690173bf6e46d2640 10.4.119.162:6379
Master 2: bf13c68f55e848f40c68a0f833470fdc6cda8f03 10.3.0.113:6379
Could not connect to Redis at 10.3.0.113:6379: Operation timed out
WARN: could not fetch node CONFIG 10.3.0.113:6379
 
 
排查过程
 
首先排查了 Redis 本身的服务状态,发现服务都正常。然后多执行了几遍,复现一下问题,也总结了一些规律。
在多次执行后,发现有时 nodes 列表中有 service 的 ip,这种情况下,很大概率失败。比如上面的执行记录,用的 service 的 ip 为 10.4.119.162,按理来说,nodes 列表里面不应该有这个 ip 的。在使用 cluster nodes 执行多遍过后,也是正常的。那么为什么会出现 service 的 ip 呢?随即我翻了一下 redis-benchmark 的源码,在其中发现 redis-benchmark 执行的时候会获取节点的 master ip,如果 redis-benchmark 的 host 参数和 master ip 列表里面的某个 ip 一致,则使用 host 的信息。也就是 redis-benchmark 打印的 master ip 可能会包含 service 的 ip。下面就是这段逻辑的代码。我使用的 redis 6.0.9 的版本,这个版本的实现会有这个问题,在最新的版本中,已经被 #8154  修复了。
 1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
  
static  int  fetchClusterConfiguration ()  { 
    int  success  =  1 ; 
    redisContext  * ctx  =  NULL ; 
    redisReply  * reply  =   NULL ; 
    ctx  =  getRedisContext ( config . hostip ,  config . hostport ,  config . hostsocket ); 
    if  ( ctx  ==  NULL )  { 
        exit ( 1 ); 
    } 
    // 根据当前 ip 创建第一个节点
      clusterNode  * firstNode  =  createClusterNode (( char  * )  config . hostip , 
                                               config . hostport ); 
    if  ( ! firstNode )  { success  =  0 ;  goto  cleanup ;} 
    reply  =  redisCommand ( ctx ,  "CLUSTER NODES" ); 
    // ... 省略
      while  (( p  =  strstr ( lines ,  " \n " ))  !=  NULL )  { 
        * p  =  '\0' ; 
        line  =  lines ; 
        lines  =  p  +  1 ; 
        char  * name  =  NULL ,  * addr  =  NULL ,  * flags  =  NULL ,  * master_id  =  NULL ; 
        // ... 省略
          int  myself  =  ( strstr ( flags ,  "myself" )  !=  NULL ); 
        int  is_replica  =  ( strstr ( flags ,  "slave" )  !=  NULL  || 
                         ( master_id  !=  NULL  &&  master_id [ 0 ]  !=  '-' )); 
        // 如果是从节点,则返回
          if  ( is_replica )  continue ; 
        if  ( addr  ==  NULL )  { 
            fprintf ( stderr ,  "Invalid CLUSTER NODES reply: missing addr. \n " ); 
            success  =  0 ; 
            goto  cleanup ; 
        } 
        // 初始化节点
          clusterNode  * node  =  NULL ; 
        // ... 省略
          
        // 如果当前节点 ip 和 -h 设置的 ip 一致,就直接用前面创建的 firstNode 作为节点信息
          if  ( myself )  { 
            node  =  firstNode ; 
            // 旧的实现
              if  ( node -> ip  ==  NULL  &&  ip  !=  NULL )  { 
                node -> ip  =  ip ; 
            // 新的实现
              if  ( ip  !=  NULL  &&  strcmp ( node -> ip ,  ip )  !=  0 )  { 
                node -> ip  =  sdsnew ( ip ); 
                node -> port  =  port ; 
            } 
        }  else  { 
            node  =  createClusterNode ( sdsnew ( ip ),  port ); 
        } 
        // ... 省略
        
        // 添加节点至 master 节点列表中
        	if  ( ! addClusterNode ( node ))  { 
            success  =  0 ; 
            goto  cleanup ; 
        } 
    } 
} 
 
 
但是为什么这个情况是随机的呢?我想到的是,在访问 Service 时,它会随机地把流量转发到对应的 Pod 中。如果把请求转发到了某一个主节点,那么它获取的节点列表,就会包含 Service 的 IP,在接下来执行性能测试的请求中,都会使用这个 IP。在多次请求后,Service 的请求转发会和之前不同,那么这种情况下,就会发生连接超时了。在 Service 中有个会话保持的功能,目的就是一个会话中,转发的 Pod IP 不会变化,可以通过 sessionAffinity 开启。sessionAffinity: None 则是不开启,sessionAffinity: ClientIP 则是基于客户端 IP 做回话保持。
 1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
  
kind :   Service 
 apiVersion :   v1 
 metadata : 
    name :   redis-cluster-demo 
    namespace :   redis 
    labels : 
      redis/name :   redis-cluster-demo 
 spec : 
    ports : 
      - name :   tcp-6379-6379 
        protocol :   TCP 
        port :   6379 
        targetPort :   6379 
    selector : 
      redis/name :   redis-cluster-demo 
    clusterIP :   10.4.119.162 
    type :   ClusterIP 
    sessionAffinity :   ClientIP 
    sessionAffinityConfig : 
      clientIP : 
        timeoutSeconds :   10800 
 
  
 
设置会话保持后,问题没有再出现了。针对于这个问题,可以通过设置 Service 的 sessionAffinity 来解决,也可以通过升级 redis-benchmark 的版本来解决。