回顧問題
在《追蹤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 后,出現:
如圖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 的值改變為:
簡單介紹完open files,我們再來了解下file-max。這個參數相信有很多人經常與ulimit中的open files混淆,他們的區別我們必須了解。
file-max 從字面意思就可以看出是文件的最大個數,運行cat /proc/sys/fs/file-max,可以看到:
這表示當前系統所有進程一共可以打開的文件數量為387311。請務必注意”系統所有進程"這幾個字。
運行 vim /etc/sysctl.conf ,有時候你會看到類似:fs.file-max = 8192。出現這個則表示用戶手動設置了這個值,沒有則系統會有其默認值。手動設置的話,只需要在sysctl.conf 中加上上述語句即可。
回到ulimit中的open files,它與file-max的區別就是:opem filese表示當前shell以及由它啟動的進程的文件描述符的限制,也就是說ulimit中設置的open files,只是當前shell及其子進程的文件描述符的限定。是否清楚?可以簡單的理解為:
好了,對于 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,得到:
由上圖可以發現,有非常多處于ESTABLISHED狀態的TCP連接,運行 netstat -anp | grep 118:26379 | wc -l查看他們的個數:
可以看到,Sentinel同時維持了4071個TCP連接,而且過了很久之后,仍然是這么多,不會有大幅變化。
這時,也許你會想到,是否因為系統文件描述符的限制導致Sentinel無法建立更多的Socket,從而產生“Too many open files”的錯誤。所以,馬上在Sentinel服務器上運行cat /proc/sys/fs/file-max,發現:
這個值很大,看似一切正常。繼續運行sudo cat /proc/5515/limits,發現:
看到上圖,我們似乎發現了端倪,這里的hard和soft都是4096,與前面的4072比較接近。為什么會這么低?我們繼續查看一下ulimit,運行ulimit -a :
可以看到,ulimit中open files 的設置為64000,為什么會不一致?按理說sentinel應該最大有64000+的open files。
對于這個矛盾,一開始我怎么也想不明白。最后我推測:應該是在最早啟動sentinel啟動的時候,系統的設置為4096,sentinel啟動之后,又在某個時間又改為64000,而sentinel進程確保持原有設置,從而導致很快達到限制。
我馬上查看了進程的啟動時間,運行ps -eo pid,lstart,etime | grep 5515:
發現進程啟動于2015年12月2日,接著再次運行 ll /etc/security/limits.conf:
發現確實在2016年4月改動過, 然后我咨詢了運維人員,他們告知我,確實改動過,但由多少改動到多少,他們也忘了。。。
查看Sentinel日志和配置文件
為了了解Sentinel啟動時的狀況,緊接著查看了Sentinel的日志,下面是Sentinel啟動時打印的畫面:
上圖說明了進程是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的配置文件,如下圖所示:
上圖中, “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,這當然是最完美的結局。
上面的過程我們用簡單的表示為:
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上:
總計有992個連接:
而實際上在10.X.X.74上,只有5個與Sentinel的連接長期存在:
也就是說,在Sentinel中有大量的連接是無效的,客戶端并沒有持有,Sentinel一直沒有釋放。這個問題, 就涉及到了TCP保活的相關知識。
我們首先要了解,操作系統通常會自身提供TCP的keepalive機制,如在linux默認配置下,運行sysctl -a |grep keep,會看到如下信息:
上面表示如果連接的空閑時間超過 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。
在《追蹤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連接的原因了。