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