日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

redis延遲問題全面排障指南

作者:kevine

前言

在 Redis 的實際使用過程中,我們經(jīng)常會面對以下的場景:

  • 在 Redis 上執(zhí)行同樣的命令,為什么有時響應(yīng)很快,有時卻很慢;
  • 為什么 Redis 執(zhí)行 GET、SET、DEL 命令耗時也很久;
  • 為什么我的 Redis 突然慢了一波,之后又恢復(fù)正常了;
  • 為什么我的 Redis 穩(wěn)定運行了很久,突然從某個時間點開始變慢了。

這時我們還是需要一個全面的排障流程,不能無厘頭地進(jìn)行優(yōu)化;全面的排障流程可以幫助我們找到真正的根因和性能瓶頸,以及實施正確高效的優(yōu)化方案。

這篇文章我們就從可能導(dǎo)致 Redis 延遲的方方面面開始,逐步深入排障深水區(qū),以提供一個「全面」的 Redis 延遲問題排查思路。

需要了解的詞

  • Copy On WriteCOW是一種建立在虛擬內(nèi)存重映射技術(shù)之上的技術(shù),因此它需要 MMU的硬件支持, MMU會記錄當(dāng)前哪些內(nèi)存頁被標(biāo)記成只讀,當(dāng)有進(jìn)程嘗試往這些內(nèi)存頁中寫數(shù)據(jù)的時候, MMU就會拋一個異常給操作系統(tǒng)內(nèi)核,內(nèi)核處理該異常時為該進(jìn)程分配一份物理內(nèi)存并復(fù)制數(shù)據(jù)到此內(nèi)存地址,重新向 MMU發(fā)出執(zhí)行該進(jìn)程的寫操作。
  • 內(nèi)存碎片操作系統(tǒng)負(fù)責(zé)為每個進(jìn)程分配物理內(nèi)存,而操作系統(tǒng)中的虛擬內(nèi)存管理器保管著由內(nèi)存分配器分配的實際內(nèi)存映射 如果我們的應(yīng)用程序需求1GB大小的內(nèi)存,內(nèi)存分配器將首先嘗試找到一個 連續(xù)的內(nèi)存段來存儲數(shù)據(jù);如果找不到連續(xù)的段,則分配器必須將進(jìn)程的 數(shù)據(jù)分成多個段,從而導(dǎo)致內(nèi)存開銷增加。
  • SWAP顧名思義,當(dāng)某進(jìn)程向 OS 請求內(nèi)存發(fā)現(xiàn)不足時,OS 會把內(nèi)存中暫時不用的數(shù)據(jù)交換出去,放在SWAP分區(qū)中,這個過程稱為 SWAP OUT。 當(dāng)某進(jìn)程又需要這些數(shù)據(jù)且 OS 發(fā)現(xiàn)還有空閑物理內(nèi)存時,又會把 SWAP分區(qū)中的數(shù)據(jù)交換回物理內(nèi)存中,這個過程稱為 SWAP IN,詳情可參考這篇文章。
  • redis 監(jiān)控指標(biāo)合理完善的監(jiān)控指標(biāo)無疑能大大助力我們的排障,本篇文章中提到了很多的 redis 監(jiān)控指標(biāo),詳情可以參考這篇文章: redis 監(jiān)控指標(biāo)
排除無關(guān)原因

當(dāng)我們發(fā)現(xiàn)從我們的業(yè)務(wù)服務(wù)發(fā)起請求到接收到Redis的回包這條鏈路慢時,我們需要先排除其它的一些無關(guān) Redis自身的原因,如:

  • 業(yè)務(wù)自身準(zhǔn)備請求耗時過長;
  • 業(yè)務(wù)服務(wù)器到 Redis服務(wù)器之間的網(wǎng)絡(luò)存在問題,例如網(wǎng)絡(luò)線路質(zhì)量不佳,網(wǎng)絡(luò)數(shù)據(jù)包在傳輸時存在延遲、丟包等情況; 網(wǎng)絡(luò)和通信導(dǎo)致的固有延遲:客戶端使用 TCP/IP連接或 Unix域連接連接到 Redis,在 1 Gbit/s網(wǎng)絡(luò)下的延遲約為 200 us,而 Unix域Socket的延遲甚至可低至 30 us,這實際上取決于網(wǎng)絡(luò)和系統(tǒng)硬件;在網(wǎng)絡(luò)通信的基礎(chǔ)之上,操作系統(tǒng)還會增加了一些額外的延遲(如 線程調(diào)度、CPU緩存、NUMA等);并且在虛擬環(huán)境中,系統(tǒng)引起的延遲比在物理機(jī)上也要高得多的結(jié)果就是,即使 Redis 在亞微秒的時間級別上能處理大多數(shù)命令,網(wǎng)絡(luò)和系統(tǒng)相關(guān)的延遲仍然是不可避免的。
  • Redis實例所在的機(jī)器帶寬不足 / Docker網(wǎng)橋性能問題等。

排障事大,但咱也不能冤枉了Redis;首先我們還是應(yīng)該把其它因素都排除完了,再把焦點關(guān)注在業(yè)務(wù)服務(wù)到 Redis這條鏈路上。如以下的火焰圖就可以很肯定的說問題出現(xiàn)在 Redis 上了:

在排除無關(guān)因素后,如何確認(rèn) Redis 是否真的變慢了?

測試流程

排除無關(guān)因素后,我們可以按照以下基本步驟來判斷某一 Redis 實例是否變慢了:

  1. 監(jiān)控并記錄一個相對正常的 Redis 實例(相對低負(fù)載、key 存儲結(jié)構(gòu)簡單合理、連接數(shù)未滿)的相關(guān)指標(biāo);
  2. 找到認(rèn)為表現(xiàn)不符合預(yù)期的 Redis 實例(如使用該實例后業(yè)務(wù)接口明顯變慢),在相同配置的服務(wù)器上監(jiān)控并記錄這個實例的相關(guān)指標(biāo);
  3. 若表現(xiàn)不符合預(yù)期的 Redis 實例的相關(guān)指標(biāo)明顯達(dá)不到正常 Redis 實例的標(biāo)準(zhǔn)(延遲兩倍以上、OPS僅為正常實例的 1/3、 內(nèi)存碎片率較高等),即可認(rèn)為這個 Redis 實例的指標(biāo)未達(dá)到預(yù)期。

確認(rèn)是 Redis 實例的某些指標(biāo)未達(dá)到預(yù)期后,我們就可以開始逐步分析拆解可能導(dǎo)致 Redis 表現(xiàn)不佳的因素,并確認(rèn)優(yōu)化方案了。

快速清單

I've little time, give me the checklist

在線上發(fā)生故障時,我們都沒有那么多時間去深究原因,所以在深入到排障的深水區(qū)前,我們可以先從最影響最大的一些問題開始檢查,這里是一份「會對redis基本運行造成嚴(yán)重影響的問題」的 checklist:

  • 確保沒有運行阻塞服務(wù)器的緩慢命令;使用 Redis 的耗時命令記錄功能來檢查這一點;
  • 對于 EC2 用戶,請確保使用基于HVM的現(xiàn)代 EC2 實例,如 m3.dium等,否則, fork系統(tǒng)調(diào)用帶來的延遲太大了;
  • 禁用透明內(nèi)存大頁。使用echo never > /sys/kernel/mm/transparent_hugepage/enabled來禁用它們,然后重新啟動 Redis 進(jìn)程;
  • 如果使用的是虛擬機(jī),則可能存在與 Redis 本身無關(guān)的固有延遲;使用redis-cli --intrinsic-latency 100檢查延遲,確認(rèn)該延遲是否符合預(yù)期(注意:您需要在服務(wù)器上而不是在客戶機(jī)上運行此命令);
  • 啟用并使用 Redis 的延遲監(jiān)控功能,更好的監(jiān)控 Redis 實例中的延遲事件和原因。
  •  
