為什么我的Redis這么“慢”?
Redis 作為內(nèi)存數(shù)據(jù)庫(kù),擁有非常高的性能,單個(gè)實(shí)例的 QPS 能夠達(dá)到 10W 左右。
圖片來(lái)自 Pexels
但我們?cè)谑褂?Redis 時(shí),經(jīng)常時(shí)不時(shí)會(huì)出現(xiàn)訪問(wèn)延遲很大的情況,如果你不知道 Redis 的內(nèi)部實(shí)現(xiàn)原理,在排查問(wèn)題時(shí)就會(huì)一頭霧水。
很多時(shí)候,Redis 出現(xiàn)訪問(wèn)延遲變大,都與我們的使用不當(dāng)或運(yùn)維不合理導(dǎo)致的。
這篇文章我們就來(lái)分析一下 Redis 在使用過(guò)程中,經(jīng)常會(huì)遇到的延遲問(wèn)題以及如何定位和分析。
使用復(fù)雜度高的命令
如果在使用 Redis 時(shí),發(fā)現(xiàn)訪問(wèn)延遲突然增大,如何進(jìn)行排查?
首先,第一步,建議你去查看一下 Redis 的慢日志。Redis 提供了慢日志命令的統(tǒng)計(jì)功能,我們通過(guò)以下設(shè)置,就可以查看有哪些命令在執(zhí)行時(shí)延遲比較大。
首先設(shè)置 Redis 的慢日志閾值,只有超過(guò)閾值的命令才會(huì)被記錄,這里的單位是微秒。
例如設(shè)置慢日志的閾值為 5 毫秒,同時(shí)設(shè)置只保留最近 1000 條慢日志記錄:
- # 命令執(zhí)行超過(guò)5毫秒記錄慢日志
- CONFIG SET slowlog-log-slower-than 5000
- # 只保留最近1000條慢日志
- CONFIG SET slowlog-max-len 1000
設(shè)置完成之后,所有執(zhí)行的命令如果延遲大于 5 毫秒,都會(huì)被 Redis 記錄下來(lái),我們執(zhí)行 SLOWLOG get 5 查詢(xún)最近 5 條慢日志:
- 127.0.0.1:6379> SLOWLOG get 5
- 1) 1) (integer) 32693 # 慢日志ID
- 2) (integer) 1593763337 # 執(zhí)行時(shí)間
- 3) (integer) 5299 # 執(zhí)行耗時(shí)(微妙)
- 4) 1) "LRANGE" # 具體執(zhí)行的命令和參數(shù)
- 2) "user_list_2000"
- 3) "0"
- 4) "-1"
- 2) 1) (integer) 32692
- 2) (integer) 1593763337
- 3) (integer) 5044
- 4) 1) "GET"
- 2) "book_price_1000"
- ...
通過(guò)查看慢日志記錄,我們就可以知道在什么時(shí)間執(zhí)行哪些命令比較耗時(shí),如果你的業(yè)務(wù)經(jīng)常使用 O(n) 以上復(fù)雜度的命令。
例如 sort、sunion、zunionstore,或者在執(zhí)行 O(n) 命令時(shí)操作的數(shù)據(jù)量比較大,這些情況下 Redis 處理數(shù)據(jù)時(shí)就會(huì)很耗時(shí)。
如果你的服務(wù)請(qǐng)求量并不大,但 Redis 實(shí)例的 CPU 使用率很高,很有可能是使用了復(fù)雜度高的命令導(dǎo)致的。
解決方案就是,不使用這些復(fù)雜度較高的命令,并且一次不要獲取太多的數(shù)據(jù),每次盡量操作少量的數(shù)據(jù),讓 Redis 可以及時(shí)處理返回。
存儲(chǔ)大 Key
如果查詢(xún)慢日志發(fā)現(xiàn),并不是復(fù)雜度較高的命令導(dǎo)致的,例如都是 SET、DELETE 操作出現(xiàn)在慢日志記錄中,那么你就要懷疑是否存在 Redis 寫(xiě)入了大 Key 的情況。
Redis 在寫(xiě)入數(shù)據(jù)時(shí),需要為新的數(shù)據(jù)分配內(nèi)存,當(dāng)從 Redis 中刪除數(shù)據(jù)時(shí),它會(huì)釋放對(duì)應(yīng)的內(nèi)存空間。
如果一個(gè) Key 寫(xiě)入的數(shù)據(jù)非常大,Redis 在分配內(nèi)存時(shí)也會(huì)比較耗時(shí)。同樣的,當(dāng)刪除這個(gè) Key 的數(shù)據(jù)時(shí),釋放內(nèi)存也會(huì)耗時(shí)比較久。
你需要檢查你的業(yè)務(wù)代碼,是否存在寫(xiě)入大 Key 的情況,需要評(píng)估寫(xiě)入數(shù)據(jù)量的大小,業(yè)務(wù)層應(yīng)該避免一個(gè) Key 存入過(guò)大的數(shù)據(jù)量。
那么有沒(méi)有什么辦法可以掃描現(xiàn)在 Redis 中是否存在大 Key 的數(shù)據(jù)嗎?
Redis 也提供了掃描大 Key 的方法:
- redis-cli -h $host -p $port --bigkeys -i 0.01
使用上面的命令就可以掃描出整個(gè)實(shí)例 Key 大小的分布情況,它是以類(lèi)型維度來(lái)展示的。
需要注意的是當(dāng)我們?cè)诰€上實(shí)例進(jìn)行大 Key 掃描時(shí),Redis 的 QPS 會(huì)突增,為了降低掃描過(guò)程中對(duì) Redis 的影響,我們需要控制掃描的頻率,使用 -i 參數(shù)控制即可,它表示掃描過(guò)程中每次掃描的時(shí)間間隔,單位是秒。
使用這個(gè)命令的原理,其實(shí)就是 Redis 在內(nèi)部執(zhí)行 Scan 命令,遍歷所有 Key。
然后針對(duì)不同類(lèi)型的 Key 執(zhí)行 strlen、llen、hlen、scard、zcard 來(lái)獲取字符串的長(zhǎng)度以及容器類(lèi)型(list/dict/set/zset)的元素個(gè)數(shù)。
而對(duì)于容器類(lèi)型的 Key,只能掃描出元素最多的 Key,但元素最多的 Key 不一定占用內(nèi)存最多,這一點(diǎn)需要我們注意下。
不過(guò)使用這個(gè)命令一般我們是可以對(duì)整個(gè)實(shí)例中 Key 的分布情況有比較清晰的了解。
針對(duì)大 Key 的問(wèn)題,Redis 官方在 4.0 版本推出了 lazy-free 的機(jī)制,用于異步釋放大 Key 的內(nèi)存,降低對(duì) Redis 性能的影響。
即使這樣,我們也不建議使用大 Key,大 Key 在集群的遷移過(guò)程中,也會(huì)影響到遷移的性能,這個(gè)后面在介紹集群相關(guān)的文章時(shí),會(huì)再詳細(xì)介紹到。
集中過(guò)期
有時(shí)你會(huì)發(fā)現(xiàn),平時(shí)在使用 Redis 時(shí)沒(méi)有延時(shí)比較大的情況,但在某個(gè)時(shí)間點(diǎn)突然出現(xiàn)一波延時(shí),而且報(bào)慢的時(shí)間點(diǎn)很有規(guī)律,例如某個(gè)整點(diǎn),或者間隔多久就會(huì)發(fā)生一次。
如果出現(xiàn)這種情況,就需要考慮是否存在大量 Key 集中過(guò)期的情況。
如果有大量的 Key 在某個(gè)固定時(shí)間點(diǎn)集中過(guò)期,在這個(gè)時(shí)間點(diǎn)訪問(wèn) Redis 時(shí),就有可能導(dǎo)致延遲增加。
Redis 的過(guò)期策略采用主動(dòng)過(guò)期+懶惰過(guò)期兩種策略:
- 主動(dòng)過(guò)期:Redis 內(nèi)部維護(hù)一個(gè)定時(shí)任務(wù),默認(rèn)每隔 100 毫秒會(huì)從過(guò)期字典中隨機(jī)取出 20 個(gè) Key,刪除過(guò)期的 Key。
如果過(guò)期 Key 的比例超過(guò)了 25%,則繼續(xù)獲取 20 個(gè) Key,刪除過(guò)期的 Key,循環(huán)往復(fù),直到過(guò)期 Key 的比例下降到 25% 或者這次任務(wù)的執(zhí)行耗時(shí)超過(guò)了 25 毫秒,才會(huì)退出循環(huán)。
- 懶惰過(guò)期:只有當(dāng)訪問(wèn)某個(gè) Key 時(shí),才判斷這個(gè) Key 是否已過(guò)期,如果已經(jīng)過(guò)期,則從實(shí)例中刪除。
注意,Redis 的主動(dòng)過(guò)期的定時(shí)任務(wù),也是在 Redis 主線程中執(zhí)行的,也就是說(shuō)如果在執(zhí)行主動(dòng)過(guò)期的過(guò)程中,出現(xiàn)了需要大量刪除過(guò)期 Key 的情況。
那么在業(yè)務(wù)訪問(wèn)時(shí),必須等這個(gè)過(guò)期任務(wù)執(zhí)行結(jié)束,才可以處理業(yè)務(wù)請(qǐng)求。此時(shí)就會(huì)出現(xiàn),業(yè)務(wù)訪問(wèn)延時(shí)增大的問(wèn)題,最大延遲為 25 毫秒。
而且這個(gè)訪問(wèn)延遲的情況,不會(huì)記錄在慢日志里。慢日志中只記錄真正執(zhí)行某個(gè)命令的耗時(shí),Redis 主動(dòng)過(guò)期策略執(zhí)行在操作命令之前。
如果操作命令耗時(shí)達(dá)不到慢日志閾值,它是不會(huì)計(jì)算在慢日志統(tǒng)計(jì)中的,但我們的業(yè)務(wù)卻感到了延遲增大。
此時(shí)你需要檢查你的業(yè)務(wù),是否真的存在集中過(guò)期的代碼,一般集中過(guò)期使用的命令是 expireat 或 pexpireat 命令,在代碼中搜索這個(gè)關(guān)鍵字就可以了。
如果你的業(yè)務(wù)確實(shí)需要集中過(guò)期掉某些 Key,又不想導(dǎo)致 Redis 發(fā)生抖動(dòng),有什么優(yōu)化方案?
解決方案是,在集中過(guò)期時(shí)增加一個(gè)隨機(jī)時(shí)間,把這些需要過(guò)期的 Key 的時(shí)間打散即可。
偽代碼可以這么寫(xiě):
- # 在過(guò)期時(shí)間點(diǎn)之后的5分鐘內(nèi)隨機(jī)過(guò)期掉
- redis.expireat(key, expire_time + random(300))
這樣 Redis 在處理過(guò)期時(shí),不會(huì)因?yàn)榧袆h除 Key 導(dǎo)致壓力過(guò)大,阻塞主線程。
另外,除了業(yè)務(wù)使用需要注意此問(wèn)題之外,還可以通過(guò)運(yùn)維手段來(lái)及時(shí)發(fā)現(xiàn)這種情況。
做法是我們需要把 Redis 的各項(xiàng)運(yùn)行數(shù)據(jù)監(jiān)控起來(lái),執(zhí)行 info 可以拿到所有的運(yùn)行數(shù)據(jù)。
在這里我們需要重點(diǎn)關(guān)注 expired_keys 這一項(xiàng),它代表整個(gè)實(shí)例到目前為止,累計(jì)刪除過(guò)期 Key 的數(shù)量。
我們需要對(duì)這個(gè)指標(biāo)監(jiān)控,當(dāng)在很短時(shí)間內(nèi)這個(gè)指標(biāo)出現(xiàn)突增時(shí),需要及時(shí)報(bào)警出來(lái),然后與業(yè)務(wù)報(bào)慢的時(shí)間點(diǎn)對(duì)比分析,確認(rèn)時(shí)間是否一致,如果一致,則可以認(rèn)為確實(shí)是因?yàn)檫@個(gè)原因?qū)е碌难舆t增大。
實(shí)例內(nèi)存達(dá)到上限
有時(shí)我們把 Redis 當(dāng)做純緩存使用,就會(huì)給實(shí)例設(shè)置一個(gè)內(nèi)存上限 maxmemory,然后開(kāi)啟 LRU 淘汰策略。
當(dāng)實(shí)例的內(nèi)存達(dá)到了 maxmemory 后,你會(huì)發(fā)現(xiàn)之后的每次寫(xiě)入新的數(shù)據(jù),有可能變慢了。
導(dǎo)致變慢的原因是,當(dāng) Redis 內(nèi)存達(dá)到 maxmemory 后,每次寫(xiě)入新的數(shù)據(jù)之前,必須先踢出一部分?jǐn)?shù)據(jù),讓內(nèi)存維持在 maxmemory 之下。
這個(gè)踢出舊數(shù)據(jù)的邏輯也是需要消耗時(shí)間的,而具體耗時(shí)的長(zhǎng)短,要取決于配置的淘汰策略:
- allkeys-lru:不管 Key 是否設(shè)置了過(guò)期,淘汰最近最少訪問(wèn)的 Key。
- volatile-lru:只淘汰最近最少訪問(wèn)并設(shè)置過(guò)期的 Key。
- allkeys-random:不管 Key 是否設(shè)置了過(guò)期,隨機(jī)淘汰。
- volatile-random:只隨機(jī)淘汰有設(shè)置過(guò)期的 Key。
- allkeys-ttl:不管 Key 是否設(shè)置了過(guò)期,淘汰即將過(guò)期的 Key。
- noeviction:不淘汰任何 Key,滿(mǎn)容后再寫(xiě)入直接報(bào)錯(cuò)。
- allkeys-lfu:不管 Key 是否設(shè)置了過(guò)期,淘汰訪問(wèn)頻率最低的 Key(4.0+支持)。
- volatile-lfu:只淘汰訪問(wèn)頻率最低的過(guò)期 Key(4.0+支持)。
具體使用哪種策略,需要根據(jù)業(yè)務(wù)場(chǎng)景來(lái)決定。
我們最常使用的一般是 allkeys-lru 或 volatile-lru 策略,它們的處理邏輯是,每次從實(shí)例中隨機(jī)取出一批 Key(可配置),然后淘汰一個(gè)最少訪問(wèn)的 Key。
之后把剩下的 Key 暫存到一個(gè)池子中,繼續(xù)隨機(jī)取出一批 Key,并與之前池子中的 Key 比較,再淘汰一個(gè)最少訪問(wèn)的 Key。以此循環(huán),直到內(nèi)存降到 maxmemory 之下。
如果使用的是 allkeys-random 或 volatile-random 策略,那么就會(huì)快很多。
因?yàn)槭请S機(jī)淘汰,那么就少了比較 Key 訪問(wèn)頻率時(shí)間的消耗了,隨機(jī)拿出一批 Key 后直接淘汰即可,因此這個(gè)策略要比上面的 LRU 策略執(zhí)行快一些。
但以上這些邏輯都是在訪問(wèn) Redis 時(shí),真正命令執(zhí)行之前執(zhí)行的,也就是它會(huì)影響我們?cè)L問(wèn) Redis 時(shí)執(zhí)行的命令。
另外,如果此時(shí) Redis 實(shí)例中有存儲(chǔ)大 Key,那么在淘汰大 Key 釋放內(nèi)存時(shí),這個(gè)耗時(shí)會(huì)更加久,延遲更大,這需要我們格外注意。
如果你的業(yè)務(wù)訪問(wèn)量非常大,并且必須設(shè)置 maxmemory 限制實(shí)例的內(nèi)存上限,同時(shí)面臨淘汰 Key 導(dǎo)致延遲增大的的情況,要想緩解這種情況。
除了上面說(shuō)的避免存儲(chǔ)大 Key、使用隨機(jī)淘汰策略之外,也可以考慮拆分實(shí)例的方法來(lái)緩解,拆分實(shí)例可以把一個(gè)實(shí)例淘汰 Key 的壓力分?jǐn)偟蕉鄠€(gè)實(shí)例上,可以在一定程度降低延遲。
Fork 耗時(shí)嚴(yán)重
如果你的 Redis 開(kāi)啟了自動(dòng)生成 RDB 和 AOF 重寫(xiě)功能,那么有可能在后臺(tái)生成 RDB 和 AOF 重寫(xiě)時(shí)導(dǎo)致 Redis 的訪問(wèn)延遲增大,而等這些任務(wù)執(zhí)行完畢后,延遲情況消失。
遇到這種情況,一般就是執(zhí)行生成 RDB 和 AOF 重寫(xiě)任務(wù)導(dǎo)致的。
生成 RDB 和 AOF 都需要父進(jìn)程 Fork 出一個(gè)子進(jìn)程進(jìn)行數(shù)據(jù)的持久化,在 Fork 執(zhí)行過(guò)程中,父進(jìn)程需要拷貝內(nèi)存頁(yè)表給子進(jìn)程。
如果整個(gè)實(shí)例內(nèi)存占用很大,那么需要拷貝的內(nèi)存頁(yè)表會(huì)比較耗時(shí),此過(guò)程會(huì)消耗大量的 CPU 資源,在完成 Fork 之前,整個(gè)實(shí)例會(huì)被阻塞住,無(wú)法處理任何請(qǐng)求。
如果此時(shí) CPU 資源緊張,那么 Fork 的時(shí)間會(huì)更長(zhǎng),甚至達(dá)到秒級(jí)。這會(huì)嚴(yán)重影響 Redis 的性能。
我們可以執(zhí)行 info 命令,查看最后一次 Fork 執(zhí)行的耗時(shí) latest_fork_usec,單位微秒。這個(gè)時(shí)間就是整個(gè)實(shí)例阻塞無(wú)法處理請(qǐng)求的時(shí)間。
除了因?yàn)閭浞莸脑蛏?RDB 之外,在主從節(jié)點(diǎn)第一次建立數(shù)據(jù)同步時(shí),主節(jié)點(diǎn)也會(huì)生成 RDB 文件給從節(jié)點(diǎn)進(jìn)行一次全量同步,這時(shí)也會(huì)對(duì) Redis 產(chǎn)生性能影響。
要想避免這種情況,我們需要規(guī)劃好數(shù)據(jù)備份的周期,建議在從節(jié)點(diǎn)上執(zhí)行備份,而且最好放在低峰期執(zhí)行。
如果對(duì)于丟失數(shù)據(jù)不敏感的業(yè)務(wù),那么不建議開(kāi)啟 RDB 和 AOF 重寫(xiě)功能。
另外,F(xiàn)ork 的耗時(shí)也與系統(tǒng)有關(guān),如果把 Redis 部署在虛擬機(jī)上,那么這個(gè)時(shí)間也會(huì)增大。所以使用 Redis 時(shí)建議部署在物理機(jī)上,降低 Fork 的影響。
綁定 CPU
很多時(shí)候,我們?cè)诓渴鸱?wù)時(shí),為了提高性能,降低程序在使用多個(gè) CPU 時(shí)上下文切換的性能損耗,一般會(huì)采用進(jìn)程綁定 CPU 的操作。
但在使用 Redis 時(shí),我們不建議這么干,原因如下。
綁定 CPU 的 Redis,在進(jìn)行數(shù)據(jù)持久化時(shí),F(xiàn)ork 出的子進(jìn)程,子進(jìn)程會(huì)繼承父進(jìn)程的 CPU 使用偏好。
而此時(shí)子進(jìn)程會(huì)消耗大量的 CPU 資源進(jìn)行數(shù)據(jù)持久化,子進(jìn)程會(huì)與主進(jìn)程發(fā)生 CPU 爭(zhēng)搶?zhuān)@也會(huì)導(dǎo)致主進(jìn)程的 CPU 資源不足訪問(wèn)延遲增大。
所以在部署 Redis 進(jìn)程時(shí),如果需要開(kāi)啟 RDB 和 AOF 重寫(xiě)機(jī)制,一定不能進(jìn)行 CPU 綁定操作!
開(kāi)啟 AOF
上面提到了,當(dāng)執(zhí)行 AOF 文件重寫(xiě)時(shí)會(huì)因?yàn)?Fork 執(zhí)行耗時(shí)導(dǎo)致 Redis 延遲增大,除了這個(gè)之外,如果開(kāi)啟 AOF 機(jī)制,設(shè)置的策略不合理,也會(huì)導(dǎo)致性能問(wèn)題。
開(kāi)啟 AOF 后,Redis 會(huì)把寫(xiě)入的命令實(shí)時(shí)寫(xiě)入到文件中,但寫(xiě)入文件的過(guò)程是先寫(xiě)入內(nèi)存,等內(nèi)存中的數(shù)據(jù)超過(guò)一定閾值或達(dá)到一定時(shí)間后,內(nèi)存中的內(nèi)容才會(huì)被真正寫(xiě)入到磁盤(pán)中。
AOF 為了保證文件寫(xiě)入磁盤(pán)的安全性,提供了三種刷盤(pán)機(jī)制:
- appendfsync always:每次寫(xiě)入都刷盤(pán),對(duì)性能影響最大,占用磁盤(pán) IO 比較高,數(shù)據(jù)安全性最高。
- appendfsync everysec:1 秒刷一次盤(pán),對(duì)性能影響相對(duì)較小,節(jié)點(diǎn)宕機(jī)時(shí)最多丟失 1 秒的數(shù)據(jù)。
- appendfsync no:按照操作系統(tǒng)的機(jī)制刷盤(pán),對(duì)性能影響最小,數(shù)據(jù)安全性低,節(jié)點(diǎn)宕機(jī)丟失數(shù)據(jù)取決于操作系統(tǒng)刷盤(pán)機(jī)制。
當(dāng)使用第一種機(jī)制 appendfsync always 時(shí),Redis 每處理一次寫(xiě)命令,都會(huì)把這個(gè)命令寫(xiě)入磁盤(pán),而且這個(gè)操作是在主線程中執(zhí)行的。
內(nèi)存中的的數(shù)據(jù)寫(xiě)入磁盤(pán),這個(gè)會(huì)加重磁盤(pán)的 IO 負(fù)擔(dān),操作磁盤(pán)成本要比操作內(nèi)存的代價(jià)大得多。
如果寫(xiě)入量很大,那么每次更新都會(huì)寫(xiě)入磁盤(pán),此時(shí)機(jī)器的磁盤(pán) IO 就會(huì)非常高,拖慢 Redis 的性能,因此我們不建議使用這種機(jī)制。
與第一種機(jī)制對(duì)比,appendfsync everysec 會(huì)每隔 1 秒刷盤(pán),而 appendfsync no 取決于操作系統(tǒng)的刷盤(pán)時(shí)間,安全性不高。
因此我們推薦使用 appendfsync everysec 這種方式,在最壞的情況下,只會(huì)丟失 1 秒的數(shù)據(jù),但它能保持較好的訪問(wèn)性能。
當(dāng)然,對(duì)于有些業(yè)務(wù)場(chǎng)景,對(duì)丟失數(shù)據(jù)并不敏感,也可以不開(kāi)啟 AOF。
使用 Swap
如果你發(fā)現(xiàn) Redis 突然變得非常慢,每次訪問(wèn)的耗時(shí)都達(dá)到了幾百毫秒甚至秒級(jí),那此時(shí)就檢查 Redis 是否使用到了 Swap,這種情況下 Redis 基本上已經(jīng)無(wú)法提供高性能的服務(wù)。
我們知道,操作系統(tǒng)提供了 Swap 機(jī)制,目的是為了當(dāng)內(nèi)存不足時(shí),可以把一部分內(nèi)存中的數(shù)據(jù)換到磁盤(pán)上,以達(dá)到對(duì)內(nèi)存使用的緩沖。
但當(dāng)內(nèi)存中的數(shù)據(jù)被換到磁盤(pán)上后,訪問(wèn)這些數(shù)據(jù)就需要從磁盤(pán)中讀取,這個(gè)速度要比內(nèi)存慢太多!
尤其是針對(duì) Redis 這種高性能的內(nèi)存數(shù)據(jù)庫(kù)來(lái)說(shuō),如果 Redis 中的內(nèi)存被換到磁盤(pán)上,對(duì)于 Redis 這種性能極其敏感的數(shù)據(jù)庫(kù),這個(gè)操作時(shí)間是無(wú)法接受的。
我們需要檢查機(jī)器的內(nèi)存使用情況,確認(rèn)是否確實(shí)是因?yàn)閮?nèi)存不足導(dǎo)致使用到了 Swap。
如果確實(shí)使用到了 Swap,要及時(shí)整理內(nèi)存空間,釋放出足夠的內(nèi)存供 Redis 使用,然后釋放 Redis 的 Swap,讓 Redis 重新使用內(nèi)存。
釋放 Redis 的 Swap 過(guò)程通常要重啟實(shí)例,為了避免重啟實(shí)例對(duì)業(yè)務(wù)的影響,一般先進(jìn)行主從切換,然后釋放舊主節(jié)點(diǎn)的 Swap,重新啟動(dòng)服務(wù),待數(shù)據(jù)同步完成后,再切換回主節(jié)點(diǎn)即可。
可見(jiàn),當(dāng) Redis 使用到 Swap 后,此時(shí)的 Redis 的高性能基本被廢掉,所以我們需要提前預(yù)防這種情況。
我們需要對(duì) Redis 機(jī)器的內(nèi)存和 Swap 使用情況進(jìn)行監(jiān)控,在內(nèi)存不足和使用到 Swap 時(shí)及時(shí)報(bào)警出來(lái),及時(shí)進(jìn)行相應(yīng)的處理。
網(wǎng)卡負(fù)載過(guò)高
如果以上產(chǎn)生性能問(wèn)題的場(chǎng)景,你都規(guī)避掉了,而且 Redis 也穩(wěn)定運(yùn)行了很長(zhǎng)時(shí)間,但在某個(gè)時(shí)間點(diǎn)之后開(kāi)始,訪問(wèn) Redis 開(kāi)始變慢了,而且一直持續(xù)到現(xiàn)在,這種情況是什么原因?qū)е碌?
之前我們就遇到這種問(wèn)題,特點(diǎn)就是從某個(gè)時(shí)間點(diǎn)之后就開(kāi)始變慢,并且一直持續(xù)。這時(shí)你需要檢查一下機(jī)器的網(wǎng)卡流量,是否存在網(wǎng)卡流量被跑滿(mǎn)的情況。
網(wǎng)卡負(fù)載過(guò)高,在網(wǎng)絡(luò)層和 TCP 層就會(huì)出現(xiàn)數(shù)據(jù)發(fā)送延遲、數(shù)據(jù)丟包等情況。
Redis 的高性能除了內(nèi)存之外,就在于網(wǎng)絡(luò) IO,請(qǐng)求量突增會(huì)導(dǎo)致網(wǎng)卡負(fù)載變高。
如果出現(xiàn)這種情況,你需要排查這個(gè)機(jī)器上的哪個(gè) Redis 實(shí)例的流量過(guò)大占滿(mǎn)了網(wǎng)絡(luò)帶寬,然后確認(rèn)流量突增是否屬于業(yè)務(wù)正常情況,如果屬于那就需要及時(shí)擴(kuò)容或遷移實(shí)例,避免這個(gè)機(jī)器的其他實(shí)例受到影響。
運(yùn)維層面,我們需要對(duì)機(jī)器的各項(xiàng)指標(biāo)增加監(jiān)控,包括網(wǎng)絡(luò)流量,在達(dá)到閾值時(shí)提前報(bào)警,及時(shí)與業(yè)務(wù)確認(rèn)并擴(kuò)容。
小結(jié):以上我們總結(jié)了 Redis 中常見(jiàn)的可能導(dǎo)致延遲增大甚至阻塞的場(chǎng)景,這其中既涉及到了業(yè)務(wù)的使用問(wèn)題,也涉及到 Redis 的運(yùn)維問(wèn)題。
可見(jiàn),要想保證 Redis 高性能的運(yùn)行,其中涉及到 CPU、內(nèi)存、網(wǎng)絡(luò),甚至磁盤(pán)的方方面面,其中還包括操作系統(tǒng)的相關(guān)特性的使用。
作為開(kāi)發(fā)人員,我們需要了解 Redis 的運(yùn)行機(jī)制,例如各個(gè)命令的執(zhí)行時(shí)間復(fù)雜度、數(shù)據(jù)過(guò)期策略、數(shù)據(jù)淘汰策略等,使用合理的命令,并結(jié)合業(yè)務(wù)場(chǎng)景進(jìn)行優(yōu)化。
作為 DBA 運(yùn)維人員,需要了解數(shù)據(jù)持久化、操作系統(tǒng) Fork 原理、Swap 機(jī)制等,并對(duì) Redis 的容量進(jìn)行合理規(guī)劃,預(yù)留足夠的機(jī)器資源,對(duì)機(jī)器做好完善的監(jiān)控,才能保證 Redis 的穩(wěn)定運(yùn)行。
Redis 最佳實(shí)踐:業(yè)務(wù)層面和運(yùn)維層面優(yōu)化
在上文中,主要講解了 Redis 常見(jiàn)的導(dǎo)致變慢的場(chǎng)景以及問(wèn)題定位和分析,主要是由業(yè)務(wù)使用不合理和運(yùn)維不當(dāng)導(dǎo)致的。
我們?cè)诹私饬藢?dǎo)致 Redis 變慢的原因之后,針對(duì)性地優(yōu)化,就可以讓 Redis 穩(wěn)定發(fā)揮出更高性能。
接下來(lái)我們就來(lái)總結(jié)一下,在使用 Redis 時(shí)的最佳實(shí)踐方式,主要包含兩個(gè)層面:
- 業(yè)務(wù)層面
- 運(yùn)維層面
由于我之前寫(xiě)過(guò)很多 UGC 后端服務(wù),在大量場(chǎng)景下用到了 Redis,這個(gè)過(guò)程中也踩過(guò)很多坑,所以在使用過(guò)程中也總結(jié)了一套合理的使用方法。
后來(lái)做基礎(chǔ)架構(gòu),開(kāi)發(fā) Codis、Redis 相關(guān)的中間件,在這個(gè)階段關(guān)注領(lǐng)域從使用層面下沉到 Redis 的開(kāi)發(fā)和運(yùn)維,更多聚焦在 Redis 的內(nèi)部實(shí)現(xiàn)和運(yùn)維過(guò)程中產(chǎn)生的各種問(wèn)題,在這塊也積累了一些經(jīng)驗(yàn)。
下面就針對(duì)這兩塊,分享一下我認(rèn)為比較合理的 Redis 使用和運(yùn)維方法,不一定最全面,也可能與你使用 Redis 的方法不同,但以下這些方法都是我在踩坑之后總結(jié)的實(shí)際經(jīng)驗(yàn),供你參考。
業(yè)務(wù)層面
業(yè)務(wù)層面主要是開(kāi)發(fā)人員需要關(guān)注,也就是開(kāi)發(fā)人員在寫(xiě)業(yè)務(wù)代碼時(shí),如何合理地使用 Redis。
開(kāi)發(fā)人員需要對(duì) Redis 有基本的了解,才能在合適的業(yè)務(wù)場(chǎng)景使用 Redis,從而避免業(yè)務(wù)層面導(dǎo)致的延遲問(wèn)題。
在開(kāi)發(fā)過(guò)程中,業(yè)務(wù)層面的優(yōu)化建議如下:
- Key 的長(zhǎng)度盡量要短,在數(shù)據(jù)量非常大時(shí),過(guò)長(zhǎng)的 Key 名會(huì)占用更多的內(nèi)存。
- 一定避免存儲(chǔ)過(guò)大的數(shù)據(jù)(大 Value),過(guò)大的數(shù)據(jù)在分配內(nèi)存和釋放內(nèi)存時(shí)耗時(shí)嚴(yán)重,會(huì)阻塞主線程。
- Redis 4.0 以上建議開(kāi)啟 lazy-free 機(jī)制,釋放大 Value 時(shí)異步操作,不阻塞主線程。
- 建議設(shè)置過(guò)期時(shí)間,把 Redis 當(dāng)做緩存使用,尤其在數(shù)量很大的時(shí),不設(shè)置過(guò)期時(shí)間會(huì)導(dǎo)致內(nèi)存的無(wú)限增長(zhǎng)。
- 不使用復(fù)雜度過(guò)高的命令,例如 SORT、SINTER、SINTERSTORE、ZUNIONSTORE、ZINTERSTORE,使用這些命令耗時(shí)較久,會(huì)阻塞主線程。
- 查詢(xún)數(shù)據(jù)時(shí),一次盡量獲取較少的數(shù)據(jù),在不確定容器元素個(gè)數(shù)的情況下,避免使用 LRANGE key 0 -1,ZRANGE key 0 -1 這類(lèi)操作,應(yīng)該設(shè)置具體查詢(xún)的元素個(gè)數(shù),推薦一次查詢(xún) 100 個(gè)以下元素。
- 寫(xiě)入數(shù)據(jù)時(shí),一次盡量寫(xiě)入較少的數(shù)據(jù),例如 HSET key value1 value2 value3...,控制一次寫(xiě)入元素的數(shù)量,推薦在 100 以下,大數(shù)據(jù)量分多個(gè)批次寫(xiě)入。
- 批量操作數(shù)據(jù)時(shí),用 MGET/MSET 替換 GET/SET、HMGET/MHSET 替換 HGET/HSET,減少請(qǐng)求來(lái)回的網(wǎng)絡(luò) IO 次數(shù),降低延遲,對(duì)于沒(méi)有批量操作的命令,推薦使用 Pipeline,一次性發(fā)送多個(gè)命令到服務(wù)端。
- 禁止使用 KEYS 命令,需要掃描實(shí)例時(shí),建議使用 SCAN,線上操作一定要控制掃描的頻率,避免對(duì) Redis 產(chǎn)生性能抖動(dòng)。
- 避免某個(gè)時(shí)間點(diǎn)集中過(guò)期大量的 Key,集中過(guò)期時(shí)推薦增加一個(gè)隨機(jī)時(shí)間,把過(guò)期時(shí)間打散,降低集中過(guò)期 Key 時(shí) Redis 的壓力,避免阻塞主線程。
- 根據(jù)業(yè)務(wù)場(chǎng)景,選擇合適的淘汰策略,通常隨機(jī)過(guò)期要比 LRU 過(guò)期淘汰數(shù)據(jù)更快。
- 使用連接池訪問(wèn) Redis,并配置合理的連接池參數(shù),避免短連接,TCP 三次握手和四次揮手的耗時(shí)也很高。
- 只使用 db0,不推薦使用多個(gè) db,使用多個(gè) db 會(huì)增加 Redis 的負(fù)擔(dān),每次訪問(wèn)不同的 db 都需要執(zhí)行 SELECT 命令,如果業(yè)務(wù)線不同,建議拆分多個(gè)實(shí)例,還能提高單個(gè)實(shí)例的性能。
- 讀的請(qǐng)求量很大時(shí),推薦使用讀寫(xiě)分離,前提是可以容忍從節(jié)數(shù)據(jù)更新不及時(shí)的問(wèn)題。
- 寫(xiě)請(qǐng)求量很大時(shí),推薦使用集群,部署多個(gè)實(shí)例分?jǐn)倢?xiě)壓力。
運(yùn)維層面
運(yùn)維層面主要是 DBA 需要關(guān)注的,目的是合理規(guī)劃 Redis 的部署和保障 Redis 的穩(wěn)定運(yùn)行。
主要優(yōu)化如下:
- 不同業(yè)務(wù)線部署不同的實(shí)例,各自獨(dú)立,避免混用,推薦不同業(yè)務(wù)線使用不同的機(jī)器,根據(jù)業(yè)務(wù)重要程度劃分不同的分組來(lái)部署,避免某一個(gè)業(yè)務(wù)線出現(xiàn)問(wèn)題影響其他業(yè)務(wù)線。
- 保證機(jī)器有足夠的 CPU、內(nèi)存、帶寬、磁盤(pán)資源,防止負(fù)載過(guò)高影響 Redis 性能。
- 以 master-slave 集群方式部署實(shí)例,并分布在不同機(jī)器上,避免單點(diǎn),Slave 必須設(shè)置為 Readonly。
- Master 和 Slave 節(jié)點(diǎn)所在機(jī)器,各自獨(dú)立,不要交叉部署實(shí)例,通常備份工作會(huì)在 Slave 上做,做備份時(shí)會(huì)消耗機(jī)器資源,交叉部署會(huì)影響到 Master 的性能。
- 推薦部署哨兵節(jié)點(diǎn)增加可用性,節(jié)點(diǎn)數(shù)量至少 3 個(gè),并分布在不同機(jī)器上,實(shí)現(xiàn)故障自動(dòng)故障轉(zhuǎn)移。
- 提前做好容量規(guī)劃,一臺(tái)機(jī)器部署實(shí)例的內(nèi)存上限,最好是機(jī)器內(nèi)存的一半,主從全量同步時(shí)會(huì)占用最多額外一倍的內(nèi)存空間,防止網(wǎng)絡(luò)大面積故障引發(fā)所有 master-slave 的全量同步導(dǎo)致機(jī)器內(nèi)存被吃光。
- 做好機(jī)器的 CPU、內(nèi)存、帶寬、磁盤(pán)監(jiān)控,在資源不足時(shí)及時(shí)報(bào)警處理,Redis 使用 Swap 后性能急劇下降,網(wǎng)絡(luò)帶寬負(fù)載過(guò)高訪問(wèn)延遲明顯增大,磁盤(pán) IO 過(guò)高時(shí)開(kāi)啟 AOF 會(huì)拖慢 Redis 的性能。
- 設(shè)置最大連接數(shù)上限,防止過(guò)多的客戶(hù)端連接導(dǎo)致服務(wù)負(fù)載過(guò)高。
- 單個(gè)實(shí)例的使用內(nèi)存建議控制在 20G 以下,過(guò)大的實(shí)例會(huì)導(dǎo)致備份時(shí)間久、資源消耗多,主從全量同步數(shù)據(jù)時(shí)間阻塞時(shí)間更長(zhǎng)。
- 設(shè)置合理的 slowlog 閾值,推薦 10 毫秒,并對(duì)其進(jìn)行監(jiān)控,產(chǎn)生過(guò)多的慢日志需要及時(shí)報(bào)警。
- 設(shè)置合理的復(fù)制緩沖區(qū) repl-backlog 大小,適當(dāng)調(diào)大 repl-backlog 可以降低主從全量復(fù)制的概率。
- 設(shè)置合理的 Slave 節(jié)點(diǎn) client-output-buffer-limit 大小,對(duì)于寫(xiě)入量很大的實(shí)例,適當(dāng)調(diào)大可以避免主從復(fù)制中斷問(wèn)題。
- 備份時(shí)推薦在 Slave 節(jié)點(diǎn)上做,不影響 Master 性能。
- 不開(kāi)啟 AOF 或開(kāi)啟 AOF 配置為每秒刷盤(pán),避免磁盤(pán) IO 消耗降低 Redis 性能。
- 當(dāng)實(shí)例設(shè)置了內(nèi)存上限,需要調(diào)大內(nèi)存上限時(shí),先調(diào)整 Slave 再調(diào)整 Master,否則會(huì)導(dǎo)致主從節(jié)點(diǎn)數(shù)據(jù)不一致。
- 對(duì) Redis 增加監(jiān)控,監(jiān)控采集 info 信息時(shí),使用長(zhǎng)連接,頻繁的短連接也會(huì)影響 Redis 性能。
- 線上掃描整個(gè)實(shí)例數(shù)時(shí),記得設(shè)置休眠時(shí)間,避免掃描時(shí) QPS 突增對(duì) Redis 產(chǎn)生性能抖動(dòng)。
- 做好 Redis 的運(yùn)行時(shí)監(jiān)控,尤其是 expired_keys、evicted_keys、latest_fork_usec 指標(biāo),短時(shí)間內(nèi)這些指標(biāo)值突增可能會(huì)阻塞整個(gè)實(shí)例,引發(fā)性能問(wèn)題。
總結(jié)
以上就是我在使用 Redis 和開(kāi)發(fā) Redis 相關(guān)中間件時(shí),總結(jié)出來(lái) Redis 推薦的實(shí)踐方法,以上提出的這些方面,都或多或少在實(shí)際使用中遇到過(guò)。
可見(jiàn),要想穩(wěn)定發(fā)揮 Redis 的高性能,需要在各個(gè)方面做好工作,但凡某一個(gè)方面出現(xiàn)問(wèn)題,必然會(huì)影響到 Redis 的性能,這對(duì)我們使用和運(yùn)維提出了更高的要求。
如果你在使用 Redis 過(guò)程中,遇到更多的問(wèn)題或者有更好的使用經(jīng)驗(yàn),可以留言一起探討!
作者:Kaito
簡(jiǎn)介:90 后,坐標(biāo)北京,6 年+工作經(jīng)驗(yàn),就職于一家移動(dòng)互聯(lián)網(wǎng)公司,目前從事基礎(chǔ)架構(gòu)和數(shù)據(jù)庫(kù)中間件研發(fā)。
編輯:陶家龍
出處:http://kaito-kidd.com/