回顧問題
在《追蹤Redis Sentinel的CPU占有率長期接近100%的問題》一文中,通過結(jié)合Redis Sentinel的源碼,發(fā)現(xiàn)由于出現(xiàn)了"Too many open files"問題,致使Sentinel的acceptTcpHandler事件處理函數(shù)會被頻繁并快速調(diào)用,最終導(dǎo)致了CPU長期接近100%的現(xiàn)象。但對于為什么會出現(xiàn)“Too many open files”這個問題,本文將在上一篇的基礎(chǔ)上,繼續(xù)探討和分析。
open files 與 file-max
“Too many open files”這個錯誤其實很常見,想必大家早已對其有一定的了解,這里打算再簡單的介紹一下。
很明顯,“Too many open files”即說明打開的文件(包括socket)數(shù)量過多,已經(jīng)超出了系統(tǒng)設(shè)定給某個進(jìn)程最大的文件描述符的個數(shù),超過后即無法繼續(xù)打開新的文件,并且報這個錯誤。
首先,我們需要了解有關(guān)open files的基本知識。詳細(xì)的概念大家可以谷歌,網(wǎng)上也有各種各樣的解決辦法,這里只對open files做簡單的介紹和總結(jié)。
我們在linux上運行ulimit -a 后,出現(xiàn):
如圖open files的個數(shù)為1024,很明顯這個值太小了(linux默認(rèn)即為1024),當(dāng)進(jìn)程耗盡1024個文件或socket后,就會出現(xiàn)“Too many open files”錯誤。現(xiàn)實生產(chǎn)環(huán)境中這個值很容易達(dá)到,所以一般都會進(jìn)行相應(yīng)修改。
最簡單的修改方式是ulimit -n 65535,但這樣重啟系統(tǒng)后又會恢復(fù)。永久生效的方法是修改/etc/security/limits.conf 文件,在最后加入:
* soft nofile 65535
* hard nofile 65535
修改完成后,重啟系統(tǒng)或者運行sysctl -p使之生效。
soft 和hard,表示對進(jìn)程所能打開的文件描述符個數(shù)限制,其概念為:
- soft 軟限制,ulimit -a 默認(rèn)顯示軟限制
- hard 硬限制,表示soft的上限
- 只有root才能增加hard 普通用戶只能減小hard
他們的區(qū)別就是軟限制可以在程序的進(jìn)程中自行改變(突破限制),而硬限制則不行(除非程序進(jìn)程有root權(quán)限)。
上面的規(guī)則大家最好自己嘗試一下,以增加印象。
重啟后,運行ulimit -a,可以看到open files 的值改變?yōu)椋?/p>
簡單介紹完open files,我們再來了解下file-max。這個參數(shù)相信有很多人經(jīng)常與ulimit中的open files混淆,他們的區(qū)別我們必須了解。
file-max 從字面意思就可以看出是文件的最大個數(shù),運行cat /proc/sys/fs/file-max,可以看到:
這表示當(dāng)前系統(tǒng)所有進(jìn)程一共可以打開的文件數(shù)量為387311。請務(wù)必注意”系統(tǒng)所有進(jìn)程"這幾個字。
運行 vim /etc/sysctl.conf ,有時候你會看到類似:fs.file-max = 8192。出現(xiàn)這個則表示用戶手動設(shè)置了這個值,沒有則系統(tǒng)會有其默認(rèn)值。手動設(shè)置的話,只需要在sysctl.conf 中加上上述語句即可。
回到ulimit中的open files,它與file-max的區(qū)別就是:opem filese表示當(dāng)前shell以及由它啟動的進(jìn)程的文件描述符的限制,也就是說ulimit中設(shè)置的open files,只是當(dāng)前shell及其子進(jìn)程的文件描述符的限定。是否清楚?可以簡單的理解為:
好了,對于 file-max與open files的簡單介紹到此為止。現(xiàn)在的問題就是,“Too many open files”到底是碰到哪個設(shè)置的雷區(qū)造成的。
查看TCP連接
結(jié)合上一篇,我們知道sentinel主要在執(zhí)行accept函數(shù)時出現(xiàn)了“Too many open files”錯誤,熟悉accept這個系統(tǒng)調(diào)用的朋友很清楚,accept會接收客戶端的請求,成功的話會建立連接,并返回新的socket描述符。所以,我們確定這里的“Too many open files”指的即是socket的數(shù)目過多。
我們猜測,是否是有大量的Jedis連接同時存在,耗盡服務(wù)器的socket資源,導(dǎo)致新的連接請求無法建立。所以,我們查看一下sentinel服務(wù)器的TCP連接,運行:netstat -anp | grep 26379,得到:
由上圖可以發(fā)現(xiàn),有非常多處于ESTABLISHED狀態(tài)的TCP連接,運行 netstat -anp | grep 118:26379 | wc -l查看他們的個數(shù):
可以看到,Sentinel同時維持了4071個TCP連接,而且過了很久之后,仍然是這么多,不會有大幅變化。
這時,也許你會想到,是否因為系統(tǒng)文件描述符的限制導(dǎo)致Sentinel無法建立更多的Socket,從而產(chǎn)生“Too many open files”的錯誤。所以,馬上在Sentinel服務(wù)器上運行cat /proc/sys/fs/file-max,發(fā)現(xiàn):
這個值很大,看似一切正常。繼續(xù)運行sudo cat /proc/5515/limits,發(fā)現(xiàn):
看到上圖,我們似乎發(fā)現(xiàn)了端倪,這里的hard和soft都是4096,與前面的4072比較接近。為什么會這么低?我們繼續(xù)查看一下ulimit,運行ulimit -a :
可以看到,ulimit中open files 的設(shè)置為64000,為什么會不一致?按理說sentinel應(yīng)該最大有64000+的open files。
對于這個矛盾,一開始我怎么也想不明白。最后我推測:應(yīng)該是在最早啟動sentinel啟動的時候,系統(tǒng)的設(shè)置為4096,sentinel啟動之后,又在某個時間又改為64000,而sentinel進(jìn)程確保持原有設(shè)置,從而導(dǎo)致很快達(dá)到限制。
我馬上查看了進(jìn)程的啟動時間,運行ps -eo pid,lstart,etime | grep 5515:
發(fā)現(xiàn)進(jìn)程啟動于2015年12月2日,接著再次運行 ll /etc/security/limits.conf:
發(fā)現(xiàn)確實在2016年4月改動過, 然后我咨詢了運維人員,他們告知我,確實改動過,但由多少改動到多少,他們也忘了。。。
查看Sentinel日志和配置文件
為了了解Sentinel啟動時的狀況,緊接著查看了Sentinel的日志,下面是Sentinel啟動時打印的畫面:
上圖說明了進(jìn)程是2015年12月2日啟動的,特別注意最開頭的幾行,非常關(guān)鍵:
[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.”。系統(tǒng)權(quán)限不夠。
- 當(dāng)前最大open files限制是4096,maxclients==10000這個要求達(dá)不到,所以減少到4096了
- 最后還建議用戶最好手動修改增加下ulimit -n
問題很清楚了,redis sentinel最大可以支持10000個客戶端,也就是10032個文件描述符,但由于當(dāng)前被人為限制到4096 了,所以,自動降低了標(biāo)準(zhǔn)。
因此,我猜測,最早open files的限制為4096時,Sentinel已經(jīng)啟動了,只要進(jìn)程啟動,改多少都沒有用。很明顯,在生產(chǎn)環(huán)境上,4096個連接請求很快就會達(dá)到。
接著,我繼續(xù)查看了Sentinel的配置文件,如下圖所示:
上圖中, “Generated by CONFIG REWRITE”之前的都是是人工配置,其后為Sentinel自動重寫的配置。
熟悉Redis的朋友都知道。Sentinel可能會對其配置文件進(jìn)行更新重寫:
Sentinel 的狀態(tài)會被持久化在 Sentinel 配置文件里面。每當(dāng) Sentinel 接收到一個新的配置, 或者當(dāng)領(lǐng)頭 Sentinel 為主服務(wù)器創(chuàng)建一個新的配置時, 這個配置會與配置紀(jì)元一起被保存到磁盤里面。
我們很快注意到了maxclients 4064這個配置項,此時我很迷惑。我們知道,在Sentinel中是無法手動運行config set命令的,那這個4096必然不是來自于人工配置,Sentinel為什么要自動重寫4064這個值。其實,仔細(xì)發(fā)現(xiàn),這里Sentinel限制了最多4064個連接,加上32個預(yù)留,剛好為4096。
于是,綜上,我猜測,Sentinel在啟動的時候發(fā)現(xiàn)自己的10032個open files的預(yù)期與事實設(shè)置的4096不符,所以被迫遵守4096,減去預(yù)留的32,最終maxclients 只有4064,并且之后因為某些原因重寫了配置,所以輸出了這個值。
好吧,我的一貫作風(fēng),先猜測,再讓源碼說話。
maxclients 的秘密
我們可以通過異常信息定位異常所處源碼,所以我搜索了前面提到的Sentinel在啟動時打印的關(guān)于maxclients 的日志信息中的文本,如“You requested maxclients of ...”。
這個異常出現(xiàn)在adjustOpenFilesLimit函數(shù),通過函數(shù)名可以清楚它的作用,然后發(fā)現(xiàn)它的調(diào)用鏈只是:
``main()->initServer()->adjustOpenFilesLimit()```
所以,可以確定,在Sentinel服務(wù)器啟動并進(jìn)行初始化的時候,會調(diào)用adjustOpenFilesLimit函數(shù)對open files個數(shù)進(jìn)行調(diào)整。調(diào)整策略是什么呢?我們查看源碼:
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即預(yù)留的32,是Sentinel保留的用于額外的的操作,如listening sockets, log files 等。同時,這里讀取了server.maxclients的值,看來server.maxclients具有初始化值,通過經(jīng)過定位源碼,發(fā)現(xiàn)調(diào)用鏈:
main()->initServerConfig()
即Sentinel在啟動時,調(diào)用initServerConfig()初始化配置,執(zhí)行了server.maxclients = REDIS_MAX_CLIENTS(REDIS_MAX_CLIENTS為10000),所以server.maxclients就有了初始值10000。
回到adjustOpenFilesLimit()函數(shù),adjustOpenFilesLimit最終目的就是得到適合的soft,并存在server.maxclients中,因為該函數(shù)比較重要,下面專門作出解釋:
1 先得到maxfiles的初始值,即Sentinel的期望10032
2 然后獲取進(jìn)程當(dāng)前的soft和hard,并存入limit ,即執(zhí)行g(shù)etrlimit(RLIMIT_NOFILE,&limit) :
- 如果獲取不到,返回-1并報錯,并假設(shè)當(dāng)前soft為1024,所以Sentinel能用的最大為1024-32=993
- 如果獲取成功,則表示開始調(diào)整過程,以得到最合適的soft。
調(diào)整過程為:
1 先用oldlimit變量保存進(jìn)程當(dāng)前的soft的值(如4096)
2 然后,判斷oldlimit<maxfiles ,如果真,表示當(dāng)前soft達(dá)不到你要求,需要調(diào)整。調(diào)整的時候,策略是從最大值往下嘗試,以逐步獲得Sentinel能申請到的最大soft。
嘗試過程為:
1 首先f保存要嘗試的soft值,初始值為maxfiles (10032),即從10032開始調(diào)整。
2 然后開始一個循環(huán)判斷,只要f大于oldlimit,就執(zhí)行一次setrlimit(RLIMIT_NOFILE,&limit) ,然后f減16:
- 如果執(zhí)行失敗返回-1,說明f超過了hard,f需要再小一點,繼續(xù)循環(huán)
- 如果執(zhí)行成功,說明f剛剛小于hard(我們就需要這樣一個臨界值),結(jié)束循環(huán)
這樣,用這種一步一步嘗試的方法,最終可用得到了Sentiel能獲得的最大的soft值,最后減去32再保存在server.maxclients中。
另外,當(dāng)?shù)玫絊entinel能獲得的最合適的soft值f后,還要判斷f與oldlimit(系統(tǒng)最初的soft限制,假設(shè)為4096),原因如下:
也許會直到f==4096才設(shè)置成功,但也會出現(xiàn)f<4096的情況,這是因為跨度為16,最后一不小心就減多了,但最后的soft值不應(yīng)該比4096還小。所以,f=oldlimit就是這個意思。
最后,還有一個判斷:
- 如果f!=maxfiles(10032),說明雖然進(jìn)行了前面的調(diào)整過程,但仍然達(dá)不到Sentinel的最佳預(yù)期10032,但是Sentinel為了適應(yīng)open files已經(jīng)幫你降低了maxclients,最后建議你設(shè)置一下ulimit。
- 如果f==maxfiles,說明Sentinel在辛苦的調(diào)整之后,終于達(dá)到了預(yù)期的10032,這當(dāng)然是最完美的結(jié)局。
上面的過程我們用簡單的表示為:
adjustOpenFilesLimit()的分析到此結(jié)束。但有一點一定要明確,adjustOpenFilesLimit()只會在Sentinel初始化的時候執(zhí)行一次,目的就是將最合適的soft保存到了server.maxclients (第xx行),以后不會再調(diào)用。這樣,一旦設(shè)置了server.maxclients ,只要Sentinel不重啟,這個值就不會變化,這也就解釋了為什么Sentinel啟動之后再改變open files沒有效果的原因了。
那什么時候發(fā)生了重寫呢?即“Generated by CONFIG REWRITE”這句話什么時候會輸出?接著上面,我又在源碼里搜索了“Generated by CONFIG REWRITE”這句話,發(fā)現(xiàn)了常量REDIS_CONFIG_REWRITE_SIGNATURE,通過它繼而發(fā)現(xiàn)如下調(diào)用鏈:
*—>sentinelFlushConfig()->rewriteConfig()->rewriteConfigNumericalOption(state,"maxclients",server.maxclients,REDIS_MAX_CLIENTS)
上面的調(diào)用鏈中,*表示有很多地方會調(diào)用sentinelFlushConfig()。
什么時候調(diào)用sentinelFlushConfig()呢?經(jīng)過查找,發(fā)現(xiàn)有很多條件都可以觸發(fā)sentinelFlushConfig函數(shù)的調(diào)用,包括Leader選舉、故障轉(zhuǎn)移、使用Sentinel set 設(shè)置命令、Sentinel處理info信息等等。
而sentinelFlushConfig()則會利用rewriteConfig(),針對具體的配置項,分別進(jìn)行重寫,最終將Sentinel所有的狀態(tài)持久化到了配置文件中。如下所示,在rewriteConfig()中,可以看到非常多的重寫類型, 這些重寫類型都是與redis的各個配置選項一一對應(yīng)的:
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,省略
當(dāng)然,我們只需要找到其中關(guān)于max clients的重寫即可,所以在該函數(shù)中,我們找到了調(diào)用:
rewriteConfigNumericalOption(state,"maxclients",server.maxclients,REDIS_MAX_CLIENTS);
可以看到,該函數(shù)傳入了Sentinel當(dāng)前的server.maxclients(已經(jīng)在啟動時調(diào)整過了,前面分析過),以及默認(rèn)的REDIS_MAX_CLIENTS即10032。該函數(shù)作用就是將當(dāng)前的server.maxclients的值重寫到配置文件中去。什么時候重寫呢,即當(dāng)默認(rèn)值與當(dāng)前值不同的時候(也就是force==true的時候),具體可以查看其源碼,篇幅限制我們不做詳細(xì)介紹。
通過前面一大堆的分析,我們可以得出結(jié)論:
- Sentinel在啟動的時候,系統(tǒng)對其open files的限制為4096,無法達(dá)到Sentinel預(yù)期的10032,但Sentinel接受了這個限制并保存在了max clients(4064)這個變量中,經(jīng)過一段時間后,連接數(shù)最終達(dá)到了open files限制,導(dǎo)致出現(xiàn)了Too many open files的錯誤。
- 并且,由于主從切換或其他原因觸發(fā)了sentinelFlushConfig()->rewriteConfig()的調(diào)用,致使maxclients這個配置項出現(xiàn)在了配置文件中。
Redis 與 TCP keepalive
講到這里,還有一個問題就是,為什么Sentinel服務(wù)器會長期持有4000多個Established狀態(tài)的TCP連接而不釋放。按目前生產(chǎn)環(huán)境的規(guī)模,正常情況下業(yè)務(wù)客戶端使用的Jedis建立的TCP連接不應(yīng)該有這么多。
經(jīng)過查看,發(fā)現(xiàn)Sentinel上的很多連接在對應(yīng)的客戶端中并沒有存在。如紅框所示IP10.X.X.74上:
總計有992個連接:
而實際上在10.X.X.74上,只有5個與Sentinel的連接長期存在:
也就是說,在Sentinel中有大量的連接是無效的,客戶端并沒有持有,Sentinel一直沒有釋放。這個問題, 就涉及到了TCP保活的相關(guān)知識。
我們首先要了解,操作系統(tǒng)通常會自身提供TCP的keepalive機(jī)制,如在linux默認(rèn)配置下,運行sysctl -a |grep keep,會看到如下信息:
上面表示如果連接的空閑時間超過 7200 秒(2 小時),Linux 就發(fā)送保持活動的探測包。每隔75秒發(fā)一次,總共發(fā)9次,如果9次都失敗的話,表示連接失效。
TCP提供這種機(jī)制幫助我們判斷對端是否存活,當(dāng)TCP檢測到對端不可用時,會出錯并通知上層進(jìn)行處理。keepalive機(jī)制默認(rèn)是關(guān)閉的,應(yīng)用程序需要使用SO_KEEPALIVE進(jìn)行啟用。
了解到這個知識之后,我們開始分析。在Redis的源碼中,發(fā)現(xiàn)有如下調(diào)用鏈:
acceptTcpHandler()->acceptCommonHandler()->createClient()->anetKeepAlive()
還記得acceptTcpHandler嗎,acceptTcpHandler是TCP連接的事件處理器,當(dāng)它為客戶端成功創(chuàng)建了TCP連接后,會通過調(diào)用createClient函數(shù)為每個連接(fd)創(chuàng)建一個redisClient 實例,這個redisClient 與客戶端是一一對應(yīng)的。并且,還會設(shè)置一些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,默認(rèn)為0。
由第x-x行我們可以明確,Redis服務(wù)器與客戶端的連接默認(rèn)是關(guān)閉保活機(jī)制的,因為只有當(dāng)server.tcpkeepalive不為0(修改配置文件或config set)時,才能調(diào)用anetKeepAlive方法設(shè)置TCP的keepalive選項。
我們知道,Sentinel是特殊模式的Redis,我們無法使用config set命令去修改其配置,包括tcpkeepalive 參數(shù)。所以,當(dāng)Sentinel啟動后,Sentinel也使用默認(rèn)的tcpkeepalive ==0這個設(shè)置,不會啟用tcpkeepalive ,與客戶端的TCP連接都沒有保活機(jī)制。也就是說,Sentinel不會主動去釋放連接,哪怕是失效連接。
但是,TCP連接是雙向的,Sentinel無法處理失效連接,那Jedis客戶端呢?它是否可以主動斷掉連接?我們定位到了Jedis建立連接的函數(shù)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機(jī)制,并且沒有設(shè)置其他keepalive相關(guān)選項。也就是說,Jedis客戶端會采用linux默認(rèn)的TCP keepalive機(jī)制,每隔7200秒去探測連接的情況。這樣,即使與Sentinel的連接出問題,Jedis客戶端也能主動釋放掉,雖然時間有點久。
但是,實際上,如前面所示,Sentinel服務(wù)器上有很多失效連接持續(xù)保持,為什么會有這種現(xiàn)象?
對于上面的問題,能想到的原因就是,在Jedis去主動釋放掉TCP連接前,該連接被強(qiáng)制斷掉,沒有進(jìn)行完整的四次揮手的過程。而Sentinel卻因為沒有保活機(jī)制,沒有感知到這個動作,導(dǎo)致其一直保持這個連接。
能干掉連接的元兇,馬上想到了防火墻,于是我又詢問了運維,結(jié)果,他們告知了我一個噩耗:
目前,生產(chǎn)環(huán)境上防火墻的設(shè)置是主動斷掉超過10分鐘沒有數(shù)據(jù)交換的TCP連接。
好吧,繞了一大圈,至此,問題已經(jīng)很清楚了。
結(jié)論與解決辦法
終于,我們得出了結(jié)論:
- Sentinel默認(rèn)沒有保活機(jī)制,不會主動去釋放連接,而Jedis基于TCP的keepalive機(jī)制,會每隔2小時發(fā)送保活包,但是中間的防火墻會斷掉超過空閑時間超過10分鐘的連接,相當(dāng)于Jedis的保活機(jī)制形同虛設(shè)。
- 因此,大量的正常連接因空閑超過10分鐘被終止,而Sentinel無法感知,自以為正常,所以一直保持連接。
- 隨著客戶端連接個數(shù)的增長,又由于open files的個數(shù)過小,很快達(dá)到了4096個限制,從而產(chǎn)生各種錯誤。
有了前面的分析,其實解決辦法很簡單:
- 在Sentinel的配置文件中,手動增加keep alive的參數(shù),用來設(shè)置Sentinel的保活時間,當(dāng)其與客戶端空閑時間超過該值后,Sentinel主動去釋放連接。
- 確保open files個數(shù)設(shè)置合理,然后重啟Sentinel。
- 修改防火墻策略
關(guān)于“追蹤Redis Sentinel的CPU占有率長期接近100%的問題”到此就結(jié)束了,在寫這兩篇博文的時候,我收貨了很多自己沒有掌握的知識和技巧。現(xiàn)在覺得,寫博文真的是一件值得堅持和認(rèn)真對待的事情,早應(yīng)該開始。不要問我為什么,當(dāng)你嘗試之后,也就和我一樣明白了。
這兩篇文章的分析過程肯定有疏漏和不足之處,個人能力有限,希望大家能夠理解,并多多指教,非常感謝!我會繼續(xù)進(jìn)步!
補充:客戶端與Sentinel的5個TCP連接是什么
前面提到過,在每個客戶端上,都可以發(fā)現(xiàn)5個正常的TCP連接,他們是什么呢?讓我們重新回到Jedis。
在《追蹤Redis Sentinel的CPU占有率長期接近100%的問題 一》中,我們提到Jedis SentinelPool會為每一個Sentinel建立一個MasterListener線程,該線程用來監(jiān)聽主從切換,保證客戶端的Jedis句柄始終對應(yīng)在Master上。在這里,即會有5個MasterListener來對應(yīng)5個Sentinel。
其實,MasterListener的監(jiān)聽功能根據(jù)Redis的pub sub功能實現(xiàn)的。MasterListener線程會去訂閱+switch-master消息,該消息會在master節(jié)點地址改變時產(chǎn)生,一旦產(chǎn)生,MasterListener就重新初始化連接池,保證客戶端使用的jedis句柄始終關(guān)聯(lián)到Master上。
如下所示為MasterListener的線程函數(shù),它會在一個無限循環(huán)中不斷的創(chuàng)建Jedis句柄,利用該句柄去訂閱+switch-master消息,只要發(fā)生了主從切換,就會觸發(fā)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();
}
}
}
如何實現(xiàn)訂閱功能呢,我們需要查看subscribe函數(shù)的底層實現(xiàn),它實際使用client.setTimeoutInfinite()->connect建立了一個TCP連接,然后使用JedisPubSub的proceed方法去訂閱頻道,并且無限循環(huán)的讀取訂閱的信息。
@Override
public void subscribe(final JedisPubSub jedisPubSub, final String... channels) {
client.setTimeoutInfinite();
try {
jedisPubSub.proceed(client, channels);
} finally {
client.rollbackTimeout();
}
}
在procee的方法中,實際先通過subscribe訂閱頻道,然后調(diào)用process方法讀取訂閱信息。
public void proceed(Client client, String... channels) {
this.client = client;
client.subscribe(channels);
client.flush();
process(client);
}
其實,subscribe函數(shù)就是簡單的向服務(wù)器發(fā)送了一個SUBSCRIBE命令。
public void subscribe(final byte[]... channels) {
sendCommand(SUBSCRIBE, channels);
}
而process函數(shù),篇幅較長,此處省略,其主要功能就是以無限循環(huán)的方式不斷地讀取訂閱信息
private void process(Client client) {
do {
} while (isSubscribed());
}
綜上,MasterListener線程會向Sentinel創(chuàng)建+switch-master頻道的TCP訂閱連接,并且會do while循環(huán)讀取該頻道信息。如果訂閱或讀取過程中出現(xiàn)Tcp連接異常,則釋放Jedis句柄,然后等待5000ms 后重新創(chuàng)建Jedis句柄進(jìn)行訂閱。當(dāng)然,這個過程會在一個循環(huán)之中。
至此,也就解釋了為何每個業(yè)務(wù)客戶端服務(wù)器和Sentinel服務(wù)器上,都有5個長期保持的、狀態(tài)正常的TCP連接的原因了。