導(dǎo)致 Redis Latency 的具體原因

如果使用我們的快速清單并不能解決實際的延遲問題,我們就得深入 redis 性能排障的深水區(qū),多方面逐步深究其中的具體原因了。

使用復(fù)雜度過高的命令 / 「大型」命令

要找到這樣的命令執(zhí)行記錄,需要使用 Redis 提供的耗時命令統(tǒng)計的功能,查看 Redis 耗時命令之前,我們需要先在redis.conf中設(shè)置耗時命令的閾值;如:設(shè)置耗時命令的閾值為 5ms,保留近 500 條耗時命令記錄:

# The following time is expressed in microseconds, so 1000000 is equivalent# to one second. Note that a negative number disables the slow log, while# a value of zero forces the logging of every command.slowlog-log-slower-than 10000# There is no limit to this length. Just be aware that it will consume memory.# You can reclAIm memory used by the slow log with SLOWLOG RESET.slowlog-max-len 128

或是直接在redis-cli中使用 CONFIG命令配置:

# 命令執(zhí)行耗時超過 5 毫秒,記錄耗時命令CONFIG SET slowlog-log-slower-than 5000# 只保留最近 500 條耗時命令CONFIG SET slowlog-max-len 500

通過查看耗時命令記錄,我們就可以知道在什么時間點,執(zhí)行了哪些比較耗時的命令。

如果應(yīng)用程序執(zhí)行的 Redis 命令有以下特點,那么有可能會導(dǎo)致操作延遲變大:

  1. 經(jīng)常使用 O(N) 以上復(fù)雜度的命令,例如 SORT, SUNION, ZUNIONSTORE 等聚合類命令
  2. 使用 O(N) 復(fù)雜度的命令,但 N 的值非常大

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

第二種情況導(dǎo)致變慢的原因是 處理「大型」redis 命令(大請求包體 / 大返回包體的 redis 請求),對于這樣的命令來說,雖然其只有兩次內(nèi)核態(tài)與用戶態(tài)的上下文切換,但由于 redis 是單線程處理回調(diào)事件的,所以后續(xù)請求很有可能被這一個大型請求阻塞,這時可能需要考慮業(yè)務(wù)請求拆解盡量分批執(zhí)行,以保證 redis 服務(wù)的穩(wěn)定性。

Bigkey

bigkey 一般指包含大量數(shù)據(jù)或大量成員和列表的 key,如下所示就是一些典型的 bigkey(根據(jù) Redis 的實際用例和業(yè)務(wù)場景,bigkey 的定義可能會有所不同):

  • value 大小為 5 MB(數(shù)據(jù)太大)的 String
  • 包含 20000 個元素的List(列表中的元素數(shù)量過多)
  • 有 10000 個成員的ZSET密鑰(成員數(shù)量過多)
  • 一個大小為 100 MB 的Hash key,即便只包含 1000 個成員(key 太大)

在上一節(jié)的耗時命令查詢中,如果我們發(fā)現(xiàn)榜首并不是復(fù)雜度過高的命令,而是 SET / DEL 等簡單命令,這很有可能就是 redis 實例中存在 bigkey導(dǎo)致的。

bigkey 會導(dǎo)致包括但不限于以下的問題:

  • Redis 的內(nèi)存使用量不斷增長,最終導(dǎo)致實例 OOM,或者因為達(dá)到最大內(nèi)存限制而導(dǎo)致寫入被阻塞和重要 key 被驅(qū)逐;
  • 訪問偏差導(dǎo)致的資源傾斜,bigkey的存在可能會導(dǎo)致某個 Redis 實例達(dá)到性能瓶頸,從而導(dǎo)致整個集群也達(dá)到性能瓶頸;在這種情況下,Redis 集群中一個節(jié)點的內(nèi)存使用量通常會因為對 bigkey的訪問需求而遠(yuǎn)遠(yuǎn)超過其他節(jié)點,而 Redis 集群中數(shù)據(jù)遷移時有一個最小粒度,這意味著該節(jié)點上的 bigkey 占用的內(nèi)存無法進(jìn)行 balance;
  • 由于將 bigkey 請求從 socket 讀取到 Redis 占用了幾乎所有帶寬,Redis 的其它請求都會受到影響;
  • 刪除 BigKey 時,由于主庫長時間阻塞(釋放 bigkey 占用的內(nèi)存)導(dǎo)致同步中斷或主從切換。

如何定位 bigkey

  1. 使用 redis-cli 提供的—-bigkeys參數(shù) redis-cli提供了掃描 bigkey 的 option —-bigkeys,執(zhí)行以下命令就可以掃描 redis 實例中 bigkey 的分布情況,以 key 類型維度輸出結(jié)果: $ redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.01

    [00.00%] Biggest string found so far

    ...

    [98.23%] Biggest string found so far

    -------- summary -------

    Sampled 829675 keys inthe keyspace!

    Total key length inbytes is 10059825 (avg len 12.13)

    Biggest string found 'key:291880'has 10 bytes

    Biggest list found 'mylist:004'has 40 items

    Biggest setfound 'myset:2386'has 38 members

    Biggest hashfound '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ù)量。 bigkey 掃描實際上是 Redis 執(zhí)行了 SCAN 命令,遍歷整個實例中所有的 key,然后針對 key 的類型,分別執(zhí)行 STRLEN, LLEN, HLEN, SCARD和ZCARD命令,來獲取 String 類型的長度,容器類型(List, Hash, Set, ZSet)的元素個數(shù)。 ??NOTICE: 當(dāng)執(zhí)行 bigkey 掃描時,要注意 2 個問題:以下是 bigkey 掃描實際用到的命令的時間復(fù)雜度:

    1. 對線上實例進(jìn)行 bigkey 掃描時,Redis 的 OPS 會突增,為了降低掃描過程中對 Redis 的影響,最好控制一下掃描的頻率,指定 -i參數(shù)即可,它表示掃描過程中每次掃描后休息的時間間隔(秒);
    2. 掃描結(jié)果中,對于容器類型(List, Hash, Set, ZSet)的 key,只能掃描出元素最多的 key;但一個 key 的元素多,不一定表示內(nèi)存占用也多,我們還需要根據(jù)業(yè)務(wù)情況,進(jìn)一步評估內(nèi)存占用情況。

  1. 使用開源的 redis-rdb-tools通過 redis-rdb-Tools,我們可以根據(jù)自己的標(biāo)準(zhǔn)準(zhǔn)確分析 Redis 實例中所有密鑰的實際內(nèi)存使用情況,同時它還可以避免中斷在線服務(wù),分析完成后,您可以獲得簡潔、易于理解的報告。 redis-rdb-Tools對 rdb文件的分析是離線的,對在線的 redis 服務(wù)沒有影響;這無疑是它對比第一種方案最大的優(yōu)勢,但也正是因為是離線分析,其分析結(jié)果的實時性可能達(dá)不到某些場景下的標(biāo)準(zhǔn),對大型 rdb文件的分析可能需要較長的時間。

