美團(tuán)二面:細(xì)數(shù) Redis 阻塞的九種情況
哈嘍大家好,我是阿Q!
前兩天去美團(tuán)面試的陳同學(xué)回來(lái)了,看他滿臉泄氣的樣子,準(zhǔn)是沒(méi)拿到 Offer。
聽(tīng)了他面試的經(jīng)過(guò),真替他感到惋惜。究其原因,是被一道面試題攔住了去路:看你簡(jiǎn)歷上寫著精通 Redis,請(qǐng)你總結(jié)一下 Redis 中存在的阻塞問(wèn)題吧。
正好阿Q這幾天正在研究 Redis,就順便在這兒給大家做個(gè)總結(jié)。
命令阻塞
使用不當(dāng)?shù)拿钤斐煽蛻舳俗枞?/p>
- keys * :獲取所有的 key 操作;
- Hgetall:返回哈希表中所有的字段和;
- smembers:返回集合中的所有成員;
這些命令時(shí)間復(fù)雜度是O(n),有時(shí)候也會(huì)全表掃描,隨著n的增大耗時(shí)也會(huì)越大從而導(dǎo)致客戶端阻塞。
SAVE 阻塞
大家都知道 Redis 在進(jìn)行 RDB 快照的時(shí)候,會(huì)調(diào)用系統(tǒng)函數(shù) fork() ,創(chuàng)建一個(gè)子線程來(lái)完成臨時(shí)文件的寫入,而觸發(fā)條件正是配置文件中的 save 配置。
當(dāng)達(dá)到我們的配置時(shí),就會(huì)觸發(fā) bgsave 命令創(chuàng)建快照,這種方式是不會(huì)阻塞主線程的,而手動(dòng)執(zhí)行 save 命令會(huì)在主線程中執(zhí)行,阻塞主線程。
同步持久化
當(dāng) Redis 直接記錄 AOF 日志時(shí),如果有大量的寫操作,并且配置為同步持久化
即每次發(fā)生數(shù)據(jù)變更會(huì)被立即記錄到磁盤,因?yàn)閷懘疟P比較耗時(shí),性能較差,所以有時(shí)會(huì)阻塞主線程。
AOF 重寫
- fork 出一條子線程來(lái)將文件重寫,在執(zhí)行 ?
?BGREWRITEAOF?
? 命令時(shí),Redis 服務(wù)器會(huì)維護(hù)一個(gè) AOF 重寫緩沖區(qū),該緩沖區(qū)會(huì)在子線程創(chuàng)建新 AOF 文件期間,記錄服務(wù)器執(zhí)行的所有寫命令。 - 當(dāng)子線程完成創(chuàng)建新 AOF 文件的工作之后,服務(wù)器會(huì)將重寫緩沖區(qū)中的所有內(nèi)容追加到新 AOF 文件的末尾,使得新的 AOF 文件保存的數(shù)據(jù)庫(kù)狀態(tài)與現(xiàn)有的數(shù)據(jù)庫(kù)狀態(tài)一致。
- 最后,服務(wù)器用新的 AOF 文件替換舊的 AOF 文件,以此來(lái)完成 AOF 文件重寫操作。
阻塞就是出現(xiàn)在第2步的過(guò)程中,將緩沖區(qū)中新數(shù)據(jù)寫到新文件的過(guò)程中會(huì)產(chǎn)生阻塞。
AOF 日志
AOF 的日志記錄不像關(guān)系型數(shù)據(jù)庫(kù)那樣在執(zhí)行命令之前記錄日志(方便故障恢復(fù)),而是采用先執(zhí)行命令后記錄日志的方式。
原因就是 AOF 記錄日志是不會(huì)對(duì)命令進(jìn)行語(yǔ)法檢查的,這樣就能減少額外的檢查開(kāi)銷,不會(huì)對(duì)當(dāng)前命令的執(zhí)行產(chǎn)生阻塞,但可能會(huì)給下一個(gè)操作帶來(lái)阻塞風(fēng)險(xiǎn)。
這是因?yàn)?AOF 日志也是在主線程中執(zhí)行的,如果在把日志文件寫入磁盤時(shí),磁盤寫壓力大,就會(huì)導(dǎo)致寫盤很慢,進(jìn)而導(dǎo)致后續(xù)的操作也無(wú)法執(zhí)行了。
大 Key 問(wèn)題
大 key 并不是指 key 的值很大,而是 key 對(duì)應(yīng)的 value 很大。
大 key 造成的阻塞問(wèn)題如下:
- 客戶端超時(shí)阻塞:由于 Redis 執(zhí)行命令是單線程處理,然后在操作大 key 時(shí)會(huì)比較耗時(shí),那么就會(huì)阻塞 Redis,從客戶端這一視角看,就是很久很久都沒(méi)有響應(yīng)。
- 引發(fā)網(wǎng)絡(luò)阻塞:每次獲取大 key 產(chǎn)生的網(wǎng)絡(luò)流量較大,如果一個(gè) key 的大小是 1 MB,每秒訪問(wèn)量為 1000,那么每秒會(huì)產(chǎn)生 1000MB 的流量,這對(duì)于普通千兆網(wǎng)卡的服務(wù)器來(lái)說(shuō)是災(zāi)難性的。
- 阻塞工作線程:如果使用 del 刪除大 key 時(shí),會(huì)阻塞工作線程,這樣就沒(méi)辦法處理后續(xù)的命令。
查找大 key
當(dāng)我們?cè)谑褂?Redis 自帶的 ??--bigkeys?
? 參數(shù)查找大 key 時(shí),最好選擇在從節(jié)點(diǎn)上執(zhí)行該命令,因?yàn)橹鞴?jié)點(diǎn)上執(zhí)行時(shí),會(huì)阻塞主節(jié)點(diǎn)。
- 我們還可以使用 SCAN 命令來(lái)查找大 key;
- 通過(guò)分析 RDB 文件來(lái)找出 big key,這種方案的前提是 Redis 采用的是 RDB 持久化。網(wǎng)上有現(xiàn)成的工具:
- redis-rdb-tools:Python 語(yǔ)言寫的用來(lái)分析 Redis 的 RDB 快照文件用的工具
- rdb_bigkeys:Go 語(yǔ)言寫的用來(lái)分析 Redis 的 RDB 快照文件用的工具,性能更好。
刪除大 key
刪除操作的本質(zhì)是要釋放鍵值對(duì)占用的內(nèi)存空間。
釋放內(nèi)存只是第一步,為了更加高效地管理內(nèi)存空間,在應(yīng)用程序釋放內(nèi)存時(shí),操作系統(tǒng)需要把釋放掉的內(nèi)存塊插入一個(gè)空閑內(nèi)存塊的鏈表,以便后續(xù)進(jìn)行管理和再分配。這個(gè)過(guò)程本身需要一定時(shí)間,而且會(huì)阻塞當(dāng)前釋放內(nèi)存的應(yīng)用程序。
所以,如果一下子釋放了大量?jī)?nèi)存,空閑內(nèi)存塊鏈表操作時(shí)間就會(huì)增加,相應(yīng)地就會(huì)造成 Redis 主線程的阻塞,如果主線程發(fā)生了阻塞,其他所有請(qǐng)求可能都會(huì)超時(shí),超時(shí)越來(lái)越多,會(huì)造成 Redis 連接耗盡,產(chǎn)生各種異常。
刪除大 key 時(shí)建議采用分批次刪除和異步刪除的方式進(jìn)行。
清空數(shù)據(jù)庫(kù)
清空數(shù)據(jù)庫(kù)和上面 bigkey 刪除也是同樣道理,flushdb、flushall 也涉及到刪除和釋放所有的鍵值對(duì),也是 Redis 的阻塞點(diǎn)。
集群擴(kuò)容
Redis 集群可以進(jìn)行節(jié)點(diǎn)的動(dòng)態(tài)擴(kuò)容縮容,這一過(guò)程目前還處于半自動(dòng)狀態(tài),需要人工介入。
在擴(kuò)縮容的時(shí)候,需要進(jìn)行數(shù)據(jù)遷移。而 Redis 為了保證遷移的一致性,遷移所有操作都是同步操作。
執(zhí)行遷移時(shí),兩端的 Redis 均會(huì)進(jìn)入時(shí)長(zhǎng)不等的阻塞狀態(tài),對(duì)于小Key,該時(shí)間可以忽略不計(jì),但如果一旦 Key 的內(nèi)存使用過(guò)大,嚴(yán)重的時(shí)候會(huì)觸發(fā)集群內(nèi)的故障轉(zhuǎn)移,造成不必要的切換。