如何排查Redis性能問(wèn)題 Redis內(nèi)部實(shí)現(xiàn)原理解析

2023-07-05 13:18:13 來(lái)源:數(shù)據(jù)分析與開(kāi)發(fā)

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)境不適合專注閱讀,我建議你先收藏此文章,然后在合適的時(shí)間專注閱讀這篇文章。

如果你能耐心且認(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)外部依賴的出入口,記錄下每次請(qǐng)求外部依賴的響應(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ò)線路質(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-h127.0.0.1-p6379--intrinsic-latency60Maxlatencysofar:1microseconds.Maxlatencysofar:15microseconds.Maxlatencysofar:17microseconds.Maxlatencysofar:18microseconds.Maxlatencysofar:31microseconds.Maxlatencysofar:32microseconds.Maxlatencysofar:59microseconds.Maxlatencysofar:72microseconds.1428669267totalruns(avglatency:0.0420microseconds/42.00nanosecondsperrun).Worstruntook1429xlongerthantheaveragelatency.

從輸出結(jié)果可以看到,這 60 秒內(nèi)的最大響應(yīng)延遲為 72 微秒(0.072毫秒)。

你還可以使用以下命令,查看一段時(shí)間內(nèi) Redis 的最小、最大、平均訪問(wèn)延遲:

$redis-cli-h127.0.0.1-p6379--latency-history-i1min:0,max:1,avg:0.13(100samples)--1.01secondsrangemin:0,max:1,avg:0.12(99samples)--1.01secondsrangemin:0,max:1,avg:0.13(99samples)--1.01secondsrangemin:0,max:1,avg:0.10(99samples)--1.01secondsrangemin:0,max:1,avg:0.13(98samples)--1.00secondsrangemin:0,max:1,avg:0.08(99samples)--1.01secondsrange...

以上輸出結(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毫秒,記錄慢日志CONFIGSETslowlog-log-slower-than5000#只保留最近500條慢日志CONFIGSETslowlog-max-len500

設(shè)置完成之后,所有執(zhí)行的命令如果操作耗時(shí)超過(guò)了 5 毫秒,都會(huì)被 Redis 記錄下來(lái)。

此時(shí),你可以執(zhí)行以下命令,就可以查詢到最近記錄的慢日志:

127.0.0.1:6379>SLOWLOGget51)1)(integer)32693#慢日志ID2)(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)326922)(integer)15937633373)(integer)50444)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 聚合類命令

使用 O(N) 復(fù)雜度的命令,但 N 的值非常大

第一種情況導(dǎo)致變慢的原因在于,Redis 在操作內(nèi)存數(shù)據(jù)時(shí),時(shí)間復(fù)雜度過(guò)高,要花費(fèi)更多的 CPU資源。