針對 bigkey 問題的優(yōu)化措施:

  1. 上游業(yè)務(wù)應(yīng)避免在不合適的場景寫入 bigkey(夸張一點:用String存儲大型 binary file),如必須使用,可以考慮進(jìn)行 大key拆分,如:對于 string 類型的 Bigkey,可以考慮拆分成多個 key-value;對于 hash 或者 list 類型,可以考慮拆分成多個 hash 或者 list。
  2. 定期清理HASH key中的無效數(shù)據(jù)(使用 HSCAN和 HDEL),避免 HASH key中的成員持續(xù)增加帶來的 bigkey 問題。
  3. Redis ≥ 4.0中,用 UNLINK命令替代 DEL,此命令可以把釋放 key 內(nèi)存的操作,放到后臺線程中去執(zhí)行,從而降低對 Redis 的影響。
  4. Redis ≥ 6.0中,可以開啟 lazy-free 機(jī)制(lazyfree-lazy-user-del = yes),在執(zhí)行 DEL 命令時,釋放內(nèi)存也會放到后臺線程中執(zhí)行。
  5. 針對消息隊列 / 生產(chǎn)消費場景的 List, Set 等,設(shè)置過期時間或?qū)崿F(xiàn)定期清理任務(wù),并配置相關(guān)監(jiān)控以及時處理突發(fā)情況(如線上流量暴增,下有服務(wù)無法消費等產(chǎn)生的消費積壓)。

即便我們有一系列的解決方案,我們也要盡量避免在實例中存入 bigkey。這是因為 bigkey 在很多場景下,依舊會產(chǎn)生性能問題;例如,bigkey 在分片集群模式下,對于數(shù)據(jù)的遷移也會有性能影響;以及資源傾斜、數(shù)據(jù)過期、數(shù)據(jù)淘汰、透明大頁等,都會受到 bigkey 的影響。

Hotkey

在討論 bigkey 時,我們也經(jīng)常談到 hotkey ,當(dāng)訪問某個密鑰的工作量明顯高于其他密鑰時,我們可以稱之為 hotkey;以下就是一些 hotkey 的例子:

  • 在一個 QPS 10w 的 Redis 實例中,只有一個 key 的 QPS 達(dá)到了 7000 次;
  • 擁有數(shù)千個成員、總大小為 1MB 的哈希鍵每秒會收到大量的 HGETALL 請求(在這種情況下,我們將其稱為熱鍵,因為訪問一個鍵比訪問其他鍵消耗的帶寬要大得多);
  • 擁有數(shù)萬個 member 的 ZSET 每秒處理大量的 ZRANGE 請求(cpu時間明顯高于用于其他 key 請求的 cpu時間。同樣,我們可以說這種消耗大量 CPU 的 Key 就是 HotKey)。

hotkey 通常會帶來以下的問題:

  • hotkey 會導(dǎo)致較高的 CPU 負(fù)載,并影響其它請求的處理;
  • 資源傾斜,對 hotkey 的請求會集中在個別 Redis 節(jié)點/機(jī)器上,而不是shard到不同的 Redis 節(jié)點上,導(dǎo)致 內(nèi)存/CPU 負(fù)載集中在這個別的節(jié)點上,Redis 集群利用率不能達(dá)到預(yù)期;
  • hotkey 上的流量可能在流量高峰時突然飆升,導(dǎo)致 redis CPU 滿載甚至緩存服務(wù)崩潰,在緩存場景下導(dǎo)致緩存雪崩,大量的請求會直接命中其它較慢的數(shù)據(jù)源,最終導(dǎo)致業(yè)務(wù)不可用等不可接受的后果。

如何定位 hotkey:

  1. 使用redis-cli提供的 —hotkeys參數(shù) Redis 從 4.0版本開始在 redis-cli中提供 hotkey 參數(shù),以方便實例粒度的 hotkey 分析;它可以返回所有 key 被訪問的次數(shù),但需要先將 maxmemory policy設(shè)置為 allkey-LFU。# Scanning the entire keyspace to find hot keys as well as

    # average sizes per key type. You can use -i 0.1 to sleep 0.1 sec

    # per 100 SCAN commands (not usually needed).

    Error: ERR An LFU maxmemory policy is not selected, access frequency not tracked. Please note that when switching between policies at runtime LRU and LFU data will take some time to adjust.

  2. 使用monitor命令 Redis 的 monitor命令可以實時輸出 Redis 接收到的所有請求,包括 訪問時間、客戶端 IP、命令和 key;我們可以短時間執(zhí)行 monitor 命令,并將輸出重定向到文件;結(jié)束后,可以通過對文件中的請求進(jìn)行分類和分析來找到這段時間的 hotkey。 monitor命令會消耗大量 CPU、內(nèi)存和網(wǎng)絡(luò)資源;因此,對于本身就負(fù)載較大的 Redis 實例來說, monitor命令可能會讓性能問題進(jìn)一步惡化;同時,這種異步采集分析方案的時效性較差,分析的準(zhǔn)確性依賴于 monitor命令的執(zhí)行時長;因此,在大多數(shù)無法長時間執(zhí)行該命令的在線場景中,結(jié)果的準(zhǔn)確性并不好。
  3. 上游服務(wù)針對 redis 請求進(jìn)行監(jiān)控 所有的 redis 請求都來自于上游服務(wù),上游服務(wù)可以在上報時進(jìn)行相關(guān)的指標(biāo)監(jiān)控、匯總及分析,以定位 hotkey ;但這樣的方式需要上游服務(wù)支持,并不獨立。

針對 hotkey 問題的優(yōu)化方案:

1.使用pipeline

在一些非實時的 bigkey 請求場景下,我們可以使用 pipeline來大幅度降低 Redis 實例的 CPU 負(fù)載。

首先我們要知道,Redis 核心的工作負(fù)荷是一個單線程在處理,這里指的是——網(wǎng)絡(luò) IO命令執(zhí)行是由一個線程來完成的;而 Redis 6.0 中引入了多線程,在 Redis 6.0之前,從網(wǎng)絡(luò) IO 處理到實際的讀寫命令處理都是由單個線程完成的,但隨著網(wǎng)絡(luò)硬件的性能提升,Redis 的性能瓶頸有可能會出現(xiàn)在網(wǎng)絡(luò) IO 的處理上,也就是說單個主線程處理網(wǎng)絡(luò)請求的速度跟不上底層網(wǎng)絡(luò)硬件的速度。針對此問題,Redis 采用多個 IO 線程來處理網(wǎng)絡(luò)請求,提高網(wǎng)絡(luò)請求處理的并行度,但多 IO 線程只用于處理網(wǎng)絡(luò)請求,對于命令處理,Redis 仍然使用單線程處理

而 Redis 6.0 以前的單線程網(wǎng)絡(luò) IO 模型的處理具體的負(fù)載在哪里呢?雖然 Redis 利用epoll機(jī)制實現(xiàn) IO 多路復(fù)用(即使用 epoll監(jiān)聽各類事件,通過事件回調(diào)函數(shù)進(jìn)行事件處理),但 I/O 這一步驟是無法避免且始終由單線程串行處理的,且涉及用戶態(tài)/內(nèi)核態(tài)的切換,即:

  • 從socket中讀取請求數(shù)據(jù),會從內(nèi)核態(tài)將數(shù)據(jù)拷貝到用戶態(tài) ( read調(diào)用)
  • 將數(shù)據(jù)回寫到socket,會將數(shù)據(jù)從用戶態(tài)拷貝到內(nèi)核態(tài) ( write調(diào)用)

高頻簡單命令請求下,用戶態(tài)/內(nèi)核態(tài)的切換帶來的開銷被更加放大,最終會導(dǎo)致redis-servercpu滿載 → redis-serverOPS不及預(yù)期 → 上游服務(wù)海量請求超時 → 最終造成類似 緩存穿透的結(jié)果,這時我們就可以使用 pipeline來處理這樣的場景了:redis pipeline。

