Redis cluster 请求路由

548 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情

Redis cluster 请求路由

请求重定向

redis 客户端在发起任一的关于键的命令是会有,计算key的slot值,计算slot的节点位置,对指定的节点发起该命令等过程,但是在整个过程中可能存在MOVED重定向的问题。即在发起命令的时候key的slot已经被迁移并且迁移的整个过程已经结束,但是客户端本地的slot<->node映射缓存并没有更新,所以redis server就会响应MOVED重定向,其中会包含重定向的详细信息,客户端可以用来更新本地缓存,可以重新对新的节点发起命令。

注意: 在Jedis中ASK重定向和MOVED重定向是存在差别的,ASK重定向表示在迁移过程中,并且不会更新客户端本地的缓存,只是临时的用于客户端去发起新的请求,但是MOVED重定向则表示迁移已经结束但是本地缓存没有被刷新,需要用最新的信息刷新客户端缓存的。

key的槽计算

对key的槽计算默认是使用CRC16算法获取key的散列值再除余16384得到的槽位置的,一般计算的时候都是使用整个key值,但是在一些需求下需要进行批量操作比如pipeline或者mget、mset等他们都是不能跨slot进行的,所以redis提供了一种hash_tag格式命名key,型如:test1:{abc}:test2 这样的格式,在计算key值对应得slot的过程中只会使用只会使用{}中的标记字,这个标记字就叫做hash_tag,hash_tag在涉及实务Lua脚本pipeline批操作上是很好的解决方案。

Jedis客户端分析

Jedis 客户端中维护了一组slot→node的映射关系, 本地就可实现键到节点的查找, 从而保证IO效率的最大化, 而MOVED重定向负责协助Jedis客户端更新slot→node映射,以下是Jedis操作Redis Cluster流程:

1.在Jedis的JedisCluster中,在客户端初始化运行的时候会随机的选择一个节点发送cluster slots命令,用于初始化本地的slots-节点缓存(RedisClusterInfoCache)

2.JedisCluster解析cluster slots的响应,将信息保存到JedisClusterInfoCache中,并且为每一个节点创建一个单独的JedisPool连接池

3.执行相应的键命令,这个过程相对比较复杂,键执行流程:

**a.**计算slot并根据slots缓存获取目标节点连接, 发送命令。

**b.**如果出现连接错误, 使用随机连接重新执行键命令, 每次命令重试对maxAttempts参数减1。

**c.**捕获到MOVED重定向错误, 使用cluster slots命令更新slots缓存(renewSlotCache方法) 。捕获到MOVED重定向错误, 使用cluster slots命令更新slots缓存(renewSlotCache方法) 。

**d.**重复执行1) ~3) 步, 直到命令执行成功, 或者当maxAttempts<=0时抛出Jedis ClusterMaxRedirectionsException异常。

相关的代码实现如下(Jedis2.9.0):

// redis.clients.jedis.JedisClusterCommand`中。
public abstract class JedisClusterCommand<T> {
    //集群节点连接处理器
    private JedisClusterConnectionHandler connectionHandler;
    //最大重试次数,默认5次
    private int maxAttempts;
    private ThreadLocal<Jedis> askConnection = new ThreadLocal();

    public JedisClusterCommand(JedisClusterConnectionHandler connectionHandler, int maxAttempts) {
        this.connectionHandler = connectionHandler;
        this.maxAttempts = maxAttempts;
    }
    //模板回调方法
    public abstract T execute(Jedis var1);

