redis internal【转】
原文地址:https://zhuanlan.zhihu.com/p/37800945
本篇文章介紹Redis協議、內存模型、持久化以及分布式解決方案。
1. redis協議:
redisClient與redisServer之間的通信協議叫:RESP (REdis Serialization Protocol)。
RESP遵循Request-Response模型,具體實現如下:
- Clients send commands to a Redis server as a RESP Array of?Bulk Strings.
- The server replies with one of the RESP types according to the command implementation.
在RESP中,數據的類型取決于該數據的第一個字節:
- For?Simple Strings?the first byte of the reply is "+"
- For?Errors?the first byte of the reply is "-"
- For?Integers?the first byte of the reply is ":"
- For?Bulk Strings?the first byte of the reply is "$"
- For?Arrays?the first byte of the reply is "*"
RESP Arrays 的格式如下:
- A * character as the first byte, followed by the number of elements in the array as a decimal number, followed by CRLF.
- An additional RESP type for every element of the Array.
RESP空數組:
"*0\r\n"
兩個元素的RESP數組:
"*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"
2. 內存模型
2.1 redis內存統計
redis info memory- used_memory?: 由 Redis 分配器分配的內存總量,以字節(byte)為單位
- used_memory_human?: 以人類可讀的格式返回 Redis 分配的內存總量
- used_memory_rss?: 從操作系統的角度,返回 Redis 已分配的內存總量(俗稱常駐集大小)。這個值和?top?、?ps?等命令的輸出一致。
- used_memory_peak?: Redis 的內存消耗峰值(以字節為單位)
- used_memory_peak_human?: 以人類可讀的格式返回 Redis 的內存消耗峰值
- used_memory_lua?: Lua 引擎所使用的內存大小(以字節為單位)
- mem_fragmentation_ratio?:?used_memory_rss?和?used_memory?之間的比率
- mem_allocator?: 在編譯時指定的, Redis 所使用的內存分配器。可以是 libc 、 jemalloc 或者 tcmalloc
2.2 redis內存劃分
- 數據
作為數據庫,數據是最主要的部分;這部分占用的內存會統計在used_memory中。
- 進程本身需要的內存:
Redis主進程本身運行肯定需要占用內存,如代碼、常量池等等;這部分內存大約幾兆,在大多數生產環境中與Redis數據占用的內存相比可以忽略。這部分內存不是由jemalloc分配,因此不會統計在used_memory中。
補充說明:除了主進程外,Redis創建的子進程運行也會占用內存,如Redis執行AOF、RDB重寫時創建的子進程。當然,這部分內存不屬于Redis進程,也不會統計在used_memory和used_memory_rss中。
- 緩沖內存
緩沖內存包括客戶端緩沖區、復制積壓緩沖區、AOF緩沖區等;其中,客戶端緩沖存儲客戶端連接的輸入輸出緩沖;復制積壓緩沖用于部分復制功能;AOF緩沖區用于在進行AOF重寫時,保存最近的寫入命令。在了解相應功能之前,不需要知道這些緩沖的細節;這部分內存由jemalloc分配,因此會統計在used_memory中。
- 內存碎片
內存碎片是Redis在分配、回收物理內存過程中產生的。例如,如果對數據的更改頻繁,而且數據之間的大小相差很大,可能導致redis釋放的空間在物理內存中并沒有釋放,但redis又無法有效利用,這就形成了內存碎片。內存碎片不會統計在used_memory中。
內存碎片的產生與對數據進行的操作、數據的特點等都有關;此外,與使用的內存分配器也有關系:如果內存分配器設計合理,可以盡可能的減少內存碎片的產生。后面將要說到的jemalloc便在控制內存碎片方面做的很好。
如果Redis服務器中的內存碎片已經很大,可以通過安全重啟的方式減小內存碎片:因為重啟之后,Redis重新從備份文件中讀取數據,在內存中進行重排,為每個數據重新選擇合適的內存單元,減小內存碎片。
2.3 數據存儲
下面是執行`SET HELLO WORLD`,所涉及到的數據模型:
redis dictEntry- dictEntry:Redis是Key-Value數據庫,因此對每個鍵值對都會有一個dictEntry,里面存儲了指向Key和Value的指針;next指向下一個dictEntry,與本Key-Value無關。
- Key:圖中右上角可見,Key(”hello”)并不是直接以字符串存儲,而是存儲在SDS結構中。
- redisObject:Value(“world”)既不是直接以字符串存儲,也不是像Key一樣直接存儲在SDS中,而是存儲在redisObject中。實際上,不論Value是5種類型的哪一種,都是通過redisObject來存儲的;而redisObject中的type字段指明了Value對象的類型,ptr字段則指向對象所在的地址。不過可以看出,字符串對象雖然經過了redisObject的包裝,但仍然需要通過SDS存儲。
- jemalloc:無論是DictEntry對象,還是redisObject、SDS對象,都需要內存分配器(如jemalloc)分配內存進行存儲。以DictEntry對象為例,有3個指針組成,在64位機器下占24個字節,jemalloc會為它分配32字節大小的內存單元。
2.4 redisObject
前面說到,Redis對象有5種類型;無論是哪種類型,Redis都不會直接存儲,而是通過redisObject對象進行存儲。
redisObject對象非常重要,Redis對象的類型、內部編碼、內存回收、共享對象等功能,都需要redisObject支持,下面將通過redisObject的結構來說明它是如何起作用的。
redisObject的定義如下(不同版本的Redis可能稍稍有所不同):
typedef struct redisObject {unsigned type:4; unsigned encoding:4; unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */ int refcount; void *ptr; } robj;redisObject的每個字段的含義和作用如下:
- type:
type字段表示對象的類型,占4個比特;目前包括REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。
- encoding
encoding表示對象的內部編碼,占4個比特。
對于Redis支持的每種類型,都有至少兩種內部編碼,例如對于字符串,有int、embstr、raw三種編碼。通過encoding屬性,Redis可以根據不同的使用場景來為對象設置不同的編碼,大大提高了Redis的靈活性和效率。以列表對象為例,有壓縮列表和雙端鏈表兩種編碼方式;如果列表中的元素較少,Redis傾向于使用壓縮列表進行存儲,因為壓縮列表占用內存更少,而且比雙端鏈表可以更快載入;當列表對象元素較多時,壓縮列表就會轉化為更適合存儲大量元素的雙端鏈表。
- lru
lru記錄的是對象最后一次被命令程序訪問的時間,占據的比特數不同的版本有所不同(如4.0版本占24比特,2.6版本占22比特)。
通過對比lru時間與當前時間,可以計算某個對象的空轉時間;object idletime命令可以顯示該空轉時間(單位是秒)。object idletime命令的一個特殊之處在于它不改變對象的lru值。
lru值除了通過object idletime命令打印之外,還與Redis的內存回收有關系:如果Redis打開了maxmemory選項,且內存回收算法選擇的是volatile-lru或allkeys—lru,那么當Redis內存占用超過maxmemory指定的值時,Redis會優先選擇空轉時間最長的對象進行釋放。
- refcount
refcount記錄的是該對象被引用的次數,類型為整型。refcount的作用,主要在于對象的引用計數和內存回收。當創建新對象時,refcount初始化為1;當有新程序使用該對象時,refcount加1;當對象不再被一個新程序使用時,refcount減1;當refcount變為0時,對象占用的內存會被釋放。
- 共享對象
Redis中被多次使用的對象(refcount>1),稱為共享對象。Redis為了節省內存,當有一些對象重復出現時,新的程序不會創建新的對象,而是仍然使用原來的對象。這個被重復使用的對象,就是共享對象。目前共享對象僅支持整數值的字符串對象。
Redis的共享對象目前只支持整數值的字符串對象。之所以如此,實際上是對內存和CPU(時間)的平衡:共享對象雖然會降低內存消耗,但是判斷兩個對象是否相等卻需要消耗額外的時間。對于整數值,判斷操作復雜度為O(1);對于普通字符串,判斷復雜度為O(n);而對于哈希、列表、集合和有序集合,判斷的復雜度為O(n^2)。
雖然共享對象只能是整數值的字符串對象,但是5種類型都可能使用共享對象(如哈希、列表等的元素可以使用)。
就目前的實現來說,Redis服務器在初始化時,會創建10000個字符串對象,值分別是0~9999的整數值;當Redis需要使用值為0~9999的字符串對象時,可以直接使用這些共享對象。10000這個數字可以通過調整參數REDIS_SHARED_INTEGERS(4.0中是OBJ_SHARED_INTEGERS)的值進行改變。
共享對象的引用次數可以通過object refcount命令查看,如下圖所示。命令執行的結果頁佐證了只有0~9999之間的整數會作為共享對象。
- ptr
ptr指針指向具體的數據,如前面的例子中,set hello world,ptr指向包含字符串world的SDS。
?
2.5 SDS (Simple Dynamic String)
sds的結構如下:
struct sdshdr { int len; int free; char buf[]; };其中,buf表示字節數組,用來存儲字符串;len表示buf已使用的長度,free表示buf未使用的長度。下面是兩個例子。
?
圖片來源:《Redis設計與實現》圖片來源:《Redis設計與實現》?
通過SDS的結構可以看出,buf數組的長度=free+len+1(其中1表示字符串結尾的空字符);所以,一個SDS結構占據的空間為:free所占長度+len所占長度+ buf數組的長度 =4+4+free+len+1=free+len+9。
SDS在C字符串的基礎上加入了free和len字段,帶來了很多好處:
- 獲取字符串長度:SDS是O(1),C字符串是O(n)
- 緩沖區溢出:使用C字符串的API時,如果字符串長度增加(如strcat操作)而忘記重新分配內存,很容易造成緩沖區的溢出;而SDS由于記錄了長度,相應的API在可能造成緩沖區溢出時會自動重新分配內存,杜絕了緩沖區溢出。
- 修改字符串時內存的重分配:對于C字符串,如果要修改字符串,必須要重新分配內存(先釋放再申請),因為如果沒有重新分配,字符串長度增大時會造成內存緩沖區溢出,字符串長度減小時會造成內存泄露。而對于SDS,由于可以記錄len和free,因此解除了字符串長度和空間數組長度之間的關聯,可以在此基礎上進行優化:空間預分配策略(即分配內存時比實際需要的多)使得字符串長度增大時重新分配內存的概率大大減小;惰性空間釋放策略使得字符串長度減小時重新分配內存的概率大大減小。
- 存取二進制數據:SDS可以,C字符串不可以。因為C字符串以空字符作為字符串結束的標識,而對于一些二進制文件(如圖片等),內容可能包括空字符串,因此C字符串無法正確存取;而SDS以字符串長度len來作為字符串結束標識,因此沒有這個問題。
此外,由于SDS中的buf仍然使用了C字符串(即以’\0’結尾),因此SDS可以使用C字符串庫中的部分函數;但是需要注意的是,只有當SDS用來存儲文本數據時才可以這樣使用,在存儲二進制數據時則不行(’\0’不一定是結尾)。
Redis在存儲對象時,一律使用SDS代替C字符串。例如set hello world命令,hello和world都是以SDS的形式存儲的。而sadd myset member1 member2 member3命令,不論是鍵(”myset”),還是集合中的元素(”member1”、 ”member2”和”member3”),都是以SDS的形式存儲。除了存儲對象,SDS還用于存儲各種緩沖區。
只有在字符串不會改變的情況下,如打印日志時,才會使用C字符串。
2.6 估算Redis內存使用量
假設有90000個鍵值對,每個key的長度是7個字節,每個value的長度也是7個字節(且key和value都不是整數);下面來估算這90000個鍵值對所占用的空間。
每個dictEntry占據的空間包括:
- dictEntry: 24字節,jemalloc會分配32字節的內存塊
- key: 7字節,所以SDS(key)需要7+9=16個字節,jemalloc會分配16字節的內存塊
- redisObject: 16字節,jemalloc會分配16字節的內存塊
- value: 7字節,所以SDS(value)需要7+9=16個字節,jemalloc會分配16字節的內存塊
- bucket空間:bucket數組的大小為大于90000的最小的2^n,是131072;每個bucket元素為8字節(因為64位系統中指針大小為8字節)。
綜上:M = (32 + 16 + 16 + 16) * 90000 + 131072 * 80 = 8248576
驗證代碼如下:
3. redis 持久化:
Redis有兩種持久化方式,AOF和RDB,AOF持久化是指追加寫命令到aof文件的方式,RDB是指定期保存內存快照到rdb文件的方式。
RDB雖然可以通過bgsave指令后臺保存快照,但fork()子進程是有開銷的,在內存數據集較大的情況下會占用很長的cpu時間,fork新進程時,雖然可共享的數據內容不需要復制,但會復制之前進程空間的內存頁表,如果內存空間有40G(考慮每個頁表條目消耗 8 個字節),那么頁表大小就有80M,這個復制是需要時間的,在有的服務器結點上測試,35G的數據bgsave瞬間會阻塞200ms以上,一般建議Redis使用內存不超過20g。I/O消耗,我們線上是在Slave節點開啟rdb持久化,磁盤性能一般,1.2g的rdb文件持久化一分鐘一次,一次大概耗時30s左右,所以rdb的頻率也不能太頻繁,需要根據情況做好配置。
AOF是追加寫命令到aof文件的方式,優點是可以基本做到數據無損,缺點是文件增長較快,需要間歇性bgrewrite,bgrewrite也是一個既耗cpu又耗磁盤IO的操作,單cpu利用率最高可達100%。bgrewrite期間可以設置將新的寫請求暫時緩存,bgrewrite完成后同步寫盤,同步會暫時停止處理客戶端請求,如果bgrewrite時間較長,緩沖區積壓數據較多,核心阻塞時間會很長,所以如果必須要開啟aof,一般建議找幾個空閑時段設置腳本來做bgrewrite。
AOF還有一個比較坑的地方是刷盤策略fsync的設置,這個設置一般有3種方式:always、everysec、no,如果設置為no,就將寫盤的時機交給操作系統,這在很大程度上犧牲了aof數據無損的優勢,如果設置為always就意味著每條命令都會同步刷盤,會造成頻繁I/O,所以一般建議是設置everysec,Redis會默認每隔一秒進行一次fsync調用,將緩沖區中的數據寫到磁盤。但是當這一次的fsync調用時長超過1秒時。Redis會采取延遲fsync的策略,再等一秒鐘。也就是在兩秒后再進行fsync,這一次的fsync就不管會執行多長時間都會進行。
?
3.1 持久化策略選擇
持久化為Redis提供了異常情況下的數據恢復機制,但開啟持久化是有代價的,哪一種持久化都可能造成CPU卡頓,影響對客戶端請求的處理。不開啟持久化又存在風險,如果一旦誤重啟master節點,或者試想這樣一種場景,主從切換失敗,很可能因為疏忽直接重啟master,這時沒有開啟持久化的master會把所有slave的數據清0。所以是否開啟持久化,怎樣開啟持久化是一個難題。和運維同事探討了一些方案,這里總結一下供大家參考:
1、極端情況下可以容忍全量數據丟失,那么建議master關閉持久化,slave關閉持久化;
2、極端情況下不能容忍全量數據丟失,但可以容忍部分數據丟失,如果內存數據集較小且不會增長建議master開啟rdb,slave開啟rdb;如果數據集很大,或不確定數據集增長趨勢,建議master關閉持久化,slave開啟rdb
開啟rdb需要cpu和磁盤性能保障。如果master關閉持久化,slave開啟rdb需要保證slave的rdb不會被master誤重啟所覆蓋,這里提供幾種方案:
- 重啟腳本包一層命令先網絡請求加載備機備份目錄下的rdb文件后再執行start,可以防止誤重啟,但備機調整部署可能需要調整腳本,主機打開持久化也需要調整腳本
- 定時將rdb文件通過網絡io傳給master節點(文件大比較耗時,文件增長需要考慮定時腳本執行間隔,否則會造成持續的網絡io),而且也會有一定數據損失
- 定時備份Slave的rdb到備份目錄,不做任何其他操作,誤重啟時人工拷貝rdb到master節點(會有一定數據損失)
3、最大限度需要數據無損,建議master開啟aof,slave開啟aof
開啟aof需要cpu和磁盤性能保障。開啟aof建議fsync同步刷盤使用everysec,自定義腳本在應用空閑時定時做bgrewrite,bgrewrite期間增量數據做緩沖。
4. 集群解決方案
Redis是一個內存數據庫,也就是說存儲數據的容量不能超過主機內存大小。普通主機服務器的內存一般幾十G,但是我們需要存儲大容量的數據(比如上百G的數據)怎么辦? 由于內存大小的限制,使用一臺 Redis 實例顯然無法滿足需求,這時就需要使用 多臺 Redis (集群)作為緩存數據庫,才能在用戶請求時快速的進行響應。
?
4.1 類 codis 的架構
- 分片算法:基于 slot hash 桶;
- 分片實例之間相互獨立,每組 一個 master 實例和多個 slave;
- 路由信息存放到第三方存儲組件,如 zookeeper 或 etcd
- 旁路組件探活;
slot的遷移方案:
slots 方案:劃分了 1024 個 slot, slots 信息在 proxy 層感知; redis 進程中維護本實例上的所有 key 的一個 slot map;
遷移過程中的讀寫沖突處理:最小遷移單位為 key; 訪問邏輯都是先訪問 src 節點,再根據結果判斷是否需要進一步訪問 target 節點;
訪問的 key 還未被遷移:讀寫請求訪問 src 節點,處理后訪問:
訪問的 key 正在遷移:讀請求訪問 src 節點后直接返回; 寫請求無法處理,返回 retry
訪問的 key 已被遷移 (或不存在):讀寫請求訪問 src 節點,收到 moved 回復,繼續訪問 target 節點處理
?
4.2 基于官方redis cluster的方案
?
特點:
- 無中心架構,支持動態擴容,對業務透明
- 具備Sentinel的監控和自動Failover能力
- 客戶端不需要連接集群所有節點,連接集群中任何一個可用節點即可
- 高性能,客戶端直連redis服務,免去了proxy代理的損耗
缺點是不兼容原有客戶端,未在生產環境中大規模驗證,運維也很復雜,數據遷移需要人工干預,只能使用0號數據庫,不支持批量操作,分布式邏輯和存儲模塊耦合等。
?
參考鏈接:
Redis Protocol specification - Redishttps://redis.io/presentation/Redis_Cluster.pdf深入學習Redis(1):Redis內存模型 - 編程迷思 - 博客園?
?
轉載于:https://www.cnblogs.com/davidwang456/articles/11258196.html
總結
以上是生活随笔為你收集整理的redis internal【转】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: flink入门实战总结
- 下一篇: redis 面试问题问答Top 10