第二種情況導(dǎo)致變慢的原因在于,Redis 一次需要返回給客戶端的數(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 是單線程處理客戶端請(qǐng)求的,如果你經(jīng)常使用以上命令,那么當(dāng) Redis 處理客戶端請(qǐng)求時(shí),一旦前面某個(gè)命令發(fā)生耗時(shí),就會(huì)導(dǎo)致后面的請(qǐng)求發(fā)生排隊(duì),對(duì)于客戶端來(lái)說(shuō),響應(yīng)延遲也會(huì)變長(zhǎng)。

針對(duì)這種情況如何解決呢?

答案很簡(jiǎn)單,你可以使用以下方法優(yōu)化你的業(yè)務(wù):

盡量不使用 O(N) 以上復(fù)雜度過(guò)高的命令,對(duì)于數(shù)據(jù)的聚合操作,放在客戶端做

執(zhí)行 O(N) 命令,保證 N 盡量的小(推薦N <= 300),每次獲取盡量少的數(shù)據(jù),讓 Redis 可以及時(shí)處理返回

操作bigkey

如果你查詢慢日志發(fā)現(xiàn),并不是復(fù)雜度過(guò)高的命令導(dǎo)致的,而都是 SET / DEL 這種簡(jiǎn)單命令出現(xiàn)在慢日志中,那么你就要懷疑你的實(shí)例否寫入了 bigkey。

Redis 在寫入數(shù)據(jù)時(shí),需要為新的數(shù)據(jù)分配內(nèi)存,相對(duì)應(yīng)的,當(dāng)從 Redis 中刪除數(shù)據(jù)時(shí),它會(huì)釋放對(duì)應(yīng)的內(nèi)存空間。

如果一個(gè) key 寫入的 value 非常大,那么 Redis 在分配內(nèi)存時(shí)就會(huì)比較耗時(shí)。同樣的,當(dāng)刪除這個(gè) key 時(shí),釋放內(nèi)存也會(huì)比較耗時(shí),這種類型的 key 我們一般稱之為 bigkey。

此時(shí),你需要檢查你的業(yè)務(wù)代碼,是否存在寫入 bigkey 的情況。你需要評(píng)估寫入一個(gè) key 的數(shù)據(jù)大小,盡量避免一個(gè) key 存入過(guò)大的數(shù)據(jù)。

如果已經(jīng)寫入了 bigkey,那有沒(méi)有什么辦法可以掃描出實(shí)例中 bigkey 的分布情況呢?

答案是可以的。

Redis 提供了掃描 bigkey 的命令,執(zhí)行以下命令就可以掃描出,一個(gè)實(shí)例中 bigkey 的分布情況,輸出結(jié)果是以類型維度展示的:

$redis-cli-h127.0.0.1-p6379--bigkeys-i0.01...--------summary-------Sampled829675keysinthekeyspace!Totalkeylengthinbytesis10059825(avglen12.13)Biggeststringfound"key:291880"has10bytesBiggestlistfound"mylist:004"has40itemsBiggestsetfound"myset:2386"has38membersBiggesthashfound"myhash:3574"has37fieldsBiggestzsetfound"myzset:2704"has42members36313stringswith363130bytes(04.38%ofkeys,avgsize10.00)787393listswith896540items(94.90%ofkeys,avgsize1.14)1994setswith40052members(00.24%ofkeys,avgsize20.09)1990hashswith39632fields(00.24%ofkeys,avgsize19.92)1985zsetswith39750members(00.24%ofkeys,avgsize20.03)

從輸出結(jié)果我們可以很清晰地看到,每種數(shù)據(jù)類型所占用的最大內(nèi)存 / 擁有最多元素的 key 是哪一個(gè),以及每種數(shù)據(jù)類型在整個(gè)實(shí)例中的占比和平均大小 / 元素?cái)?shù)量。

其實(shí),使用這個(gè)命令的原理,就是 Redis 在內(nèi)部執(zhí)行了 SCAN命令,遍歷整個(gè)實(shí)例中所有的 key,然后針對(duì) key 的類型,分別執(zhí)行 STRLEN、LLEN、HLEN、SCARD、ZCARD 命令,來(lái)獲取 String 類型的長(zhǎng)度、容器類型(List、Hash、Set、ZSet)的元素個(gè)數(shù)。

這里我需要提醒你的是,當(dāng)執(zhí)行這個(gè)命令時(shí),要注意 2 個(gè)問(wèn)題:

對(duì)線上實(shí)例進(jìn)行 bigkey 掃描時(shí),Redis 的 OPS 會(huì)突增,為了降低掃描過(guò)程中對(duì) Redis 的影響,最好控制一下掃描的頻率,指定 -i 參數(shù)即可,它表示掃描過(guò)程中每次掃描后休息的時(shí)間間隔,單位是秒

掃描結(jié)果中,對(duì)于容器類型(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)可以優(yōu)化:

業(yè)務(wù)應(yīng)用盡量避免寫入 bigkey

如果你使用的 Redis 是 4.0 以上版本,用 UNLINK 命令替代 DEL,此命令可以把釋放 key 內(nèi)存的操作,放到后臺(tái)線程中去執(zhí)行,從而降低對(duì) Redis 的影響