眾所周知,redis pipeline可以讓 redis-server一次接收一組指令(在內(nèi)核態(tài)中存入輸入緩沖區(qū),收到客戶端的 Exec指令再調(diào)用 read syscall)后再執(zhí)行,減少 I/O(即 accept -> read -> write)次數(shù),在 高頻可聚合命令的場景下使用 pipeline可以大大減少 socket I/O帶來的 內(nèi)核態(tài)與用戶態(tài)之間的上下文切換開銷

下面我們進(jìn)行跑一組基于golang redis客戶端的簡單高頻命令的 Benchmark測試(不使用 pipeline和使用 pipeline對比),同時使用perf對 Redis 4 實例監(jiān)控上下文切換次數(shù):

  • Set without Pipeline(redis 4.0.14)perf stat-p 15537 -e context-switches -a sleep 10

    Performance counter stats forprocess id '15537':

    96,301 context-switches

    10.001575750 seconds time elapsed

  • Set using Pipeline(redis 4.0.14)perf stat-p 15537 -e context-switches -a sleep 10

    Performance counter stats forprocess id '15537':

    17 context-switches

    10.001722488 seconds time elapsed

可以看到在不使用pipeline執(zhí)行高頻簡單命令時產(chǎn)生了大量的上下文切換,這無疑會占用大量的 cpu時間。

另一方面,pipeline雖然好用,但是每次 pipeline組裝的命令個數(shù)不能沒有節(jié)制,否則一次組裝 pipeline數(shù)據(jù)量過大,一方面會增加客戶端的等待時間,另一方面會造成一定的網(wǎng)絡(luò)阻塞,可以將一次包含大量命令的 pipeline拆分成多次較小的 pipeline來完成,比如可以將 pipeline的總發(fā)送大小控制在內(nèi)核輸入輸出緩沖區(qū)大小之內(nèi)(內(nèi)核的輸入輸出緩沖區(qū)大小一般是 4K-8K,在不同操作系統(tǒng)中有所差異,可配置修改),同時控制在 單個 TCP 報文最大值 1460 字節(jié)之內(nèi)。

最大傳輸單元(MTU — Maximum Transmission Unit)在以太網(wǎng)中的最大值是1500 字節(jié),扣減 20 個字節(jié)的 IP頭和 20 個字節(jié)的 TCP頭,即 1460 字節(jié)

2.MemCache當(dāng) hotkey 本身可預(yù)估,且總大小可控時,我們可以考慮使用MemCache直接存儲:

  • 省去了 Redis 接入
  • 直接的內(nèi)存讀取,保證高性能
  • 擺脫帶寬限制

但同時它也帶來了新的問題:

  • 在像k8s這樣的高可用多實例架構(gòu)下,多 pod間的同步以及和原始數(shù)據(jù)庫的同步是一個大問題,很有可能導(dǎo)致 臟讀
  • 同樣是在多實例的情況下,會帶來很多的內(nèi)存浪費。

同時 MemCache 相比于 Redis 也少了很多 feature ,可能不能滿足業(yè)務(wù)需求

FeatureRedisMemCache原生支持不同的數(shù)據(jù)結(jié)構(gòu)??原生支持持久化??橫向擴(kuò)展(replication)??聚合操作??支持高并發(fā)??

3.Redis 讀寫分離

當(dāng)對 hotkey 的請求僅僅集中在讀上時,我們可以考慮讀寫分離的 Redis 集群方案(很多公有云廠商都有提供),針對 hotkey 的讀請求,新增read-only replica來承擔(dān)讀流量,原 replica作為熱備不提供服務(wù),如下圖所示(鏈?zhǔn)綇?fù)制架構(gòu))。

這里我們不展開講讀寫分離的其它優(yōu)勢,僅針對讀多寫少的業(yè)務(wù)場景來說,使用讀寫分離的 Redis 提供了更多的選擇,業(yè)務(wù)可以根據(jù)場景選擇最適合的規(guī)格,充分利用每一個read-only replica的資源,且讀寫分離架構(gòu)還有比較好的橫向擴(kuò)容能力、客戶端友好等優(yōu)勢。

規(guī)格QPS帶寬1 master8-10 萬讀寫10-48 MB1 master + 1 read-only replica10 萬寫 + 10 萬讀20-64 MB1 master + 3 read-only replica10 萬寫 + 30 萬讀40-128 MBn _ master + m _ read-only replican _ 100,000 write + m _ 100,000 read10(m+n) MB - 32(m+n) MB

當(dāng)然我們也不能忽略讀寫分離架構(gòu)的缺點,在有大量寫請求的場景中,讀寫分離架構(gòu)將不可避免地產(chǎn)生延遲,這很有可能造成臟讀,所以讀寫分離架構(gòu)不適用于讀寫負(fù)載都較高以及實時性要求較高的場景。

Key 集中過期

當(dāng) Redis 實例表現(xiàn)出的現(xiàn)象是:周期性地在一個小的時間段出現(xiàn)一波延遲高峰時,我們就需要 check 一下是否有大批量的 key 集中過期;那么為什么 key 集中過期會導(dǎo)致 Redis 延遲變大呢?

我們首先來了解一下 Redis 的過期策略是怎樣的,Redis 處理過期 key 的方式有兩種——被動方式和主動方式

被動方式

key過期的時候不刪除,每次從 Redis 獲取 key時檢查是否過期,若過期,則刪除,返回 null。

優(yōu)點:刪除操作只發(fā)生在從數(shù)據(jù)庫取出 key 的時候發(fā)生,而且只刪除當(dāng)前key,所以對 CPU 時間的占用是比較少的。

缺點:若大量的key在超出超時時間后,很久一段時間內(nèi),都沒有被獲取過,此時的無效緩存是永久暫用在內(nèi)存中的,那么可能發(fā)生內(nèi)存泄露(無效 key占用了大量的內(nèi)存)。

主動方式

Redis 每100ms執(zhí)行以下步驟:

  1. 抽樣檢查附加了TTL的 20 個隨機(jī) key(環(huán)境變量 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP,默認(rèn)為 20);
  2. 刪除抽樣中所有過期的key;
  3. 如果超過25%的 key過期,重復(fù)步驟 1。

優(yōu)點:通過限制刪除操作的時長和頻率,來限制刪除操作對 CPU 時間的占用;同時解決被動方式中無效 key存留的問題。

缺點: 仍然可能有最高達(dá)到25%的無效key存留;在 CPU時間友好方面,不如 被動方式,主動方式會 block住主線程。

難點: 需要合理設(shè)置刪除操作的執(zhí)行時長(每次刪除執(zhí)行多長時間)和執(zhí)行頻率(每隔多長時間做一次刪除,這要根據(jù)服務(wù)器運行情況和實際需求來決定)。

如果 Redis 實例配置為上面的主動方式的,當(dāng) Redis 中的 key 集中過期時,Redis 需要處理大量的過期 key;這無疑會增加 Redis 的 CPU 負(fù)載和內(nèi)存使用,可能會使 Redis 變慢,特別當(dāng) Redis 實例中存在 bigkey 時,這個耗時會更久;而且這個耗時不會被記錄在slow log中:

解決方案:

為了避免這種情況,可以考慮以下幾種方法:

  • 盡量避免 key 集中過期,如果需要批量插入 key(如批量插入一批設(shè)置了同樣ExpireAt的 key),可以通過額外的小量隨機(jī)過期時間來打散 key 的過期時間;
  • 在 Redis 4.0 以上的版本中提供了 lazy-free 選項,當(dāng)刪除過期 key 時,把釋放內(nèi)存的操作放到后臺線程中執(zhí)行,避免阻塞主線程。
