Redis 性能優(yōu)化思路
來(lái)源丨www.jianshu.com/p/67093716547b
那么,針對(duì) Redis 服務(wù),我們能做哪些性能優(yōu)化呢?或者說(shuō),應(yīng)該避免哪些性能浪費(fèi)呢?
Redis 性能的基本面
在討論優(yōu)化之前,我們需要知道,Redis 服務(wù)本身就有一些特性,比如單線程運(yùn)行。除非修改 Redis 的源代碼,不然這些特性,就是我們思考性能優(yōu)化的基本面。
那么,有哪些 Redis 基本特性需要我們考慮呢?Redis 的項(xiàng)目介紹中概括了它特性:
Redis is an in-memory database that persists on disk. The data model is key-value, but many different kind of values are supported.
優(yōu)化網(wǎng)絡(luò)延時(shí)
Redis 的官方博客在幾個(gè)地方都說(shuō),性能瓶頸更可能是網(wǎng)絡(luò)[6],那么我們?nèi)绾蝺?yōu)化網(wǎng)絡(luò)上的延時(shí)呢?
首先,如果你們使用單機(jī)部署(應(yīng)用服務(wù)和 Redis 在同一臺(tái)機(jī)器上)的話,使用 Unix 進(jìn)程間通訊來(lái)請(qǐng)求 Redis 服務(wù),速度比 localhost 局域網(wǎng)(學(xué)名 loopback)更快。官方文檔[7]是這么說(shuō)的,想一想,理論上也應(yīng)該是這樣的。
但很多公司的業(yè)務(wù)規(guī)模不是單機(jī)部署能支撐的,所以還是得用 TCP。
Redis 客戶(hù)端和服務(wù)器的通訊一般使用 TCP 長(zhǎng)鏈接。如果客戶(hù)端發(fā)送請(qǐng)求后需要等待 Redis 返回結(jié)果再發(fā)送下一個(gè)指令,客戶(hù)端和 Redis 的多個(gè)請(qǐng)求就構(gòu)成下面的關(guān)系:

(備注:如果不是你要發(fā)送的 key 特別長(zhǎng),一個(gè) TCP 包完全能放下 Redis 指令,所以只畫(huà)了一個(gè) push 包)這樣這兩次請(qǐng)求中,客戶(hù)端都需要經(jīng)歷一段網(wǎng)絡(luò)傳輸時(shí)間。
但如果有可能,完全可以使用 multi-key 類(lèi)的指令來(lái)合并請(qǐng)求,比如兩個(gè) GET key 可以用 MGET key1 key2 合并。這樣在實(shí)際通訊中,請(qǐng)求數(shù)也減少了,延時(shí)自然得到好轉(zhuǎn)。
如果不能用 multi-key 指令來(lái)合并,比如一個(gè) SET,一個(gè) GET 無(wú)法合并。怎么辦?
MULTI/EXEC,一個(gè)是 script。前者本來(lái)是構(gòu)建 Redis 事務(wù)的方法,但確實(shí)可以合并多個(gè)指令為一個(gè) request,它到通訊過(guò)程如下。至于 script,最好利用緩存腳本的 sha1 hash key 來(lái)調(diào)起腳本,這樣通訊量更小。
這樣確實(shí)更能減少網(wǎng)絡(luò)傳輸時(shí)間,不是么?但如此以來(lái),就必須要求這個(gè) transaction / script 中涉及的 key 在同一個(gè) node 上,所以要酌情考慮。