如果你使用的 Redis 是 6.0 以上版本,可以開(kāi)啟 lazy-free 機(jī)制(lazyfree-lazy-user-del = yes),在執(zhí)行 DEL 命令時(shí),釋放內(nèi)存也會(huì)放到后臺(tái)線程中執(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 主線程中執(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è)客戶端請(qǐng)求。

此時(shí)就會(huì)出現(xiàn),應(yīng)用訪問(wèn) Redis 延時(shí)變大。

如果此時(shí)需要過(guò)期刪除的是一個(gè) bigkey,那么這個(gè)耗時(shí)會(huì)更久。而且,這個(gè)操作延遲的命令并不會(huì)記錄在慢日志中

因?yàn)槁罩局?strong>只記錄一個(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)線程中執(zhí)行,避免阻塞主線程

第一種方案,在設(shè)置 key 的過(guò)期時(shí)間時(shí),增加一個(gè)隨機(jī)時(shí)間,偽代碼可以這么寫:

#在過(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ò)大,從而避免阻塞主線程。

第二種方案,Redis 4.0 以上版本,開(kāi)啟 lazy-free 機(jī)制:

#釋放過(guò)期key的內(nèi)存,放到后臺(tái)線程執(zhí)行l(wèi)azyfree-lazy-expireyes

另外,除了業(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),在此之后每次寫入新數(shù)據(jù),操作延遲變大了。

這是為什么?

原因在于,當(dāng) Redis 內(nèi)存達(dá)到 maxmemory 后,每次寫入新的數(shù)據(jù)之前,Redis 必須先從實(shí)例中踢出一部分?jǐn)?shù)據(jù),讓整個(gè)實(shí)例的內(nèi)存維持在 maxmemory 之下,然后才能把新數(shù)據(jù)寫進(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 后,再寫入新數(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 的延遲,而且,寫 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)線程中執(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ú)法處理任何客戶端請(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ú)法處理客戶端請(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)程依舊是可以接收寫請(qǐng)求的,而進(jìn)來(lái)的寫請(qǐng)求,會(huì)采用 Copy On Write(寫時(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ù),這就是所謂的「寫時(shí)復(fù)制」。

寫時(shí)復(fù)制你也可以理解成,誰(shuí)需要發(fā)生寫操作,誰(shuí)就需要先拷貝,再修改。

這樣做的好處是,父進(jìn)程有任何寫操作,并不會(huì)影響子進(jìn)程的數(shù)據(jù)持久化(子進(jìn)程只持久化 fork 這一瞬間整個(gè)實(shí)例中的所有數(shù)據(jù)即可,不關(guān)心新的數(shù)據(jù)變更,因?yàn)樽舆M(jìn)程只需要一份內(nèi)存快照,然后持久化到磁盤上)。

但是請(qǐng)注意,主進(jìn)程在拷貝內(nèi)存數(shù)據(jù)時(shí),這個(gè)階段就涉及到新內(nèi)存的申請(qǐng),如果此時(shí)操作系統(tǒng)開(kāi)啟了內(nèi)存大頁(yè),那么在此期間,客戶端即便只修改 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è)寫請(qǐng)求的延遲增加,影響到 Redis 性能。

同樣地,如果這個(gè)寫請(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]madvisenever

如果輸出選項(xiàng)是 always,就表示目前開(kāi)啟了內(nèi)存大頁(yè)機(jī)制,我們需要關(guān)掉它:

$echonever>/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í)行寫命令后,把這個(gè)命令寫入到 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í)行寫操作后立即刷盤,此方案會(huì)占用比較大的磁盤 IO 資源,但數(shù)據(jù)安全性最高

appendfsync no:主線程每次寫操作只寫內(nèi)存就返回,內(nèi)存數(shù)據(jù)什么時(shí)候刷到磁盤,交由操作系統(tǒng)決定,此方案對(duì)性能影響最小,但數(shù)據(jù)安全性也最低,Redis 宕機(jī)時(shí)丟失的數(shù)據(jù)取決于操作系統(tǒng)刷盤時(shí)機(jī)