lazyfree-lazy-expire yes

從監(jiān)控的角度出發(fā),我們還需要建立對expired_keys的實時監(jiān)控和突增告警,以及時發(fā)出告警幫助我們定位到業(yè)務(wù)中的相關(guān)問題。

觸及 maxmemory

當(dāng)我們的 Redis 實例達(dá)到了設(shè)置的內(nèi)存上限時,我們也會很明顯地感知到 Redis 延遲增大。

究其原因,當(dāng) Redis 達(dá)到 maxmemory 后,如果繼續(xù)往 Redis 中寫入數(shù)據(jù),Redis 將會觸發(fā)內(nèi)存淘汰策略來清理一些數(shù)據(jù)以騰出內(nèi)存空間,這個過程需要耗費一定的 CPU 和內(nèi)存資源,如果淘汰過程中產(chǎn)生了大量的 Swap 交換或者內(nèi)存回收,將會導(dǎo)致 Redis 變慢,甚至可能導(dǎo)致 Redis 崩潰。

常見的驅(qū)逐策略有以下幾種:

  • noeviction: 不刪除策略,達(dá)到最大內(nèi)存限制時,如果需要更多內(nèi)存,直接返回錯誤信息;大多數(shù)寫命令都會導(dǎo)致占用更多的內(nèi)存(有極少數(shù)會例外, 如 DEL );
  • allkeys-lru: 所有 key 通用; 優(yōu)先刪除最長時間未被使用(less recently used ,LRU) 的 key;
  • volatile-lru: 只限于設(shè)置了 expire 的部分; 優(yōu)先刪除最長時間未被使用(less recently used ,LRU) 的 key;
  • allkeys-random: 所有 key 通用; 隨機(jī)刪除一部分 key;
  • volatile-random: 只限于設(shè)置了 expire 的部分; 隨機(jī)刪除一部分 key;
  • volatile-ttl: 只限于設(shè)置了 expire 的部分; 優(yōu)先刪除剩余時間(time to live,TTL) 短的 key;
  • volatile-lfu: added in Redis 4, 從設(shè)置了 expire 的 key 中刪除使用頻率最低的 key;
  • allkeys-lfu: added in Redis 4, 從所有 key 中刪除使用頻率最低的 key。

最常用的驅(qū)逐策略是allkeys-lru / volatile-lru。

?? 需要注意的是:Redis 的淘汰數(shù)據(jù)的邏輯與刪除過期 key 的一樣,也是在命令真正執(zhí)行之前執(zhí)行的,也就是說它也會增加我們操作 Redis 的延遲,并且寫 OPS 越高,延遲也會越明顯。

另外,如果 Redis 實例中還存儲了 bigkey,那么在淘汰刪除 bigkey 釋放內(nèi)存時,也會耗時比較久

解決方案:

為了避免 Redis 達(dá)到 maxmemory 后變慢,可以考慮以下幾種解決方案:

  1. 設(shè)置合理的 maxmemory,可以根據(jù)實際情況設(shè)置 Redis 的 maxmemory,避免 Redis 在運行過程中出現(xiàn)內(nèi)存不足的情況(大白話就是加錢加內(nèi)存);
  2. 開啟 Redis 的持久化功能,以將 Redis 中的數(shù)據(jù)持久化到磁盤中,避免數(shù)據(jù)丟失,并且在 Redis 重啟后可以快速地恢復(fù)加載數(shù)據(jù);
  3. 使用 Redis 的分區(qū)功能,將 Redis 中的數(shù)據(jù)分散到多個 Redis 實例中,以減輕單個 Redis 實例內(nèi)存淘汰的負(fù)載壓力;
  4. 與刪除過期 key 一樣,針對淘汰 key 也可以開啟layz-free,把淘汰 key 釋放內(nèi)存的操作放到后臺線程中執(zhí)行。
lazyfree-lazy-eviction yes 持久化耗時

為了保證 Redis 數(shù)據(jù)的安全性,我們可能會開啟后臺定時 RDB 和 AOF rewrite 功能:

而為了在后臺生成RDB文件,或者在啟用 AOF持久化的情況下追加寫只讀 AOF文件,Redis 都需要 fork一個子進(jìn)程, fork操作(在主線程中運行)本身可能會導(dǎo)致延遲。

下圖分別是AOF持久化和 RDB持久化的流程圖:

在大多數(shù)類 Unix 系統(tǒng)上,fork的成本都很高,因為它涉及復(fù)制與進(jìn)程相關(guān)聯(lián)的許多對象,尤其是與虛擬內(nèi)存機(jī)制相關(guān)聯(lián)的 頁表。

例如,在linux/AMD64系統(tǒng)上,內(nèi)存被劃分為 4kB的頁(如不開啟內(nèi)存大頁);而為了將虛擬地址轉(zhuǎn)換為物理地址,每個進(jìn)程存儲了一個頁表,該頁表包含該進(jìn)程的地址空間每一頁的至少一個指針;一個大小為 24 GB的 Redis 實例就會需要一個 24 GB / 4 kB * 8 = 48 MB的頁表。

在執(zhí)行后臺持久化時,就需要fork此實例,也就需要為頁表分配和復(fù)制 48MB的內(nèi)存;這無疑會耗費大量 CPU 時間,特別是在部分虛擬機(jī)上,分配和初始化大內(nèi)存塊本身成本就更高。

可以看到在Xen上運行的某些 VM的 fork耗時比在物理機(jī)上要高一個數(shù)量級到兩個數(shù)量級。

如何查看 fork 耗時:

我們可以在 redis-cli上執(zhí)行 INFO 命令,查看 latest_fork_usec項:

INFO latest_fork_usec# 上一次 fork 耗時,單位為微秒latest_fork_usec:59477

這個時間就是主進(jìn)程在 fork子進(jìn)程期間,整個實例阻塞無法處理客戶端請求的時間;這個時間對于大多數(shù)業(yè)務(wù)來說無疑是不能過高的(如達(dá)到秒級)。

除了定時的數(shù)據(jù)持久化會生成 RDB之外,當(dāng)主從節(jié)點第一次建立數(shù)據(jù)同步時,主節(jié)點也會創(chuàng)建子進(jìn)程生成 RDB,然后發(fā)給從節(jié)點進(jìn)行一次全量同步,所以,這個過程也會對 Redis 產(chǎn)生性能影響。

解決方案:

  1. 更改持久化模式 如果 Redis 的持久化模式為RDB,我們可以嘗試使用 AOF模式來減少持久化的耗時的突增(AOF rewrite 可以是多次的追加寫)。
  2. 優(yōu)化寫入磁盤的速度 如果 Redis 所在的磁盤寫入速度較慢,我們可以嘗試將 Redis 遷移到寫入速度更快的磁盤上。
  3. 控制 Redis 實例的內(nèi)存 用作緩存的 Redis 實例盡量在 10G 以下,執(zhí)行 fork 的耗時與實例大小有關(guān),實例越大,耗時越久。
  4. 避免虛擬化部署 Redis 實例不要部署在虛擬機(jī)上,fork 的耗時也與系統(tǒng)也有關(guān),虛擬機(jī)比物理機(jī)耗時更久。
  5. 合理配置數(shù)據(jù)持久化策略 于低峰期在 slave 節(jié)點執(zhí)行 RDB 備份;而對于丟失數(shù)據(jù)不敏感的業(yè)務(wù)(例如把 Redis 當(dāng)做純緩存使用),可以關(guān)閉 AOF 和 AOF rewrite。
  6. 降低主從庫全量同步的概率 適當(dāng)調(diào)大 repl-backlog-size參數(shù),避免主從全量同步。
