美文网首页redisredis学习程序员
追踪Redis Sentinel的CPU占有率长期接近100%的

追踪Redis Sentinel的CPU占有率长期接近100%的

作者: 宅楠军 | 来源:发表于2017-07-01 16:41 被阅读714次

    回顾问题


    《追踪Redis Sentinel的CPU占有率长期接近100%的问题》一文中,通过结合Redis Sentinel的源码,发现由于出现了"Too many open files"问题,致使Sentinel的acceptTcpHandler事件处理函数会被频繁并快速调用,最终导致了CPU长期接近100%的现象。但对于为什么会出现“Too many open files”这个问题,本文将在上一篇的基础上,继续探讨和分析。

    open files 与 file-max


    “Too many open files”这个错误其实很常见,想必大家早已对其有一定的了解,这里打算再简单的介绍一下。

    很明显,“Too many open files”即说明打开的文件(包括socket)数量过多,已经超出了系统设定给某个进程最大的文件描述符的个数,超过后即无法继续打开新的文件,并且报这个错误。

    首先,我们需要了解有关open files的基本知识。详细的概念大家可以谷歌,网上也有各种各样的解决办法,这里只对open files做简单的介绍和总结。

    我们在linux上运行ulimit -a 后,出现:

    image.png

    如图open files的个数为1024,很明显这个值太小了(linux默认即为1024),当进程耗尽1024个文件或socket后,就会出现“Too many open files”错误。现实生产环境中这个值很容易达到,所以一般都会进行相应修改。

    最简单的修改方式是ulimit -n 65535,但这样重启系统后又会恢复。永久生效的方法是修改/etc/security/limits.conf 文件,在最后加入:

    * soft nofile 65535 
    * hard nofile 65535 
    

    修改完成后,重启系统或者运行sysctl -p使之生效。

    soft 和hard,表示对进程所能打开的文件描述符个数限制,其概念为:

    • soft 软限制,ulimit -a 默认显示软限制
    • hard 硬限制,表示soft的上限
    • 只有root才能增加hard 普通用户只能减小hard

    他们的区别就是软限制可以在程序的进程中自行改变(突破限制),而硬限制则不行(除非程序进程有root权限)。
    上面的规则大家最好自己尝试一下,以增加印象。

    重启后,运行ulimit -a,可以看到open files 的值改变为:

    image.png

    简单介绍完open files,我们再来了解下file-max。这个参数相信有很多人经常与ulimit中的open files混淆,他们的区别我们必须了解。

    file-max 从字面意思就可以看出是文件的最大个数,运行cat /proc/sys/fs/file-max,可以看到:

    image.png

    这表示当前系统所有进程一共可以打开的文件数量为387311。请务必注意”系统所有进程"这几个字。

    运行 vim /etc/sysctl.conf ,有时候你会看到类似:fs.file-max = 8192。出现这个则表示用户手动设置了这个值,没有则系统会有其默认值。手动设置的话,只需要在sysctl.conf 中加上上述语句即可。

    回到ulimit中的open files,它与file-max的区别就是:opem filese表示当前shell以及由它启动的进程的文件描述符的限制,也就是说ulimit中设置的open files,只是当前shell及其子进程的文件描述符的限定。是否清楚?可以简单的理解为:

    image.png

    好了,对于 file-max与open files的简单介绍到此为止。现在的问题就是,“Too many open files”到底是碰到哪个设置的雷区造成的。

    查看TCP连接


    结合上一篇,我们知道sentinel主要在执行accept函数时出现了“Too many open files”错误,熟悉accept这个系统调用的朋友很清楚,accept会接收客户端的请求,成功的话会建立连接,并返回新的socket描述符。所以,我们确定这里的“Too many open files”指的即是socket的数目过多。

    我们猜测,是否是有大量的Jedis连接同时存在,耗尽服务器的socket资源,导致新的连接请求无法建立。所以,我们查看一下sentinel服务器的TCP连接,运行:netstat -anp | grep 26379,得到:

    image.png

    由上图可以发现,有非常多处于ESTABLISHED状态的TCP连接,运行 netstat -anp | grep 118:26379 | wc -l查看他们的个数:

    image.png

    可以看到,Sentinel同时维持了4071个TCP连接,而且过了很久之后,仍然是这么多,不会有大幅变化。

    这时,也许你会想到,是否因为系统文件描述符的限制导致Sentinel无法建立更多的Socket,从而产生“Too many open files”的错误。所以,马上在Sentinel服务器上运行cat /proc/sys/fs/file-max,发现:

    image.png

    这个值很大,看似一切正常。继续运行sudo cat /proc/5515/limits,发现:

    image.png

    看到上图,我们似乎发现了端倪,这里的hard和soft都是4096,与前面的4072比较接近。为什么会这么低?我们继续查看一下ulimit,运行ulimit -a :

    image.png

    可以看到,ulimit中open files 的设置为64000,为什么会不一致?按理说sentinel应该最大有64000+的open files。

    对于这个矛盾,一开始我怎么也想不明白。最后我推测:应该是在最早启动sentinel启动的时候,系统的设置为4096,sentinel启动之后,又在某个时间又改为64000,而sentinel进程确保持原有设置,从而导致很快达到限制。

    我马上查看了进程的启动时间,运行ps -eo pid,lstart,etime | grep 5515:

    image.png

    发现进程启动于2015年12月2日,接着再次运行 ll /etc/security/limits.conf:

    image.png

    发现确实在2016年4月改动过, 然后我咨询了运维人员,他们告知我,确实改动过,但由多少改动到多少,他们也忘了。。。

    查看Sentinel日志和配置文件


    为了了解Sentinel启动时的状况,紧接着查看了Sentinel的日志,下面是Sentinel启动时打印的画面:

    image.png

    上图说明了进程是2015年12月2日启动的,特别注意最开头的几行,非常关键:

    [5515] 02 Dec 16:09:49.524 # You requested maxclients of 10000 requiring at least 10032 max file descriptors.
    [5515] 02 Dec 16:09:49.524 # Redis can't set maximum open files to 10032 because of OS error: Operation not permitted.
    [5515] 02 Dec 16:09:49.524 # Current maximum open files is 4096. maxclients has been reduced to 4064 to compensate for low ulimit. If you need higher maxclients increase 'ulimit -n'.
    
    

    这几句的意思是:

    • maxclients是10000个,也就是最大可以接受10000个连接,但至少要有10032个(剩下32个可能是Sentinel自身保留需要)
    • 但是:
    • Sentinel不能搞到10032个,因为——“OS error: Operation not permitted.”。系统权限不够。
    • 当前最大open files限制是4096,maxclients==10000这个要求达不到,所以减少到4096了
    • 最后还建议用户最好手动修改增加下ulimit -n

    问题很清楚了,redis sentinel最大可以支持10000个客户端,也就是10032个文件描述符,但由于当前被人为限制到4096 了,所以,自动降低了标准。

    因此,我猜测,最早open files的限制为4096时,Sentinel已经启动了,只要进程启动,改多少都没有用。很明显,在生产环境上,4096个连接请求很快就会达到。

    接着,我继续查看了Sentinel的配置文件,如下图所示:

    image.png

    上图中, “Generated by CONFIG REWRITE”之前的都是是人工配置,其后为Sentinel自动重写的配置。

    熟悉Redis的朋友都知道。Sentinel可能会对其配置文件进行更新重写:

    Sentinel 的状态会被持久化在 Sentinel 配置文件里面。每当 Sentinel 接收到一个新的配置, 或者当领头 Sentinel 为主服务器创建一个新的配置时, 这个配置会与配置纪元一起被保存到磁盘里面。

    我们很快注意到了maxclients 4064这个配置项,此时我很迷惑。我们知道,在Sentinel中是无法手动运行config set命令的,那这个4096必然不是来自于人工配置,Sentinel为什么要自动重写4064这个值。其实,仔细发现,这里Sentinel限制了最多4064个连接,加上32个预留,刚好为4096。

    于是,综上,我猜测,Sentinel在启动的时候发现自己的10032个open files的预期与事实设置的4096不符,所以被迫遵守4096,减去预留的32,最终maxclients 只有4064,并且之后因为某些原因重写了配置,所以输出了这个值。

    好吧,我的一贯作风,先猜测,再让源码说话。

    maxclients 的秘密


    我们可以通过异常信息定位异常所处源码,所以我搜索了前面提到的Sentinel在启动时打印的关于maxclients 的日志信息中的文本,如“You requested maxclients of ...”。

    这个异常出现在adjustOpenFilesLimit函数,通过函数名可以清楚它的作用,然后发现它的调用链只是:
    ``main()->initServer()->adjustOpenFilesLimit()```

    所以,可以确定,在Sentinel服务器启动并进行初始化的时候,会调用adjustOpenFilesLimit函数对open files个数进行调整。调整策略是什么呢?我们查看源码:

    void adjustOpenFilesLimit(void) {
        rlim_t maxfiles = server.maxclients+REDIS_MIN_RESERVED_FDS;
        struct rlimit limit;
    
        if (getrlimit(RLIMIT_NOFILE,&limit) == -1) {
            redisLog(REDIS_WARNING,"Unable to obtain the current NOFILE limit (%s), assuming 1024 and setting the max clients configuration accordingly.",
                strerror(errno));
            server.maxclients = 1024-REDIS_MIN_RESERVED_FDS;
        } else {
            rlim_t oldlimit = limit.rlim_cur;
    
            /* Set the max number of files if the current limit is not enough
             * for our needs. */
            if (oldlimit < maxfiles) {
                rlim_t f;
                int setrlimit_error = 0;
    
                /* Try to set the file limit to match 'maxfiles' or at least
                 * to the higher value supported less than maxfiles. */
                f = maxfiles;
                while(f > oldlimit) {
                    int decr_step = 16;
    
                    limit.rlim_cur = f;
                    limit.rlim_max = f;
                    if (setrlimit(RLIMIT_NOFILE,&limit) != -1) break;
                    setrlimit_error = errno;
    
                    /* We failed to set file limit to 'f'. Try with a
                     * smaller limit decrementing by a few FDs per iteration. */
                    if (f < decr_step) break;
                    f -= decr_step;
                }
    
                /* Assume that the limit we get initially is still valid if
                 * our last try was even lower. */
                if (f < oldlimit) f = oldlimit;
    
                if (f != maxfiles) {
                    int old_maxclients = server.maxclients;
                    server.maxclients = f-REDIS_MIN_RESERVED_FDS;
                    if (server.maxclients < 1) {
                        redisLog(REDIS_WARNING,"Your current 'ulimit -n' "
                            "of %llu is not enough for Redis to start. "
                            "Please increase your open file limit to at least "
                            "%llu. Exiting.",
                            (unsigned long long) oldlimit,
                            (unsigned long long) maxfiles);
                        exit(1);
                    }
                    redisLog(REDIS_WARNING,"You requested maxclients of %d "
                        "requiring at least %llu max file descriptors.",
                        old_maxclients,
                        (unsigned long long) maxfiles);
                    redisLog(REDIS_WARNING,"Redis can't set maximum open files "
                        "to %llu because of OS error: %s.",
                        (unsigned long long) maxfiles, strerror(setrlimit_error));
                    redisLog(REDIS_WARNING,"Current maximum open files is %llu. "
                        "maxclients has been reduced to %d to compensate for "
                        "low ulimit. "
                        "If you need higher maxclients increase 'ulimit -n'.",
                        (unsigned long long) oldlimit, server.maxclients);
                } else {
                    redisLog(REDIS_NOTICE,"Increased maximum number of open files "
                        "to %llu (it was originally set to %llu).",
                        (unsigned long long) maxfiles,
                        (unsigned long long) oldlimit);
                }
            }
        }
    }
    
    

    在第1行中,REDIS_MIN_RESERVED_FDS即预留的32,是Sentinel保留的用于额外的的操作,如listening sockets, log files 等。同时,这里读取了server.maxclients的值,看来server.maxclients具有初始化值,通过经过定位源码,发现调用链:

    main()->initServerConfig()
    

    即Sentinel在启动时,调用initServerConfig()初始化配置,执行了server.maxclients = REDIS_MAX_CLIENTS(REDIS_MAX_CLIENTS为10000),所以server.maxclients就有了初始值10000。

    回到adjustOpenFilesLimit()函数,adjustOpenFilesLimit最终目的就是得到适合的soft,并存在server.maxclients中,因为该函数比较重要,下面专门作出解释:
    1 先得到maxfiles的初始值,即Sentinel的期望10032
    2 然后获取进程当前的soft和hard,并存入limit ,即执行getrlimit(RLIMIT_NOFILE,&limit) :

    • 如果获取不到,返回-1并报错,并假设当前soft为1024,所以Sentinel能用的最大为1024-32=993
    • 如果获取成功,则表示开始调整过程,以得到最合适的soft。

    调整过程为:
    1 先用oldlimit变量保存进程当前的soft的值(如4096)
    2 然后,判断oldlimit<maxfiles ,如果真,表示当前soft达不到你要求,需要调整。调整的时候,策略是从最大值往下尝试,以逐步获得Sentinel能申请到的最大soft。

    尝试过程为:
    1 首先f保存要尝试的soft值,初始值为maxfiles (10032),即从10032开始调整。
    2 然后开始一个循环判断,只要f大于oldlimit,就执行一次setrlimit(RLIMIT_NOFILE,&limit) ,然后f减16:

    • 如果执行失败返回-1,说明f超过了hard,f需要再小一点,继续循环
    • 如果执行成功,说明f刚刚小于hard(我们就需要这样一个临界值),结束循环

    这样,用这种一步一步尝试的方法,最终可用得到了Sentiel能获得的最大的soft值,最后减去32再保存在server.maxclients中。

    另外,当得到Sentinel能获得的最合适的soft值f后,还要判断f与oldlimit(系统最初的soft限制,假设为4096),原因如下:
    也许会直到f==4096才设置成功,但也会出现f<4096的情况,这是因为跨度为16,最后一不小心就减多了,但最后的soft值不应该比4096还小。所以,f=oldlimit就是这个意思。

    最后,还有一个判断:

    • 如果f!=maxfiles(10032),说明虽然进行了前面的调整过程,但仍然达不到Sentinel的最佳预期10032,但是Sentinel为了适应open files已经帮你降低了maxclients,最后建议你设置一下ulimit。
    • 如果f==maxfiles,说明Sentinel在辛苦的调整之后,终于达到了预期的10032,这当然是最完美的结局。

    上面的过程我们用简单的表示为:

    image.png

    adjustOpenFilesLimit()的分析到此结束。但有一点一定要明确,adjustOpenFilesLimit()只会在Sentinel初始化的时候执行一次,目的就是将最合适的soft保存到了server.maxclients (第xx行),以后不会再调用。这样,一旦设置了server.maxclients ,只要Sentinel不重启,这个值就不会变化,这也就解释了为什么Sentinel启动之后再改变open files没有效果的原因了。

    那什么时候发生了重写呢?即“Generated by CONFIG REWRITE”这句话什么时候会输出?接着上面,我又在源码里搜索了“Generated by CONFIG REWRITE”这句话,发现了常量REDIS_CONFIG_REWRITE_SIGNATURE,通过它继而发现如下调用链:

    *—>sentinelFlushConfig()->rewriteConfig()->rewriteConfigNumericalOption(state,"maxclients",server.maxclients,REDIS_MAX_CLIENTS)
    

    上面的调用链中,*表示有很多地方会调用sentinelFlushConfig()。

    什么时候调用sentinelFlushConfig()呢?经过查找,发现有很多条件都可以触发sentinelFlushConfig函数的调用,包括Leader选举、故障转移、使用Sentinel set 设置命令、Sentinel处理info信息等等。

    而sentinelFlushConfig()则会利用rewriteConfig(),针对具体的配置项,分别进行重写,最终将Sentinel所有的状态持久化到了配置文件中。如下所示,在rewriteConfig()中,可以看到非常多的重写类型, 这些重写类型都是与redis的各个配置选项一一对应的:

    
       rewriteConfigYesNoOption(state,"slave-read-only",server.repl_slave_ro,REDIS_DEFAULT_SLAVE_READ_ONLY);
        rewriteConfigNumericalOption(state,"repl-ping-slave-period",server.repl_ping_slave_period,REDIS_REPL_PING_SLAVE_PERIOD);
        rewriteConfigNumericalOption(state,"repl-timeout",server.repl_timeout,REDIS_REPL_TIMEOUT);
        rewriteConfigBytesOption(state,"repl-backlog-size",server.repl_backlog_size,REDIS_DEFAULT_REPL_BACKLOG_SIZE);
        rewriteConfigBytesOption(state,"repl-backlog-ttl",server.repl_backlog_time_limit,REDIS_DEFAULT_REPL_BACKLOG_TIME_LIMIT);
        rewriteConfigYesNoOption(state,"repl-disable-tcp-nodelay",server.repl_disable_tcp_nodelay,REDIS_DEFAULT_REPL_DISABLE_TCP_NODELAY);
        rewriteConfigNumericalOption(state,"slave-priority",server.slave_priority,REDIS_DEFAULT_SLAVE_PRIORITY);
        rewriteConfigNumericalOption(state,"min-slaves-to-write",server.repl_min_slaves_to_write,REDIS_DEFAULT_MIN_SLAVES_TO_WRITE);
        rewriteConfigNumericalOption(state,"min-slaves-max-lag",server.repl_min_slaves_max_lag,REDIS_DEFAULT_MIN_SLAVES_MAX_LAG);
        rewriteConfigStringOption(state,"requirepass",server.requirepass,NULL);
        rewriteConfigNumericalOption(state,"maxclients",server.maxclients,REDIS_MAX_CLIENTS);
        rewriteConfigBytesOption(state,"maxmemory",server.maxmemory,REDIS_DEFAULT_MAXMEMORY);
        rewriteConfigEnumOption(state,"maxmemory-policy",server.maxmemory_policy,
            "volatile-lru", REDIS_MAXMEMORY_VOLATILE_LRU,省略
    

    当然,我们只需要找到其中关于max clients的重写即可,所以在该函数中,我们找到了调用:

    rewriteConfigNumericalOption(state,"maxclients",server.maxclients,REDIS_MAX_CLIENTS);
    

    可以看到,该函数传入了Sentinel当前的server.maxclients(已经在启动时调整过了,前面分析过),以及默认的REDIS_MAX_CLIENTS即10032。该函数作用就是将当前的server.maxclients的值重写到配置文件中去。什么时候重写呢,即当默认值与当前值不同的时候(也就是force==true的时候),具体可以查看其源码,篇幅限制我们不做详细介绍。

    通过前面一大堆的分析,我们可以得出结论:

    • Sentinel在启动的时候,系统对其open files的限制为4096,无法达到Sentinel预期的10032,但Sentinel接受了这个限制并保存在了max clients(4064)这个变量中,经过一段时间后,连接数最终达到了open files限制,导致出现了Too many open files的错误。
    • 并且,由于主从切换或其他原因触发了sentinelFlushConfig()->rewriteConfig()的调用,致使maxclients这个配置项出现在了配置文件中。

    Redis 与 TCP keepalive


    讲到这里,还有一个问题就是,为什么Sentinel服务器会长期持有4000多个Established状态的TCP连接而不释放。按目前生产环境的规模,正常情况下业务客户端使用的Jedis建立的TCP连接不应该有这么多。

    经过查看,发现Sentinel上的很多连接在对应的客户端中并没有存在。如红框所示IP10.X.X.74上:

    image.png

    总计有992个连接:

    image.png

    而实际上在10.X.X.74上,只有5个与Sentinel的连接长期存在:

    image.png

    也就是说,在Sentinel中有大量的连接是无效的,客户端并没有持有,Sentinel一直没有释放。这个问题, 就涉及到了TCP保活的相关知识。

    我们首先要了解,操作系统通常会自身提供TCP的keepalive机制,如在linux默认配置下,运行sysctl -a |grep keep,会看到如下信息:

    image.png

    上面表示如果连接的空闲时间超过 7200 秒(2 小时),Linux 就发送保持活动的探测包。每隔75秒发一次,总共发9次,如果9次都失败的话,表示连接失效。

    TCP提供这种机制帮助我们判断对端是否存活,当TCP检测到对端不可用时,会出错并通知上层进行处理。keepalive机制默认是关闭的,应用程序需要使用SO_KEEPALIVE进行启用。

    了解到这个知识之后,我们开始分析。在Redis的源码中,发现有如下调用链:

    acceptTcpHandler()->acceptCommonHandler()->createClient()->anetKeepAlive()
    

    还记得acceptTcpHandler吗,acceptTcpHandler是TCP连接的事件处理器,当它为客户端成功创建了TCP连接后,会通过调用createClient函数为每个连接(fd)创建一个redisClient 实例,这个redisClient 与客户端是一一对应的。并且,还会设置一些TCP选项,如下所示。

    redisClient *createClient(int fd) {
        redisClient *c = zmalloc(sizeof(redisClient));
    
        if (fd != -1) {
            anetNonBlock(NULL,fd);
            anetEnableTcpNoDelay(NULL,fd);
            if (server.tcpkeepalive)
                anetKeepAlive(NULL,fd,server.tcpkeepalive);
            if (aeCreateFileEvent(server.el,fd,AE_READABLE,
                readQueryFromClient, c) == AE_ERR)
            {
                close(fd);
                zfree(c);
                return NULL;
            }
        }
    
    

    如果用户在Redis中没有手动配置tcpkeepalive的话,server.tcpkeepalive = REDIS_DEFAULT_TCP_KEEPALIVE,默认为0。
    由第x-x行我们可以明确,Redis服务器与客户端的连接默认是关闭保活机制的,因为只有当server.tcpkeepalive不为0(修改配置文件或config set)时,才能调用anetKeepAlive方法设置TCP的keepalive选项。

    我们知道,Sentinel是特殊模式的Redis,我们无法使用config set命令去修改其配置,包括tcpkeepalive 参数。所以,当Sentinel启动后,Sentinel也使用默认的tcpkeepalive ==0这个设置,不会启用tcpkeepalive ,与客户端的TCP连接都没有保活机制。也就是说,Sentinel不会主动去释放连接,哪怕是失效连接。

    但是,TCP连接是双向的,Sentinel无法处理失效连接,那Jedis客户端呢?它是否可以主动断掉连接?我们定位到了Jedis建立连接的函数connect(),如下所示:

     public void connect() {
            if (!isConnected()) {
                try {
    
                    socket = new Socket();
                    // ->@wjw_add
                    socket.setReuseAddress(true);
                    socket.setKeepAlive(true); // Will monitor the TCP connection is
                    // valid
                    socket.setTcpNoDelay(true); // Socket buffer Whetherclosed, to
                    // ensure timely delivery of data
                    socket.setSoLinger(true, 0); // Control calls close () method,
                    // the underlying socket is closed
                    // immediately
                    // <-@wjw_add
    
                    socket.connect(new InetSocketAddress(host, port), connectionTimeout);
                    socket.setSoTimeout(soTimeout);
                    outputStream = new RedisOutputStream(socket.getOutputStream());
                    inputStream = new RedisInputStream(socket.getInputStream());
                } catch (IOException ex) {
                    broken = true;
                    throw new JedisConnectionException(ex);
                }
            }
        }
    

    由第x行可以看到,Jedis启用了TCP的keepalive机制,并且没有设置其他keepalive相关选项。也就是说,Jedis客户端会采用linux默认的TCP keepalive机制,每隔7200秒去探测连接的情况。这样,即使与Sentinel的连接出问题,Jedis客户端也能主动释放掉,虽然时间有点久。

    但是,实际上,如前面所示,Sentinel服务器上有很多失效连接持续保持,为什么会有这种现象?

    对于上面的问题,能想到的原因就是,在Jedis去主动释放掉TCP连接前,该连接被强制断掉,没有进行完整的四次挥手的过程。而Sentinel却因为没有保活机制,没有感知到这个动作,导致其一直保持这个连接。

    能干掉连接的元凶,马上想到了防火墙,于是我又询问了运维,结果,他们告知了我一个噩耗:
    目前,生产环境上防火墙的设置是主动断掉超过10分钟没有数据交换的TCP连接。

    好吧,绕了一大圈,至此,问题已经很清楚了。

    结论与解决办法


    终于,我们得出了结论:

    • Sentinel默认没有保活机制,不会主动去释放连接,而Jedis基于TCP的keepalive机制,会每隔2小时发送保活包,但是中间的防火墙会断掉超过空闲时间超过10分钟的连接,相当于Jedis的保活机制形同虚设。
    • 因此,大量的正常连接因空闲超过10分钟被终止,而Sentinel无法感知,自以为正常,所以一直保持连接。
    • 随着客户端连接个数的增长,又由于open files的个数过小,很快达到了4096个限制,从而产生各种错误。

    有了前面的分析,其实解决办法很简单:

    • 在Sentinel的配置文件中,手动增加keep alive的参数,用来设置Sentinel的保活时间,当其与客户端空闲时间超过该值后,Sentinel主动去释放连接。
    • 确保open files个数设置合理,然后重启Sentinel。
    • 修改防火墙策略

    关于“追踪Redis Sentinel的CPU占有率长期接近100%的问题”到此就结束了,在写这两篇博文的时候,我收货了很多自己没有掌握的知识和技巧。现在觉得,写博文真的是一件值得坚持和认真对待的事情,早应该开始。不要问我为什么,当你尝试之后,也就和我一样明白了。

    这两篇文章的分析过程肯定有疏漏和不足之处,个人能力有限,希望大家能够理解,并多多指教,非常感谢!我会继续进步!

    补充:客户端与Sentinel的5个TCP连接是什么


    前面提到过,在每个客户端上,都可以发现5个正常的TCP连接,他们是什么呢?让我们重新回到Jedis。

    image.png

    在《追踪Redis Sentinel的CPU占有率长期接近100%的问题 一》中,我们提到Jedis SentinelPool会为每一个Sentinel建立一个MasterListener线程,该线程用来监听主从切换,保证客户端的Jedis句柄始终对应在Master上。在这里,即会有5个MasterListener来对应5个Sentinel。

    其实,MasterListener的监听功能根据Redis的pub sub功能实现的。MasterListener线程会去订阅+switch-master消息,该消息会在master节点地址改变时产生,一旦产生,MasterListener就重新初始化连接池,保证客户端使用的jedis句柄始终关联到Master上。

    如下所示为MasterListener的线程函数,它会在一个无限循环中不断的创建Jedis句柄,利用该句柄去订阅+switch-master消息,只要发生了主从切换,就会触发onMessage。

     public void run() {
    
          running.set(true);
    
          while (running.get()) {
    
            j = new Jedis(host, port);
    
            try {
              // double check that it is not being shutdown
              if (!running.get()) {
                break;
              }
    
              j.subscribe(new JedisPubSub() {
                @Override
                public void onMessage(String channel, String message) {
                  log.fine("Sentinel " + host + ":" + port + " published: " + message + ".");
    
                  String[] switchMasterMsg = message.split(" ");
    
                  if (switchMasterMsg.length > 3) {
    
                    if (masterName.equals(switchMasterMsg[0])) {
                      initPool(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));
                    } else {
                      log.fine("Ignoring message on +switch-master for master name "
                          + switchMasterMsg[0] + ", our master name is " + masterName);
                    }
    
                  } else {
                    log.severe("Invalid message received on Sentinel " + host + ":" + port
                        + " on channel +switch-master: " + message);
                  }
                }
              }, "+switch-master");
    
            } catch (JedisConnectionException e) {
    
              if (running.get()) {
                log.log(Level.SEVERE, "Lost connection to Sentinel at " + host + ":" + port
                    + ". Sleeping 5000ms and retrying.", e);
                try {
                  Thread.sleep(subscribeRetryWaitTimeMillis);
                } catch (InterruptedException e1) {
                  log.log(Level.SEVERE, "Sleep interrupted: ", e1);
                }
              } else {
                log.fine("Unsubscribing from Sentinel at " + host + ":" + port);
              }
            } finally {
              j.close();
            }
          }
        }
    

    如何实现订阅功能呢,我们需要查看subscribe函数的底层实现,它实际使用client.setTimeoutInfinite()->connect建立了一个TCP连接,然后使用JedisPubSub的proceed方法去订阅频道,并且无限循环的读取订阅的信息。

     @Override
      public void subscribe(final JedisPubSub jedisPubSub, final String... channels) {
        client.setTimeoutInfinite();
        try {
          jedisPubSub.proceed(client, channels);
        } finally {
          client.rollbackTimeout();
        }
      }
    
    

    在procee的方法中,实际先通过subscribe订阅频道,然后调用process方法读取订阅信息。

    public void proceed(Client client, String... channels) {
        this.client = client;
        client.subscribe(channels);
        client.flush();
        process(client);
      }
    
    

    其实,subscribe函数就是简单的向服务器发送了一个SUBSCRIBE命令。

    public void subscribe(final byte[]... channels) { 
           sendCommand(SUBSCRIBE, channels); 
       } 
    
    

    而process函数,篇幅较长,此处省略,其主要功能就是以无限循环的方式不断地读取订阅信息

    private void process(Client client) {
    
        do {
         
        } while (isSubscribed());
    
      }
    
    

    综上,MasterListener线程会向Sentinel创建+switch-master频道的TCP订阅连接,并且会do while循环读取该频道信息。如果订阅或读取过程中出现Tcp连接异常,则释放Jedis句柄,然后等待5000ms 后重新创建Jedis句柄进行订阅。当然,这个过程会在一个循环之中。

    至此,也就解释了为何每个业务客户端服务器和Sentinel服务器上,都有5个长期保持的、状态正常的TCP连接的原因了。

    欢迎关注本人微信公众号:


    爱你之心.jpg

    相关文章

      网友评论

      本文标题:追踪Redis Sentinel的CPU占有率长期接近100%的

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