這樣,理論上可以省去 1 次回復(fù)所用的網(wǎng)絡(luò)傳輸時(shí)間。這就是 pipeline 做的事情。舉個(gè) ruby 客戶(hù)端使用 pipeline 的例子:
require 'redis'
@redis = Redis.new()
@redis.pipelined do
@redis.get 'key1'
@redis.set 'key2' 'some value'
end
# => [1, 2]
據(jù)說(shuō),有些語(yǔ)言的客戶(hù)端,甚至默認(rèn)就使用 pipeline 來(lái)優(yōu)化延時(shí)問(wèn)題,比如 node_redis。
另外,不是任意多個(gè)回復(fù)信息都可以放進(jìn)一個(gè) TCP 包中,如果請(qǐng)求數(shù)太多,回復(fù)的數(shù)據(jù)很長(zhǎng)(比如 get 一個(gè)長(zhǎng)字符串),TCP 還是會(huì)分包傳輸,但使用 pipeline,依然可以減少傳輸次數(shù)。
pipeline 和上面的其他方法都不一樣的是,它不具有原子性。所以在 cluster 狀態(tài)下的集群上,實(shí)現(xiàn) pipeline 比那些原子性的方法更有可能。
小結(jié)一下:
-
使用 unix 進(jìn)程間通信,如果單機(jī)部署 -
使用 multi-key 指令合并多個(gè)指令,減少請(qǐng)求數(shù),如果有可能的話 -
使用 transaction、script 合并 requests 以及 responses -
使用 pipeline 合并 response
警惕執(zhí)行時(shí)間長(zhǎng)的操作
在大數(shù)據(jù)量的情況下,有些操作的執(zhí)行時(shí)間會(huì)相對(duì)長(zhǎng),比如 KEYS *,LRANGE mylist 0 -1,以及其他算法復(fù)雜度為 O(n) 的指令。因?yàn)?Redis 只用一個(gè)線程來(lái)做數(shù)據(jù)查詢(xún),如果這些指令耗時(shí)很長(zhǎng),就會(huì)阻塞 Redis,造成大量延時(shí)。
盡管官方文檔中說(shuō) KEYS *的查詢(xún)挺快的,(在普通筆記本上)掃描 1 百萬(wàn)個(gè) key,只需 40 毫秒(參見(jiàn):https://redis.io/commands/keys),但幾十 ms 對(duì)于一個(gè)性能要求很高的系統(tǒng)來(lái)說(shuō),已經(jīng)不短了,更何況如果有幾億個(gè) key(一臺(tái)機(jī)器完全可能存幾億個(gè) key,比如一個(gè) key 100字節(jié),1 億個(gè) key 只有 10GB),時(shí)間更長(zhǎng)。
所以,盡量不要在生產(chǎn)環(huán)境的代碼使用這些執(zhí)行很慢的指令,這一點(diǎn) Redis 的作者在博客[8]中也提到了。另外,運(yùn)維同學(xué)查詢(xún) Redis 的時(shí)候也盡量不要用。甚至,Redis Essential 這本書(shū)建議利用 rename-command KEYS '' 來(lái)禁止使用這個(gè)耗時(shí)的指令。
除了這些耗時(shí)的指令,Redis 中 transaction,script,因?yàn)榭梢院喜⒍鄠€(gè) commands 為一個(gè)具有原子性的執(zhí)行過(guò)程,所以也可能占用 Redis 很長(zhǎng)時(shí)間,需要注意。
如果你想找出生產(chǎn)環(huán)境使用的「慢指令」,那么可以利用 SLOWLOG GET count 來(lái)查看最近的 count 個(gè)執(zhí)行時(shí)間很長(zhǎng)的指令。至于多長(zhǎng)算長(zhǎng),可以通過(guò)在 redis.conf 中設(shè)置 slowlog-log-slower-than 來(lái)定義。
除此之外,在很多地方都沒(méi)有提到的一個(gè)可能的慢指令是 DEL,但 redis.conf 文件的注釋[9]中倒是說(shuō)了。長(zhǎng)話短說(shuō)就是 DEL 一個(gè)大的 object 時(shí)候,回收相應(yīng)的內(nèi)存可能會(huì)需要很長(zhǎng)時(shí)間(甚至幾秒),所以,建議用 DEL 的異步版本:UNLINK。后者會(huì)啟動(dòng)一個(gè)新的 thread 來(lái)刪除目標(biāo) key,而不阻塞原來(lái)的線程。
更進(jìn)一步,當(dāng)一個(gè) key 過(guò)期之后,Redis 一般也需要同步的把它刪除。其中一種刪除 keys 的方式是,每秒 10 次的檢查一次有設(shè)置過(guò)期時(shí)間的 keys,這些 keys 存儲(chǔ)在一個(gè)全局的 struct 中,可以用 server.db->expires 訪問(wèn)。
檢查的方式是:
-
從中隨機(jī)取出 20 個(gè) keys -
把過(guò)期的刪掉。 -
如果剛剛 20 個(gè) keys 中,有 25% 以上(也就是 5 個(gè)以上)都是過(guò)期的,Redis 認(rèn)為,過(guò)期的 keys 還挺多的,繼續(xù)重復(fù)步驟 1,直到滿足退出條件:某次取出的 keys 中沒(méi)有那么多過(guò)去的 keys。
這里對(duì)于性能的影響是,如果真的有很多的 keys 在同一時(shí)間過(guò)期,那么 Redis 真的會(huì)一直循環(huán)執(zhí)行刪除,占用主線程。
對(duì)此,Redis 作者的建議[10]是警惕 EXPIREAT 這個(gè)指令,因?yàn)樗菀桩a(chǎn)生 keys 同時(shí)過(guò)期的現(xiàn)象。我還見(jiàn)到過(guò)一些建議是給 keys 的過(guò)期時(shí)間設(shè)置一個(gè)隨機(jī)波動(dòng)量。最后,redis.conf 中也給出了一個(gè)方法,把 keys 的過(guò)期刪除操作變?yōu)楫惒降模矗?redis.conf 中設(shè)置 lazyfree-lazy-expire yes。
優(yōu)化數(shù)據(jù)結(jié)構(gòu)、使用正確的算法
一種數(shù)據(jù)類(lèi)型(比如 string,list)進(jìn)行增刪改查的效率是由其底層的存儲(chǔ)結(jié)構(gòu)決定的。
我們?cè)谑褂靡环N數(shù)據(jù)類(lèi)型時(shí),可以適當(dāng)關(guān)注一下它底層的存儲(chǔ)結(jié)構(gòu)及其算法,避免使用復(fù)雜度太高的方法。
舉兩個(gè)例子:
-
ZADD的時(shí)間復(fù)雜度是 O(log(N)),這比其他數(shù)據(jù)類(lèi)型增加一個(gè)新元素的操作更復(fù)雜,所以要小心使用。 -
若 Hash 類(lèi)型的值的 fields 數(shù)量有限,它很有可能采用 ziplist 這種結(jié)構(gòu)做存儲(chǔ),而 ziplist 的查詢(xún)效率可能沒(méi)有同等字段數(shù)量的 hashtable 效率高,在必要時(shí),可以調(diào)整 Redis 的存儲(chǔ)結(jié)構(gòu)。
如何做出更好的權(quán)衡?我覺(jué)得得深挖 Redis 的存儲(chǔ)結(jié)構(gòu)才能讓自己安心。這方面的內(nèi)容我們下次再說(shuō)。
考慮操作系統(tǒng)和硬件是否影響性能
Redis 運(yùn)行的外部環(huán)境,也就是操作系統(tǒng)和硬件顯然也會(huì)影響 Redis 的性能。在官方文檔中,就給出了一些例子:
-
CPU:Intel 多種 CPU 都比 AMD 皓龍系列好 -
虛擬化:實(shí)體機(jī)比虛擬機(jī)好,主要是因?yàn)椴糠痔摂M機(jī)上,硬盤(pán)不是本地硬盤(pán),監(jiān)控軟件導(dǎo)致 fork 指令的速度慢(持久化時(shí)會(huì)用到 fork),尤其是用 Xen 來(lái)做虛擬化時(shí)。 -
內(nèi)存管理:在 linux 操作系統(tǒng)中,為了讓 translation lookaside buffer,即 TLB,能夠管理更多內(nèi)存空間(TLB 只能緩存有限個(gè) page),操作系統(tǒng)把一些 memory page 變得更大,比如 2MB 或者 1GB,而不是通常的 4096 字節(jié),這些大的內(nèi)存頁(yè)叫做 huge pages。同時(shí),為了方便程序員使用這些大的內(nèi)存 page,操作系統(tǒng)中實(shí)現(xiàn)了一個(gè) transparent huge pages(THP)機(jī)制,使得大內(nèi)存頁(yè)對(duì)他們來(lái)說(shuō)是透明的,可以像使用正常的內(nèi)存 page 一樣使用他們。但這種機(jī)制并不是數(shù)據(jù)庫(kù)所需要的,可能是因?yàn)?THP 會(huì)把內(nèi)存空間變得緊湊而連續(xù)吧,就像mongodb 的文檔[11]中明確說(shuō)的,數(shù)據(jù)庫(kù)需要的是稀疏的內(nèi)存空間,所以請(qǐng)禁掉 THP 功能。Redis 也不例外,但 Redis 官方博客上給出的理由是:使用大內(nèi)存 page 會(huì)使 bgsave 時(shí),fork 的速度變慢;如果 fork 之后,這些內(nèi)存 page 在原進(jìn)程中被修改了,他們就需要被復(fù)制(即 copy on write),這樣的復(fù)制會(huì)消耗大量的內(nèi)存(畢竟,人家是 huge pages,復(fù)制一份消耗成本很大)。所以,請(qǐng)禁止掉操作系統(tǒng)中的 transparent huge pages 功能。 -
交換空間:當(dāng)一些內(nèi)存 page 被存儲(chǔ)在交換空間文件上,而 Redis 又要請(qǐng)求那些數(shù)據(jù),那么操作系統(tǒng)會(huì)阻塞 Redis 進(jìn)程,然后把想要的 page,從交換空間中拿出來(lái),放進(jìn)內(nèi)存。這其中涉及整個(gè)進(jìn)程的阻塞,所以可能會(huì)造成延時(shí)問(wèn)題,一個(gè)解決方法是禁止使用交換空間(Redis Essentials 中如是建議,如果內(nèi)存空間不足,請(qǐng)用別的方法處理)。
考慮持久化帶來(lái)的開(kāi)銷(xiāo)
Redis 的一項(xiàng)重要功能就是持久化,也就是把數(shù)據(jù)復(fù)制到硬盤(pán)上。基于持久化,才有了 Redis 的數(shù)據(jù)恢復(fù)等功能。
但維護(hù)這個(gè)持久化的功能,也是有性能開(kāi)銷(xiāo)的。
接下來(lái),我們看另外一種持久化方式:AOF 增量持久化。
這種持久化方式會(huì)把你發(fā)到 redis server 的指令以文本的形式保存下來(lái)(格式遵循 redis protocol),這個(gè)過(guò)程中,會(huì)調(diào)用兩個(gè)系統(tǒng)調(diào)用,一個(gè)是 write(2),同步完成,一個(gè)是 fsync(2),異步完成。
這兩部都可能是延時(shí)問(wèn)題的原因:
-
write 可能會(huì)因?yàn)檩敵龅?buffer 滿了,或者 kernal 正在把 buffer 中的數(shù)據(jù)同步到硬盤(pán),就被阻塞了。 -
fsync 的作用是確保 write 寫(xiě)入到 aof 文件的數(shù)據(jù)落到了硬盤(pán)上,在一個(gè) 7200 轉(zhuǎn)/分的硬盤(pán)上可能要延時(shí) 20 毫秒左右,消耗還是挺大的。更重要的是,在 fsync 進(jìn)行的時(shí)候,write 可能會(huì)被阻塞。
其中,write 的阻塞貌似只能接受,因?yàn)闆](méi)有更好的方法把數(shù)據(jù)寫(xiě)到一個(gè)文件中了。但對(duì)于 fsync,Redis 允許三種配置,選用哪種取決于你對(duì)備份及時(shí)性和性能的平衡:
-
always:當(dāng)把 appendfsync 設(shè)置為 always,fsync 會(huì)和客戶(hù)端的指令同步執(zhí)行,因此最可能造成延時(shí)問(wèn)題,但備份及時(shí)性最好。 -
everysec:每秒鐘異步執(zhí)行一次 fsync,此時(shí) redis 的性能表現(xiàn)會(huì)更好,但是 fsync 依然可能阻塞 write,算是一個(gè)折中選擇。 -
no:redis 不會(huì)主動(dòng)出發(fā) fsync (并不是永遠(yuǎn)不 fsync,那是不太可能的),而由 kernel 決定何時(shí) fsync
使用分布式架構(gòu) —— 讀寫(xiě)分離、數(shù)據(jù)分片
以上,我們都是基于單臺(tái),或者單個(gè) Redis 服務(wù)進(jìn)行優(yōu)化。下面,我們考慮當(dāng)網(wǎng)站的規(guī)模變大時(shí),利用分布式架構(gòu)來(lái)保障 Redis 性能的問(wèn)題。
首先說(shuō),哪些情況下不得不(或者最好)使用分布式架構(gòu):
-
數(shù)據(jù)量很大,單臺(tái)服務(wù)器內(nèi)存不可能裝得下,比如 1 個(gè) T 這種量級(jí) -
需要服務(wù)高可用 -
單臺(tái)的請(qǐng)求壓力過(guò)大
解決這些問(wèn)題可以采用數(shù)據(jù)分片或者主從分離,或者兩者都用(即,在分片用的 cluster 節(jié)點(diǎn)上,也設(shè)置主從結(jié)構(gòu))。
這樣的架構(gòu),可以為性能提升加入新的切入點(diǎn):
-
把慢速的指令發(fā)到某些從庫(kù)中執(zhí)行 -
把持久化功能放在一個(gè)很少使用的從庫(kù)上 -
把某些大 list 分片
其中前兩條都是根據(jù) Redis 單線程的特性,用其他進(jìn)程(甚至機(jī)器)做性能補(bǔ)充的方法。
當(dāng)然,使用分布式架構(gòu),也可能對(duì)性能有影響,比如請(qǐng)求需要被轉(zhuǎn)發(fā),數(shù)據(jù)需要被不斷復(fù)制分發(fā)。(待查)
后話
其實(shí)還有很多東西也影響 Redis 的性能,比如 active rehashing(keys 主表的再哈希,每秒 10 次,關(guān)掉它可以提升一點(diǎn)點(diǎn)性能),但是這篇博客已經(jīng)寫(xiě)的很長(zhǎng)了。而且,更重要不是收集已經(jīng)被別人提出的問(wèn)題,然后記憶解決方案;而是掌握 Redis 的基本原理,以不變應(yīng)萬(wàn)變的方式杜絕新出現(xiàn)的問(wèn)題。
參考資料
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來(lái),可以說(shuō)是程序員面試必備!所有資料都整理到網(wǎng)盤(pán)了,歡迎下載!

面試題】即可獲取
