美文网首页
「码呗学院」Redis 集群部署

「码呗学院」Redis 集群部署

作者: 码呗学院 | 来源:发表于2019-02-26 20:33 被阅读0次

    简介

    redis以高效的读写性能,被非常多的企业用户用来做数据缓存层,很多公司会根据自己业务场景,部署单点或者是redis的集群,但是单点的redis如果发生故障,是无法保证服务高可用的,所以现在缓存服务通常都是redis的集群。

    redis的集群方案目前使用的比较多的几种方式;

        基于codis的架构,部署的redis集群。

        基于redis的哨兵模式,部署一个master多slave的集群模式。

        redis从3.0开始支持集群的部署方式,而且是无中心化的,多master多slave模式。 

    本次主要介绍redis自己支持的cluster的方式部署以及简单应用

    集群架构

    首先是我们准备的三台服务器资源,规划每台服务器上部署一个master实例和一个slave实例。在每台服务器上安装好redis,规划服务器列表如下:

    规划服务器列表

    介绍一下redis支持的cluster集群架构,redis cluster 集群节点类型分为master和slave,我们用组的概念理解一下,以master为类型进行分组,一个组里面只有一个master,我们可以认为是组长,其他的slave是组员,那数据存到redis集群里面,怎么存储呢,redis集群默认实现了一个hash slot的方式,哈希槽默认是16384个,也就是0-16383个槽,集群在创建的时候,每个组master会均匀分配这些槽,每个master占据一定的hash slot 范围,往redis集群里面存储数据的时候,redis默认通过crc16(key)%16384 计算这个key的hash值,然后根据hash值找到对应的master,直连该master进行数据访问和存储。redis集群里面所有实例都有hash slot对应的master信息,所以连接任何一个实例信息,都能获取到集群的ip和端口列表,然后根据计算出来的hash值,选择具体的ip进行直接访问。

    相同颜色的实例为一组,master负责读写请求,slave为备份

    安装Redis

    下载安装redis:

    ## redis下载目录可以参考:http://download.redis.io/releases/

    wget http://download.redis.io/releases/redis-4.0.8.tar.gz

    ## 解压文件

    tar -xzf redis-4.0.8.tar.gz

    ## 进入解压目录,编译安装

    make

    make install

    centos服务器,也可以通过yum install redis 方式安装,测试安装结果。

    redis-cli-v

    在每台服务器上启动redis-server,因为服务器资源有限,所以每台服务器上部署两个redis实例,当然如果只有一台服务器的情况下,也可以在一个服务器上部署多个实例,redis的配置文件如下,如果要部署多个实例注意修改端口号以及工作目录和日志文件信息。

    port 9002

    bind    0.0.0.0

    daemonize  yes

    logfile  "/root/temp/redis/redis-4.0.8/logs/node-9002.log"

    cluster-enabled  yes

    cluster-config-file  /root/temp/redis/redis-4.0.8/config/nodes-9002.conf

    cluster-node-timeout  5000

    cluster-slave-validity-factor    10

    appendonly  yes

    dir  /root/temp/redis/redis-4.0.8/data/9002

    dbfilename    dump-9002.rdb

    appendfilename    "appendonly-9002.aof"

    redis-server启动以后可以查看每台上的实例状态,下面开始构建集群。

    如果是通过源码安装的redis,在src目录下会有redis-trib.rb文件,这是ruby写的一个脚本文件,redis的集群创建管理就是通过这个脚本来操作,所以在执行这个脚本之前,首先还要保证服务器可以执行ruby文件。执行该脚本需要ruby版本>=2.2.2或者以上,检查本机ruby的版本,如果版本过旧,可以通过以下操作升级ruby版本:

    ## 安装rvm,Ruby version manager,Ruby的一个版本管理工具,如果使用过node,就跟nvm一样,通过rvm可以看到ruby版本列表,可以很方便的进行安装和删除。

    ## 安装rvm的方式;

    ## gpg2是mac上的命令,linux服务器命令是gpg

    gpg2 --keyserver hkp://keys.gnupg.net --recv-keys \ 409B6B1796C275462A1703113804BB82D39DC0E3 \ 7D2BAF1CF37B13E2069D6956105BD0E739499BDB

    curl -sSL https://get.rvm.io | bash -s stable

    执行命令安装好以后,会有提示让执行source命令,让rvm生效:

    source $HOME/.rvm/scripts/rvm

    ## 查看安装好的信息

    rvm -v

    ## 查看ruby 版本列表

    rvm list known

    ##安装ruby版本2.5

    rvm install 2.5

    ##安装好ruby以后,还需要安装redis的模块。

    gem install redis

    ## 如果在安装redis模块的时候很慢,可以更换ruby仓库源,使用aliyun的源仓库。

    ## gem source -a http://mirrors.aliyun.com/rubygems/

    以上就安装好ruby相关的环境了,然后我们就可以通过redis-trib.rb命令来管理集群了。

    Redis集群创建

    redis-trib.rb我是通过脚本的形式执行的,也可以把他放到/usr/bin/目录下,然后就可以执行redis-trib命令了,可以查看redis-trib命令的先关帮助信息:

    ruby redis-trib.rb help

    Usage: redis-trib <command> <options> <arguments ...>

      create          host1:port1 ... hostN:portN

                      --replicas <arg>

      check          host:port

      info            host:port

      fix            host:port

                      --timeout <arg>

      reshard        host:port

                      --from <arg>

                      --to <arg>

                      --slots <arg>

                      --yes

                      --timeout <arg>

                      --pipeline <arg>

      rebalance      host:port

                      --weight <arg>

                      --auto-weights

                      --use-empty-masters

                      --timeout <arg>

                      --simulate

                      --pipeline <arg>

                      --threshold <arg>

      add-node        new_host:new_port existing_host:existing_port

                      --slave

                      --master-id <arg>

      del-node        host:port node_id

      set-timeout    host:port milliseconds

      call            host:port command arg arg .. arg

      import          host:port

                      --from <arg>

                      --copy

                      --replace

      help            (show this help)

    For check, fix, reshard, del-node, set-timeout you can specify the host and port of any working node in the cluster.

    上面的帮助详细介绍了redis-trib的使用说明,首先我们创建一个集群:

    ruby redis-trib.rb create --replicas 1 192.168.35.66:7001 192.168.35.73:7001 192.168.35.74:7001 192.168.35.66:9002 192.168.35.73:9002 192.168.35.74:9002

    介绍一下命令的参数信息:

    create 是子命令,表示创建一个新的集群,并且创建集群的master节点必须>=3个才可以创建集群,replicas表示每个master有几个slave,这里是1,表示每个master至少有一个slave实例。集群创建会弹出一个提示窗口,确认没有问题的话,输入yes,即可完成集群创建,然后通过check检查集群状态:

    ruby redis-trib.rb check 192.168.35.74:7001

    >>> Performing Cluster Check (using node 192.168.35.74:7001)

    M: c0be24752e6a23b566112c5389c20c35e97a5182 192.168.35.74:7001

      slots:5461-5797,10923-16383 (5798 slots) master

      1 additional replica(s)

    M: c1e9e762e553b5b62a674c439c8272447b2efc82 192.168.35.73:7001

      slots:5798-10922 (5125 slots) master

      1 additional replica(s)

    S: e00b99df51149512a4b4e6d64eec4d7550a63ede 192.168.35.73:9002

      slots: (0 slots) slave

      replicates 7a70746b2e23ee06e1bedb07f1952bfc9501a859

    M: 7a70746b2e23ee06e1bedb07f1952bfc9501a859 192.168.35.66:7001

      slots:0-5460 (5461 slots) master

      1 additional replica(s)

    S: 82da7bd42cdd96785a2f131445c7b189c48c75a3 192.168.35.74:9002

      slots: (0 slots) slave

      replicates c0be24752e6a23b566112c5389c20c35e97a5182

    S: 899a7a0199eeab0fcc5bce108efc2a57cf3d1d23 192.168.35.66:9002

      slots: (0 slots) slave

      replicates c1e9e762e553b5b62a674c439c8272447b2efc82

    [OK] All nodes agree about slots configuration.

    >>> Check for open slots...

    >>> Check slots coverage...

    [OK] All 16384 slots covered.

    上面显示我的集群状态信息,slot分配的结果看上去可能不太均匀,这是因为我做过增减节点,slot重新分配过,如果想让均衡一下,可以执行如下命令:

    ##这个命令用来重新规划集群各个势力的负载

    ruby redis-trib.rb rebalance  192.168.35.74:7001

    ##重新规划以后再通过check命令检查,会发现redis的各个实例分配的slots很均匀了(命令输出结果就不在这里贴出来了)

    ruby redis-trib.rb check 192.168.35.74:7001

    然后就会发现每个master分配的hash slot非常均衡了,有两个是5461个slots,一个是5462个slots,执行的命令就是rebalance。到此,我们的redis集群已经创建完成了。

    访问redis集群

    现在通过redis-cli和应用程序来访问一下集群信息。

    ## redis 访问集群的时候,需要执行-c参数。

    redis-cli -c -h 192.168.35.74 -p 7001

    192.168.35.74:7001> get name

    -> Redirected to slot [5798] located at 192.168.35.73:7001

    "ggcc"

    192.168.35.73:7001>

    通过上面的执行,发现获取name值的时候一开始我们连的是74这台服务器,get命令执行的时候,却重定向到了73这台服务器,所以client访问集群的时候,首先是根据key计算出来hash值,再根据hash值找到对应的master实例节点信息,直连对应的master实例,进行数据的读写。

    redis-cli命令是这样执行的,那么我们看Java程序是怎么读写数据的。

    package redis;

    import redis.clients.jedis.HostAndPort;

    import redis.clients.jedis.JedisCluster;

    import java.util.HashSet;

    import java.util.Scanner;

    import java.util.Set;

    public class RedisClusterCli {

        public static void main(String[] args) {

            HostAndPort hostAndPort = new HostAndPort("192.168.35.74", 9002);

            Set<HostAndPort> hostAndPorts = new HashSet<>();

            hostAndPorts.add(hostAndPort);

            JedisCluster cluster = new JedisCluster(hostAndPorts);

            String value;

            while (true) {

                try {

                    System.out.println("请输入key:");

                    Scanner scanner = new Scanner(System.in);

                    String key = scanner.next();

                    value = cluster.get(key);

                    System.out.println(value);

                } catch (Exception ex) {

                    ex.printStackTrace();

                    System.out.println("异常信息忽略");

                }

            }

        }

    }

    测试代码写的比较简单,单独执行的话,能够正常运行,但是看不出具体的执行细节,我们查看一下关键部分的源码,然后看一下它执行的具体逻辑。redis.clients.jedis.JedisClusterCommand.runWithRetries方法

    private T runWithRetries(byte[] key, int attempts, boolean tryRandomNode, boolean asking) {

      if (attempts <= 0) {

        throw new JedisClusterMaxRedirectionsException("Too many Cluster redirections?");

      }

      Jedis connection = null;

      try {

        if (asking) {

          // TODO: Pipeline asking with the original command to make it

          // faster....

          connection = askConnection.get();

          connection.asking();

          // if asking success, reset asking flag

          asking = false;

        } else {

          if (tryRandomNode) {

            connection = connectionHandler.getConnection();

          } else {

            connection = connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key));

          }

        }

        return execute(connection);

      } catch (JedisNoReachableClusterNodeException jnrcne) {

        throw jnrcne;

      } catch (JedisConnectionException jce) {

        // release current connection before recursion

        releaseConnection(connection);

        connection = null;

        if (attempts <= 1) {

          //We need this because if node is not reachable anymore - we need to finally initiate slots renewing,

          //or we can stuck with cluster state without one node in opposite case.

          //But now if maxAttempts = 1 or 2 we will do it too often. For each time-outed request.

          //TODO make tracking of successful/unsuccessful operations for node - do renewing only

          //if there were no successful responses from this node last few seconds

          this.connectionHandler.renewSlotCache();

          //no more redirections left, throw original exception, not JedisClusterMaxRedirectionsException, because it's not MOVED situation

          throw jce;

        }

        return runWithRetries(key, attempts - 1, tryRandomNode, asking);

      } catch (JedisRedirectionException jre) {

        // if MOVED redirection occurred,

        if (jre instanceof JedisMovedDataException) {

          // it rebuilds cluster's slot cache

          // recommended by Redis cluster specification

          this.connectionHandler.renewSlotCache(connection);

        }

        // release current connection before recursion or renewing

        releaseConnection(connection);

        connection = null;

        if (jre instanceof JedisAskDataException) {

          asking = true;

          askConnection.set(this.connectionHandler.getConnectionFromNode(jre.getTargetNode()));

        } else if (jre instanceof JedisMovedDataException) {

        } else {

          throw new JedisClusterException(jre);

        }

        return runWithRetries(key, attempts - 1, false, asking);

      } finally {

        releaseConnection(connection);

      }

    }

    首先注意看第21行代码,这里是表示根据key的hash值,计算出来slot,然后根据slot信息获取相应的节点ip和端口信息,获取具体IP的代码逻辑在redis.clients.jedis.JedisSlotBasedConnectionHandler.getConnectionFromSlot这个方法中,具体代码如下:

    public Jedis getConnectionFromSlot(int slot) {

        JedisPool connectionPool = cache.getSlotPool(slot);

        if (connectionPool != null) {

          // It can't guaranteed to get valid connection because of node

          // assignment

          return connectionPool.getResource();

        } else {

          renewSlotCache(); //It's abnormal situation for cluster mode, that we have just nothing for slot, try to rediscover state

          connectionPool = cache.getSlotPool(slot);

          if (connectionPool != null) {

            return connectionPool.getResource();

          } else {

            //no choice, fallback to new connection to random node

            return getConnection();

          }

        }

      }

    }

    这个代码逻辑比较简单,首先是从缓存获取,缓存的数据结构是个map对象,map对象用了一个最简单直接的方法,存储了16384对象信息,key分别是对应的0-16383个数字,value就是JedisPool对象,也就是保存的具体的服务器IP和端口。如果缓存没有获取到,执行第8行代码,加载信息,加载到缓存以后,如果依然没有获取到,那就返回一个随机的,理论上应该都是能获取到的,当然也不排除极端的情况,比如访问其中一个master节点挂机,redis.clients.jedis.JedisClusterCommand.runWithRetries这个方法会抛出一个异常JedisConnectionException,但是这个异常里面会递归调用多次,也就是说会默认重试调用,默认的次数是5次,当最后一次调用的时候,依然报了这个异常,那么就会重新加载redis的集群信息,因为master挂机以后,新的slave产生,slots对应的IP信息也会出现变化。

    redis集群中的一个master挂机以后,新的slave晋升为master,这中间有个心跳检查时间,根据redis的参数cluster-slave-validity-factor配置,在这个时间内,如果获取挂机master实例上的数据,将会频繁报错,通常在应用场景中,就会穿透缓存层,直接访问数据库。因为是redis集群,每个master上面分配的数据都只是一部分,即便是穿透缓存层,也是一部分数据而已,而且根据我们部署的redis集群的数量,redis master实例越多,数据分配的越是均匀,故障的发生率越是低,影响的面就越小,这样redis集群就能很好的保证我们服务的高可用性。

    Redis集群扩容

    redis集群的扩容和缩容,同样使用redis-trib来操作,下面是操作执行流程,执行记录比较多,而且比较长,可以直接跳过,看后面操作命令的总结,实际操作时,有问题再看具体的执行流程记录:

    ## 新增加一个master节点

    ruby redis-trib.rb add-node 192.168.35.73:9003 192.168.35.74:9002

    >>> Adding node 192.168.35.73:9003 to cluster 192.168.35.74:9002

    >>> Performing Cluster Check (using node 192.168.35.74:9002)

    S: 82da7bd42cdd96785a2f131445c7b189c48c75a3 192.168.35.74:9002

      slots: (0 slots) slave

      replicates c0be24752e6a23b566112c5389c20c35e97a5182

    S: e00b99df51149512a4b4e6d64eec4d7550a63ede 192.168.35.73:9002

      slots: (0 slots) slave

      replicates 7a70746b2e23ee06e1bedb07f1952bfc9501a859

    M: 7a70746b2e23ee06e1bedb07f1952bfc9501a859 192.168.35.66:7001

      slots:0-5460 (5461 slots) master

      1 additional replica(s)

    M: c1e9e762e553b5b62a674c439c8272447b2efc82 192.168.35.73:7001

      slots:5461-10922 (5462 slots) master

      1 additional replica(s)

    S: 899a7a0199eeab0fcc5bce108efc2a57cf3d1d23 192.168.35.66:9002

      slots: (0 slots) slave

      replicates c1e9e762e553b5b62a674c439c8272447b2efc82

    M: c0be24752e6a23b566112c5389c20c35e97a5182 192.168.35.74:7001

      slots:10923-16383 (5461 slots) master

      1 additional replica(s)

    [OK] All nodes agree about slots configuration.

    >>> Check for open slots...

    >>> Check slots coverage...

    [OK] All 16384 slots covered.

    >>> Send CLUSTER MEET to node 192.168.35.73:9003 to make it join the cluster.

    [OK] New node added correctly.

    以上记录很多,但是实际上操作步骤没有多少,这里总结一下:

    ##第一个命令,首先是增加一个master实例,第一个IP:PORT信息是需要新增的实例地址,后面的IP:PORT是集群中任意一个实例的端口和ip信息,就是集群的入口。

    ruby redis-trib.rb add-node 192.168.35.73:9003 192.168.35.74:9002

    ##第二个命令,这个命令就是为刚才新增的那个master增加一个slave实例,注意--slave参数表示增加的是slave实例,master-id就是刚才增加的master的node id值,可以通过check查看。第一个IP:PORT信息就是新增的slave实例信息,第二个参数和第一个命令一样,就是告诉集群的入口。

    ruby redis-trib.rb add-node --slave --master-id 67d82f2e6e1f9c0afbc15090732d7f2f41064fbc 192.168.35.73:9004 192.168.35.74:9002

    ##第三个命令,新增的实例信息,其实是没有分配slot值的,即便是执行rebalance也是没有值的,就是认为该实例还没有启用,所以需要执行一下,给他分配一些slots值过去。reshard就是slots迁移命令,from和to 分别表示从哪里迁移到目标实例。如果我们需要下掉某一个master实例,我们就可以通过这个操作把当前实例的所有slots值全部迁移走。

    ruby redis-trib.rb reshard --from e00b99df51149512a4b4e6d64eec4d7550a63ede --to 67d82f2e6e1f9c0afbc15090732d7f2f41064fbc --slots 3099 --yes --timeout 30 192.168.35.74:9002

    ##最后一步命令是可选的,因为你手动迁移的slots值,其实是不够均衡的,所以对于强迫症的用户来说,必须要执行一下rebalance命令,然后系统会重新分配slots信息,均匀分配

    ruby redis-trib.rb rebalance 192.168.35.74:9002

    以上操作对于应用程序来说都是无感知的,除非是迁移的时候搞down机了,应用程序会报错,但是报错的范围也会很小。

    总结

    Redis Cluster方案,适合高并发读写的场景,无中心化的配置,保障集群的高可用性,并且通过指定多个slave的方式,保障了数据的安全性,而且提供了相对简单的集群管理配置,没有三方服务的依赖,用来构建成产线缓存服务集群,再合适不过。

    相关文章

      网友评论

          本文标题:「码呗学院」Redis 集群部署

          本文链接:https://www.haomeiwen.com/subject/ytvzyqtx.html