appendfsync everysec:主線程每次寫操作只寫內(nèi)存就返回,然后由后臺(tái)線程每隔 1 秒執(zhí)行一次刷盤操作(觸發(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 每處理一次寫操作,都會(huì)把這個(gè)命令寫入到磁盤中才返回,整個(gè)過(guò)程都是在主線程執(zhí)行的,這個(gè)過(guò)程必然會(huì)加重 Redis 寫負(fù)擔(dān)。

原因也很簡(jiǎn)單,操作磁盤要比操作內(nèi)存慢幾百倍,采用這個(gè)配置會(huì)嚴(yán)重拖慢 Redis 的性能,因此我不建議你把 AOF 刷盤方式配置為 always。

我們接著來(lái)看 appendfsync no 配置項(xiàng)。

在這種配置下,Redis 每次寫操作只寫內(nèi)存,什么時(shí)候把內(nèi)存中的數(shù)據(jù)刷到磁盤,交給操作系統(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 主線程寫完內(nèi)存后就返回,具體的刷盤操作是放到后臺(tái)線程中執(zhí)行的,后臺(tái)線程每隔 1 秒把內(nèi)存中的數(shù)據(jù)刷到磁盤中。

這種方案既兼顧了性能,又盡可能地保證了數(shù)據(jù)安全,是不是覺(jué)得很完美?

但是,這里我要給你潑一盆冷水了,采用這種方案你也要警惕一下,因?yàn)檫@種方案還是存在導(dǎo)致 Redis 延遲變大的情況發(fā)生,甚至?xí)枞麄€(gè) Redis。

這是為什么?我把 AOF 最耗時(shí)的刷盤操作,放到后臺(tái)線程中也會(huì)影響到 Redis 主線程?

你試想這樣一種情況:當(dāng) Redis 后臺(tái)線程在執(zhí)行 AOF 文件刷盤時(shí),如果此時(shí)磁盤的 IO 負(fù)載很高,那這個(gè)后臺(tái)線程在執(zhí)行刷盤操作(fsync系統(tǒng)調(diào)用)時(shí)就會(huì)被阻塞住。

此時(shí)的主線程依舊會(huì)接收寫請(qǐng)求,緊接著,主線程又需要把數(shù)據(jù)寫到文件內(nèi)存中(write 系統(tǒng)調(diào)用),但此時(shí)的后臺(tái)子線程由于磁盤負(fù)載過(guò)高,導(dǎo)致 fsync 發(fā)生阻塞,遲遲不能返回,那主線程在執(zhí)行 write 系統(tǒng)調(diào)用時(shí),也會(huì)被阻塞住,直到后臺(tái)線程 fsync 執(zhí)行完成后,主線程執(zhí)行 write 才能成功返回。

看到了么?在這個(gè)過(guò)程中,主線程依舊有阻塞的風(fēng)險(xiǎn)。

所以,盡管你的 AOF 配置為 appendfsync everysec,也不能掉以輕心,要警惕磁盤壓力過(guò)大導(dǎo)致的 Redis 有性能問(wèn)題。

那什么情況下會(huì)導(dǎo)致磁盤 IO 負(fù)載過(guò)大?以及如何解決這個(gè)問(wèn)題呢?

我總結(jié)了以下幾種情況,你可以參考進(jìn)行問(wèn)題排查:

子進(jìn)程正在執(zhí)行 AOF rewrite,這個(gè)過(guò)程會(huì)占用大量的磁盤 IO 資源

有其他應(yīng)用程序在執(zhí)行大量的寫文件操作,也會(huì)占用磁盤 IO 資源

對(duì)于情況1,說(shuō)白了就是,Redis 的 AOF 后臺(tái)子線程刷盤操作,撞上了子進(jìn)程 AOF rewrite!

這怎么辦?難道要關(guān)閉 AOF rewrite 才行?

幸運(yùn)的是,Redis 提供了一個(gè)配置項(xiàng),當(dāng)子進(jìn)程在 AOF rewrite 期間,可以讓后臺(tái)子線程不執(zhí)行刷盤(不觸發(fā) fsync 系統(tǒng)調(diào)用)操作。

這相當(dāng)于在 AOF rewrite 期間,臨時(shí)把 appendfsync 設(shè)置為了 none,配置如下:

#AOFrewrite期間,AOF后臺(tái)子線程不進(jìn)行刷盤操作#相當(dāng)于在這期間,臨時(shí)把a(bǔ)ppendfsync設(shè)置為了noneno-appendfsync-on-rewriteyes

當(dāng)然,開(kāi)啟這個(gè)配置項(xiàng),在 AOF rewrite 期間,如果實(shí)例發(fā)生宕機(jī),那么此時(shí)會(huì)丟失更多的數(shù)據(jù),性能和數(shù)據(jù)安全性,你需要權(quán)衡后進(jìn)行選擇。

如果占用磁盤資源的是其他應(yīng)用程序,那就比較簡(jiǎn)單了,你需要定位到是哪個(gè)應(yīng)用程序在大量寫磁盤,然后把這個(gè)應(yīng)用程序遷移到其他機(jī)器上執(zhí)行就好了,避免對(duì) Redis 產(chǎn)生影響。

當(dāng)然,如果你對(duì) Redis 的性能和數(shù)據(jù)安全都有很高的要求,那么我建議從硬件層面來(lái)優(yōu)化,更換為 SSD磁盤,提高磁盤的 IO 能力,保證 AOF 期間有充足的磁盤資源可以使用。

綁定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 除了主線程服務(wù)客戶端請(qǐng)求之外,還會(huì)創(chuàng)建子進(jìn)程、子線程。

其中子進(jìn)程用于數(shù)據(jù)持久化,而子線程用于執(zhí)行一些比較耗時(shí)操作,例如異步釋放 fd、異步 AOF 刷盤、異步 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)搶,進(jìn)而影響到主進(jìn)程服務(wù)客戶端請(qǐng)求,訪問(wèn)延遲變大。