    public T run(String key) {
        if (key == null) {
            throw new JedisClusterException("No way to dispatch this command to Redis Cluster.");
        } else {
            return this.runWithRetries(SafeEncoder.encode(key), this.maxAttempts, false, false);
        }
    }
    //有重试的执行命令
    private T runWithRetries(byte[] key, int attempts, boolean tryRandomNode, boolean asking) {
        //超过最大重试次数则抛出JedisClusterMaxRedirectionsException
        if (attempts <= 0) {
            throw new JedisClusterMaxRedirectionsException("Too many Cluster redirections?");
        } else {
            Jedis connection = null;

            Object var7;
            try {
                if (asking) {//如果key计算得到slot在第一次请求后,redis -server响应了ASK重定向执行ASK重定向逻辑
                    connection = (Jedis)this.askConnection.get();
                    connection.asking();
                    asking = false;
                } else if (tryRandomNode) {//如果是第一次访问或者是Moved重定向以后的访问随机获取活跃节点连接
                    connection = this.connectionHandler.getConnection();
                } else {
                    //使用slot缓存获取目标连接
                    connection = this.connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key));
                }

                Object var6 = this.execute(connection);
                return var6;
            } catch (JedisNoReachableClusterNodeException var13) {
                throw var13;
            } catch (JedisConnectionException var14) {
                //出现连接异常,释放连接
                this.releaseConnection(connection);
                connection = null;
                if (attempts <= 1) {

                    this.connectionHandler.renewSlotCache();
                    throw var14;
                }

                var7 = this.runWithRetries(key, attempts - 1, tryRandomNode, asking);
                return var7;
            } catch (JedisRedirectionException var15) {
                if (var15 instanceof JedisMovedDataException) {
                    //出现MOVED重定向异常,则再执行cluster slots获取集群信息刷新缓存
                    this.connectionHandler.renewSlotCache(connection);
                }

                this.releaseConnection(connection);
                connection = null;
                if (var15 instanceof JedisAskDataException) {
                    asking = true;
                    this.askConnection.set(this.connectionHandler.getConnectionFromNode(var15.getTargetNode()));
                } else if (!(var15 instanceof JedisMovedDataException)) {//如果Redis Server 响应Moevd重定向则抛出JedisMovedDataException,runWithRetries是嵌套调用这个异常在外面一层的该方法中捕获,并且发送cluster slots命令且使用renewSlotCache更新本地的slot-node映射。
                    throw new JedisClusterException(var15);
                }
                //每次重试maxAttempts-1
                var7 = this.runWithRetries(key, attempts - 1, false, asking);
            } finally {
                this.releaseConnection(connection);
            }

            return var7;
        }
    }


问题分析:

1.JedisCluster 内部维护了一个数据槽(slot)到集群节点的映射,并且对于每一个节点都单独的维护了一个JedisPool,每一个pool里面又存在多个连接,当集群非常大的时候会维护很多的连接,对内存的消耗会很大;

2.常见异常---JedisClusterMaxRedirectionsException(重定向超过次数) 原因是节点碟机或者连接超时时会抛出JedisConnectionException,这个异常会导致重试,maxAttempts<=0时就会抛出该异常

3.JedisConnectionException,收到这个异常Jedis就会认为节点连接存在异常,需要随机重试来更新本地的JedisClusterInfoCache缓存。以下是几种会导致该异常的情况: a.Jedis节点发生socket错误时候抛出;

b.所有命令或者是Lua脚本读写超时的时候抛出;

c.另外在老版本的Jedis中,从JedisPool中获取Jedis对象超时也会抛出,但是2.8.1以后对于c连接池超时的情况改为抛出JedisException,避免触发随机重试。

4.Redis Cluster支持自动故障迁移,这个过程需要一定的时间,节点宕机期间所有指向这个节点的命令都会触发随机重试, 每次收到MOVED重定向后会调用JedisClusterInfoCache类的renewSlotCache方法。代码如下:


public void renewClusterSlots(Jedis jedis) {
    if (!this.rediscovering) {
        try {
            //获取读写锁
            this.w.lock();
            this.rediscovering = true;
            if (jedis != null) {
                try {
                    this.discoverClusterSlots(jedis);
                    return;
                } catch (JedisException var17) {
                    ;
                }
            }
            //如果连接为空触发以下的随机重试
            //随机获取一个连接池对象,并且发送 cluster slots命令获取集群slots分配详情(内部封装)
            Iterator var2 = this.getShuffledNodesPool().iterator();

            while(var2.hasNext()) {
                JedisPool jp = (JedisPool)var2.next();

                try {
                    jedis = jp.getResource();
                    this.discoverClusterSlots(jedis);
                    return;
                } catch (JedisConnectionException var15) {
                    ;
                } finally {
                    if (jedis != null) {
                        jedis.close();
                    }

                }
            }
        } finally {
            this.rediscovering = false;
            this.w.unlock();
        }
    }

}


个别节点操作异常导致频繁的更新slots缓存, 多次调用cluster slots命令, 高并发时将过度消耗Redis节点资源, 如果集群slot<->node映射庞大则cluster slots返回信息越多,占用带宽越大,问题越严重。当出现JedisConnectionException时,命令发送次数为5次: 4次重试命令+1次cluster slots命令只有一次cluster slots执行是因为rediscovering变量保证了同一时刻只允许一个线程更改缓存。