開啟內(nèi)存大頁

在上面提到的定時 RDB 和 AOF rewrite 持久化功能中,除了fork本身帶來的頁表復(fù)制的耗時外,還會有 內(nèi)存大頁帶來的延遲。

內(nèi)存頁是用戶應(yīng)用程序向操作系統(tǒng)申請內(nèi)存的單位,常規(guī)的內(nèi)存頁大小是 4KB,而 Linux 內(nèi)核從 2.6.38 開始,支持了 內(nèi)存大頁機(jī)制,該機(jī)制允許應(yīng)用程序以 2MB大小為單位,向操作系統(tǒng)申請內(nèi)存。

在開啟內(nèi)存大頁的機(jī)器上調(diào)用bgsave或者 bgrewriteaoffork 出子進(jìn)程后,此時 主進(jìn)程依舊是可以接收寫請求的,而此時處理寫請求,會采用 Copy On Write(寫時復(fù)制)的方式操作內(nèi)存數(shù)據(jù)(兩個進(jìn)程共享內(nèi)存大頁,僅需復(fù)制一份 頁表)。

在寫負(fù)載較高的 Redis 實例中,不斷處理寫命令將導(dǎo)致命令針對幾千個內(nèi)存大頁(哪怕只涉及一個內(nèi)存大頁上的一小部分?jǐn)?shù)據(jù)更改),導(dǎo)致幾乎整個進(jìn)程內(nèi)存的COW,這將造成這些寫命令巨大的延遲,以及巨大的額外峰值內(nèi)存。

同樣的,如果這個寫請求操作的是一個 bigkey,那主進(jìn)程在拷貝這個 bigkey 內(nèi)存塊時,涉及到的內(nèi)存大頁會更多,時間也會更久,十惡不赦的 bigkey在這里又一次影響到了性能。

無疑在開啟AOF / RDB時,我們需要關(guān)閉內(nèi)存大頁。我們可以使用以下命令查看是否開啟了內(nèi)存大頁:

$ cat /sys/kernel/mm/transparent_hugepage/enabled[always] madvise never

如果該文件的輸出為 [always]或 [madvise],則透明大頁是啟用的;如果輸出為 [never],則透明大頁是禁用的

在 Linux 系統(tǒng)中,可以使用以下命令來關(guān)閉透明大頁:

typeCopy codeecho never > /sys/kernel/mm/transparent_hugepage/enabledecho never > /sys/kernel/mm/transparent_hugepage/defrag

第一行命令將透明大頁的使用模式設(shè)置為 never,第二行命令將透明大頁的碎片整理模式設(shè)置為 never;這樣就可以關(guān)閉透明大頁了。

AOF 和磁盤 I/O 造成的延遲

針對AOF(Append Only File)持久化策略來說,除了前面提到的 fork子進(jìn)程追加寫文件會帶來性能損耗造成延遲。

首先我們來詳細(xì)看一下AOF的實現(xiàn)原理,AOF 基本上依賴兩個 系統(tǒng)調(diào)用來完成其工作;一個是 WRITE(2),用于將數(shù)據(jù)寫入 Append Only文件,另一個是 fDataync(2),用于刷新磁盤上的 內(nèi)核文件緩沖區(qū),以確保用戶指定的持久性級別,而 WRITE(2)和 fDatync(2)調(diào)用都可能是延遲的來源。

對 WRITE(2)來說,當(dāng)系統(tǒng)范圍的磁盤緩沖區(qū)同步正在進(jìn)行時,或者當(dāng)輸出緩沖區(qū)已滿并且內(nèi)核需要刷新磁盤以接受新的寫入時, WRITE(2)都會因此阻塞。

對fDataync(2)來說情況更糟,因為使用了許多內(nèi)核和文件系統(tǒng)的組合,我們可能需要幾毫秒到幾秒的時間才能完成 fDataync(2),特別是在某些其它進(jìn)程正在執(zhí)行 I/O 的情況下;因此, Redis 2.4之后版本會盡可能在另一個線程執(zhí)行 fDataync(2)調(diào)用。

解決方案:

最直接的解決方案當(dāng)然是從 redis 配置出發(fā),那么有哪些配置會影響到這兩個系統(tǒng)調(diào)用的執(zhí)行策略呢。我們可以使用appendfsync配置,該配置項提供了三種磁盤緩沖區(qū)刷新策略

  • no當(dāng)appendfsync被設(shè)置為 no時,redis 不會再執(zhí)行 fsync,在這種情況下唯一的延遲來源就只有 WRITE(2)了,但這種情況很少見,除非磁盤無法處理 Redis 接收數(shù)據(jù)的速度(不太可能),或是磁盤被其他 I/O 密集型進(jìn)程嚴(yán)重減慢。 這種方案對 Redis 影響最小,但當(dāng) Redis 所在的服務(wù)器宕機(jī)時,會丟失一部分?jǐn)?shù)據(jù),為了數(shù)據(jù)的安全性,一般我們也不采取這種配置。
  • everysec當(dāng)appendfsync被設(shè)置為 everysec時,redis 每秒執(zhí)行一次 fsync,這項工作在非主線程中完成?? 需要注意的是:對于用于追加寫入 AOF文件的 WRITE(2)系統(tǒng)調(diào)用,如果執(zhí)行時 fsync仍在進(jìn)行中,Redis 將使用一個緩沖區(qū)將 WRITE(2)調(diào)用延遲兩秒(因為在 Linux上,如果正在對同一文件進(jìn)行 fsync, WRITE就會阻塞);但如果 fsync花費的時間太長,即使 fsync仍在進(jìn)行中,Redis 最終也會執(zhí)行 WRITE(2)調(diào)用,造成延遲。針對這種情況,Redis 提供了一個配置項,當(dāng)子進(jìn)程在追加寫入 AOF文件期間,可以讓后臺子線程不執(zhí)行刷盤(不觸發(fā) fsync 系統(tǒng)調(diào)用)操作,也就是相當(dāng)于在追加寫 AOF期間,臨時把 appendfsync設(shè)置為了 no,配置如下: # AOF rewrite 期間,AOF 后臺子線程不進(jìn)行刷盤操作

    # 相當(dāng)于在這期間,臨時把 appendfsync 設(shè)置為了 none

    no-appendfsync-on-rewrite yes

    當(dāng)然,開啟這個配置項,在追加寫 AOF期間,如果實例發(fā)生宕機(jī),就會丟失更多的數(shù)據(jù)。

  • always當(dāng)appendfsync被設(shè)置為 always時,每次寫入操作時都執(zhí)行 fsync,完成后才會發(fā)送 response回客戶端(實際上,Redis 會嘗試將同時執(zhí)行的多個命令聚集到單個 fsync 中)。在這種模式下,性能通常非常差,如果一定要達(dá)到這個持久化的要求并使用這個模式,就需要使用能夠在短時間內(nèi)執(zhí)行 fsync的 高速磁盤以及 文件系統(tǒng)實現(xiàn)。

大多數(shù) Redis 用戶使用no或 everysec

并且為了最小化AOF帶來的延遲,最好也要 避免其他進(jìn)程在同一系統(tǒng)中執(zhí)行 I/O;當(dāng)然,使用 SSD磁盤也會有所幫助(加 ),但通常情況下,即使是非 SSD 磁盤,如果磁盤沒有被其它進(jìn)程占用,Redis 也能在寫入 Append Only File時保持良好的性能,因為 Redis 在寫入 Append Only File時不需要任何 seek操作。