這就是 Redis 綁定 CPU 帶來(lái)的性能問(wèn)題。

那如何解決這個(gè)問(wèn)題呢?

如果你確實(shí)想要綁定 CPU,可以優(yōu)化的方案是,不要讓 Redis 進(jìn)程只綁定在一個(gè) CPU 邏輯核上,而是綁定在多個(gè)邏輯核心上,而且,綁定的多個(gè)邏輯核心最好是同一個(gè)物理核心,這樣它們還可以共用 L1/L2 Cache。

當(dāng)然,即便我們把 Redis 綁定在多個(gè)邏輯核心上,也只能在一定程度上緩解主線程、子進(jìn)程、后臺(tái)線程在 CPU 資源上的競(jìng)爭(zhēng)。

因?yàn)檫@些子進(jìn)程、子線程還是會(huì)在這多個(gè)邏輯核心上進(jìn)行切換,存在性能損耗。

如何再進(jìn)一步優(yōu)化?

可能你已經(jīng)想到了,我們是否可以讓主線程、子進(jìn)程、后臺(tái)線程,分別綁定在固定的 CPU 核心上,不讓它們來(lái)回切換,這樣一來(lái),他們各自使用的 CPU 資源互不影響。

其實(shí),這個(gè)方案 Redis 官方已經(jīng)想到了。

Redis 在 6.0 版本已經(jīng)推出了這個(gè)功能,我們可以通過(guò)以下配置,對(duì)主線程、后臺(tái)線程、后臺(tái) RDB 進(jìn)程、AOF rewrite 進(jìn)程,綁定固定的 CPU 邏輯核心:

#RedisServer和IO線程綁定到CPU核心0,2,4,6server_cpulist0-7:2#后臺(tái)子線程綁定到CPU核心1,3bio_cpulist1,3#后臺(tái)AOFrewrite進(jìn)程綁定到CPU核心8,9,10,11aof_rewrite_cpulist8-11#后臺(tái)RDB進(jìn)程綁定到CPU核心1,10,11#bgsave_cpulist1,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ù)換到磁盤上,以達(dá)到應(yīng)用程序?qū)?nèi)存使用的緩沖,這些內(nèi)存數(shù)據(jù)被換到磁盤上的區(qū)域,就是 Swap。

