轉載自:http://www.searchdatabase.com.cn/showcontent_71222.htm
文中出現的延遲(latency)均指從客戶端發出一條命令到客戶端接受到該命令的反饋所用的最長響應時間。Reids通常處理(命令的)時間非常的慢,大概在次微妙范圍內,但也有更長的情況出現。
計算延遲時間
如果你正在經歷響應延遲問題,你或許能夠根據應用程序的具體情況算出它的延遲響應時間,或者你的延遲問題非常明顯,宏觀看來,一目了然。不管怎樣吧,用redis-cli可以算出一臺Redis 服務器的到底延遲了多少毫秒。踹這句:
redis-cli --latency -h `host` -p `port`
網絡和通信引起的延遲 當用戶連接到Redis通過TCP/IP連接或Unix域連接,千兆網 絡的典型延遲大概200us,而Unix域socket可能低到30us。這完全基于你的網絡和系統硬件。在通信本身之上,系統增加了更多的延遲(線程調 度,CPU緩存,NUMA替換等等)。系統引起的延遲在虛擬機環境遠遠高于在物理機器環境。
實際情況是即使Redis處理大多數命令在微秒之下,客戶機和服務器之間的交互也必然消耗系統相關的延遲。
一個高效的客戶機因而試圖通過捆綁多個命令在一起的方式減少交互的次數。服務器和大多數客戶機支持這種方式。聚合命令象MSET/MGET也可以用作這個目的。從Redis 2.4版本起,很多命令對于所有的數據類型也支持可變參數。
這里有一些指導:
- 如果你負擔的起,盡可能的使用物理機而不是虛擬機來做服務器
- 不要經常的connect/disconnect與服務器的連接(尤其是對基于web的應用),盡可能的延長與服務器連接的時間。
- 如果你的客戶端和服務器在同一臺主機上,則使用Unix域套接字
- 盡量使用聚合命令(MSET/MGET)或可變參數命令而不是pipelining
- 如果可以盡量使用pipelining而不是序列的往返命令。
- 針對不適合使用原始pipelining的情況,如某個命令的結果是后續命令的輸入,在以后的版本中redis提供了對服務器端的lua腳本的支持,實驗分支版本現在已經可以使用了。
在Linux上,你可以通過process placement(taskset)、cgroups、real-time priorities(chrt)、NUMA配置(numactl)或使用低延遲內核的方式來獲取較低的延遲。請注意Redis 并不適合被綁到單個CPU核上。redis會在后臺創建一些非常消耗CPU的進程,如bgsave和AOF重寫,這些任務是絕對不能和主事件循環進程放在 一個CPU核上的。
大多數情況下上述的優化方法是不需要的,除非你確實需要并且你對優化方法很熟悉的情況下再使用上述方法。
Redis的單線程屬性 Redis 使用了單線程的設計, 意味著單線程服務于所有的客戶端請求,使用一種復用的技術。這種情況下redis可以在任何時候處理單個請求, 所以所有的請求是順序處理的。這和Node.js的工作方式很像, 所有的產出通常不會有慢的感覺,因為處理單個請求的時間非常短,但是最重要的是這些產品被設計為非阻塞系統調用,比如從套接字中讀取或寫入數據。
我提到過Redis從2.4版本后幾乎是單線程的,我們使用線程在后臺運行一些效率低下的I/O操作, 主要關系到硬盤I/O,但是這不改變Redis使用單線程處理所有請求的事實。
低效操作產生的延遲
單線程的一個結果是,當一個請求執行得很慢,其他的客 戶端調用就必須等待這個請求執行完畢。當執行GET、SET或者LPUSH 命令的時候這不是個問題,因為這些操作可在很短的常數時間內完成。然而,對于多個元素的操作,像SORT,LREM, SUNION 這些,做兩個大數據集的交叉要花掉很長的時間。
文檔中提到了所有操作的算法復雜性。 在使用一個你不熟悉的命令之前系統的檢查它會是一個好辦法。
如果你對延遲有要求,那么就不要執行涉及多個元素的慢操作,你可以使用Redis的replication功能,把這類慢操作全都放到replica上執行。
可以用Redis 的Slow Log來監控慢操作。
此外,你可以用你喜歡的進程監控程序(top, htop, prstat, 等...)來快速查看Redis進程的CPU使用率。如果traffic不高而CPU占用很高,八成說明有慢操作。
延遲由fork產生
Redis不論是為了在后臺生成一個RDB文件,還是為了當AOF持久化方案被開啟時重寫Append Only文件,都會在后臺fork出一個進程。fork操作(在主線程中被執行)本身會引發延遲。在大多數的類unix操作系統中,fork是一個很消耗 的操作,因為它牽涉到復制很多與進程相關的對象。而這對于分頁表與虛擬內存機制關聯的系統尤為明顯
對于運行在一個 linux/AMD64系統上的實例來說,內存會按照每頁4KB的大小分頁。為了實現虛擬地址到物理地址的轉換,每一個進程將會存儲一個分頁表(樹狀形式 表現),分頁表將至少包含一個指向該進程地址空間的指針。所以一個空間大小為24GB的redis實例,需要的分頁表大小為 24GB/4KB*8 = 48MB。
當一個后臺的save命令執行時,實例會啟動新的線程去申請和拷貝48MB的內存空間。這將消耗一些時間和CPU資源,尤其是在虛擬機上申請和初始化大塊內存空間時,消耗更加明顯。
在不同系統中的Fork時間
除了Xen系統外,現代的硬件都可以快速完美的復制頁表。Xen系統的問題不是特定的虛擬化,而是特定的Xen.例如使用VMware或者 Virutal Box不會導致較慢的fork時間。下面的列表比較了不同Redis實例的fork時間。數據包含正在執行的BGSAVE,并通過INFO指令查看 thelatest_fork_usecfiled。
- Linux beefy VM on VMware 6.0GB RSS forked 77 微秒 (每GB 12.8 微秒 ).
- Linux running on physical machine (Unknown HW) 6.1GB RSS forked 80 微秒(每GB 13.1微秒)
- Linux running on physical machine (Xeon @ 2.27Ghz) 6.9GB RSS forked into 62 微秒 (每GB 9 微秒).
- Linux VM on 6sync (KVM) 360 MB RSS forked in 8.2 微秒 (每GB 23.3 微秒).
- Linux VM on EC2 (Xen) 6.1GB RSS forked in 1460 微秒 (每GB 239.3 微秒).
- Linux VM on Linode (Xen) 0.9GBRSS forked into 382 微秒 (每GB 424 微秒).
你能看到運行在Xen上的VM的Redis性能相差了一到兩個數量級。我們相信這是Xen系統的一個驗證問題,我們希望這個問題能盡快處理。
swapping (操作系統分頁)引起的延遲
Linux (以及其他一些操作系統) 可以把內存頁存儲在硬盤上,反之也能將存儲在硬盤上的內存頁再加載進內存,這種機制使得內存能夠得到更有效的利用。
如果內存頁被系統移到了swap文件里,而這個內存頁中的數據恰好又被redis用到了(例如要訪問某個存儲在內存頁中的key),系統就會暫停redis進程直到把需要的頁數據重新加載進內存。這個操作因為牽涉到隨機I/O,所以很慢,會導致無法預料的延遲。
系統之所以要在內存和硬盤之間置換redis頁數據主要因為以下三個原因:
- 系統總是要應對內存不足的壓力,因為每個運行的進程都想申請更多的物理內存,而這些申請的內存的數量往往超過了實際擁有的內存。簡單來說就是redis使用的內存總是比可用的內存數量更多。
- redis實例的數據,或者部分數據可能就不會被客戶端訪問,所以系統可以把這部分閑置的數據置換到硬盤上。需要把所有數據都保存在內存中的情況是非常罕見的。
- 一些進程會產生大量的讀寫I/O。因為文件通常都有緩存,這往往會導致文件緩存不斷增加,然后產生交換(swap)。請注意,redis RDB和AOF后臺線程都會產生大量文件。
所幸Linux提供了很好的工具來診斷這個問題,所以當延遲疑似是swap引起的,最簡單的辦法就是使用Linux提供的工具去確診。
首先要做的是檢查swap到硬盤上的redis內存的數量,為實現這個目的要知道redis實例的進程id:
$ redis-cli info | grep process_idprocess_id:5454
進入進程目錄:
$ cd /proc/5454
在這里你會發現一個名為smaps 的文件,它描述了redis進程的內存布局 (假定你使用的是Linux 2.6.16或者更新的版本)。這個文件包括了很多進程所使用內存的細節信息,其中有一項叫做Swap的正是我們所關心的。不過僅看這一項是不夠的,因為 smaps文件包括有redis進程的多個不同的的內存映射區域的使用情況(進程的內存布局遠不是線性排列那么簡單)。
從我們對所有進程的內存交換情況感興趣以來,我們首先要做的事情是使用grep命令顯示進程的smaps文件
$ 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
假如所有的數據顯示為0kb或者某些數據偶爾顯示為4kb,表示當前一切正常。實際上我們的例子是一個真實的運行著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
在輸出信息中,你能看到有一個720896kb的內存分配(有12kb的交換)還有一個156kb的交換是另一個進程的。基本上我們的內存只會有很小的內存交換,因此不會產生任何的問題。
假如進程的內存有相當部分花在了swap上,那么你的延遲可能就與swap有關。假如redis出現這種情況那么可以用vmstat 命令來驗證一下猜測:
$ vmstat 1
procs -----------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,這兩行分別統計了從swap文件恢復到內存的數量和swap到文件的內存數量。如果在這兩行發現了非0值那么就說明系統正在進行swap。
最后,可以用iostat命令來查看系統的全局I/O行為。
$ iostat -xk 1
avg-cpu: %user %nice %system %iowait %steal %idle
13.55 0.04 2.92 0.53 0.00 82.95
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await svctm %util
sda 0.77 0.00 0.01 0.00 0.40 0.00 73.65 0.00 3.62 2.58 0.00
sdb 1.27 4.75 0.82 3.54 38.00 32.32 32.19 0.11 24.80 4.24 1.85
如果確認延遲是由于swap引起的,那么就需要減小系統的內存壓力,要么給機器增加內存,要么不要在同一個機器上運行其他消耗內存的程序。
AOF 和硬盤I/O操作延遲
另一個延遲的根源是Redis的AOF(僅附加文件)模式。AOF基本上是通過兩個系統間的調用來完成工作的。 一個是寫,用來寫數據到AOF, 另外一個是文件數據同步,通過清除硬盤上空核心文件的緩沖來保證用戶指定的持久級別。
包括寫和文件數據同步的調用都可以導致延遲的根源。 寫實例可以阻塞系統范圍的同步操作,也可以阻塞當輸出的緩沖區滿并且內核需要清空到硬盤來接受新的寫入的操作。
文件數據同步對于延遲的影響非常大,因為它涉及到好幾步調用,可能要花掉幾毫秒以致幾秒的時間,特別是在還有其他進程后也在占用I/O的情況下。因為這個原因,從redis2.4開始用一個單獨的線程來做文件數據同步。
我們來看看當使用AOF的時候如何配置來降低延遲。
通過設置AOF相關的appendfsync項,可以使用三種不同的方式來執行文件同步(也可以在運行時使用CONFIG SET 命令來修改這個配置)。
- appendfsync 的值設置為no,redis不執行fsync。這種情況下造成延遲的唯一原因就是寫操作。這種延遲沒有辦法可以解決,因為redis接收到數據的速度是不可控的,不過這種情況也不常見,除非有其他的進程占用I/O使得硬盤速度突然下降。
- appendfsync 的值設置為everysec,每秒都會執行fsync。fsync 由一個單獨線程執行,如果需要寫操作的時候有fsync正在執行redis就會用一個buffer來延遲寫入2秒(因為在Linux如果一個fsync 正在運行那么對該文件的寫操作就會被堵塞)。如果fsync 耗時過長(譯者注:超過了2秒),即使fsync 還在進行redis也會執行寫操作,這就會造成延遲。
- appendfsync 的值設置為always ,fsync 會在每次寫操作返回成功代碼之前執行(事實上redis會積累多個命令在一次fsync 過程中執行)。這種模式下的性能表現是非常差勁的,所以最好使用一個快速的磁盤和文件系統以加快fsync 的執行。
大多數redis用戶都會把這個值設成 no 或者 everysec。要減少延遲,最好避免在同一個機器上有其他耗費I/O的程序。用SSD也有益于降低延遲,不過即使不使用SSD,如果能有冗余的硬盤專用于AOF也會減少尋址時間,從而降低延遲。
如果你想診斷AOF相關的延遲原因可以使用strace 命令:
sudo strace -p $(pidof redis-server) -T -e trace=fdatasync
上面的命令會展示redis主線程里所有的fdatasync系統調用。不包括后臺線程執行的fdatasync 調用。如果appendfsync 配置為everysec,則給strace增加-f選項。
用下面命令可以看到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有兩種方式來去除過期的key:
- lazy 方式,在key被請求的時候才檢查是否過期。 to be already expired.
- active 方式,每0.1秒進行一次過期檢查。
active過期模式是自適應的,每過100毫秒開始一次過期檢查(每秒10次),每次作如下操作:
- 根 據 REDIS_EXPIRELOOKUPS_PER_CRON 的值去除已經過期的key(是指如果過期的key數量超過了REDIS_EXPIRELOOKUPS_PER_CRON 的值才會啟動過期操作,太少就不必了。這個值默認為10), evicting all the keys already expired.
- 假如超過25%(是指REDIS_EXPIRELOOKUPS_PER_CRON這個值的25%,這個值默認為10,譯者注)的key已經過期,則重復一遍檢查失效的過程。
REDIS_EXPIRELOOKUPS_PER_CRON 默認為10, 過期檢查一秒會執行10次,通常在actively模式下1秒能處理100個key。在過期的key有一段時間沒被訪問的情況下這個清理速度已經足夠了, 所以 lazy模式基本上沒什么用。1秒只過期100個key也不會對redis造成多大的影響。
這種算法式是自適應的,如果發現有超過指定數量25%的key已經過期就會循環執行。這個過程每秒會運行10次,這意味著隨機樣本中超過25%的key會在1秒內過期。
通常來講如果有很多key在同一秒過期,超過了所有key的25%,redis就會阻塞直到過期key的比例下降到25%以下。
使用這種策略是為了避免清除過期key的過程占用太多內存,這種方法對系統幾乎不會有不良影響,因為大量key同時到期并非是一種常見現象,不過如果用戶使用了 EXPIREAT 來設置過期時間的話也是有可能的。
總而言之: 要知道大量key同時過期會對系統延遲造成影響。
Redis 看門狗
Redis2.6版本引進了redis看門狗(watchdog)軟件,這是個調試工具用于診斷Redis的延遲問題
這個看門狗軟件還是一個實驗性功能,當用于生產環境時,請小心并做好備份工作,可能有意想不到的問題影響正常的redis服務。
當你沒有更好的工具追蹤問題時,可以使用它。
這個功能是這樣工作的:
- 用戶通過命令CONFIG SET開啟軟件看門狗
- Redis啟動監測程序監測自己的狀態
- 如果Redis檢測到服務器被某些操作阻塞了,并運行速度不夠快,也許是因為延遲導致的,Redis就會在log文件中寫入一份關于被阻塞服務器的底層監測數據報表
- 用戶通過Redis Google Group發送消息給開發人員,消息包括看門狗報表。
請注意,這項功能不能通過redis.conf文件開啟,因為這項夠能設計之初就是面向正在運行的服務器,而且只是為了調試程序。
如果要開啟該功能,只需運行如下命令:
CONFIG SET watchdog-period 500
時間間隔以毫秒為單位。在上面的例子中,我指定了,當服務器檢測到500毫秒或更大的延遲的時候,才記錄延遲事件。最小的時間間隔是200毫秒。
當你運行完了軟件看門狗,你可以通過設置時間間隔參數為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:我們獲得越多的信息,我們就越容易分析得到你的服務器到底有什么問題。
大內存頁的實驗
fork產生的延遲,可以通過大內存頁來減緩,只是需要耗費更大的內存。下面的附錄將詳細描述在Linux內核中實現的這個功能。
雖然某些CPU會使用不同大小的頁面。AMD和Intel CPU可以支持2MB的頁面大小。這些頁面有個別名,叫做“大頁面”。某些操作系統可以實時地優化頁面大小,透明地把小頁面聚合成大頁面。
在Linux系統,顯式的huge page管理在2.6.16中得到支持,并且隱式透明的huge page管理也在2.6.38中得到支持。如果你的是最近的Linux發行版本(例如 RH6或者其派生版本),透明的huge page可以被開啟,并且你可以使用包含這項夠能的Redis版本。
這個是在Linux中,實驗/使用huge page的最佳方法。
現在,如果運行舊版本的Linux(RH5, SLES 10-11, 或者其派生版本),不要害怕使用一些技巧,Redis可以通過補丁來支持huge page。
第一步,閱讀Mel Gorman's primer on huge pages
現在有兩個方法給Redis打補丁,讓它支持huge page
- 對于Redis 2.4,內置的jemalloc 分配器需要打上補丁。Pieter Noordhuis的補丁patch 。需要注意,這個補丁依賴于匿名mmap huge page的支持,這項功能只能從2.6.32之后才得到支持,所以這個方法不能用于舊的版本(RH5 ,SLES 10, 和其派生版本)
- 對于Redis 2.2 或者2.4,附帶libc分配器,必須修改redis makefile,使Redis和the libhugetlbfs library進行連接。這個是最直接的更改
然后,系統必須配置為支持huge page
以下命令分配和創建 N個huge page:
$ sudo sysctl -w vm.nr_hugepages=<N>
以下命令掛載huge page到文件系統
$ sudo mount -t hugetlbfs none /mnt/hugetlbfs
在所有的情況下,一旦Redis運行huge page(透明或者非透明),將會得到如下的好處:
- 由于fork引起的延遲將得到緩解。尤其是對超大的實例,尤其是在VM上運行的實例。
- Redis速度得到提夠,是因為CPU的轉換旁視緩沖(TLB)更有效的緩存頁面(例如命中率會更高)。不要期望有奇跡發生,性能至多只能提高一點。
- Redis內存不會再被換走,這樣就能避免由于虛擬內存造成的不必的延遲。
很不幸,除了更多的復雜操作,還有redis使用huge page會帶來一個明顯的缺陷。COW機制的粒度是頁面。伴隨2MB頁面,頁面被后臺存儲操作修改的可能性是4KB頁面的512倍。實際的內存需要后臺存 儲,所以可能性會增加很多,尤其是,當寫操作很隨機,并且伴隨很差的定位。通過huge page,使用兩倍的內存,而存儲將不再是理論的突發事件。它真的會發生。
完整的性能評估結果可以參閱鏈接:https://gist.github.com/1272254
原文出處:http://www.oschina.net/translate/redis-latency-problems-troubleshooting
posted on 2014-07-22 17:11
zhangxl 閱讀(1837)
評論(0) 編輯 收藏 所屬分類:
nosql