我們可以使用strace命令查看 AOF帶來的延遲:

sudo strace -p $(pidof redis-server) -T -e trace=fdatasync

上面的命令將展示 Redis 在主線程中執(zhí)行的所有fdatync(2)系統(tǒng)調(diào)用,但當(dāng) appendfsync配置選項設(shè)置為 everysec時,我們監(jiān)控不到 后臺線程執(zhí)行的 fdatync(2);為此我們需將 -f option加到上述命令中,這樣就可以看到子線程執(zhí)行的 fdatync(2)了。

如果需要的話,我們還可以將write添加到 trace項中以監(jiān)控 WRITE(2)系統(tǒng)調(diào)用:

sudo strace -p $(pidof redis-server) -T -e trace=fdatasync,write

但是,由于WRITE(2)也用于將數(shù)據(jù)寫入客戶端 socket以回復(fù)客戶端請求,該命令也會顯示許多與磁盤 I/O 無關(guān)的內(nèi)容;為了解決這個問題我們可以使用以下命令:

sudo strace -f -p $(pidof redis-server) -T -e trace=fdatasync,write 2>&1 | grep -v '0.0' | grep -v unfinished SWAP 導(dǎo)致的延遲

Linux(以及許多其它現(xiàn)代操作系統(tǒng))能夠?qū)?nèi)存頁面從內(nèi)存重新定位到磁盤,反之亦然,以便有效地使用系統(tǒng)內(nèi)存。

如果內(nèi)核將 Redis 內(nèi)存頁從內(nèi)存移動到SWAP 分區(qū),則當(dāng)存儲在該內(nèi)存頁中的數(shù)據(jù)被 Redis 使用時(例如,訪問存儲在該內(nèi)存頁中的 key),內(nèi)核將停止 Redis 進(jìn)程,以便將該內(nèi)存頁移回內(nèi)存;這是一個涉及 隨機(jī)I/O的緩慢磁盤操作(與訪問已在內(nèi)存中的內(nèi)存頁相比慢一到兩個數(shù)量級),并將導(dǎo)致 Redis 客戶端的異常延遲。

Linux 內(nèi)核執(zhí)行 SWAP主要有以下三個原因:

  • 系統(tǒng)已使用內(nèi)存達(dá)到內(nèi)存上限,有可能是 Redis 使用的內(nèi)存超過了系統(tǒng)可用內(nèi)存,也可能是其它進(jìn)程導(dǎo)致的;
  • Redis 實例的數(shù)據(jù)集或數(shù)據(jù)集的一部分幾乎是完全空閑的(客戶端從未訪問過),因此內(nèi)核可以交換內(nèi)存中的空閑內(nèi)存頁到磁盤;這種問題非常少見,因為即使是中等速度的實例也會經(jīng)常接觸所有內(nèi)存頁,迫使內(nèi)核將所有內(nèi)存頁保留在內(nèi)存中;
  • 一些進(jìn)程在系統(tǒng)上產(chǎn)生大量讀寫 I/O。因為文件通常是緩存的,所以它往往會給內(nèi)核帶來增加文件系統(tǒng)緩存的壓力,從而產(chǎn)生SWAP;當(dāng)然,這里說的進(jìn)程也包括可能產(chǎn)生大文件的 Redis RDB和 AOF后臺線程。

我們可以通過以下命令查看 Redis 的SWAP情況:

首先我們獲取到 redis-server的 pid:

$ redis-cli info | grep process_idprocess_id:9

接下來查看Redis Swap的使用情況:

# $pid改為剛剛獲取到的redis-server的pidcat /proc/$pid/smaps | egrep '^(Swap|Size)'

產(chǎn)生類似下面的輸出:

Size: 316 kBSwap: 0 kBSize: 4 kBSwap: 0 kBSize: 8 kBSwap: 0 kBSize: 40 kBSwap: 0 kBSize: 132 kBSwap: 0 kBSize: 720896 kBSwap: 12 kBSize: 4096 kBSwap: 156 kBSize: 4096 kBSwap: 8 kBSize: 4096 kBSwap: 0 kBSize: 4 kBSwap: 0 kBSize: 1272 kBSwap: 0 kBSize: 8 kBSwap: 0 kBSize: 4 kBSwap: 0 kBSize: 16 kBSwap: 0 kBSize: 84 kBSwap: 0 kBSize: 4 kBSwap: 0 kBSize: 4 kBSwap: 0 kBSize: 8 kBSwap: 4 kBSize: 8 kBSwap: 0 kBSize: 4 kBSwap: 0 kBSize: 4 kBSwap: 4 kBSize: 144 kBSwap: 0 kBSize: 4 kBSwap: 0 kBSize: 4 kBSwap: 4 kBSize: 12 kBSwap: 4 kBSize: 108 kBSwap: 0 kBSize: 4 kBSwap: 0 kBSize: 4 kBSwap: 0 kBSize: 272 kBSwap: 0 kBSize: 4 kBSwap: 0 kB

每一行 Size 表示 Redis 所用的一塊內(nèi)存大小,Size 下面的 Swap 就表示這塊 Size 大小的內(nèi)存有多少數(shù)據(jù)已經(jīng)被換到磁盤上了。

但如果存在SWAP比例較大的輸出,那么 Redis 的延遲很大可能就是 SWAP導(dǎo)致的。我們可以使用 vmstat命令進(jìn)一步驗證:

$ vmstat 1procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu---- r b swpd free buff cache si so bi bo in cs us sy id wa 0 0 3980 697932 147180 1406456 0 0 2 2 2 0 4 4 91 0 0 0 3980 697428 147180 1406580 0 0 0 0 19088 16104 9 6 84 0 0 0 3980 697296 147180 1406616 0 0 0 28 18936 16193 7 6 87 0 0 0 3980 697048 147180 1406640 0 0 0 0 18613 15987 6 6 88 0 2 0 3980 696924 147180 1406656 0 0 0 0 18744 16299 6 5 88 0 0 0 3980 697048 147180 1406688 0 0 0 4 18520 15974 6 6 88 0

我們看到si和 so這兩列,它們分別是內(nèi)存中 SWAP到文件的 Size 以及從文件中 SWAP到內(nèi)存的 Size;如果這兩列中存在非零值,則表示系統(tǒng)中存在 SWAP活動。

最后我們還可以使用IOStat命令查看系統(tǒng)的全局 I/O活動:

$ iostat -xk 1avg-cpu: %user %nice %system %iowait %steal %idle 13.55 0.04 2.92 0.53 0.00 82.95Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await svctm %utilsda 0.77 0.00 0.01 0.00 0.40 0.00 73.65 0.00 3.62 2.58 0.00sdb 1.27 4.75 0.82 3.54 38.00 32.32 32.19 0.11 24.80 4.24 1.85

解決方案:

這種情況下基本沒有什么可以多說的解決方案,無非就兩方面:

  1. 加內(nèi)存(加 ),沒有什么是懟資源無法解決的;
  2. 減少業(yè)務(wù)側(cè)對 Redis 的使用量,包括調(diào)整過期時間、優(yōu)化數(shù)據(jù)結(jié)構(gòu)、調(diào)整緩存策略等等;

另一方面自然是做好 Redis 機(jī)器的內(nèi)存監(jiān)控以及SWAP事件監(jiān)控,在內(nèi)存不足及 SWAP事件激增時及時告警。

內(nèi)存碎片

