本文是對 redis 官方文檔 Redis latency problems troubleshooting 的翻譯,諸位看官可結合原文食用。node
這個文檔將會幫助你理解在使用 Redis 遇到延遲問題時發生了什麼。ios
在這個上下文中,延遲指的是從客戶端發送命令和接收到應答之間所消耗的時間。一般狀況下,Redis 處理命令的時間十分短,在亞微妙之間,可是某些狀況會致使高延遲。web
接下來的內容對於以低延遲方式運行 Redis 是很是重要的。然而,我理解咱們都是很忙的,因此就以快速清單開始吧。若是你在嘗試了下面這些步驟失敗了,請回來閱讀完整的文檔。redis
一般,使用下表來進行持久化和延遲/性能之間的權衡,從更好的安全性到更好的延遲排序。算法
如今,咱們花 15 分鐘來了解下細節...shell
若是你遇到了延遲問題,你可能知道如何在你的程序中測量延遲,或者延遲現象十分明顯。其實,redis-cli 能夠在毫秒級內測量 Redis 服務的延遲,只要使用下面的命令:數據庫
redis-cli --latency -h `host` -p `port`
複製代碼
從 Redis 2.8.13 開始,Redis 提供了延遲監控功能,它經過對不一樣執行路徑(譯者注:這個概念能夠在延遲監控文檔查看)採樣來檢測服務阻塞的位置。這將會使調試本文檔所說明的問題更加方便,因此咱們建議儘快啓用延遲監控。請查看延遲監控文檔。緩存
延遲監控採樣和報告功能會使你更方便的找出 Redis 系統延遲的緣由,不過咱們仍是建議你詳細閱讀本文以更好的理解 Redis 和延遲尖峯。安全
有一種延遲自己就是你運行 Redis 的環境的一部分,即操做系統內核以及--若是你使用了虛擬化--管理程序形成的延遲。bash
這種延遲沒法消除,學習它是十分重要的,由於它是基準線,換句話說,由於內核和管理程序的存在,你不可能使 Redis 的延遲比在你的環境中運行的程序的延遲更低。
咱們將這種延遲稱爲內部延遲,redis-cli 從 Redis 2.8.7 開始就能夠測量它了。這是一個在基於 Linux 3.11.0 的入門級服務器上運行的例子。
注意:參數 100 是測試執行的秒數。執行測試的時間越長,咱們越有可能發現延遲峯值。100 秒一般足夠了,可是你可能想要運行屢次不一樣時長的測試。請注意這個測試是 CPU 密集型的,將可能會使你的系統的一個核滿載。
注意: redis-cli 在這個例子中須要運行在你運行 Redis 或者計劃運行 Redis 的服務器上,而不是在客戶端。在這個特殊模式下 redis-cli 根本不會鏈接 Redis 服務:它只是嘗試測試內核不提供 CPU 時間去運行 redis-cli 進程本身的最大時長。
在上面的例子中,系統的內部延遲只是 0.115毫秒(或 115 微妙),這是一個好的消息,可是請記住,內部延遲可能會隨運行時間變化和變化,這取決與系統的負載。
虛擬化環境表現將會差點,特別是當高負載或者受其餘虛擬化環境影響。下面是在 Linode 4096 實例上運行 Redis 和 Apache 的結果:
這裏咱們有 9.7 毫秒的內部延遲:這意味着咱們不能要求 Redis 作的比這個更好了。然而,在負載更高或有其它鄰居的不一樣虛擬化環境中運行時,能夠更容易獲得更差的結果。咱們可能在正常運行的系統中獲得 40 毫秒的測試結果。
客戶端經過 TCP/IP 或者 Unix 域來鏈接到 Redis。1 Gbit/s 的網絡的典型延遲是 200 微妙,使用 Unix 域 socket 的延遲可能低至 30 微妙。它實際上取決於你的網絡和系統硬件。在通信之上,系統添加了更多的延遲(因爲線程調度,CPU 緩存,NUMA 配置,等等)。系統在虛擬環境中形成的延遲明顯高於物理機。
結論是,即便 Redis 處理大部分命令只花費亞微秒級的時間,客戶端和服務端之間大量往返的命令將必須爲網絡和系統相關延遲付出代價。
所以,高效的客戶端會將多個命令組成流水線來減小往返的次數。這被服務器和大多數客戶端支持。批量操做好比 MSET/MGET 也是爲了這個目的。從 Redis 2.4 開始,一些命令還支持全部數據類型的可變參數。
這裏是一些準則:
在 Linux,用戶能夠經過進程設置(taskset),cgroups,實時優先級(chrt),NUMA 設置,或者使用低延遲內核等來實現更好的延遲。請注意 Redis 並不適合綁定在單個 CPU 核心上。Redis 會 fork 後臺任務像 bgsave 或 AOF,這操做會十分消耗 CPU。這些任務必須永遠不和主進程在同一個核心上運行。
在大多環境中,這些系統級的優化不是必須的。只有當你須要它們或者熟悉它們的時候再去作這些操做。
Redis 被設計爲大部分狀況下使用單線程。這意味着一個線程處理全部客戶端的請求,它使用了多路複用的技術。這意味着 Redis 在每一個是簡單都只處理一個請求,因此全部的請求都是按順序處理的。這很像 Node.js 的工做方式。然而,這兩個產品一般都不會被認爲很慢。這是由於他們處理任務的時間很短,不過主要是由於它們設計爲不在系統調用阻塞,特別是從套接字讀取數據或者往套接字寫數據的時候。
我說 Redis 大多隻用單線程,是由於從 Redis 2.4 開始,咱們使用多線程去在後臺執行一些慢 I/O 操做,主要是和磁盤 I/O 相關,可是這不改變 Redis 使用單線程處理全部請求的事實。
一個單線程的後果是當有一個慢請求時,全部其它的客戶端將會等待它完成。當執行像 GET 或 SET 或 LPUSH 這樣的一般命令時是沒有問題的,這些命令的執行時間都是常數的(很是短)。然而,有幾個命令操做了大量的元素,像 SORT,LREM,SUNION 和其它的命令。例如,去兩個大集合的交集會花費很是多的時間。
全部命令的算法複雜度都有文檔記錄。一個好的實踐是,當你使用不熟悉的命令時先系統地測試一下它。
若是你有延遲的顧慮,那麼你不該該用慢查詢處理有大量元素的值,或者你應該運行 Redis 的副本去運行慢查詢。
可使用 Redis 的慢日誌功能來監控慢查詢。
另外,你可使用你喜歡的進程監控程序(top,htop,prstat,等等)去快速的檢查主 Redis 進程 所消耗的 CPU。若是它很高,但流量卻不高,那麼它一般表示正在執行慢查詢。
重點:一個很是常見的由執行慢查詢致使延遲的緣由是在生產環境執行 KEYS 命令。KEYS 命令在文檔中指出只能用於調試目的。從 Redis 2.8 開始,引入了一些新的命令來迭代鍵空間和其它大集合,請查看 SCAN,SSCAN,HSCAN 和 ZSCAN 命令來獲取更多的信息。
爲了在後臺生成 RDB 文件或者在啓用 AOF 持久化時重寫 AOF 文件,Redis 必須 fork 一個進程。fork 操做(在主線程運行)會引發延遲。fork 是在類 Unix 系統中一個開銷很高的操做,由於它涉及到複製大量與進程關聯的對象。對於和虛擬內存相關聯的頁表尤爲如此。
例如,在 Linux/AMD64 系統,內存被分爲每頁 4 kB。爲了將虛擬地址轉換爲物理地址,每一個進程保存了一個頁表(實際是一棵樹),它至少包含一個指向進程的每頁地址空間的指針。因此,一個擁有 24 GB 的 Redis 實例須要 24 GB/4 KB*8 = 48 MB 的頁表。
當 bgsave 執行的時候,實例將會被 fork,這將建立和拷貝 48 MB 的內存。它會花費時間和 CPU,特別是在虛擬機上建立和初始化大頁面將會是很是大的開銷。
現代硬件拷貝頁表很是快,除了 Xen。這個問題不是出在 Xen 虛擬化,而是 Xen 自己。例如,使用 VMware 或 Virtual Box 不會致使緩慢的 fork 時間。下表是比較不一樣的 Redis 實例 fork 所消耗的時間。數據來自於執行BGSAVE,並觀察INFO命令輸出的latest_fork_usec
信息。
然而,好消息是基於 EC2 HVM 的實例執行 fork 操做的表現很好,幾乎和在物理機上執行差很少,因此使用 m3.medium (或高性能)的實例將會獲得更好的結果。
正如您所看到的,在 Xen 上運行的某些虛擬機的性能損失介於一個數量級到兩個數量級之間。對於 EC2 用戶,建議很簡單:使用基於 HVM 的現代實例。
很不幸,若是 Linux 內核開啓了 transparent huge pages 功能,Redis 將會在調用 fork 來持久化到磁盤時形成很是大的延遲。大內存頁致使了下面這些問題:
請確認使用如下命令禁用 transparent huge pages:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
複製代碼
Linux(還有不少其它現代操做系統)能夠將內存頁從內存緩存到磁盤,或從磁盤讀入內存,這是爲了更有效的使用系統內存。
若是 Redis 頁被內核從內存保存到了交換文件,當保存在這個內存頁中的數據被 Redis 使用到的時候(好比訪問保存在這個頁中的鍵)內核爲了將這頁移動到主存,會中止 Redis 進程。訪問隨機 I/O 是一個緩慢的操做(和訪問在內存中的頁相比)並且 Redis 客戶端將會經歷異常的延遲。
內核將 Redis 內存頁保存到磁盤主要有三個緣由:
幸運的是 Linux 提供了好的工具去驗證這個問題,因此,最簡單的事是當懷疑是交換內存引發延遲時,那就去檢查它吧。
第一件事要作的是檢查 Redis 內存交換到磁盤的數量。這以前,你須要獲取 Redis 實例的 pid:
$ redis-cli info | grep process_id
process_id:5454
複製代碼
如今進入 /proc 目錄下該進程的目錄:
$ cd /proc/5454
複製代碼
你將會在這裏找到一個叫作 smaps 的文件,它描述了 Redis 進程的內存佈局(假設你使用的是 Linux 2.6.16 以上的系統)。這個文件包含了和咱們的進程內存映射相關的很是詳細的信息,一個名爲 Swap 的字段就是咱們所須要的。然而,當 smaps 文件包含了不一樣的關於 Redis 進程的內存映射以後,它就不是單單一個 swap 字段了(進程的內存佈局比一頁簡單的線性表要遠遠複雜)。
由於咱們對進程相關的內存交換都感興趣,因此第一件事要作的是用 grep 獲得文件中全部的 Swap 字段:
$ cat smaps | grep 'Swap:'
Swap: 0 kB
Swap: 0 kB
Swap: 0 kB
Swap: 0 kB
Swap: 0 kB
Swap: 12 kB
Swap: 156 kB
Swap: 8 kB
Swap: 0 kB
Swap: 0 kB
Swap: 0 kB
Swap: 0 kB
Swap: 0 kB
Swap: 0 kB
Swap: 0 kB
Swap: 0 kB
Swap: 0 kB
Swap: 4 kB
Swap: 0 kB
Swap: 0 kB
Swap: 4 kB
Swap: 0 kB
Swap: 0 kB
Swap: 4 kB
Swap: 4 kB
Swap: 0 kB
Swap: 0 kB
Swap: 0 kB
Swap: 0 kB
Swap: 0 kB
複製代碼
若是全部都是 0 KB,或者零散一個 4k 的條目,那麼一切都很完美。事實上,在咱們的示例中(一個運行 Redis 的網站,每秒服務百餘用戶)有些條目會顯示更多的交換頁。爲了研究這是不是一個嚴重的問題,咱們更改命令以便打印內存映射的大小:
$ 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: 4096 kB
Swap: 156 kB
Size: 4096 kB
Swap: 8 kB
Size: 4096 kB
Swap: 0 kB
Size: 4 kB
Swap: 0 kB
Size: 1272 kB
Swap: 0 kB
Size: 8 kB
Swap: 0 kB
Size: 4 kB
Swap: 0 kB
Size: 16 kB
Swap: 0 kB
Size: 84 kB
Swap: 0 kB
Size: 4 kB
Swap: 0 kB
Size: 4 kB
Swap: 0 kB
Size: 8 kB
Swap: 4 kB
Size: 8 kB
Swap: 0 kB
Size: 4 kB
Swap: 0 kB
Size: 4 kB
Swap: 4 kB
Size: 144 kB
Swap: 0 kB
Size: 4 kB
Swap: 0 kB
Size: 4 kB
Swap: 4 kB
Size: 12 kB
Swap: 4 kB
Size: 108 kB
Swap: 0 kB
Size: 4 kB
Swap: 0 kB
Size: 4 kB
Swap: 0 kB
Size: 272 kB
Swap: 0 kB
Size: 4 kB
Swap: 0 kB
複製代碼
正如你從輸出所見,這裏有一個映射有 720896 kB(只有 12 KB 被交換),在另外一個映射中有 156 KB 被交換:基本上佔咱們內存很是少的部分被交換,因此不會形成大的問題。
相反,若是有大量進程內存頁被交換到磁盤,那麼你的響應延遲問題可能和內存頁交換有關。若是是這樣的話,你可使用vmstat
命令來進一步檢查你的 Redis 實例:
輸出中咱們須要的一部分是兩列 si
和 so
,它們記錄了內存從 swap 文件交換出和交換入的次數。若是你看到的這兩列是非 0 的,那麼你的系統上存在內存交換。
最後, iostat 命令能夠用於檢測系統的全局 I/O 活動。
若是延遲問題是因爲Redis內存在磁盤上交換形成的,則須要下降系統內存壓力,若是Redis使用的內存超過可用內存,則增長更多內存,或者避免在同一系統中運行其餘內存耗盡的進程。
另外一個致使延遲的緣由是 Redis 支持的 AOF 功能。AOF 基本上只使用兩個系統調用來完成工做。一個是 write(2)
,它用來追加數據到文件,另外一個是 fdatasync(2)
,用來刷新內核文件緩存到磁盤,用來確保用戶指定的持久化級別。
write(2)
和 fdatasync(2)
都有可能致使延遲。例如,當正在進行系統大範圍同步,或者輸出緩衝區已滿,內核須要將數據刷新到磁盤來保證接受新的寫操做時,都有可能阻塞 write(2)
。
fdatasync(2)
調用是一個致使延遲的更糟糕的緣由,由於使用的內核和文件系統的許多組合可能須要幾毫秒到幾秒才能完成,特別是在某些其餘進程執行 I/O 的狀況下。出於這個緣由,Redis 2.4 可能會在不一樣的線程中執行fdatasync(2)
調用。
咱們將會看到如何設置能夠改善使用 AOF 文件引發的延遲問題。
可使用appendfsync
配置選項將 AOF配 置爲以三種不一樣方式在磁盤上執行 fsync(可使用CONFIG SET
命令在運行時修改此設置)。
appendfsync
被設置爲 no
時,Redis 將不會執行 fsync。這種設置方式裏,只有write(2)
會致使延遲。這種狀況下發生延遲一般沒有解決方法,緣由很是簡單,由於磁盤拷貝的速度沒法跟上 Redis 接收數據的速度,不過這種狀況十分不常見,除非由於其餘進程在操做 I/O 致使磁盤讀寫很慢。appendfsync
被設置爲 everysec
時,Redis 每秒執行一次 fsync。它會使用一個不一樣的線程,而且當 fsync 在運行的時候,Redis 會使用 buffer 來延遲 write(2)
的調用 2 秒左右(由於在 Linux 中,當 write 和 fsync 在進程中競爭同一個文件時會致使阻塞)。然而,若是 fsync 佔用了太長的時間,Redis 最終會執行 write 調用,這將會致使延遲。appendfsync
被設置爲 always
時,fsync 會在每次寫操做時執行,這個操做發生在回覆 OK 應答給客戶端以前(事實上 Redis 會嘗試將多個同時執行的命令集合到一個 fsync 中)。在這種模式下性能一般會很是慢,很是推薦使用快的磁盤和執行 fsync 很快的文件系統。大多數 Redis 用戶會使用 no
或者 everysec
來設置 appendfsync
。得到最少的延遲的建議時避免其它進程在同一系統中操做 I/O。使用 SSD 硬盤能夠提供很好的幫助,不過若是磁盤是空閒的,那麼非 SSD 磁盤也能有很好的表現,由於 Redis 寫 AOF 文件的時候不須要執行任何查找。
若是想要確認延遲是否和 AOF 文件相關,在 Linux 下你可使用 strace
命令來查看:
sudo strace -p $(pidof redis-server) -T -e trace=fdatasync
複製代碼
上面的命令將會顯示 Redis 在主線程執行的全部 fdatasync(2)
命令。你不能經過上面的命令查看當 appendfsync 設置爲 everysec
時在後臺線程執行的 fdatasync
命令。能夠添加 -f 參數來查看後臺執行的 fdatasync
命令。
若是你想要同時查看 fdatasync
和 write
兩個系統調用,可使用下面的命令:
sudo strace -p $(pidof redis-server) -T -e trace=fdatasync,write
複製代碼
不過,由於 write
命令還被用於寫數據到客戶端的套接字,因此可能會顯示不少和磁盤 I/O 無關的數據。顯然沒有辦法告訴 strace 只顯示慢速系統調用,因此我使用如下命令:
sudo strace -f -p $(pidof redis-server) -T -e trace=fdatasync,write 2>&1 | grep -v '0.0' | grep -v unfinished
複製代碼
Redis 使用下面兩種方法來處理過時的鍵:
主動過時的方式被設計爲自適應的。每 100 毫秒一個週期(每秒 10 次),它將進行下面的操做:
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
鍵,刪除全部已通過期的鍵。ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
的默認值是 20,這個過程每秒執行 10 次,一般沒有將會有 200 個鍵因過時被主動刪除。這已經可以快地清除 DB 了,即便有些鍵已經好久沒有被訪問了,因此被動算法是沒什麼幫助的。同時,每秒刪除 200 個鍵並不會對 Redis 的延遲形成影響。
然而,這個算法是自適應的,若是在採樣鍵集合中找到了超過 25% 的鍵是過時的,它會循環執行。可是運行這個算法的週期是每秒 10 次,這意味着可能發生在同一秒內被採樣的鍵 25% 以上都過時的狀況。
基本上,這意味着 若是數據庫在同一秒內有很是多鍵過時了,並且它們構成了當前鍵集合 25% 的失效鍵集合,Redis 可能會阻塞着直到失效鍵的比例降到 25% 如下。
這種方式是爲了不由於失效的鍵使用過多的內存,並且一般都是無害的,由於在同一秒內出現大量失效的鍵是十分奇怪的,可是用戶在同一 Unix 時間普遍使用 EXPIREAT
命令也不是不可能。
簡而言之:注意大量鍵同時過時引發的響應延遲。
Redis 2.6 引入了 Redis 軟件看門狗這個調試工具,它被設計用來跟蹤使用其它一般工具沒法分析的致使延遲的問題。
軟件看門狗是一個試驗性的特性。雖然它被設計爲用於開發環境,不過在繼續使用以前仍是應該備份數據庫,由於它可能會對 Redis 服務器的正常操做形成不可預知的影響。
它只有在經過其它方法沒有辦法找到緣由的狀況下才能使用。
下面是這個特性的工做方式:
CONFIG SET
命令啓用軟件看門狗。注意,這個特性不能在 redis.conf
文件中啓用,由於它被設計爲只能在已經運行的實例中啓用,而且只能用於測試用途。
使用下面的命令來啓用這個特性:
CONFIG SET watchdog-period 500
複製代碼
period 被指定爲毫秒級。上面這個例子是指在檢測到服務器有 500 毫秒或以上的延遲時則記錄到日誌文件中。最小可配置的延遲時間是 200 毫秒。
當你使用完了軟件看門狗,你能夠將watchdog-period
參數設置爲 0 來關閉這一功能。
重點:必定要記得關閉它,由於長時間開啓看門狗不是一個好的注意。
下面的內容是當看門狗檢測到延遲大於設置的值時會記錄到日誌文件中的內容:
[8547 | signal handler] (1333114359)
--- WATCHDOG TIMER EXPIRED ---
/lib/libc.so.6(nanosleep+0x2d) [0x7f16b5c2d39d]
/lib/libpthread.so.0(+0xf8f0) [0x7f16b5f158f0]
/lib/libc.so.6(nanosleep+0x2d) [0x7f16b5c2d39d]
/lib/libc.so.6(usleep+0x34) [0x7f16b5c62844]
./redis-server(debugCommand+0x3e1) [0x43ab41]
./redis-server(call+0x5d) [0x415a9d]
./redis-server(processCommand+0x375) [0x415fc5]
./redis-server(processInputBuffer+0x4f) [0x4203cf]
./redis-server(readQueryFromClient+0xa0) [0x4204e0]
./redis-server(aeProcessEvents+0x128) [0x411b48]
./redis-server(aeMain+0x2b) [0x411dbb]
./redis-server(main+0x2b6) [0x418556]
/lib/libc.so.6(__libc_start_main+0xfd) [0x7f16b5ba1c4d]
./redis-server() [0x411099]
------
複製代碼
注意:在例子中 DEBUG SLEEP 命令是用來組在服務器的。若是服務器阻塞在不一樣的位置,那麼堆棧信息將會是不一樣的。
若是你碰巧收集了多份看門狗堆棧信息,咱們鼓勵你將他們都發送到 Redis 的 Google Group:咱們收集到越多的堆棧信息,那麼你的實例的問題將越容易被理解。