追蹤Redis Sentinel的CPU占有率長期接近100%的問題 二

回顧問題


《追蹤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連接的原因了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,517評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,087評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,521評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,493評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,207評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,603評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,624評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,813評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,364評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,110評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,305評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,874評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,532評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,953評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,209評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,033評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,268評論 2 375

推薦閱讀更多精彩內容