Redis 內(nèi)存碎片率(used_memory_rss / used_memory)大于 1表示正在發(fā)生碎片,內(nèi)存碎片率超過 1.5 表示碎片過多,Redis 實例消耗了其實際申請的物理內(nèi)存的 150%的內(nèi)存;另一方面,如果 內(nèi)存碎片率低于 1,則表示 Redis 需要的內(nèi)存多于系統(tǒng)上的可用內(nèi)存,這會導(dǎo)致 SWAP 操作,其中內(nèi)存交換到磁盤的 CPU 時間成本將導(dǎo)致 Redis 延遲顯著增加。

為什么會產(chǎn)生內(nèi)存碎片:

主要有兩大原因:

  • redis自己實現(xiàn)的內(nèi)存分配器:在 redis中新建 key-value值時, redis需要向操作系統(tǒng)申請內(nèi)存,一般的進(jìn)程在不需要使用申請的內(nèi)存后,會直接釋放掉、歸還內(nèi)存;但 redis不一樣, redis在使用完內(nèi)存后并不會直接歸還內(nèi)存,而是放在 redis自己實現(xiàn)的內(nèi)存分配器中管理,這樣就不需要每次都向操作系統(tǒng)申請內(nèi)存了,實現(xiàn)了高性能;但另一方面,未歸還的內(nèi)存自然也就造成了 內(nèi)存碎片。
  • value的更新: redis的每個 key-value對初始化的內(nèi)存大小是最適合的,當(dāng)這個 value改變的并且原來內(nèi)存塊不適用的時候,就需要重新分配內(nèi)存了;而重新分配之后,就會有一部分內(nèi)存 redis無法正常回收,造成了 內(nèi)存碎片。

我們可以通過執(zhí)行 INFO 命令快速查詢到一個 Redis 實例的內(nèi)存碎片率(mem_fragmentation_ratio):

[db0] > INFO memory# Memoryused_memory:215489640used_memory_human:205.51M...mem_fragmentation_ratio:1.13mem_fragmentation_bytes:27071448...

理想情況下,操作系統(tǒng)將在物理內(nèi)存中分配一個連續(xù)的段,Redis 的內(nèi)存碎片率等于 1 或略大于 1;碎片率過大會導(dǎo)致內(nèi)存無法有效利用,進(jìn)而導(dǎo)致 redis 頻繁進(jìn)行內(nèi)存分配和回收,從而導(dǎo)致用戶請求延遲,并且這個延遲是不會計入slowlog的。

如何清理內(nèi)存碎片:

若在Redis < 4.0的版本,如果內(nèi)存碎片率高于 1.5,直接重啟 Redis 實例就可以讓操作系統(tǒng)恢復(fù)之前因碎片而無法使用的內(nèi)存;但在這種情況下,也許監(jiān)控并發(fā)出告警就足夠了,直接重啟在大多數(shù)場景下并不適用;但當(dāng)內(nèi)存碎片率低于 1 時,我們就需要一個高級別的告警,以快速增加可用內(nèi)存或減少內(nèi)存使用量。

Redis ≥ 4.0開始,當(dāng) Redis 配置為使用包含的 jemalloc副本時,可以使用主動碎片整理功能;它可以配置為在碎片達(dá)到一定百分比時啟動,將數(shù)據(jù)復(fù)制到 連續(xù)的內(nèi)存區(qū)域并釋放舊數(shù)據(jù),從而減少內(nèi)存碎片。

redis-cli開啟自動內(nèi)存碎片清理:

127.0.0.1:6379[6]> config set activedefrag yesOK

redis.conf中相關(guān)的配置項:

# Enabled active defragmentation# 碎片整理總開關(guān)# activedefrag yes# Minimum amount of fragmentation waste to start active defrag# 內(nèi)存碎片達(dá)到多少的時候開啟整理active-defrag-ignore-bytes 100mb# Minimum percentage of fragmentation to start active defrag# 碎片率達(dá)到百分之多少開啟整理active-defrag-threshold-lower 10# Maximum percentage of fragmentation at which we use maximum effort# 碎片率小余多少百分比開啟整理active-defrag-threshold-upper 100

當(dāng)然,在面對一些復(fù)雜的場景時我們希望能根據(jù)自己設(shè)計的策略來進(jìn)行內(nèi)存碎片清理,redis也提供了手動內(nèi)存碎片清理的命令:

127.0.0.1:6379> memory purgeOK 綁定 CPU 單核

很多時候,我們在部署服務(wù)時,為了提高服務(wù)性能,降低應(yīng)用程序在多個 CPU 核心之間的上下文切換帶來的性能損耗,通常采用的方案是進(jìn)程綁定 CPU 的方式提高性能。

但 Vanilla Redis并不適合綁定到單個 CPU 核心上。一般現(xiàn)代的服務(wù)器會有多個 CPU,而每個 CPU 又包含多個 物理核心,每個 物理核心又分為多個 邏輯核心,每個物理核下的邏輯核共用 L1/L2 Cache。而 Redis 會 fork出非常消耗 CPU 的后臺任務(wù),如 BGSAVE或 BGREWRITEAOF、異步釋放 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)程發(fā)生 CPU 爭搶,進(jìn)而影響到主進(jìn)程服務(wù)客戶端請求,訪問延遲變大。

解決方案:

  1. 綁定多個邏輯核心如果你確實想要綁定 CPU,可以優(yōu)化的方案是,不要讓 Redis 進(jìn)程只綁定在一個 CPU 邏輯核上,而是綁定在多個邏輯核心上,而且,綁定的多個邏輯核心最好是同一個物理核心,這樣它們還可以共用 L1/L2 Cache。當(dāng)然,即便我們把 Redis 綁定在多個邏輯核心上,也只能在一定程度上緩解主線程、子進(jìn)程、后臺線程在 CPU 資源上的競爭,因為這些子進(jìn)程、子線程還是會在這多個邏輯核心上進(jìn)行切換,依舊存在性能損耗。
  2. 針對各個場景綁定固定的 CPU 邏輯核心Redis 6.0 以上的版本中,我們可以通過以下配置,對主線程、后臺線程、后臺 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。

總結(jié)

Redis 排障是一個循序漸進(jìn)的復(fù)雜流程,涉及到 Redis 運行原理,設(shè)計架構(gòu)以及操作系統(tǒng),網(wǎng)絡(luò)等等。

作為業(yè)務(wù)方的 Redis 使用者,我們需要了解 Redis 的基本原理,如各個命令的時間復(fù)雜度、數(shù)據(jù)過期策略、數(shù)據(jù)淘汰策略以及讀寫分離架構(gòu)等,從而更合理地使用 Redis 命令,并結(jié)合業(yè)務(wù)場景進(jìn)行相關(guān)的性能優(yōu)化。

Redis 在性能優(yōu)秀的同時,又是脆弱的;作為 Redis 的運維者,我們需要在部署 Redis 時,需要結(jié)合實際業(yè)務(wù)進(jìn)行容量規(guī)劃,預(yù)留足夠的機(jī)器資源,配置良好的網(wǎng)絡(luò)支持,還要對 Redis 機(jī)器和實例做好完善的監(jiān)控,以保障 Redis 實例的穩(wěn)定運行。

分享到:
標(biāo)簽:Redis
用戶無頭像

網(wǎng)友整理

注冊時間:

網(wǎng)站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨大挑戰(zhàn)2018-06-03

數(shù)獨一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運動步數(shù)有氧達(dá)人2018-06-03

記錄運動步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評定2018-06-03

通用課目體育訓(xùn)練成績評定