半夜數(shù)據(jù)庫突發(fā)宕機,竟是Redis惹的鍋?
?誰曾想,凌晨 12 點之后,用戶量暴增,出現(xiàn)了一個技術(shù)故障,用戶無法下單,當(dāng)時老大火冒三丈!
經(jīng)過查找發(fā)現(xiàn) Redis 報 Could not get a resource from the pool。
獲取不到連接資源,并且集群中的單臺 Redis 連接量很高。
大量的流量沒了 Redis 的緩存響應(yīng),直接打到了 MySQL,最后數(shù)據(jù)庫也宕機了……
于是各種更改最大連接數(shù)、連接等待數(shù),雖然報錯信息頻率有所緩解,但還是持續(xù)報錯。
后來經(jīng)過線下測試,發(fā)現(xiàn)存放 Redis 中的字符數(shù)據(jù)很大,平均 1s 返回數(shù)據(jù)。
可以發(fā)現(xiàn),一旦 Redis 延遲過高,會引發(fā)各種問題。
今天跟大家一起來分析下如何確定 Redis 有性能問題和解決方案。
目錄:
1、延遲基線測量
2、慢指令監(jiān)控
- 慢日志功能
- Latency Monitoring
3、網(wǎng)絡(luò)通信導(dǎo)致的延遲
4、慢指令導(dǎo)致的延遲
5、Fork 生成 RDB 導(dǎo)致的延遲
6、內(nèi)存大頁(transparent huge pages)
7、swap:操作系統(tǒng)分頁
- 獲取 Redis 實例 pid
- 解決方案
8、AOF 和磁盤 I/O 導(dǎo)致的延遲
9、expires 淘汰過期數(shù)據(jù)
- 解決方案
10、bigkey
- 查找 bigkey
- 解決方案
Redis 性能出問題了么?
最大延遲是客戶端發(fā)出命令到客戶端收到命令的響應(yīng)的時間,正常情況下 Redis 處理的時間極短,在微秒級別。
當(dāng) Redis 出現(xiàn)性能波動的時候,比如達到幾秒到十幾秒,這個很明顯我們可以認定 Redis 性能變慢了。
有的硬件配置比較高,當(dāng)延遲 0.6ms,我們可能就認定變慢了。硬件比較差的可能 3 ms 我們才認為出現(xiàn)問題。
那我們該如何定義 Redis 真的變慢了呢?
所以,我們需要對當(dāng)前環(huán)境的 Redis 基線性能做測量,也就是在一個系統(tǒng)在低壓力、無干擾情況下的基本性能。
當(dāng)你發(fā)現(xiàn) Redis 運行時時的延遲是基線性能的 2 倍以上,就可以判定 Redis 性能變慢了。
1、延遲基線測量
redis-cli 命令提供了–intrinsic-latency 選項,用來監(jiān)測和統(tǒng)計測試期間內(nèi)的最大延遲(以毫秒為單位),這個延遲可以作為 Redis 的基線性能。
redis-cli --latency -h `host` -p `port`
比如執(zhí)行如下指令:
redis-cli --intrinsic-latency 100
Max latency so far: 4 microseconds.
Max latency so far: 18 microseconds.
Max latency so far: 41 microseconds.
Max latency so far: 57 microseconds.
Max latency so far: 78 microseconds.
Max latency so far: 170 microseconds.
Max latency so far: 342 microseconds.
Max latency so far: 3079 microseconds.
45026981 total runs (avg latency: 2.2209 microseconds / 2220.89 nanoseconds per run).
Worst run took 1386x longer than the average latency.
注意:參數(shù)100是測試將執(zhí)行的秒數(shù)。我們運行測試的時間越長,我們就越有可能發(fā)現(xiàn)延遲峰值。
通常運行 100 秒通常是合適的,足以發(fā)現(xiàn)延遲問題了,當(dāng)然我們可以選擇不同時間運行幾次,避免誤差。
運行的最大延遲是 3079 微秒,所以基線性能是 3079 (3 毫秒)微秒。
需要注意的是,我們要在 Redis 的服務(wù)端運行,而不是客戶端。這樣,可以避免網(wǎng)絡(luò)對基線性能的影響。
可以通過 -h host -p port 來連接服務(wù)端,如果想監(jiān)測網(wǎng)絡(luò)對 Redis 的性能影響,可以使用 Iperf 測量客戶端到服務(wù)端的網(wǎng)絡(luò)延遲。
如果網(wǎng)絡(luò)延遲幾百毫秒,說明網(wǎng)絡(luò)可能有其他大流量的程序在運行導(dǎo)致網(wǎng)絡(luò)擁塞,需要找運維協(xié)調(diào)網(wǎng)絡(luò)的流量分配。
2、慢指令監(jiān)控
如何判斷是否是慢指令呢?
看操作復(fù)雜度是否是O(N)。官方文檔對每個命令的復(fù)雜度都有介紹,盡可能使用O(1) 和 O(log N)命令。
涉及到集合操作的復(fù)雜度一般為O(N),比如集合全量查詢HGETALL、SMEMBERS,以及集合的聚合操作:SORT、LREM、 SUNION等。
有監(jiān)控數(shù)據(jù)可以觀測呢?代碼不是我寫的,不知道有沒有人用了慢指令。
有兩種方式可以排查到:
- 使用 Redis 慢日志功能查出慢命令;
- latency-monitor(延遲監(jiān)控)工具。
此外,可以使用自己(top、htop、prstat 等)快速檢查 Redis 主進程的 CPU 消耗。如果 CPU 使用率很高而流量不高,通常表明使用了慢速命令。
1)慢日志功能
Redis 中的 slowlog 命令可以讓我們快速定位到那些超出指定執(zhí)行時間的慢命令,默認情況下命令若是執(zhí)行時間超過 10ms 就會被記錄到日志。
slowlog 只會記錄其命令執(zhí)行的時間,不包含 io 往返操作,也不記錄單由網(wǎng)絡(luò)延遲引起的響應(yīng)慢。
我們可以根據(jù)基線性能來自定義慢命令的標(biāo)準(zhǔn)(配置成基線性能最大延遲的 2 倍),調(diào)整觸發(fā)記錄慢命令的閾值。
可以在 redis-cli 中輸入以下命令配置記錄 6 毫秒以上的指令:
redis-cli CONFIG SET slowlog-log-slower-than 6000
也可以在 Redis.config 配置文件中設(shè)置,以微秒為單位。
想要查看所有執(zhí)行時間比較慢的命令,可以通過使用 Redis-cli 工具,輸入 slowlog get 命令查看,返回結(jié)果的第三個字段以微秒位單位顯示命令的執(zhí)行時間。
假如只需要查看最后 2 個慢命令,輸入 slowlog get 2 即可。
示例:獲取最近2個慢查詢命令
127.0.0.1:6381> SLOWLOG get 2
1) 1) (integer) 6
2) (integer) 1458734263
3) (integer) 74372
4) 1) "hgetall"
2) "max.dsp.blacklist"
2) 1) (integer) 5
2) (integer) 1458734258
3) (integer) 5411075
4) 1) "keys"
2) "max.dsp.blacklist"
以第一個 HGET 命令為例分析,每個 slowlog 實體共 4 個字段:
- 字段 1:1 個整數(shù),表示這個 slowlog 出現(xiàn)的序號,server 啟動后遞增,當(dāng)前為 6。
- 字段 2:表示查詢執(zhí)行時的 Unix 時間戳。
- 字段 3:表示查詢執(zhí)行微秒數(shù),當(dāng)前是 74372 微秒,約 74ms。
- 字段 4: 表示查詢的命令和參數(shù),如果參數(shù)很多或很大,只會顯示部分參數(shù)個數(shù)。當(dāng)前命令是hgetall max.dsp.blacklist。
2)Latency Monitoring
Redis 在 2.8.13 版本引入了 Latency Monitoring 功能,用于以秒為粒度監(jiān)控各種事件的發(fā)生頻率。
啟用延遲監(jiān)視器的第一步是設(shè)置延遲閾值(單位毫秒)。只有超過該閾值的時間才會被記錄,比如我們根據(jù)基線性能(3ms)的 3 倍設(shè)置閾值為 9 ms。
可以用 redis-cli 設(shè)置也可以在 Redis.config 中設(shè)置;
CONFIG SET latency-monitor-threshold 9
工具記錄的相關(guān)事件的詳情可查看官方文檔:https://redis.io/topics/latency-monitor
如獲取最近的 latency
127.0.0.1:6379> debug sleep 2
OK
(2.00s)
127.0.0.1:6379> latency latest
1) 1) "command"
2) (integer) 1645330616
3) (integer) 2003
4) (integer) 2003
- 事件的名稱;
- 事件發(fā)生的最新延遲的 Unix 時間戳;
- 毫秒為單位的時間延遲;
- 該事件的最大延遲。
如何解決 Redis 變慢?
Redis 的數(shù)據(jù)讀寫由單線程執(zhí)行,如果主線程執(zhí)行的操作時間太長,就會導(dǎo)致主線程阻塞。
一起分析下都有哪些操作會阻塞主線程,我們又該如何解決?
3、網(wǎng)絡(luò)通信導(dǎo)致的延遲
客戶端使用 TCP/IP 連接或 Unix 域連接連接到 Redis。1 Gbit/s 網(wǎng)絡(luò)的典型延遲約為 200 us。
redis 客戶端執(zhí)行一條命令分 4 個過程:
發(fā)送命令-〉 命令排隊 -〉 命令執(zhí)行-〉 返回結(jié)果
這個過程稱為 Round trip time(簡稱 RTT, 往返時間),mget mset 有效節(jié)約了 RTT,但大部分命令(如 hgetall,并沒有 mhgetall)不支持批量操作,需要消耗 N 次 RTT ,這個時候需要 pipeline 來解決這個問題。
Redis pipeline 將多個命令連接在一起來減少網(wǎng)絡(luò)響應(yīng)往返次數(shù)。
redis-pipeline
4、慢指令導(dǎo)致的延遲
根據(jù)上文的慢指令監(jiān)控查詢文檔,查詢到慢查詢指令??梢酝ㄟ^以下兩種方式解決:
- 比如在 Cluster 集群中,將聚合運算等 O(N) 操作運行在 slave 上,或者在客戶端完成。
- 使用高效的命令代替。使用增量迭代的方式,避免一次查詢大量數(shù)據(jù),具體請查看SCAN、SSCAN、HSCAN和ZSCAN命令。
除此之外,生產(chǎn)中禁用KEYS 命令,它只適用于調(diào)試。因為它會遍歷所有的鍵值對,所以操作延時高。
5、Fork 生成 RDB 導(dǎo)致的延遲
生成 RDB 快照,Redis 必須 fork 后臺進程。fork 操作(在主線程中運行)本身會導(dǎo)致延遲。
Redis 使用操作系統(tǒng)的多進程寫時復(fù)制技術(shù) COW(Copy On Write) 來實現(xiàn)快照持久化,減少內(nèi)存占用。
?
寫時復(fù)制技術(shù)保證快照期間數(shù)據(jù)可修改
但 fork 會涉及到復(fù)制大量鏈接對象,一個 24 GB 的大型 Redis 實例需要 24 GB / 4 kB * 8 = 48 MB 的頁表。
執(zhí)行 bgsave 時,這將涉及分配和復(fù)制 48 MB 內(nèi)存。
此外,從庫加載 RDB 期間無法提供讀寫服務(wù),所以主庫的數(shù)據(jù)量大小控制在 2~4G 左右,讓從庫快速的加載完成。?
6、內(nèi)存大頁(transparent huge pages)
常規(guī)的內(nèi)存頁是按照 4 KB 來分配,Linux 內(nèi)核從 2.6.38 開始支持內(nèi)存大頁機制,該機制支持 2MB 大小的內(nèi)存頁分配。
Redis 使用了 fork 生成 RDB 做持久化提供了數(shù)據(jù)可靠性保證。
當(dāng)生成 RDB 快照的過程中,Redis 采用**寫時復(fù)制**技術(shù)使得主線程依然可以接收客戶端的寫請求。
也就是當(dāng)數(shù)據(jù)被修改的時候,Redis 會復(fù)制一份這個數(shù)據(jù),再進行修改。
采用了內(nèi)存大頁,生成 RDB 期間,即使客戶端修改的數(shù)據(jù)只有 50B 的數(shù)據(jù),Redis 需要復(fù)制 2MB 的大頁。當(dāng)寫的指令比較多的時候就會導(dǎo)致大量的拷貝,導(dǎo)致性能變慢。
使用以下指令禁用 Linux 內(nèi)存大頁即可:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
7、swap:操作系統(tǒng)分頁
當(dāng)物理內(nèi)存(內(nèi)存條)不夠用的時候,將部分內(nèi)存上的數(shù)據(jù)交換到 swap 空間上,以便讓系統(tǒng)不會因內(nèi)存不夠用而導(dǎo)致 oom 或者更致命的情況出現(xiàn)。
當(dāng)某進程向 OS 請求內(nèi)存發(fā)現(xiàn)不足時,OS 會把內(nèi)存中暫時不用的數(shù)據(jù)交換出去,放在 SWAP 分區(qū)中,這個過程稱為 SWAP OUT。
當(dāng)某進程又需要這些數(shù)據(jù)且 OS 發(fā)現(xiàn)還有空閑物理內(nèi)存時,又會把 SWAP 分區(qū)中的數(shù)據(jù)交換回物理內(nèi)存中,這個過程稱為 SWAP IN。
內(nèi)存 swap 是操作系統(tǒng)里將內(nèi)存數(shù)據(jù)在內(nèi)存和磁盤間來回換入和換出的機制,涉及到磁盤的讀寫。?
觸發(fā) swap 的情況有哪些呢?
對于 Redis 而言,有兩種常見的情況:
- Redis 使用了比可用內(nèi)存更多的內(nèi)存;
- 與 Redis 在同一機器運行的其他進程在執(zhí)行大量的文件讀寫 I/O 操作(包括生成大文件的 RDB 文件和 AOF 后臺線程),文件讀寫占用內(nèi)存,導(dǎo)致 Redis 獲得的內(nèi)存減少,觸發(fā)了 swap。
我要如何排查是否因為 swap 導(dǎo)致的性能變慢呢?
Linux 提供了很好的工具來排查這個問題,所以當(dāng)懷疑由于交換導(dǎo)致的延遲時,只需按照以下步驟排查。
1)獲取 Redis 實例 pid
$ redis-cli info | grep process_id
process_id:13160
進入此進程的 /proc 文件系統(tǒng)目錄:
cd /proc/13160
在這里有一個 smaps 的文件,該文件描述了 Redis 進程的內(nèi)存布局,運行以下指令,用 grep 查找所有文件中的 Swap 字段。
$ cat smaps | egrep '^(Swap|Size)'
Size: 316 kB
Swap: 0 kB
Size: 4 kB
Swap: 0 kB
Size: 8 kB
Swap: 0 kB
Size: 40 kB
Swap: 0 kB
Size: 132 kB
Swap: 0 kB
Size: 720896 kB
Swap: 12 kB
每行 Size 表示 Redis 實例所用的一塊內(nèi)存大小,和 Size 下方的 Swap 對應(yīng)這塊 Size 大小的內(nèi)存區(qū)域有多少數(shù)據(jù)已經(jīng)被換出到磁盤上了。
如果 Size == Swap 則說明數(shù)據(jù)被完全換出了。
可以看到有一個 720896 kB 的內(nèi)存大小有 12 kb 被換出到了磁盤上(僅交換了 12 kB),這就沒什么問題。
Redis 本身會使用很多大小不一的內(nèi)存塊,所以,你可以看到有很多 Size 行,有的很小,就是 4KB,而有的很大,例如 720896KB。不同內(nèi)存塊被換出到磁盤上的大小也不一樣。
敲重點了!
如果 Swap 一切都是 0 kb,或者零星的 4k ,那么一切正常。
當(dāng)出現(xiàn)百 MB,甚至 GB 級別的 swap 大小時,就表明,此時,Redis 實例的內(nèi)存壓力很大,很有可能會變慢。
2)解決方案
- 增加機器內(nèi)存;
- 將 Redis 放在單獨的機器上運行,避免在同一機器上運行需要大量內(nèi)存的進程,從而滿足 Redis 的內(nèi)存需求;
- 增加 Cluster 集群的數(shù)量分擔(dān)數(shù)據(jù)量,減少每個實例所需的內(nèi)存。
8、AOF 和磁盤 I/O 導(dǎo)致的延遲
為了保證數(shù)據(jù)可靠性,Redis 使用 AOF 和 RDB 快照實現(xiàn)快速恢復(fù)和持久化。
可以使用 appendfsync 配置將 AOF 配置為以三種不同的方式在磁盤上執(zhí)行 write 或者 fsync (可以在運行時使用 CONFIG SET命令修改此設(shè)置,比如:redis-cli CONFIG SET appendfsync no)。
- no:Redis 不執(zhí)行 fsync,唯一的延遲來自于 write 調(diào)用,write 只需要把日志記錄寫到內(nèi)核緩沖區(qū)就可以返回。
- everysec:Redis 每秒執(zhí)行一次 fsync。使用后臺子線程異步完成 fsync 操作。最多丟失 1s 的數(shù)據(jù)。
- always:每次寫入操作都會執(zhí)行 fsync,然后用 OK 代碼回復(fù)客戶端(實際上 Redis 會嘗試將同時執(zhí)行的許多命令聚集到單個 fsync 中),沒有數(shù)據(jù)丟失。在這種模式下,性能通常非常低,強烈建議使用快速磁盤和可以在短時間內(nèi)執(zhí)行 fsync 的文件系統(tǒng)實現(xiàn)。
我們通常將 Redis 用于緩存,數(shù)據(jù)丟失完全惡意從數(shù)據(jù)獲取,并不需要很高的數(shù)據(jù)可靠性,建議設(shè)置成 no 或者 everysec。
除此之外,避免 AOF 文件過大, Redis 會進行 AOF 重寫,生成縮小的 AOF 文件。
可以把配置項 no-appendfsync-on-rewrite設(shè)置為 yes,表示在 AOF 重寫時,不進行 fsync 操作。
也就是說,Redis 實例把寫命令寫到內(nèi)存后,不調(diào)用后臺線程進行 fsync 操作,就直接返回了。
9、expires 淘汰過期數(shù)據(jù)
Redis 有兩種方式淘汰過期數(shù)據(jù):
- 惰性刪除:當(dāng)接收請求的時候發(fā)現(xiàn) key 已經(jīng)過期,才執(zhí)行刪除;
- 定時刪除:每 100 毫秒刪除一些過期的 key。
定時刪除的算法如下:
- 隨機采樣 A CTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP個數(shù)的 key,刪除所有過期的 key;
- 如果發(fā)現(xiàn)還有超過 25% 的 key 已過期,則執(zhí)行步驟一。
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP默認設(shè)置為 20,每秒執(zhí)行 10 次,刪除 200 個 key 問題不大。
如果觸發(fā)了第二條,就會導(dǎo)致 Redis 一致在刪除過期數(shù)據(jù)去釋放內(nèi)存。而刪除是阻塞的。
碼哥,觸發(fā)條件是什么呀?
也就是大量的 key 設(shè)置了相同的時間參數(shù)。同一秒內(nèi),大量 key 過期,需要重復(fù)刪除多次才能降低到 25% 以下。
簡而言之:大量同時到期的 key 可能會導(dǎo)致性能波動。
1)解決方案
如果一批 key 的確是同時過期,可以在 EXPIREAT 和 EXPIRE 的過期時間參數(shù)上,加上一個一定大小范圍內(nèi)的隨機數(shù),這樣,既保證了 key 在一個鄰近時間范圍內(nèi)被刪除,又避免了同時過期造成的壓力。
10、bigkey
通常我們會將含有較大數(shù)據(jù)或含有大量成員、列表數(shù)的 Key 稱之為大 Key,下面我們將用幾個實際的例子對大 Key 的特征進行描述:
- 一個 STRING 類型的 Key,它的值為 5MB(數(shù)據(jù)過大)
- 一個 LIST 類型的 Key,它的列表數(shù)量為 10000 個(列表數(shù)量過多)
- 一個 ZSET 類型的 Key,它的成員數(shù)量為 10000 個(成員數(shù)量過多)
- 一個 HASH 格式的 Key,它的成員數(shù)量雖然只有 1000 個但這些成員的 value 總大小為 10MB(成員體積過大)
bigkey 帶來問題如下:
- Redis 內(nèi)存不斷變大引發(fā) OOM,或者達到 maxmemory 設(shè) 置值引發(fā)寫阻塞或重要 Key 被逐出;
- Redis Cluster 中的某個 node 內(nèi)存遠超其余 node,但因 Redis Cluster 的數(shù)據(jù)遷移最小粒度為 Key 而無法將 node 上的內(nèi)存均衡化;
- bigkey 的讀請求占用過大帶寬,自身變慢的同時影響到該服務(wù)器上的其它服務(wù);
- 刪除一個 bigkey 造成主庫較長時間的阻塞并引發(fā)同步中斷或主從切換;
1)查找 bigkey
使用 redis-rdb-tools 工具以定制化方式找出大 Key。
2)解決方案
- 對大 key 拆分
如將一個含有數(shù)萬成員的 HASH Key 拆分為多個 HASH Key,并確保每個 Key 的成員數(shù)量在合理范圍,在 Redis Cluster 結(jié)構(gòu)中,大 Key 的拆分對 node 間的內(nèi)存平衡能夠起到顯著作用。
- 異步清理大 key
Redis 自 4.0 起提供了 UNLINK 命令,該命令能夠以非阻塞的方式緩慢逐步的清理傳入的 Key,通過 UNLINK,你可以安全的刪除大 Key 甚至特大 Key。
總結(jié)
如下檢查清單,幫助你在遇到 Redis 性能變慢的時候能高效解決問題:
- 獲取當(dāng)前 Redis 的基線性能;
- 開啟慢指令監(jiān)控,定位慢指令導(dǎo)致的問題;
- 找到慢指令,使用 scan 的方式;
- 將實例的數(shù)據(jù)大小控制在 2-4GB,避免主從復(fù)制加載過大 RDB 文件而阻塞;
- 禁用內(nèi)存大頁,采用了內(nèi)存大頁,生成 RDB 期間,即使客戶端修改的數(shù)據(jù)只有 50B 的數(shù)據(jù),Redis 需要復(fù)制 2MB 的大頁。當(dāng)寫的指令比較多的時候就會導(dǎo)致大量的拷貝,導(dǎo)致性能變慢。
- Redis 使用的內(nèi)存是否過大導(dǎo)致 swap;
- AOF 配置是否合理,可以將配置項 no-appendfsync-on-rewrite 設(shè)置為 yes,避免 AOF 重寫和 fsync 競爭磁盤 IO 資源,導(dǎo)致 Redis 延遲增加。
- bigkey 會帶來一系列問題,我們需要進行拆分防止出現(xiàn) bigkey,并通過 UNLINK 異步刪除。?