問(wèn)題就在于,當(dāng)內(nèi)存中的數(shù)據(jù)被換到磁盤上后,Redis 再訪問(wèn)這些數(shù)據(jù)時(shí),就需要從磁盤上讀取,訪問(wè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|grepredis-server#查看RedisSwap使用情況$cat/proc/$pid/smaps|egrep"^(Swap|Size)"

輸出結(jié)果如下:

Size:1256kBSwap:0kBSize:4kBSwap:0kBSize:132kBSwap:0kBSize:63488kBSwap:0kBSize:132kBSwap:0kBSize:65404kBSwap:0kBSize:1921024kBSwap:0kB...

這個(gè)結(jié)果會(huì)列出 Redis 進(jìn)程的內(nèi)存使用情況。

每一行 Size 表示 Redis 所用的一塊內(nèi)存大小,Size 下面的 Swap 就表示這塊 Size 大小的內(nèi)存,有多少數(shù)據(jù)已經(jīng)被換到磁盤上了,如果這兩個(gè)值相等,說(shuō)明這塊內(nèi)存的數(shù)據(jù)都已經(jīng)完全被換到磁盤上了。

如果只是少量數(shù)據(jù)被換到磁盤上,例如每一塊 Swap 占對(duì)應(yīng) Size 的比例很小,那影響并不是很大。如果是幾百兆甚至上 GB 的內(nèi)存被換到了磁盤上,那么你就需要警惕了,這種情況 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)存碎片率:

#Memoryused_memory:5709194824used_memory_human:5.32Gused_memory_rss:8264855552used_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 的碎片整理工作是也在主線程中執(zhí)行的,當(dāng)其進(jìn)行碎片整理時(shí),必然會(huì)消耗 CPU 資源,產(chǎn)生更多的耗時(shí),從而影響到客戶端的請(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))activedefragyes#內(nèi)存使用100MB以下,不進(jìn)行碎片整理active-defrag-ignore-bytes100mb#內(nèi)存碎片率超過(guò)10%,開(kāi)始碎片整理active-defrag-threshold-lower10#內(nèi)存碎片率超過(guò)100%,盡最大努力碎片整理active-defrag-threshold-upper100#內(nèi)存碎片整理占用CPU資源最小百分比active-defrag-cycle-min1#內(nèi)存碎片整理占用CPU資源最大百分比active-defrag-cycle-max25#碎片整理期間,對(duì)于List/Set/Hash/ZSet類型元素一次Scan的數(shù)量active-defrag-max-scan-fields1000

你需要結(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ū)е拢?/p>

此時(shí)你需要排查一下 Redis 機(jī)器的網(wǎng)絡(luò)帶寬是否過(guò)載,是否存在某個(gè)實(shí)例把整個(gè)機(jī)器的網(wǎng)路帶寬占滿的情況。

網(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)占滿網(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)警。

這里我需要提醒你的是,在寫一些監(jiān)控腳本,或使用開(kāi)源的監(jiān)控組件時(shí),也不能掉以輕心。

在寫監(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ī)器最好專項(xiàng)專用,只用來(lái)部署 Redis 實(shí)例,不要部署其他應(yīng)用程序,盡量給 Redis 提供一個(gè)相對(duì)「安靜」的環(huán)境,避免其它程序占用 CPU、內(nèi)存、磁盤資源,導(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í),例如寫時(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ò)、甚至磁盤的方方面面,同時(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)存寫時(shí)復(fù)制都與內(nèi)存息息相關(guān)

磁盤相關(guān):數(shù)據(jù)持久化、AOF 刷盤策略,也會(huì)受到磁盤的影響

網(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):寫時(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)講,都可以寫出一篇文章出來(lái)。

例如,關(guān)于 Redis 進(jìn)程綁定 CPU,以及操作系統(tǒng)使用 Swap,其實(shí)這些還涉及到了非一致性內(nèi)存訪問(wèn) NUMA 架構(gòu)的影響,其中也有很多細(xì)節(jié)沒(méi)有展開(kāi)來(lái)講。

編輯:黃飛

標(biāo)簽:

上一篇:常用儀器儀表的功能與使用方法 每日快訊
下一篇:最后一頁(yè)