Redis為什么變慢了?一文講透如何排查Redis性能問(wèn)題 | 萬(wàn)字長(zhǎng)文
掃描二維碼
隨時(shí)隨地手機(jī)看文章
Redis 作為優(yōu)秀的內(nèi)存數(shù)據(jù)庫(kù),其擁有非常高的性能,單個(gè)實(shí)例的 OPS 能夠達(dá)到 10W 左右。但也正因此如此,當(dāng)我們?cè)谑褂?Redis 時(shí),如果發(fā)現(xiàn)操作延遲變大的情況,就會(huì)與我們的預(yù)期不符。
你也許或多或少地,也遇到過(guò)以下這些場(chǎng)景:
-
在 Redis 上執(zhí)行同樣的命令,為什么有時(shí)響應(yīng)很快,有時(shí)卻很慢? -
為什么 Redis 執(zhí)行 SET、DEL 命令耗時(shí)也很久? -
為什么我的 Redis 突然慢了一波,之后又恢復(fù)正常了? -
為什么我的 Redis 穩(wěn)定運(yùn)行了很久,突然從某個(gè)時(shí)間點(diǎn)開(kāi)始變慢了? -
...
如果你并不清楚 Redis 內(nèi)部的實(shí)現(xiàn)原理,那么在排查這種延遲問(wèn)題時(shí)就會(huì)一頭霧水。
如果你也遇到了以上情況,那么,這篇文章將會(huì)給你一個(gè)「全面」的問(wèn)題排查思路,并且針對(duì)這些導(dǎo)致變慢的場(chǎng)景,我還會(huì)給你一個(gè)高效的解決方案。
在正文開(kāi)始之前,我需要提醒你的是,這篇文章很長(zhǎng),涵蓋的 Redis 知識(shí)點(diǎn)也非常廣,全篇文章接近 2W 字,如果此時(shí)你的閱讀環(huán)境不適合專(zhuān)注閱讀,我建議你先收藏此文章,然后在合適的時(shí)間專(zhuān)注閱讀這篇文章。
如果你能耐心且認(rèn)真地讀完這篇文章,我可以保證,你對(duì) Redis 的性能調(diào)優(yōu)將會(huì)有非常大的收獲。
如果你準(zhǔn)備好了,那就跟著我的思路開(kāi)始吧!
Redis真的變慢了嗎?
首先,在開(kāi)始之前,你需要弄清楚 Redis 是否真的變慢了?
如果你發(fā)現(xiàn)你的業(yè)務(wù)服務(wù) API 響應(yīng)延遲變長(zhǎng),首先你需要先排查服務(wù)內(nèi)部,究竟是哪個(gè)環(huán)節(jié)拖慢了整個(gè)服務(wù)。
比較高效的做法是,在服務(wù)內(nèi)部集成鏈路追蹤,也就是在服務(wù)訪問(wèn)外部依賴(lài)的出入口,記錄下每次請(qǐng)求外部依賴(lài)的響應(yīng)延時(shí)。
如果你發(fā)現(xiàn)確實(shí)是操作 Redis 的這條鏈路耗時(shí)變長(zhǎng)了,那么此刻你需要把焦點(diǎn)關(guān)注在業(yè)務(wù)服務(wù)到 Redis 這條鏈路上。
從你的業(yè)務(wù)服務(wù)到 Redis 這條鏈路變慢的原因可能也有 2 個(gè):
-
業(yè)務(wù)服務(wù)器到 Redis 服務(wù)器之間的網(wǎng)絡(luò)存在問(wèn)題,例如網(wǎng)絡(luò)線(xiàn)路質(zhì)量不佳,網(wǎng)絡(luò)數(shù)據(jù)包在傳輸時(shí)存在延遲、丟包等情況 -
Redis 本身存在問(wèn)題,需要進(jìn)一步排查是什么原因?qū)е?Redis 變慢
通常來(lái)說(shuō),第一種情況發(fā)生的概率比較小,如果是服務(wù)器之間網(wǎng)絡(luò)存在問(wèn)題,那部署在這臺(tái)業(yè)務(wù)服務(wù)器上的所有服務(wù)都會(huì)發(fā)生網(wǎng)絡(luò)延遲的情況,此時(shí)你需要聯(lián)系網(wǎng)絡(luò)運(yùn)維同事,讓其協(xié)助解決網(wǎng)絡(luò)問(wèn)題。
我們這篇文章,重點(diǎn)關(guān)注的是第二種情況。
也就是從 Redis 角度來(lái)排查,是否存在導(dǎo)致變慢的場(chǎng)景,以及都有哪些因素會(huì)導(dǎo)致 Redis 的延遲增加,然后針對(duì)性地進(jìn)行優(yōu)化。
排除網(wǎng)絡(luò)原因,如何確認(rèn)你的 Redis 是否真的變慢了?
首先,你需要對(duì) Redis 進(jìn)行基準(zhǔn)性能測(cè)試,了解你的 Redis 在生產(chǎn)環(huán)境服務(wù)器上的基準(zhǔn)性能。
什么是基準(zhǔn)性能?
簡(jiǎn)單來(lái)講,基準(zhǔn)性能就是指 Redis 在一臺(tái)負(fù)載正常的機(jī)器上,其最大的響應(yīng)延遲和平均響應(yīng)延遲分別是怎樣的?
為什么要測(cè)試基準(zhǔn)性能?我參考別人提供的響應(yīng)延遲,判斷自己的 Redis 是否變慢不行嗎?
答案是否定的。
因?yàn)?Redis 在不同的軟硬件環(huán)境下,它的性能是各不相同的。
例如,我的機(jī)器配置比較低,當(dāng)延遲為 2ms 時(shí),我就認(rèn)為 Redis 變慢了,但是如果你的硬件配置比較高,那么在你的運(yùn)行環(huán)境下,可能延遲是 0.5ms 時(shí)就可以認(rèn)為 Redis 變慢了。
所以,你只有了解了你的 Redis 在生產(chǎn)環(huán)境服務(wù)器上的基準(zhǔn)性能,才能進(jìn)一步評(píng)估,當(dāng)其延遲達(dá)到什么程度時(shí),才認(rèn)為 Redis 確實(shí)變慢了。
具體如何做?
為了避免業(yè)務(wù)服務(wù)器到 Redis 服務(wù)器之間的網(wǎng)絡(luò)延遲,你需要直接在 Redis 服務(wù)器上測(cè)試實(shí)例的響應(yīng)延遲情況。執(zhí)行以下命令,就可以測(cè)試出這個(gè)實(shí)例 60 秒內(nèi)的最大響應(yīng)延遲:
$ redis-cli -h 127.0.0.1 -p 6379 --intrinsic-latency 60 Max latency so far: 1 microseconds. Max latency so far: 15 microseconds. Max latency so far: 17 microseconds. Max latency so far: 18 microseconds. Max latency so far: 31 microseconds. Max latency so far: 32 microseconds. Max latency so far: 59 microseconds. Max latency so far: 72 microseconds. 1428669267 total runs (avg latency: 0.0420 microseconds / 42.00 nanoseconds per run). Worst run took 1429x longer than the average latency.
從輸出結(jié)果可以看到,這 60 秒內(nèi)的最大響應(yīng)延遲為 72 微秒(0.072毫秒)。
你還可以使用以下命令,查看一段時(shí)間內(nèi) Redis 的最小、最大、平均訪問(wèn)延遲:
$ redis-cli -h 127.0.0.1 -p 6379 --latency-history -i 1 min: 0, max: 1, avg: 0.13 (100 samples) -- 1.01 seconds range min: 0, max: 1, avg: 0.12 (99 samples) -- 1.01 seconds range min: 0, max: 1, avg: 0.13 (99 samples) -- 1.01 seconds range min: 0, max: 1, avg: 0.10 (99 samples) -- 1.01 seconds range min: 0, max: 1, avg: 0.13 (98 samples) -- 1.00 seconds range min: 0, max: 1, avg: 0.08 (99 samples) -- 1.01 seconds range ...
以上輸出結(jié)果是,每間隔 1 秒,采樣 Redis 的平均操作耗時(shí),其結(jié)果分布在 0.08 ~ 0.13 毫秒之間。
了解了基準(zhǔn)性能測(cè)試方法,那么你就可以按照以下幾步,來(lái)判斷你的 Redis 是否真的變慢了:
-
在相同配置的服務(wù)器上,測(cè)試一個(gè)正常 Redis 實(shí)例的基準(zhǔn)性能 -
找到你認(rèn)為可能變慢的 Redis 實(shí)例,測(cè)試這個(gè)實(shí)例的基準(zhǔn)性能 -
如果你觀察到,這個(gè)實(shí)例的運(yùn)行延遲是正常 Redis 基準(zhǔn)性能的 2 倍以上,即可認(rèn)為這個(gè) Redis 實(shí)例確實(shí)變慢了
確認(rèn)是 Redis 變慢了,那如何排查是哪里發(fā)生了問(wèn)題呢?
下面跟著我的思路,我們從易到難,一步步來(lái)分析可能導(dǎo)致 Redis 變慢的因素。
使用復(fù)雜度過(guò)高的命令
首先,第一步,你需要去查看一下 Redis 的慢日志(slowlog)。
Redis 提供了慢日志命令的統(tǒng)計(jì)功能,它記錄了有哪些命令在執(zhí)行時(shí)耗時(shí)比較久。
查看 Redis 慢日志之前,你需要設(shè)置慢日志的閾值。例如,設(shè)置慢日志的閾值為 5 毫秒,并且保留最近 500 條慢日志記錄:
# 命令執(zhí)行耗時(shí)超過(guò) 5 毫秒,記錄慢日志 CONFIG SET slowlog-log-slower-than 5000 # 只保留最近 500 條慢日志 CONFIG SET slowlog-max-len 500
設(shè)置完成之后,所有執(zhí)行的命令如果操作耗時(shí)超過(guò)了 5 毫秒,都會(huì)被 Redis 記錄下來(lái)。
此時(shí),你可以執(zhí)行以下命令,就可以查詢(xún)到最近記錄的慢日志:
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) "user_info:1000" ...
通過(guò)查看慢日志,我們就可以知道在什么時(shí)間點(diǎn),執(zhí)行了哪些命令比較耗時(shí)。
如果你的應(yīng)用程序執(zhí)行的 Redis 命令有以下特點(diǎn),那么有可能會(huì)導(dǎo)致操作延遲變大:
-
經(jīng)常使用 O(N) 以上復(fù)雜度的命令,例如 SORT、SUNION、ZUNIONSTORE 聚合類(lèi)命令 -
使用 O(N) 復(fù)雜度的命令,但 N 的值非常大
第一種情況導(dǎo)致變慢的原因在于,Redis 在操作內(nèi)存數(shù)據(jù)時(shí),時(shí)間復(fù)雜度過(guò)高,要花費(fèi)更多的 CPU 資源。
第二種情況導(dǎo)致變慢的原因在于,Redis 一次需要返回給客戶(hù)端的數(shù)據(jù)過(guò)多,更多時(shí)間花費(fèi)在數(shù)據(jù)協(xié)議的組裝和網(wǎng)絡(luò)傳輸過(guò)程中。
另外,我們還可以從資源使用率層面來(lái)分析,如果你的應(yīng)用程序操作 Redis 的 OPS 不是很大,但 Redis 實(shí)例的 CPU 使用率卻很高,那么很有可能是使用了復(fù)雜度過(guò)高的命令導(dǎo)致的。
除此之外,我們都知道,Redis 是單線(xiàn)程處理客戶(hù)端請(qǐng)求的,如果你經(jīng)常使用以上命令,那么當(dāng) Redis 處理客戶(hù)端請(qǐng)求時(shí),一旦前面某個(gè)命令發(fā)生耗時(shí),就會(huì)導(dǎo)致后面的請(qǐng)求發(fā)生排隊(duì),對(duì)于客戶(hù)端來(lái)說(shuō),響應(yīng)延遲也會(huì)變長(zhǎng)。
針對(duì)這種情況如何解決呢?
答案很簡(jiǎn)單,你可以使用以下方法優(yōu)化你的業(yè)務(wù):
-
盡量不使用 O(N) 以上復(fù)雜度過(guò)高的命令,對(duì)于數(shù)據(jù)的聚合操作,放在客戶(hù)端做 -
執(zhí)行 O(N) 命令,保證 N 盡量的?。ㄍ扑] N <= 300),每次獲取盡量少的數(shù)據(jù),讓 Redis 可以及時(shí)處理返回
操作bigkey
如果你查詢(xún)慢日志發(fā)現(xiàn),并不是復(fù)雜度過(guò)高的命令導(dǎo)致的,而都是 SET / DEL 這種簡(jiǎn)單命令出現(xiàn)在慢日志中,那么你就要懷疑你的實(shí)例否寫(xiě)入了 bigkey。
Redis 在寫(xiě)入數(shù)據(jù)時(shí),需要為新的數(shù)據(jù)分配內(nèi)存,相對(duì)應(yīng)的,當(dāng)從 Redis 中刪除數(shù)據(jù)時(shí),它會(huì)釋放對(duì)應(yīng)的內(nèi)存空間。
如果一個(gè) key 寫(xiě)入的 value 非常大,那么 Redis 在分配內(nèi)存時(shí)就會(huì)比較耗時(shí)。同樣的,當(dāng)刪除這個(gè) key 時(shí),釋放內(nèi)存也會(huì)比較耗時(shí),這種類(lèi)型的 key 我們一般稱(chēng)之為 bigkey。
此時(shí),你需要檢查你的業(yè)務(wù)代碼,是否存在寫(xiě)入 bigkey 的情況。你需要評(píng)估寫(xiě)入一個(gè) key 的數(shù)據(jù)大小,盡量避免一個(gè) key 存入過(guò)大的數(shù)據(jù)。
如果已經(jīng)寫(xiě)入了 bigkey,那有沒(méi)有什么辦法可以?huà)呙璩鰧?shí)例中 bigkey 的分布情況呢?
答案是可以的。
Redis 提供了掃描 bigkey 的命令,執(zhí)行以下命令就可以?huà)呙璩?,一個(gè)實(shí)例中 bigkey 的分布情況,輸出結(jié)果是以類(lèi)型維度展示的:
$ redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.01 ... -------- summary ------- Sampled 829675 keys in the keyspace! Total key length in bytes is 10059825 (avg len 12.13) Biggest string found 'key:291880' has 10 bytes Biggest list found 'mylist:004' has 40 items Biggest set found 'myset:2386' has 38 members Biggest hash found 'myhash:3574' has 37 fields Biggest zset found 'myzset:2704' has 42 members 36313 strings with 363130 bytes (04.38% of keys, avg size 10.00) 787393 lists with 896540 items (94.90% of keys, avg size 1.14) 1994 sets with 40052 members (00.24% of keys, avg size 20.09) 1990 hashs with 39632 fields (00.24% of keys, avg size 19.92) 1985 zsets with 39750 members (00.24% of keys, avg size 20.03)
從輸出結(jié)果我們可以很清晰地看到,每種數(shù)據(jù)類(lèi)型所占用的最大內(nèi)存 / 擁有最多元素的 key 是哪一個(gè),以及每種數(shù)據(jù)類(lèi)型在整個(gè)實(shí)例中的占比和平均大小 / 元素?cái)?shù)量。
其實(shí),使用這個(gè)命令的原理,就是 Redis 在內(nèi)部執(zhí)行了 SCAN 命令,遍歷整個(gè)實(shí)例中所有的 key,然后針對(duì) key 的類(lèi)型,分別執(zhí)行 STRLEN、LLEN、HLEN、SCARD、ZCARD 命令,來(lái)獲取 String 類(lèi)型的長(zhǎng)度、容器類(lèi)型(List、Hash、Set、ZSet)的元素個(gè)數(shù)。
這里我需要提醒你的是,當(dāng)執(zhí)行這個(gè)命令時(shí),要注意 2 個(gè)問(wèn)題:
-
對(duì)線(xiàn)上實(shí)例進(jìn)行 bigkey 掃描時(shí),Redis 的 OPS 會(huì)突增,為了降低掃描過(guò)程中對(duì) Redis 的影響,最好控制一下掃描的頻率,指定 -i 參數(shù)即可,它表示掃描過(guò)程中每次掃描后休息的時(shí)間間隔,單位是秒 -
掃描結(jié)果中,對(duì)于容器類(lèi)型(List、Hash、Set、ZSet)的 key,只能掃描出元素最多的 key。但一個(gè) key 的元素多,不一定表示占用內(nèi)存也多,你還需要根據(jù)業(yè)務(wù)情況,進(jìn)一步評(píng)估內(nèi)存占用情況
那針對(duì) bigkey 導(dǎo)致延遲的問(wèn)題,有什么好的解決方案呢?
這里有兩點(diǎn)可以?xún)?yōu)化:
-
業(yè)務(wù)應(yīng)用盡量避免寫(xiě)入 bigkey -
如果你使用的 Redis 是 4.0 以上版本,用 UNLINK 命令替代 DEL,此命令可以把釋放 key 內(nèi)存的操作,放到后臺(tái)線(xiàn)程中去執(zhí)行,從而降低對(duì) Redis 的影響 -
如果你使用的 Redis 是 6.0 以上版本,可以開(kāi)啟 lazy-free 機(jī)制(lazyfree-lazy-user-del = yes),在執(zhí)行 DEL 命令時(shí),釋放內(nèi)存也會(huì)放到后臺(tái)線(xiàn)程中執(zhí)行
但即便可以使用方案 2,我也不建議你在實(shí)例中存入 bigkey。
這是因?yàn)?bigkey 在很多場(chǎng)景下,依舊會(huì)產(chǎn)生性能問(wèn)題。例如,bigkey 在分片集群模式下,對(duì)于數(shù)據(jù)的遷移也會(huì)有性能影響,以及我后面即將講到的數(shù)據(jù)過(guò)期、數(shù)據(jù)淘汰、透明大頁(yè),都會(huì)受到 bigkey 的影響。
集中過(guò)期
如果你發(fā)現(xiàn),平時(shí)在操作 Redis 時(shí),并沒(méi)有延遲很大的情況發(fā)生,但在某個(gè)時(shí)間點(diǎn)突然出現(xiàn)一波延時(shí),其現(xiàn)象表現(xiàn)為:變慢的時(shí)間點(diǎn)很有規(guī)律,例如某個(gè)整點(diǎn),或者每間隔多久就會(huì)發(fā)生一波延遲。
如果是出現(xiàn)這種情況,那么你需要排查一下,業(yè)務(wù)代碼中是否存在設(shè)置大量 key 集中過(guò)期的情況。
如果有大量的 key 在某個(gè)固定時(shí)間點(diǎn)集中過(guò)期,在這個(gè)時(shí)間點(diǎn)訪問(wèn) Redis 時(shí),就有可能導(dǎo)致延時(shí)變大。
為什么集中過(guò)期會(huì)導(dǎo)致 Redis 延遲變大?
這就需要我們了解 Redis 的過(guò)期策略是怎樣的。
Redis 的過(guò)期數(shù)據(jù)采用被動(dòng)過(guò)期 + 主動(dòng)過(guò)期兩種策略:
-
被動(dòng)過(guò)期:只有當(dāng)訪問(wèn)某個(gè) key 時(shí),才判斷這個(gè) key 是否已過(guò)期,如果已過(guò)期,則從實(shí)例中刪除 -
主動(dòng)過(guò)期:Redis 內(nèi)部維護(hù)了一個(gè)定時(shí)任務(wù),默認(rèn)每隔 100 毫秒(1秒10次)就會(huì)從全局的過(guò)期哈希表中隨機(jī)取出 20 個(gè) key,然后刪除其中過(guò)期的 key,如果過(guò)期 key 的比例超過(guò)了 25%,則繼續(xù)重復(fù)此過(guò)程,直到過(guò)期 key 的比例下降到 25% 以下,或者這次任務(wù)的執(zhí)行耗時(shí)超過(guò)了 25 毫秒,才會(huì)退出循環(huán)
注意,這個(gè)主動(dòng)過(guò)期 key 的定時(shí)任務(wù),是在 Redis 主線(xiàn)程中執(zhí)行的。
也就是說(shuō)如果在執(zhí)行主動(dòng)過(guò)期的過(guò)程中,出現(xiàn)了需要大量刪除過(guò)期 key 的情況,那么此時(shí)應(yīng)用程序在訪問(wèn) Redis 時(shí),必須要等待這個(gè)過(guò)期任務(wù)執(zhí)行結(jié)束,Redis 才可以服務(wù)這個(gè)客戶(hù)端請(qǐng)求。
此時(shí)就會(huì)出現(xiàn),應(yīng)用訪問(wèn) Redis 延時(shí)變大。
如果此時(shí)需要過(guò)期刪除的是一個(gè) bigkey,那么這個(gè)耗時(shí)會(huì)更久。而且,這個(gè)操作延遲的命令并不會(huì)記錄在慢日志中。
因?yàn)槁罩局?/span>只記錄一個(gè)命令真正操作內(nèi)存數(shù)據(jù)的耗時(shí),而 Redis 主動(dòng)刪除過(guò)期 key 的邏輯,是在命令真正執(zhí)行之前執(zhí)行的。
所以,此時(shí)你會(huì)看到,慢日志中沒(méi)有操作耗時(shí)的命令,但我們的應(yīng)用程序卻感知到了延遲變大,其實(shí)時(shí)間都花費(fèi)在了刪除過(guò)期 key 上,這種情況我們需要尤為注意。
那遇到這種情況,如何分析和排查?
此時(shí),你需要檢查你的業(yè)務(wù)代碼,是否存在集中過(guò)期 key 的邏輯。
一般集中過(guò)期使用的是 expireat / pexpireat 命令,你需要在代碼中搜索這個(gè)關(guān)鍵字。
排查代碼后,如果確實(shí)存在集中過(guò)期 key 的邏輯存在,但這種邏輯又是業(yè)務(wù)所必須的,那此時(shí)如何優(yōu)化,同時(shí)又不對(duì) Redis 有性能影響呢?
一般有兩種方案來(lái)規(guī)避這個(gè)問(wèn)題:
-
集中過(guò)期 key 增加一個(gè)隨機(jī)過(guò)期時(shí)間,把集中過(guò)期的時(shí)間打散,降低 Redis 清理過(guò)期 key 的壓力 -
如果你使用的 Redis 是 4.0 以上版本,可以開(kāi)啟 lazy-free 機(jī)制,當(dāng)刪除過(guò)期 key 時(shí),把釋放內(nèi)存的操作放到后臺(tái)線(xiàn)程中執(zhí)行,避免阻塞主線(xiàn)程
第一種方案,在設(shè)置 key 的過(guò)期時(shí)間時(shí),增加一個(gè)隨機(jī)時(shí)間,偽代碼可以這么寫(xiě):
# 在過(guò)期時(shí)間點(diǎn)之后的 5 分鐘內(nèi)隨機(jī)過(guò)期掉 redis.expireat(key, expire_time + random(300))
這樣一來(lái),Redis 在處理過(guò)期時(shí),不會(huì)因?yàn)榧袆h除過(guò)多的 key 導(dǎo)致壓力過(guò)大,從而避免阻塞主線(xiàn)程。
第二種方案,Redis 4.0 以上版本,開(kāi)啟 lazy-free 機(jī)制:
# 釋放過(guò)期 key 的內(nèi)存,放到后臺(tái)線(xiàn)程執(zhí)行 lazyfree-lazy-expire yes
另外,除了業(yè)務(wù)層面的優(yōu)化和修改配置之外,你還可以通過(guò)運(yùn)維手段及時(shí)發(fā)現(xiàn)這種情況。
運(yùn)維層面,你需要把 Redis 的各項(xiàng)運(yùn)行狀態(tài)數(shù)據(jù)監(jiān)控起來(lái),在 Redis 上執(zhí)行 INFO 命令就可以拿到這個(gè)實(shí)例所有的運(yùn)行狀態(tài)數(shù)據(jù)。
在這里我們需要重點(diǎn)關(guān)注 expired_keys 這一項(xiàng),它代表整個(gè)實(shí)例到目前為止,累計(jì)刪除過(guò)期 key 的數(shù)量。
你需要把這個(gè)指標(biāo)監(jiān)控起來(lái),當(dāng)這個(gè)指標(biāo)在很短時(shí)間內(nèi)出現(xiàn)了突增,需要及時(shí)報(bào)警出來(lái),然后與業(yè)務(wù)應(yīng)用報(bào)慢的時(shí)間點(diǎn)進(jìn)行對(duì)比分析,確認(rèn)時(shí)間是否一致,如果一致,則可以確認(rèn)確實(shí)是因?yàn)榧羞^(guò)期 key 導(dǎo)致的延遲變大。
實(shí)例內(nèi)存達(dá)到上限
如果你的 Redis 實(shí)例設(shè)置了內(nèi)存上限 maxmemory,那么也有可能導(dǎo)致 Redis 變慢。
當(dāng)我們把 Redis 當(dāng)做純緩存使用時(shí),通常會(huì)給這個(gè)實(shí)例設(shè)置一個(gè)內(nèi)存上限 maxmemory,然后設(shè)置一個(gè)數(shù)據(jù)淘汰策略。
而當(dāng)實(shí)例的內(nèi)存達(dá)到了 maxmemory 后,你可能會(huì)發(fā)現(xiàn),在此之后每次寫(xiě)入新數(shù)據(jù),操作延遲變大了。
這是為什么?
原因在于,當(dāng) Redis 內(nèi)存達(dá)到 maxmemory 后,每次寫(xiě)入新的數(shù)據(jù)之前,Redis 必須先從實(shí)例中踢出一部分?jǐn)?shù)據(jù),讓整個(gè)實(shí)例的內(nèi)存維持在 maxmemory 之下,然后才能把新數(shù)據(jù)寫(xiě)進(jìn)來(lái)。
這個(gè)踢出舊數(shù)據(jù)的邏輯也是需要消耗時(shí)間的,而具體耗時(shí)的長(zhǎng)短,要取決于你配置的淘汰策略:
-
allkeys-lru:不管 key 是否設(shè)置了過(guò)期,淘汰最近最少訪問(wèn)的 key -
volatile-lru:只淘汰最近最少訪問(wèn)、并設(shè)置了過(guò)期時(shí)間的 key -
allkeys-random:不管 key 是否設(shè)置了過(guò)期,隨機(jī)淘汰 key -
volatile-random:只隨機(jī)淘汰設(shè)置了過(guò)期時(shí)間的 key -
allkeys-ttl:不管 key 是否設(shè)置了過(guò)期,淘汰即將過(guò)期的 key -
noeviction:不淘汰任何 key,實(shí)例內(nèi)存達(dá)到 maxmeory 后,再寫(xiě)入新數(shù)據(jù)直接返回錯(cuò)誤 -
allkeys-lfu:不管 key 是否設(shè)置了過(guò)期,淘汰訪問(wèn)頻率最低的 key(4.0+版本支持) -
volatile-lfu:只淘汰訪問(wèn)頻率最低、并設(shè)置了過(guò)期時(shí)間 key(4.0+版本支持)
具體使用哪種策略,我們需要根據(jù)具體的業(yè)務(wù)場(chǎng)景來(lái)配置。
一般最常使用的是 allkeys-lru / volatile-lru 淘汰策略,它們的處理邏輯是,每次從實(shí)例中隨機(jī)取出一批 key(這個(gè)數(shù)量可配置),然后淘汰一個(gè)最少訪問(wèn)的 key,之后把剩下的 key 暫存到一個(gè)池子中,繼續(xù)隨機(jī)取一批 key,并與之前池子中的 key 比較,再淘汰一個(gè)最少訪問(wèn)的 key。以此往復(fù),直到實(shí)例內(nèi)存降到 maxmemory 之下。
需要注意的是,Redis 的淘汰數(shù)據(jù)的邏輯與刪除過(guò)期 key 的一樣,也是在命令真正執(zhí)行之前執(zhí)行的,也就是說(shuō)它也會(huì)增加我們操作 Redis 的延遲,而且,寫(xiě) OPS 越高,延遲也會(huì)越明顯。
另外,如果此時(shí)你的 Redis 實(shí)例中還存儲(chǔ)了 bigkey,那么在淘汰刪除 bigkey 釋放內(nèi)存時(shí),也會(huì)耗時(shí)比較久。
看到了么?bigkey 的危害到處都是,這也是前面我提醒你盡量不存儲(chǔ) bigkey 的原因。
針對(duì)這種情況,如何解決呢?
我給你 4 個(gè)方面的優(yōu)化建議:
-
避免存儲(chǔ) bigkey,降低釋放內(nèi)存的耗時(shí) -
淘汰策略改為隨機(jī)淘汰,隨機(jī)淘汰比 LRU 要快很多(視業(yè)務(wù)情況調(diào)整) -
拆分實(shí)例,把淘汰 key 的壓力分?jǐn)偟蕉鄠€(gè)實(shí)例上 -
如果使用的是 Redis 4.0 以上版本,開(kāi)啟 layz-free 機(jī)制,把淘汰 key 釋放內(nèi)存的操作放到后臺(tái)線(xiàn)程中執(zhí)行(配置 lazyfree-lazy-eviction = yes)
fork耗時(shí)嚴(yán)重
為了保證 Redis 數(shù)據(jù)的安全性,我們可能會(huì)開(kāi)啟后臺(tái)定時(shí) RDB 和 AOF rewrite 功能。
但如果你發(fā)現(xiàn),操作 Redis 延遲變大,都發(fā)生在 Redis 后臺(tái) RDB 和 AOF rewrite 期間,那你就需要排查,在這期間有可能導(dǎo)致變慢的情況。
當(dāng) Redis 開(kāi)啟了后臺(tái) RDB 和 AOF rewrite 后,在執(zhí)行時(shí),它們都需要主進(jìn)程創(chuàng)建出一個(gè)子進(jìn)程進(jìn)行數(shù)據(jù)的持久化。
主進(jìn)程創(chuàng)建子進(jìn)程,會(huì)調(diào)用操作系統(tǒng)提供的 fork 函數(shù)。
而 fork 在執(zhí)行過(guò)程中,主進(jìn)程需要拷貝自己的內(nèi)存頁(yè)表給子進(jìn)程,如果這個(gè)實(shí)例很大,那么這個(gè)拷貝的過(guò)程也會(huì)比較耗時(shí)。
而且這個(gè) fork 過(guò)程會(huì)消耗大量的 CPU 資源,在完成 fork 之前,整個(gè) Redis 實(shí)例會(huì)被阻塞住,無(wú)法處理任何客戶(hù)端請(qǐng)求。
如果此時(shí)你的 CPU 資源本來(lái)就很緊張,那么 fork 的耗時(shí)會(huì)更長(zhǎng),甚至達(dá)到秒級(jí),這會(huì)嚴(yán)重影響 Redis 的性能。
那如何確認(rèn)確實(shí)是因?yàn)?fork 耗時(shí)導(dǎo)致的 Redis 延遲變大呢?
你可以在 Redis 上執(zhí)行 INFO 命令,查看 latest_fork_usec 項(xiàng),單位微秒。
# 上一次 fork 耗時(shí),單位微秒 latest_fork_usec:59477
這個(gè)時(shí)間就是主進(jìn)程在 fork 子進(jìn)程期間,整個(gè)實(shí)例阻塞無(wú)法處理客戶(hù)端請(qǐng)求的時(shí)間。
如果你發(fā)現(xiàn)這個(gè)耗時(shí)很久,就要警惕起來(lái)了,這意味在這期間,你的整個(gè) Redis 實(shí)例都處于不可用的狀態(tài)。
除了數(shù)據(jù)持久化會(huì)生成 RDB 之外,當(dāng)主從節(jié)點(diǎn)第一次建立數(shù)據(jù)同步時(shí),主節(jié)點(diǎn)也創(chuàng)建子進(jìn)程生成 RDB,然后發(fā)給從節(jié)點(diǎn)進(jìn)行一次全量同步,所以,這個(gè)過(guò)程也會(huì)對(duì) Redis 產(chǎn)生性能影響。
要想避免這種情況,你可以采取以下方案進(jìn)行優(yōu)化:
-
控制 Redis 實(shí)例的內(nèi)存:盡量在 10G 以下,執(zhí)行 fork 的耗時(shí)與實(shí)例大小有關(guān),實(shí)例越大,耗時(shí)越久 -
合理配置數(shù)據(jù)持久化策略:在 slave 節(jié)點(diǎn)執(zhí)行 RDB 備份,推薦在低峰期執(zhí)行,而對(duì)于丟失數(shù)據(jù)不敏感的業(yè)務(wù)(例如把 Redis 當(dāng)做純緩存使用),可以關(guān)閉 AOF 和 AOF rewrite -
Redis 實(shí)例不要部署在虛擬機(jī)上:fork 的耗時(shí)也與系統(tǒng)也有關(guān),虛擬機(jī)比物理機(jī)耗時(shí)更久 -
降低主從庫(kù)全量同步的概率:適當(dāng)調(diào)大 repl-backlog-size 參數(shù),避免主從全量同步
開(kāi)啟內(nèi)存大頁(yè)
除了上面講到的子進(jìn)程 RDB 和 AOF rewrite 期間,fork 耗時(shí)導(dǎo)致的延時(shí)變大之外,這里還有一個(gè)方面也會(huì)導(dǎo)致性能問(wèn)題,這就是操作系統(tǒng)是否開(kāi)啟了內(nèi)存大頁(yè)機(jī)制。
什么是內(nèi)存大頁(yè)?
我們都知道,應(yīng)用程序向操作系統(tǒng)申請(qǐng)內(nèi)存時(shí),是按內(nèi)存頁(yè)進(jìn)行申請(qǐng)的,而常規(guī)的內(nèi)存頁(yè)大小是 4KB。
Linux 內(nèi)核從 2.6.38 開(kāi)始,支持了內(nèi)存大頁(yè)機(jī)制,該機(jī)制允許應(yīng)用程序以 2MB 大小為單位,向操作系統(tǒng)申請(qǐng)內(nèi)存。
應(yīng)用程序每次向操作系統(tǒng)申請(qǐng)的內(nèi)存單位變大了,但這也意味著申請(qǐng)內(nèi)存的耗時(shí)變長(zhǎng)。
這對(duì) Redis 會(huì)有什么影響呢?
當(dāng) Redis 在執(zhí)行后臺(tái) RDB 和 AOF rewrite 時(shí),采用 fork 子進(jìn)程的方式來(lái)處理。但主進(jìn)程 fork 子進(jìn)程后,此時(shí)的主進(jìn)程依舊是可以接收寫(xiě)請(qǐng)求的,而進(jìn)來(lái)的寫(xiě)請(qǐng)求,會(huì)采用 Copy On Write(寫(xiě)時(shí)復(fù)制)的方式操作內(nèi)存數(shù)據(jù)。
也就是說(shuō),主進(jìn)程一旦有數(shù)據(jù)需要修改,Redis 并不會(huì)直接修改現(xiàn)有內(nèi)存中的數(shù)據(jù),而是先將這塊內(nèi)存數(shù)據(jù)拷貝出來(lái),再修改這塊新內(nèi)存的數(shù)據(jù),這就是所謂的「寫(xiě)時(shí)復(fù)制」。
寫(xiě)時(shí)復(fù)制你也可以理解成,誰(shuí)需要發(fā)生寫(xiě)操作,誰(shuí)就需要先拷貝,再修改。
這樣做的好處是,父進(jìn)程有任何寫(xiě)操作,并不會(huì)影響子進(jìn)程的數(shù)據(jù)持久化(子進(jìn)程只持久化 fork 這一瞬間整個(gè)實(shí)例中的所有數(shù)據(jù)即可,不關(guān)心新的數(shù)據(jù)變更,因?yàn)樽舆M(jìn)程只需要一份內(nèi)存快照,然后持久化到磁盤(pán)上)。
但是請(qǐng)注意,主進(jìn)程在拷貝內(nèi)存數(shù)據(jù)時(shí),這個(gè)階段就涉及到新內(nèi)存的申請(qǐng),如果此時(shí)操作系統(tǒng)開(kāi)啟了內(nèi)存大頁(yè),那么在此期間,客戶(hù)端即便只修改 10B 的數(shù)據(jù),Redis 在申請(qǐng)內(nèi)存時(shí)也會(huì)以 2MB 為單位向操作系統(tǒng)申請(qǐng),申請(qǐng)內(nèi)存的耗時(shí)變長(zhǎng),進(jìn)而導(dǎo)致每個(gè)寫(xiě)請(qǐng)求的延遲增加,影響到 Redis 性能。
同樣地,如果這個(gè)寫(xiě)請(qǐng)求操作的是一個(gè) bigkey,那主進(jìn)程在拷貝這個(gè) bigkey 內(nèi)存塊時(shí),一次申請(qǐng)的內(nèi)存會(huì)更大,時(shí)間也會(huì)更久??梢?jiàn),bigkey 在這里又一次影響到了性能。
那如何解決這個(gè)問(wèn)題?
很簡(jiǎn)單,你只需要關(guān)閉內(nèi)存大頁(yè)機(jī)制就可以了。
首先,你需要查看 Redis 機(jī)器是否開(kāi)啟了內(nèi)存大頁(yè):
$ cat /sys/kernel/mm/transparent_hugepage/enabled [always] madvise never
如果輸出選項(xiàng)是 always,就表示目前開(kāi)啟了內(nèi)存大頁(yè)機(jī)制,我們需要關(guān)掉它:
$ echo never > /sys/kernel/mm/transparent_hugepage/enabled
其實(shí),操作系統(tǒng)提供的內(nèi)存大頁(yè)機(jī)制,其優(yōu)勢(shì)是,可以在一定程序上降低應(yīng)用程序申請(qǐng)內(nèi)存的次數(shù)。
但是對(duì)于 Redis 這種對(duì)性能和延遲極其敏感的數(shù)據(jù)庫(kù)來(lái)說(shuō),我們希望 Redis 在每次申請(qǐng)內(nèi)存時(shí),耗時(shí)盡量短,所以我不建議你在 Redis 機(jī)器上開(kāi)啟這個(gè)機(jī)制。
開(kāi)啟AOF
前面我們分析了 RDB 和 AOF rewrite 對(duì) Redis 性能的影響,主要關(guān)注點(diǎn)在 fork 上。
其實(shí),關(guān)于數(shù)據(jù)持久化方面,還有影響 Redis 性能的因素,這次我們重點(diǎn)來(lái)看 AOF 數(shù)據(jù)持久化。
如果你的 AOF 配置不合理,還是有可能會(huì)導(dǎo)致性能問(wèn)題。
當(dāng) Redis 開(kāi)啟 AOF 后,其工作原理如下:
-
Redis 執(zhí)行寫(xiě)命令后,把這個(gè)命令寫(xiě)入到 AOF 文件內(nèi)存中(write 系統(tǒng)調(diào)用) -
Redis 根據(jù)配置的 AOF 刷盤(pán)策略,把 AOF 內(nèi)存數(shù)據(jù)刷到磁盤(pán)上(fsync 系統(tǒng)調(diào)用)
為了保證 AOF 文件數(shù)據(jù)的安全性,Redis 提供了 3 種刷盤(pán)機(jī)制:
-
appendfsync always:主線(xiàn)程每次執(zhí)行寫(xiě)操作后立即刷盤(pán),此方案會(huì)占用比較大的磁盤(pán) IO 資源,但數(shù)據(jù)安全性最高 -
appendfsync no:主線(xiàn)程每次寫(xiě)操作只寫(xiě)內(nèi)存就返回,內(nèi)存數(shù)據(jù)什么時(shí)候刷到磁盤(pán),交由操作系統(tǒng)決定,此方案對(duì)性能影響最小,但數(shù)據(jù)安全性也最低,Redis 宕機(jī)時(shí)丟失的數(shù)據(jù)取決于操作系統(tǒng)刷盤(pán)時(shí)機(jī) -
appendfsync everysec:主線(xiàn)程每次寫(xiě)操作只寫(xiě)內(nèi)存就返回,然后由后臺(tái)線(xiàn)程每隔 1 秒執(zhí)行一次刷盤(pán)操作(觸發(fā)fsync系統(tǒng)調(diào)用),此方案對(duì)性能影響相對(duì)較小,但當(dāng) Redis 宕機(jī)時(shí)會(huì)丟失 1 秒的數(shù)據(jù)
下面我們依次來(lái)分析,這幾個(gè)機(jī)制對(duì)性能的影響。
如果你的 AOF 配置為 appendfsync always,那么 Redis 每處理一次寫(xiě)操作,都會(huì)把這個(gè)命令寫(xiě)入到磁盤(pán)中才返回,整個(gè)過(guò)程都是在主線(xiàn)程執(zhí)行的,這個(gè)過(guò)程必然會(huì)加重 Redis 寫(xiě)負(fù)擔(dān)。
原因也很簡(jiǎn)單,操作磁盤(pán)要比操作內(nèi)存慢幾百倍,采用這個(gè)配置會(huì)嚴(yán)重拖慢 Redis 的性能,因此我不建議你把 AOF 刷盤(pán)方式配置為 always。
我們接著來(lái)看 appendfsync no 配置項(xiàng)。
在這種配置下,Redis 每次寫(xiě)操作只寫(xiě)內(nèi)存,什么時(shí)候把內(nèi)存中的數(shù)據(jù)刷到磁盤(pán),交給操作系統(tǒng)決定,此方案對(duì) Redis 的性能影響最小,但當(dāng) Redis 宕機(jī)時(shí),會(huì)丟失一部分?jǐn)?shù)據(jù),為了數(shù)據(jù)的安全性,一般我們也不采取這種配置。
如果你的 Redis 只用作純緩存,對(duì)于數(shù)據(jù)丟失不敏感,采用配置 appendfsync no 也是可以的。
看到這里,我猜你肯定和大多數(shù)人的想法一樣,選比較折中的方案 appendfsync everysec 就沒(méi)問(wèn)題了吧?
這個(gè)方案優(yōu)勢(shì)在于,Redis 主線(xiàn)程寫(xiě)完內(nèi)存后就返回,具體的刷盤(pán)操作是放到后臺(tái)線(xiàn)程中執(zhí)行的,后臺(tái)線(xiàn)程每隔 1 秒把內(nèi)存中的數(shù)據(jù)刷到磁盤(pán)中。
這種方案既兼顧了性能,又盡可能地保證了數(shù)據(jù)安全,是不是覺(jué)得很完美?
但是,這里我要給你潑一盆冷水了,采用這種方案你也要警惕一下,因?yàn)檫@種方案還是存在導(dǎo)致 Redis 延遲變大的情況發(fā)生,甚至?xí)枞麄€(gè) Redis。
這是為什么?我把 AOF 最耗時(shí)的刷盤(pán)操作,放到后臺(tái)線(xiàn)程中也會(huì)影響到 Redis 主線(xiàn)程?
你試想這樣一種情況:當(dāng) Redis 后臺(tái)線(xiàn)程在執(zhí)行 AOF 文件刷盤(pán)時(shí),如果此時(shí)磁盤(pán)的 IO 負(fù)載很高,那這個(gè)后臺(tái)線(xiàn)程在執(zhí)行刷盤(pán)操作(fsync系統(tǒng)調(diào)用)時(shí)就會(huì)被阻塞住。
此時(shí)的主線(xiàn)程依舊會(huì)接收寫(xiě)請(qǐng)求,緊接著,主線(xiàn)程又需要把數(shù)據(jù)寫(xiě)到文件內(nèi)存中(write 系統(tǒng)調(diào)用),但此時(shí)的后臺(tái)子線(xiàn)程由于磁盤(pán)負(fù)載過(guò)高,導(dǎo)致 fsync 發(fā)生阻塞,遲遲不能返回,那主線(xiàn)程在執(zhí)行 write 系統(tǒng)調(diào)用時(shí),也會(huì)被阻塞住,直到后臺(tái)線(xiàn)程 fsync 執(zhí)行完成后,主線(xiàn)程執(zhí)行 write 才能成功返回。
看到了么?在這個(gè)過(guò)程中,主線(xiàn)程依舊有阻塞的風(fēng)險(xiǎn)。
所以,盡管你的 AOF 配置為 appendfsync everysec,也不能掉以輕心,要警惕磁盤(pán)壓力過(guò)大導(dǎo)致的 Redis 有性能問(wèn)題。
那什么情況下會(huì)導(dǎo)致磁盤(pán) IO 負(fù)載過(guò)大?以及如何解決這個(gè)問(wèn)題呢?
我總結(jié)了以下幾種情況,你可以參考進(jìn)行問(wèn)題排查:
-
子進(jìn)程正在執(zhí)行 AOF rewrite,這個(gè)過(guò)程會(huì)占用大量的磁盤(pán) IO 資源 -
有其他應(yīng)用程序在執(zhí)行大量的寫(xiě)文件操作,也會(huì)占用磁盤(pán) IO 資源
對(duì)于情況1,說(shuō)白了就是,Redis 的 AOF 后臺(tái)子線(xiàn)程刷盤(pán)操作,撞上了子進(jìn)程 AOF rewrite!
這怎么辦?難道要關(guān)閉 AOF rewrite 才行?
幸運(yùn)的是,Redis 提供了一個(gè)配置項(xiàng),當(dāng)子進(jìn)程在 AOF rewrite 期間,可以讓后臺(tái)子線(xiàn)程不執(zhí)行刷盤(pán)(不觸發(fā) fsync 系統(tǒng)調(diào)用)操作。
這相當(dāng)于在 AOF rewrite 期間,臨時(shí)把 appendfsync 設(shè)置為了 none,配置如下:
# AOF rewrite 期間,AOF 后臺(tái)子線(xiàn)程不進(jìn)行刷盤(pán)操作 # 相當(dāng)于在這期間,臨時(shí)把 appendfsync 設(shè)置為了 none no-appendfsync-on-rewrite yes
當(dāng)然,開(kāi)啟這個(gè)配置項(xiàng),在 AOF rewrite 期間,如果實(shí)例發(fā)生宕機(jī),那么此時(shí)會(huì)丟失更多的數(shù)據(jù),性能和數(shù)據(jù)安全性,你需要權(quán)衡后進(jìn)行選擇。
如果占用磁盤(pán)資源的是其他應(yīng)用程序,那就比較簡(jiǎn)單了,你需要定位到是哪個(gè)應(yīng)用程序在大量寫(xiě)磁盤(pán),然后把這個(gè)應(yīng)用程序遷移到其他機(jī)器上執(zhí)行就好了,避免對(duì) Redis 產(chǎn)生影響。
當(dāng)然,如果你對(duì) Redis 的性能和數(shù)據(jù)安全都有很高的要求,那么我建議從硬件層面來(lái)優(yōu)化,更換為 SSD 磁盤(pán),提高磁盤(pán)的 IO 能力,保證 AOF 期間有充足的磁盤(pán)資源可以使用。
綁定CPU
很多時(shí)候,我們?cè)诓渴鸱?wù)時(shí),為了提高服務(wù)性能,降低應(yīng)用程序在多個(gè) CPU 核心之間的上下文切換帶來(lái)的性能損耗,通常采用的方案是進(jìn)程綁定 CPU 的方式提高性能。
但在部署 Redis 時(shí),如果你需要綁定 CPU 來(lái)提高其性能,我建議你仔細(xì)斟酌后再做操作。
為什么?
因?yàn)?Redis 在綁定 CPU 時(shí),是有很多考究的,如果你不了解 Redis 的運(yùn)行原理,隨意綁定 CPU 不僅不會(huì)提高性能,甚至有可能會(huì)帶來(lái)相反的效果。
我們都知道,一般現(xiàn)代的服務(wù)器會(huì)有多個(gè) CPU,而每個(gè) CPU 又包含多個(gè)物理核心,每個(gè)物理核心又分為多個(gè)邏輯核心,每個(gè)物理核下的邏輯核共用 L1/L2 Cache。
而 Redis Server 除了主線(xiàn)程服務(wù)客戶(hù)端請(qǐng)求之外,還會(huì)創(chuàng)建子進(jìn)程、子線(xiàn)程。
其中子進(jìn)程用于數(shù)據(jù)持久化,而子線(xiàn)程用于執(zhí)行一些比較耗時(shí)操作,例如異步釋放 fd、異步 AOF 刷盤(pán)、異步 lazy-free 等等。
如果你把 Redis 進(jìn)程只綁定了一個(gè) CPU 邏輯核心上,那么當(dāng) Redis 在進(jìn)行數(shù)據(jù)持久化時(shí),fork 出的子進(jìn)程會(huì)繼承父進(jìn)程的 CPU 使用偏好。
而此時(shí)的子進(jìn)程會(huì)消耗大量的 CPU 資源進(jìn)行數(shù)據(jù)持久化(把實(shí)例數(shù)據(jù)全部掃描出來(lái)需要耗費(fèi)CPU),這就會(huì)導(dǎo)致子進(jìn)程會(huì)與主進(jìn)程發(fā)生 CPU 爭(zhēng)搶?zhuān)M(jìn)而影響到主進(jìn)程服務(wù)客戶(hù)端請(qǐng)求,訪問(wèn)延遲變大。
這就是 Redis 綁定 CPU 帶來(lái)的性能問(wèn)題。
那如何解決這個(gè)問(wèn)題呢?
如果你確實(shí)想要綁定 CPU,可以?xún)?yōu)化的方案是,不要讓 Redis 進(jìn)程只綁定在一個(gè) CPU 邏輯核上,而是綁定在多個(gè)邏輯核心上,而且,綁定的多個(gè)邏輯核心最好是同一個(gè)物理核心,這樣它們還可以共用 L1/L2 Cache。
當(dāng)然,即便我們把 Redis 綁定在多個(gè)邏輯核心上,也只能在一定程度上緩解主線(xiàn)程、子進(jìn)程、后臺(tái)線(xiàn)程在 CPU 資源上的競(jìng)爭(zhēng)。
因?yàn)檫@些子進(jìn)程、子線(xiàn)程還是會(huì)在這多個(gè)邏輯核心上進(jìn)行切換,存在性能損耗。
如何再進(jìn)一步優(yōu)化?
可能你已經(jīng)想到了,我們是否可以讓主線(xiàn)程、子進(jìn)程、后臺(tái)線(xiàn)程,分別綁定在固定的 CPU 核心上,不讓它們來(lái)回切換,這樣一來(lái),他們各自使用的 CPU 資源互不影響。
其實(shí),這個(gè)方案 Redis 官方已經(jīng)想到了。
Redis 在 6.0 版本已經(jīng)推出了這個(gè)功能,我們可以通過(guò)以下配置,對(duì)主線(xiàn)程、后臺(tái)線(xiàn)程、后臺(tái) RDB 進(jìn)程、AOF rewrite 進(jìn)程,綁定固定的 CPU 邏輯核心:
# Redis Server 和 IO 線(xiàn)程綁定到 CPU核心 0,2,4,6 server_cpulist 0-7:2 # 后臺(tái)子線(xiàn)程綁定到 CPU核心 1,3 bio_cpulist 1,3 # 后臺(tái) AOF rewrite 進(jìn)程綁定到 CPU 核心 8,9,10,11 aof_rewrite_cpulist 8-11 # 后臺(tái) RDB 進(jìn)程綁定到 CPU 核心 1,10,11 # bgsave_cpulist 1,10-1
如果你使用的正好是 Redis 6.0 版本,就可以通過(guò)以上配置,來(lái)進(jìn)一步提高 Redis 性能。
這里我需要提醒你的是,一般來(lái)說(shuō),Redis 的性能已經(jīng)足夠優(yōu)秀,除非你對(duì) Redis 的性能有更加嚴(yán)苛的要求,否則不建議你綁定 CPU。
從上面的分析你也能看出,綁定 CPU 需要你對(duì)計(jì)算機(jī)體系結(jié)構(gòu)有非常清晰的了解,否則謹(jǐn)慎操作。
我們繼續(xù)分析還有什么場(chǎng)景會(huì)導(dǎo)致 Redis 變慢。
使用Swap
如果你發(fā)現(xiàn) Redis 突然變得非常慢,每次的操作耗時(shí)都達(dá)到了幾百毫秒甚至秒級(jí),那此時(shí)你就需要檢查 Redis 是否使用到了 Swap,在這種情況下 Redis 基本上已經(jīng)無(wú)法提供高性能的服務(wù)了。
什么是 Swap?為什么使用 Swap 會(huì)導(dǎo)致 Redis 的性能下降?
如果你對(duì)操作系統(tǒng)有些了解,就會(huì)知道操作系統(tǒng)為了緩解內(nèi)存不足對(duì)應(yīng)用程序的影響,允許把一部分內(nèi)存中的數(shù)據(jù)換到磁盤(pán)上,以達(dá)到應(yīng)用程序?qū)?nèi)存使用的緩沖,這些內(nèi)存數(shù)據(jù)被換到磁盤(pán)上的區(qū)域,就是 Swap。
問(wèn)題就在于,當(dāng)內(nèi)存中的數(shù)據(jù)被換到磁盤(pán)上后,Redis 再訪問(wèn)這些數(shù)據(jù)時(shí),就需要從磁盤(pán)上讀取,訪問(wèn)磁盤(pán)的速度要比訪問(wèn)內(nèi)存慢幾百倍!
尤其是針對(duì) Redis 這種對(duì)性能要求極高、性能極其敏感的數(shù)據(jù)庫(kù)來(lái)說(shuō),這個(gè)操作延時(shí)是無(wú)法接受的。
此時(shí),你需要檢查 Redis 機(jī)器的內(nèi)存使用情況,確認(rèn)是否存在使用了 Swap。
你可以通過(guò)以下方式來(lái)查看 Redis 進(jìn)程是否使用到了 Swap:
# 先找到 Redis 的進(jìn)程 ID $ ps -aux | grep redis-server # 查看 Redis Swap 使用情況 $ cat /proc/$pid/smaps | egrep '^(Swap|Size)'
輸出結(jié)果如下:
Size: 1256 kB Swap: 0 kB Size: 4 kB Swap: 0 kB Size: 132 kB Swap: 0 kB Size: 63488 kB Swap: 0 kB Size: 132 kB Swap: 0 kB Size: 65404 kB Swap: 0 kB Size: 1921024 kB Swap: 0 kB ...
這個(gè)結(jié)果會(huì)列出 Redis 進(jìn)程的內(nèi)存使用情況。
每一行 Size 表示 Redis 所用的一塊內(nèi)存大小,Size 下面的 Swap 就表示這塊 Size 大小的內(nèi)存,有多少數(shù)據(jù)已經(jīng)被換到磁盤(pán)上了,如果這兩個(gè)值相等,說(shuō)明這塊內(nèi)存的數(shù)據(jù)都已經(jīng)完全被換到磁盤(pán)上了。
如果只是少量數(shù)據(jù)被換到磁盤(pán)上,例如每一塊 Swap 占對(duì)應(yīng) Size 的比例很小,那影響并不是很大。如果是幾百兆甚至上 GB 的內(nèi)存被換到了磁盤(pán)上,那么你就需要警惕了,這種情況 Redis 的性能肯定會(huì)急劇下降。
此時(shí)的解決方案是:
-
增加機(jī)器的內(nèi)存,讓 Redis 有足夠的內(nèi)存可以使用 -
整理內(nèi)存空間,釋放出足夠的內(nèi)存供 Redis 使用,然后釋放 Redis 的 Swap,讓 Redis 重新使用內(nèi)存
釋放 Redis 的 Swap 過(guò)程通常要重啟實(shí)例,為了避免重啟實(shí)例對(duì)業(yè)務(wù)的影響,一般會(huì)先進(jìn)行主從切換,然后釋放舊主節(jié)點(diǎn)的 Swap,重啟舊主節(jié)點(diǎn)實(shí)例,待從庫(kù)數(shù)據(jù)同步完成后,再進(jìn)行主從切換即可。
可見(jiàn),當(dāng) Redis 使用到 Swap 后,此時(shí)的 Redis 性能基本已達(dá)不到高性能的要求(你可以理解為武功被廢),所以你也需要提前預(yù)防這種情況。
預(yù)防的辦法就是,你需要對(duì) Redis 機(jī)器的內(nèi)存和 Swap 使用情況進(jìn)行監(jiān)控,在內(nèi)存不足或使用到 Swap 時(shí)報(bào)警出來(lái),及時(shí)處理。
碎片整理
Redis 的數(shù)據(jù)都存儲(chǔ)在內(nèi)存中,當(dāng)我們的應(yīng)用程序頻繁修改 Redis 中的數(shù)據(jù)時(shí),就有可能會(huì)導(dǎo)致 Redis 產(chǎn)生內(nèi)存碎片。
內(nèi)存碎片會(huì)降低 Redis 的內(nèi)存使用率,我們可以通過(guò)執(zhí)行 INFO 命令,得到這個(gè)實(shí)例的內(nèi)存碎片率:
# Memory used_memory:5709194824 used_memory_human:5.32G used_memory_rss:8264855552 used_memory_rss_human:7.70G ... mem_fragmentation_ratio:1.45
這個(gè)內(nèi)存碎片率是怎么計(jì)算的?
很簡(jiǎn)單,mem_fragmentation_ratio = used_memory_rss / used_memory。
其中 used_memory 表示 Redis 存儲(chǔ)數(shù)據(jù)的內(nèi)存大小,而 used_memory_rss 表示操作系統(tǒng)實(shí)際分配給 Redis 進(jìn)程的大小。
如果 mem_fragmentation_ratio > 1.5,說(shuō)明內(nèi)存碎片率已經(jīng)超過(guò)了 50%,這時(shí)我們就需要采取一些措施來(lái)降低內(nèi)存碎片了。
解決的方案一般如下:
-
如果你使用的是 Redis 4.0 以下版本,只能通過(guò)重啟實(shí)例來(lái)解決 -
如果你使用的是 Redis 4.0 版本,它正好提供了自動(dòng)碎片整理的功能,可以通過(guò)配置開(kāi)啟碎片自動(dòng)整理
但是,開(kāi)啟內(nèi)存碎片整理,它也有可能會(huì)導(dǎo)致 Redis 性能下降。
原因在于,Redis 的碎片整理工作是也在主線(xiàn)程中執(zhí)行的,當(dāng)其進(jìn)行碎片整理時(shí),必然會(huì)消耗 CPU 資源,產(chǎn)生更多的耗時(shí),從而影響到客戶(hù)端的請(qǐng)求。
所以,當(dāng)你需要開(kāi)啟這個(gè)功能時(shí),最好提前測(cè)試評(píng)估它對(duì) Redis 的影響。
Redis 碎片整理的參數(shù)配置如下:
# 開(kāi)啟自動(dòng)內(nèi)存碎片整理(總開(kāi)關(guān)) activedefrag yes # 內(nèi)存使用 100MB 以下,不進(jìn)行碎片整理 active-defrag-ignore-bytes 100mb # 內(nèi)存碎片率超過(guò) 10%,開(kāi)始碎片整理 active-defrag-threshold-lower 10 # 內(nèi)存碎片率超過(guò) 100%,盡最大努力碎片整理 active-defrag-threshold-upper 100 # 內(nèi)存碎片整理占用 CPU 資源最小百分比 active-defrag-cycle-min 1 # 內(nèi)存碎片整理占用 CPU 資源最大百分比 active-defrag-cycle-max 25 # 碎片整理期間,對(duì)于 List/Set/Hash/ZSet 類(lèi)型元素一次 Scan 的數(shù)量 active-defrag-max-scan-fields 1000
你需要結(jié)合 Redis 機(jī)器的負(fù)載情況,以及應(yīng)用程序可接受的延遲范圍進(jìn)行評(píng)估,合理調(diào)整碎片整理的參數(shù),盡可能降低碎片整理期間對(duì) Redis 的影響。
網(wǎng)絡(luò)帶寬過(guò)載
如果以上產(chǎn)生性能問(wèn)題的場(chǎng)景,你都規(guī)避掉了,而且 Redis 也穩(wěn)定運(yùn)行了很長(zhǎng)時(shí)間,但在某個(gè)時(shí)間點(diǎn)之后開(kāi)始,操作 Redis 突然開(kāi)始變慢了,而且一直持續(xù)下去,這種情況又是什么原因?qū)е拢?/span>
此時(shí)你需要排查一下 Redis 機(jī)器的網(wǎng)絡(luò)帶寬是否過(guò)載,是否存在某個(gè)實(shí)例把整個(gè)機(jī)器的網(wǎng)路帶寬占滿(mǎn)的情況。
網(wǎng)絡(luò)帶寬過(guò)載的情況下,服務(wù)器在 TCP 層和網(wǎng)絡(luò)層就會(huì)出現(xiàn)數(shù)據(jù)包發(fā)送延遲、丟包等情況。
Redis 的高性能,除了操作內(nèi)存之外,就在于網(wǎng)絡(luò) IO 了,如果網(wǎng)絡(luò) IO 存在瓶頸,那么也會(huì)嚴(yán)重影響 Redis 的性能。
如果確實(shí)出現(xiàn)這種情況,你需要及時(shí)確認(rèn)占滿(mǎn)網(wǎng)絡(luò)帶寬 Redis 實(shí)例,如果屬于正常的業(yè)務(wù)訪問(wèn),那就需要及時(shí)擴(kuò)容或遷移實(shí)例了,避免因?yàn)檫@個(gè)實(shí)例流量過(guò)大,影響這個(gè)機(jī)器的其他實(shí)例。
運(yùn)維層面,你需要對(duì) Redis 機(jī)器的各項(xiàng)指標(biāo)增加監(jiān)控,包括網(wǎng)絡(luò)流量,在網(wǎng)絡(luò)流量達(dá)到一定閾值時(shí)提前報(bào)警,及時(shí)確認(rèn)和擴(kuò)容。
其他原因
好了,以上這些方面就是如何排查 Redis 延遲問(wèn)題的思路和路徑。
除了以上這些,還有一些比較小的點(diǎn),你也需要注意一下:
1) 頻繁短連接
你的業(yè)務(wù)應(yīng)用,應(yīng)該使用長(zhǎng)連接操作 Redis,避免頻繁的短連接。
頻繁的短連接會(huì)導(dǎo)致 Redis 大量時(shí)間耗費(fèi)在連接的建立和釋放上,TCP 的三次握手和四次揮手同樣也會(huì)增加訪問(wèn)延遲。
2) 運(yùn)維監(jiān)控
前面我也提到了,要想提前預(yù)知 Redis 變慢的情況發(fā)生,必不可少的就是做好完善的監(jiān)控。
監(jiān)控其實(shí)就是對(duì)采集 Redis 的各項(xiàng)運(yùn)行時(shí)指標(biāo),通常的做法是監(jiān)控程序定時(shí)采集 Redis 的 INFO 信息,然后根據(jù) INFO 信息中的狀態(tài)數(shù)據(jù)做數(shù)據(jù)展示和報(bào)警。
這里我需要提醒你的是,在寫(xiě)一些監(jiān)控腳本,或使用開(kāi)源的監(jiān)控組件時(shí),也不能掉以輕心。
在寫(xiě)監(jiān)控腳本訪問(wèn) Redis 時(shí),盡量采用長(zhǎng)連接的方式采集狀態(tài)信息,避免頻繁短連接。同時(shí),你還要注意控制訪問(wèn) Redis 的頻率,避免影響到業(yè)務(wù)請(qǐng)求。
在使用一些開(kāi)源的監(jiān)控組件時(shí),最好了解一下這些組件的實(shí)現(xiàn)原理,以及正確配置這些組件,防止出現(xiàn)監(jiān)控組件發(fā)生 Bug,導(dǎo)致短時(shí)大量操作 Redis,影響 Redis 性能的情況發(fā)生。
我們當(dāng)時(shí)就發(fā)生過(guò),DBA 在使用一些開(kāi)源組件時(shí),因?yàn)榕渲煤褪褂脝?wèn)題,導(dǎo)致監(jiān)控程序頻繁地與 Redis 建立和斷開(kāi)連接,導(dǎo)致 Redis 響應(yīng)變慢。
3)其它程序爭(zhēng)搶資源
最后需要提醒你的是,你的 Redis 機(jī)器最好專(zhuān)項(xiàng)專(zhuān)用,只用來(lái)部署 Redis 實(shí)例,不要部署其他應(yīng)用程序,盡量給 Redis 提供一個(gè)相對(duì)「安靜」的環(huán)境,避免其它程序占用 CPU、內(nèi)存、磁盤(pán)資源,導(dǎo)致分配給 Redis 的資源不足而受到影響。
總結(jié)
好了,以上就是我總結(jié)的在使用 Redis 過(guò)程中,常見(jiàn)的可能導(dǎo)致延遲、甚至阻塞的問(wèn)題場(chǎng)景,以及如何快速定位和分析這些問(wèn)題,并且針對(duì)性地提供了解決方案。
這里我也匯總成了思維導(dǎo)圖,方便你在排查 Redis 性能問(wèn)題時(shí),快速地去分析和定位。
這里再簡(jiǎn)單總結(jié)一下,Redis 的性能問(wèn)題,既涉及到了業(yè)務(wù)開(kāi)發(fā)人員的使用方面,也涉及到了 DBA 的運(yùn)維方面。
作為業(yè)務(wù)開(kāi)發(fā)人員,我們需要了解 Redis 的基本原理,例如各個(gè)命令執(zhí)行的時(shí)間復(fù)雜度、數(shù)據(jù)過(guò)期策略、數(shù)據(jù)淘汰策略等,從而更合理地使用 Redis 命令,并且結(jié)合業(yè)務(wù)場(chǎng)景進(jìn)行優(yōu)化。
作為 DBA 和運(yùn)維人員,需要了解 Redis 運(yùn)行機(jī)制,例如數(shù)據(jù)持久化、內(nèi)存碎片整理、進(jìn)程綁核配置。除此之外,還需要了解操作系統(tǒng)相關(guān)知識(shí),例如寫(xiě)時(shí)復(fù)制、內(nèi)存大頁(yè)、Swap 機(jī)制等等。
同時(shí),DBA 在部署 Redis 時(shí),需要提前對(duì)進(jìn)行容量規(guī)劃,預(yù)留足夠的機(jī)器資源,還要對(duì) Redis 機(jī)器和實(shí)例做好完善的監(jiān)控,這樣才能盡可能地保證 Redis 的穩(wěn)定運(yùn)行。
后記
如果你能耐心地看到這里,想必你肯定已經(jīng)對(duì) Redis 的性能調(diào)優(yōu)有了很大的收獲。
你應(yīng)該也發(fā)現(xiàn)了,Redis 的性能問(wèn)題,涉及到的知識(shí)點(diǎn)非常廣,幾乎涵蓋了 CPU、內(nèi)存、網(wǎng)絡(luò)、甚至磁盤(pán)的方方面面,同時(shí),你還需要了解計(jì)算機(jī)的體系結(jié)構(gòu),以及操作系統(tǒng)的各種機(jī)制。
從資源使用角度來(lái)看,包含的知識(shí)點(diǎn)如下:
-
CPU 相關(guān):使用復(fù)雜度過(guò)高命令、數(shù)據(jù)的持久化,都與耗費(fèi)過(guò)多的 CPU 資源有關(guān) -
內(nèi)存相關(guān):bigkey 內(nèi)存的申請(qǐng)和釋放、數(shù)據(jù)過(guò)期、數(shù)據(jù)淘汰、碎片整理、內(nèi)存大頁(yè)、內(nèi)存寫(xiě)時(shí)復(fù)制都與內(nèi)存息息相關(guān) -
磁盤(pán)相關(guān):數(shù)據(jù)持久化、AOF 刷盤(pán)策略,也會(huì)受到磁盤(pán)的影響 -
網(wǎng)絡(luò)相關(guān):短連接、實(shí)例流量過(guò)載、網(wǎng)絡(luò)流量過(guò)載,也會(huì)降低 Redis 性能 -
計(jì)算機(jī)系統(tǒng):CPU 結(jié)構(gòu)、內(nèi)存分配,都屬于最基礎(chǔ)的計(jì)算機(jī)系統(tǒng)知識(shí) -
操作系統(tǒng):寫(xiě)時(shí)復(fù)制、內(nèi)存大頁(yè)、Swap、CPU 綁定,都屬于操作系統(tǒng)層面的知識(shí)
沒(méi)想到吧?Redis 為了把性能做到極致,涉及到了這么多項(xiàng)優(yōu)化。
如果這篇文章內(nèi)容,你能吸收 90% 以上,說(shuō)明你對(duì) Redis 原理、計(jì)算機(jī)基礎(chǔ)、操作系統(tǒng)都已經(jīng)有了較為深刻的理解。
如果你能吸收 50% 左右,那你可以好好梳理一下,哪些方面是自己的知識(shí)盲區(qū),這樣可以針對(duì)性地去學(xué)習(xí)。
如果你吸收的只在 30% 以下,那么你可以先從 Redis 的基本原理出發(fā),先了解 Redis 的各種機(jī)制,進(jìn)而思考 Redis 為了提高性能,為什么使用這些機(jī)制?這些機(jī)制又是利用了計(jì)算機(jī)和操作系統(tǒng)的哪些特性去做的?進(jìn)而一步步地去擴(kuò)充你的知識(shí)體系,這是一個(gè)非常高效的學(xué)習(xí)路徑。
由于篇幅限制,關(guān)于 Redis 的很多細(xì)節(jié)無(wú)法全部展開(kāi),其實(shí),這篇文章提到的每一個(gè)導(dǎo)致 Redis 性能問(wèn)題的場(chǎng)景,如果展開(kāi)來(lái)講,都可以寫(xiě)出一篇文章出來(lái)。
例如,關(guān)于 Redis 進(jìn)程綁定 CPU,以及操作系統(tǒng)使用 Swap,其實(shí)這些還涉及到了非一致性?xún)?nèi)存訪問(wèn) NUMA 架構(gòu)的影響,其中也有很多細(xì)節(jié)沒(méi)有展開(kāi)來(lái)講。
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!