【4.分佈式存儲】-redis/codis

redis內存k-v,支持多種數據結構,第一個重點在於如何操做更快和適當的節省內存,第二個重點在於分佈式管理。本文redis基於3.0。第一部分將介紹全部內存數據結構實現,關注rehash的實現,對編寫內存存儲提供數據結構參考沒什麼框架,單線程,無內存池等複雜設計,基本不支持正規的ACID;還會介紹內存溢出淘汰策略,過時鍵刪除,持久化等功能;第二部分將介紹分佈式集羣,redis自身的主從模式/哨兵模式/集羣模式,經常使用的codis,公司自研的非開源集羣。順便說了些雙機房中redis同步方案,redis應用中應優化的點以及常見的redis熱key解決方案。前端

單機redis/內存數據結構

對象類型 編碼方式 選擇條件 編碼詳情
string int long類型整數 ptr直接指向整數
embstr動態字符串 長度<=44 數組形式組織sds,len/內存預分配/結尾有\0
動態字符串 長度>44 鏈表形式組織sds
列表 壓縮列表 長度<64&&元素數<512 數組形式組織ziplist
雙端鏈表 長度>=64&&元素數>=512 雙端鏈表
quicklist 3.2版本後 xx
哈希 壓縮列表 長度<64&&元素數<512
字典 長度>=64&&元素數>=512 兩個table/若干桶
集合 整數集合 元素數<512
字典 元素數>=512
有序集合 壓縮列表 長度<64&&元素數<128 分支最小元素/分值
跳錶 長度>=64&&元素數>=128 字典+跳錶

sds

clipboard.png

保持0仍然可使用部分C語言字符串的一些函數
Len 獲取長度,保證二進制安全;
多出剩餘空間,每次檢查free預分配內存,杜絕緩衝區溢出,惰性釋放,減小修改字符串帶來的內存重分配次數node

struct sdshdr {
    len = 11;
    free = 0;
    buf = "hello world\0";  // buf 的實際長度爲 len + 1
};
分配內存,刪除才釋放
     # 預分配空間足夠,無須再進行空間分配
    if (sdshdr.free >= required_len):
        return sdshdr

    # 計算新字符串的總長度
    newlen = sdshdr.len + required_len

    # 若是新字符串的總長度小於 SDS_MAX_PREALLOC
    # 那麼爲字符串分配 2 倍於所需長度的空間
    # 不然就分配所需長度加上 SDS_MAX_PREALLOC (1M)數量的空間
    if newlen < SDS_MAX_PREALLOC:
        newlen *= 2
    else:
        newlen += SDS_MAX_PREALLOC

    # 分配內存
    newsh = zrelloc(sdshdr, sizeof(struct sdshdr)+newlen+1)

ziplist

壓縮列表使用特殊的編碼來標識長度,再加上連續的內存,很是節約空間mysql

area      |<----------------------------------------------- entry ----------------------->|

size        5 byte                                    2 bit      6 bit    11 byte
          +-------------------------------------------+----------+--------+---------------+
component | pre_entry_length                          | encoding | length | content       |
          |                                           |          |        |               |
value     | 11111110 00000000000000000010011101100110 |    00    | 001011 | hello world   |
          +-------------------------------------------+----------+--------+---------------+

Pre_entry_length
1 字節:若是前一節點的長度小於 254 字節,便使用一個字節保存它的值。
5 字節:若是前一節點的長度大於等於 254 字節,那麼將第 1 個字節的值設爲 254 ,而後用接下來的 4 個字節保存實際長度。
encodinng/length/content
以 00 、 01 和 10 開頭的字符數組的編碼方式以下:redis

編碼 編碼長度 content 部分保存的值
00bbbbbb 1 byte 長度小於等於 63 字節的字符數組。
01bbbbbb xxxxxxxx 2 byte 長度小於等於 16383 字節的字符數組。
10____ aaaaaaaa bbbbbbbb cccccccc dddddddd 5 byte 長度小於等於 4294967295 的字符數組。

具體如何省內存:相好比雙向,指針加sds的len,free結尾空,2*4+1+2*4(32位指針和Int都是4字節);壓縮鏈表2/6字節。算法

添加節點在前面,要更新pre_entry_length,next 的 pre_entry_length 只有 1 字節長,但編碼 new 的長度須要 5 字節的時候可能連鎖更新。next 的 pre_entry_length 有 5 字節長,但編碼 new 的長度只須要 1 字節不作處理。sql

dict

  • MurmurHash2 32 bit 算法:這種算法的分佈率和速度都很是好
    當插入元素要檢查是否應該rehash。漸進式rehash,若在rehash中直接操做Ht[1],不然ht[0]
  • rehash觸發條件:
    ratio=used/size
    天然 rehash : ratio >= 1 ,且變量 dict_can_resize 爲真(非持久化中)。
    強制 rehash : ratio 大於變量 dict_force_resize_ratio (目前版本中, dict_force_resize_ratio 的值爲 5 )
    當字典的填充率低於 10% 時, 程序就能夠對這個字典進行收縮操做
  • rehash過程
    ht[1]大小爲0的兩倍,rehashidx記錄ht[0]的rehash索引位置。
    漸進式:在 rehash 開始進行以後(d->rehashidx 不爲 -1), 每次執行一次添加、查找、刪除操做, _dictRehashStep 都會被執行一次。每次執行 _dictRehashStep , ht[0]->table 哈希表第一個不爲空的索引上的全部節點就會所有遷移到 ht[1]->table 。當 Redis 的服務器常規任務執行時, dictRehashMilliseconds 會被執行, 在規定的時間內, 儘量地對數據庫字典中那些須要 rehash 的字典進行 rehash , 從而加速數據庫字典的 rehash 進程(progress)。

整數集合

這裏的encoding是針對整個intset的。當某元素長度超過期要總體升級編碼方式。全存Int所以不須要length。只會升級不會降級。升級過程:數據庫

擴展內容。從後開始移動,將新值插入
bit     0    15    31    47     63        95       127
value   |  1  |  2  |  3  |  ?  |    3    |    ?    |
                       |             ^
                       |             |
                       +-------------+
                     int16_t -> int32_t

跳錶

相比於平衡二叉樹,不須要嚴格的平衡,隨機層數.插入和刪除不須要調整性能很高查找略遜色
https://www.cl.cam.ac.uk/teac...後端

int zslRandomLevel(void) {
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))    //這裏取小於0xffff的數,有0.25的機率level+1,所以level有1/4機率爲2, 1/16的機率爲3等等
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
ZSKIPLIST_MAXLEVEL=32
ZSKIPLIST_P=1/4

一個節點的平均層數 = 1/(1-p),Redis 每一個節點平均指針數爲1.33
平均時間複雜度:O(logn)

zscan在rehash的算法

若進行了rehash,先遍歷小hash表的v & t0->sizemask索引指向的鏈表,再遍歷大hash表中該索引rehash後的全部索引鏈表。
由於sizemask=sizehash-1所以低位全是1,索引取決於hashkey的低K位,
同一個節點的hashkey不變,若原來爲8位hash,hashkey爲…abcd,原索引計算爲bcd,
擴展到16位hash,索引變爲abcd,若要找出全部原bcd索引的鏈表,須要在新的hash中找0bcd,1bcd。
由於要循環高位,因此這樣從高位到低位反向來,例如:
000 --> 100 --> 010 --> 110 --> 001 --> 101 --> 011 --> 111 --> 000
0000 --> 1000 --> 0100 --> 1100 --> 0010 --> 1010 --> 0110 --> 1110 --> 0001 --> 1001 --> 0101 --> 1101 --> 0011 --> 1011 --> 0111 --> 1111 --> 0000
當rehash時,可能會有重複,但不會有遺漏數組

do {
            /* Emit entries at cursor */
            de = t1->table[v & m1];
            while (de) {
                fn(privdata, de);
                de = de->next;
            }
            /*這裏v從0開始,加1只取前m1-m0位,再與後m0位合併*/
            v = (((v | m0) + 1) & ~m0) | (v & m0);
  
            /* Continue while bits covered by mask difference is non-zero */
        } while (v & (m0 ^ m1));   //這裏異或前m1-m0位全是1,直到while中的v爲全1後加1變爲全0這裏爲0退出。所以若原來v爲110,8到32,rehash的表將遍歷00110,01110,10110,11110
而後是下一個v的確認
    v |= ~m0;   //m0低位全是0,>m0全是1,將超出m0的置1,只保留低m0位
    v = rev(v);  //二進制翻轉
    v++;      //加1,正常進位
    v = rev(v);  //二進制翻轉,這步以後至關於將v從高位+1向低位進位

過時key刪除

  • 惰性刪除:在每次獲取鍵或setnx時,調用expireIfNeeded ,過時刪除
  • 按期刪除: 遍歷每一個數據庫,隨機從db->expires中最多檢查n個Key,過時刪除。
    每次遍歷最多16個庫,記錄斷點
    對於利用率低於1%數據庫跳過檢查,等待縮容後處理
    運行超過期長退出
    每一個數據庫連續隨機抽樣過時key個數<n/4,則執行下一個數據庫
  • 按期刪除位置:
    redis的crontab執行過程當中,刪除執行時長,通常不超cpu的1/4時間(時間能夠設置)
    事件驅動循環中執行,間隔20ms執行,超過10ms退出(時間能夠設置)
  • 持久化中過時鍵處理:
    RDB 已過時鍵不會保存到新建立的EDB文件中,載入時主服務器模式過濾過時鍵,從服務模式不過濾;AOF 生成無影響,重寫過濾過時鍵;複製時 主服務器刪除過時鍵後會給從服務器發DEL,從服務器除遇到DEL不會刪除過時鍵

cache淘汰機制

Redis 用來當作LRU cache的幾種策略(使用內存已達到maxmemory):緩存

noeviction:無策略,直接返會異常
allkeys-lru:全部key進行LRU,先移除最久使用的(當前時間,減去最近訪問的時間)
allkeys-random:隨機移除
volatile-random:只隨機移除有過時時間的key
volatile-tt: 優先移除最短ttl的有過時時間的key
近似的LRU。採樣逐出(默認5個裏淘汰一個)。https://redis.io/topics/lru-c...
4.0後引入LFU(least frequently):大概原理是次數達到一個階段給個計數器初始值,隨時間遞減。採樣取最小淘汰(源碼LFULogIncr)

單機redis/持久化

RDB

  1. 存儲:將redis的內存狀態保存到磁盤裏面,三種方式: save(阻塞);bgsave(子進程負責建立RDB,不阻塞,期間拒絕save,bgsave,延遲bgrewriteaof);自動保存設置saveparams(校驗dirty與saveparam.changes,lastsave與saveparam.seconds)
  2. 載入:啓動時自動執行,已開啓AOF載入AOF,不然載入RDB
  3. RDB文件結構:REDIS | db_version | database(SELECTDB | db_number | [EXPIRETIME_MS | ms] TYPE | key | value) | EOF | check_sum,
    value根據TYPE和ENCODING結構不同,好比無壓縮字符串5 hello, 壓縮後字符串 REDIS_RDB_ENC_LZF 6 21 "?AA"

AOF

  1. 存儲:AOF持久化功能打開時,執行完一個寫命令後,會以協議格式將命令追加到aof_buf緩衝區末尾。
  2. 寫入:在每一個外層循環,處理過文件事件和時間事件後將緩衝區內容寫入AOF文件
  3. 同步:是否同步由appendfsync(always,everysec,no)決定,
  4. 載入:建立僞客戶端從新執行一遍AOF文件中的命令
  5. 重寫:BGREWRITEAFO命令,整合爲不浪費空間的命令。
    原理:遍歷數據庫,讀取鍵值,用一條set命令代替記錄當前鍵值對。當鍵值超過了redis.h/REDIS_AOF_REWITE_ITEMS_PER_CMD常量的值,此鍵值對用多條命令來存儲。
    非阻塞實現:爲了不使用鎖保證數據安全性,用子進程進行AOF重寫,父進程繼續處理命令,用重寫緩衝區解決AOF文件與重寫時間段後數據庫狀態不一致問題,在建立子進程後,全部寫命令即寫入AOF緩衝區又寫入重寫緩衝區,將重寫緩衝區內容也寫入AOF文件,緩衝區和信號等經過管道穿傳遞。

集羣

主從模式 複製 SLAVEOF

接收到SLAVEOF命令執行步驟:

設置masterhost,masterport
發送OK給客戶端
建立socket connect到主服務器,主服務器accept
發送ping給主服務器,收到PONG繼續不然斷開重連
主服務器requirepass,從服務器masterauth
發送端口給主服務器 REPLCONF listening-pot <port-number>
同步SYNC/PSYNC
命令傳播

1.SYNC
主服務器BGSAVE命令生成一個RDB文件,並使用緩衝區開始記錄寫命令
BGSAVE結束後後發送RDB文件給從服務器
從服務器載入
主服務器將和緩衝區中寫命令發送給從服務器,從服務器執行
2.命令傳播
主服務器將全部寫命令傳播給從服務器
每秒一次頻率向主服務器發送REPLCONF ACK <replication_offset>進行心跳檢測。檢測網絡和命令丟失
主服務器配置min-slaves-to-write n, min-slaves-max-lag m當從服務器數量少於3個,或者延遲大於等於10將拒絕執行寫命令
根據replication_offset檢測是否丟失命令,補發命令
3.斷線後重複製的優化 PSYNC
2.8版本以上redis使用PSYNC命令代替SYNC,斷線後使用部分重同步,其餘使用SYNC
從服務器向主服務器發送命令:首次PSYNC ? -1 ,斷線後重複製 PSYNC <runid> <offset>。主服務器返回:+FULLERSYNC <runid> <offset> ,+CONTINUE , -ERR沒法識別從服務器重發SYNC命令
4.上面2/3都是2.8以上才支持,須要用到replication_offset,複製積壓緩衝區,服務運行ID

主服務器每次向從服務器傳播N個字節,將本身的複製偏移量加N。從服務器每次收到N個字節,將本身的複製偏移量加N
主服務器進行命令傳播時,不只會將寫命令發送給從服務器,還會將寫命令寫入複製積壓緩衝區,先進先出
從服務器會記錄正在複製的主服務器的運行ID,網絡斷開後,從服務器向主服務器發送這個ID,主服務器根據本身運行ID決定是部分重同步仍是徹底同步

哨兵

哨兵系統也是一個或多個特殊的redis服務器,監視普通服務器,負責下線主服務器和故障轉移
1.啓動
(1)初始化服務器
sentinel不適用數據庫,再也不如RDB/AOF
(2)將普通redis服務器使用的代碼替換成sentinel專用代碼
使用不一樣端口,命令集(只有PING,SENTINEL,INFO.SUBSCRIBE,UNSUBSCRIBE,PSUBSCRIBE,PUNSUBSCRIBE)
(3)初始化sentinel狀態
(4)根據給定的配置文件,初始化sentinel的監視主服務器列表
(5)建立連向主服務器的網絡鏈接
命令鏈接,訂閱鏈接(在創建後發送SUBSCRIBE __sentinel__:hello,sentinel需求經過接收其餘服務器發來的頻道信息發現未知的sentinel)
2.獲取主服務器信息
sentinel默認10s一次向主服務器發INFO命令,獲取更新sentinelRedisInstance的run_id,role,slaves的等
3.獲取從服務器信息
sentinel會對主服務器的從服務器創建命令鏈接和訂閱鏈接,也是10s/次發送INFO,更新slaves的sentinelRedisInstance
4.向主服務器和從服務器發送信息
sentinel默認2s/次用命令鏈接向主服務器和從服務器發送 PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"
對每一個與sentinel鏈接的服務器,即發送信息到頻道又訂閱頻道接收信息。收到信息後提取參數檢查如果本身的丟棄,不然根據信息更新主服務sentinelRedisInstance中的sentinels,建立鏈接向其餘sentinel的命令鏈接
5.檢測主觀下線狀態
sentinel默認1s/次的頻率向全部主/從/sentinel服務器發送PING命令,有效回覆爲+PONG,-LOADING,-MASTERDOWN。當一個實例在down-after-milliseconds內,連續向sentinel返回無效回覆,sentinel修改實例中flags加入|SRI_S_DOWN標識主觀下線
6.檢查客觀下線狀態
若是被sentinel判斷爲主觀下線,sentinel當前配置紀元爲0,將向其餘sentinel發送命令 SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
返回

<down_state>
<leader_runid>        // *表明命令僅用於檢測主服務器的下線狀態
<leader_epoch>      //前一個只爲*則爲0

當接收到認爲下線的sentinel數量超過quorum(sentinel moniter 127.0.0.1 6379 2中2設置)則flags加|SRI_O_DOWN
7.選舉領頭Sentinel(raft)
也經過SENTINEL is-master-down-by-addr 看來是要分開進行,帶runid。
每一個發現主服務器進入客觀下線的sentinel向其餘sentinal發送命令
在一個配置epoch中將先到的設爲局部領頭,不能再更改。
接收回複檢查epoch的值和本身的相同就取出leader_runid,若是發現本身被半數以上選擇,則成爲領頭,epoch+1
若是在規定時間內未選舉成功,epoch+1從新選舉
8.故障轉移
領頭進行故障轉移
1) 選出新的主服務器

在線的,5s內回覆過INFO的,與主服務器斷開鏈接時間足夠短,優先級高,複製偏移量大,runid最小
 發送SLAVEOF no one
 以1s/次(其餘是10s/次)的頻率向該服務器發送INFO。當role變爲master時繼續2

2) 向下線的主服務的其餘從服務器發送SLAVEOF命令
3) 向舊的主服務器發送SLAVEOF命令

集羣

一個集羣由多個node組成,經過分片進行數據共享,CLUSTER MEET <ip> <port>將各階段加入到cluster
1.啓動
一個node就是運行在集羣模式下的redis服務器,在啓動時若cluster-enabled是yes,則開啓服務器的集羣模式。
節點繼續使用單機模式的服務器組件,只是serverCron函數會調用集羣模式特有的clusterCron函數,執行集羣的常規操做,例如向集羣的其餘節點發送Gossip消息,檢查節點是否斷線,或者檢查是否須要對下線的節點進行故障轉移操做等。節點數據庫和單機徹底相同除了智能使用0號出具庫這和個限制,另外除了將鍵值對保存在數據庫裏邊以外,節點還會用clusterState中的slots_to_keys跳躍表來保存槽和鍵,方便對屬於某槽全部數據庫鍵進行批量操做
2.客戶點向A發送CLASTER MEET <B.ip> <B.port>
A建立B的clusterNode加入到clusterState.nodes中
發送MEET給B
B返回PONG
A發送PING,握手完成
A將B的信息經過Gossip傳播給急羣衆其餘節點
3.槽指派,向節點發送CLUSTER ADDSLOTS <slot> [slot ...]
遍歷全部輸入槽,若是有已經指派的返回錯誤,若是都沒有指派,再遍歷一次:
更新當前lusterState.slots[i]設爲Myself
更新本身clusterNode 的slots,numslots屬性
將本身的slots數組經過消息發送給集羣中其餘節點,A收到B後會把本身的clusterState.nodes中查找B對應的clusterNode結構,更新其中的slots數組;更新clusterNode中的slots,numslots屬性
維護總體slots目的:查某個槽被哪一個節點處理
維護單個節點slots目的:將某節點的全部槽指派信息發送給其餘。
4.執行命令
在全部的槽都指派完畢以後,集羣就會進入上線狀態,這是客戶端就能夠向集羣中的節點發送數據命令了。客戶端向節點發送與數據庫鍵相關的命令時,若是鍵所在的槽正好就指派給了當前節點,那麼節點就直接執行命令;若是鍵所在的槽並無指派給當前節點,那麼節點返回一個MOVED錯誤,指引客戶端(redirect)至正確節點,並再次發送以前想要執行的命令。
1)計算鍵屬於哪一個槽 CLUSTER KEYSLOT [key]

CRC16(KEY) & 16383

2) 若計算的i不對應Myself 返回MOVED <slot> <ip>:<port>
3) 客戶端根據MOVED錯誤,轉向節點從新發送命令
5.從新分片
redis集羣的從新分片操做能夠將任意數量已經指派給某個節點的槽改成指派給另外一個節點,而且相關的槽所屬的鍵值對也會從源節點轉移到目標節點。能夠online下。
redis的從新分片操做時由redis的集羣管理軟件redis-trib負責執行的,redis提供了進從新分片所需的全部命令,而redis-trib則經過向源節點和目標節點發送命令來進行從新分片操做。步驟以下:
1)redis-trib對目標節點發送CLUSTER SETLOT < slot > IMPORTING < source_id> 準備好導入
2)redis-trib對源節點發送CLUSTER SETLOT < slot> MIGRATING < target_id > 準備好遷移
3)redis-trib對源節點發送CLUSTER GETKEYSINSLOT < slot > < count > 得到最多count個屬於槽slot的鍵值對的鍵名
4)對3中每一個鍵名,redis-trib對源節點發送MIGRATE < key_name> 0 < timeout> 遷移
5)重複3和4,知道槽中的鍵值對遷移到目標節點
6)redis-trib向任意節點發送CLUSTER SETLOT < slot> NODE < target_id>,將槽指派給目標節點,並經過消息告知整個集羣,最終全部節點都會知道槽slot已經指派給了目標節點。
6.ASK錯誤 處理正在遷移中槽錯誤
接到ASK錯誤的客戶端會根據錯誤提供的IP地址和端口號,轉向至正在導入槽的目標節點,而後向目標節點發送一個ASKING命令,再從新發送本來想要執行的命令。
ASKING命令加client.flags|=REDIS_ASKING。正常客戶端發送一個關於槽i的命令,而槽i又沒有指派給這個節點的話,會返回MOVED錯誤,設置了REDIS_ASKING後,則會破例執行
MOVED錯誤表明槽的負責權已經從一個節點轉移到另外一個,每次遇到都自動發到MOVED指向的節點。而ASK只是遷移槽中臨時的,下次對下次有影響
7.複製與故障轉移
1)複製
redis集羣中的節點分爲主節點和從節點,其中主節點用於處理槽,而從節點則用於複製主節點,並在被複制的主節點下線以後代替下線的主節點繼續處理命令請求。
設置從節點:CLUSTER REPLICATE < node_id> 讓接收命令的節點成爲node_id的從節點
接收到該命令的節點首先會在本身的clusterState.nodes字典裏面找到node_id對應的節點clusterNode結構,並將本身的clusterState.myself.slaveof指針指向這個結構;
節點會修改本身clusterState.myself.flags中的屬性,關閉原來的REDIS_NODE_MASTER標識,打開REDIS_NODE_SLAVE標識;
調用複製代碼,至關於向從節點發送SLAVEOF <master_ip> <master_port>。
2)故障檢測
集羣中的每一個節點都會按期地向集羣中的其餘節點發送PING消息,若是規定時間內沒有返回PONG,發送消息的節點就會把接受消息的節點標記爲疑似下線PFAIL。clusterNode的flags標識(REDIS_NODE_PFAIL)
集羣中各節點經過互相發送消息的方式交換集羣中各個節點的狀態信息,當A經過消息得知B認爲C進入疑似下線,A在本身clusterState.nodes中找到C對應的clusterNode結構將B的下線報告添加到該clusterNode的fail_reports中
半數以上主節點都報告x意思下線,則標記爲FAIL,將主節點x標記爲下線的節點向集羣廣播FAIL消息,全部接受者都將x標記爲FAIL
3)故障轉移
當一個從節點發現本身複製的主節點進入了下線狀態的時候,從節點將開始對下線主節點進行故障轉移,步驟以下:

選舉新的主節點 
 新的主節點執行SLAVEOF no one命令,成爲新的主節點
 新的主節點將下線主節點的槽指派給本身 
 新的主節點向集羣廣播PONG消息,代表本身接管了原來下線節點的槽 
 新的節點開始接收和本身複製處理槽有關的命令請求。

選舉新的主節點

一樣基於Raft實現
 從節點廣播CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST,未投過票的主節點返回CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK。配置紀元自增,半數以上。

8.消息
消息由消息頭(header)和消息正文(data)組成。
cluster.h/clusterMsg結構表示消息頭,cluster.h/clusterMsgData聯合體指向消息的正文。
節點消息分爲5類:
1)MEET
A接到客戶端發送的CLUSTER MEET B命令後,會向B發送MEET消息,B加入到A當前所處的集羣裏
2)PING
每一個節點默認1s/次從已知節點隨機選5個,對最長時間未發送PING的節點發送PING,或當有節點超過cluster-node-timeout的一半未收到PONG也發送PING,檢查節點是否在線
3)PONG
確認MEET,PING;或主動發送讓集羣中其餘節點當即刷新該節點信息,好比故障轉移操做成功後
以上三種消息都使用Gossip協議交換各自不一樣節點的信息,三種消息的正文都是由兩個cluster.h/clusterMsgDataGossip結構組成
發送者從本身已知節點列表中隨機選擇兩個節點(主、從),保存在兩個clusterMsgDataGossip結構中。接收者發現節點不在已知節點列表則與節點握手,不然更新信息。注意PONG也會帶兩個回去
4)FAIL
主節點判斷FAIL狀態,廣播
clusterMsgDataFail。(gossip隨機會慢)
5)PUBLISH
當節點收到一個PUBLISH,會執行這個命令並向集羣中廣播一條PUBLISH。即向集羣中某個節點發送PUBLISH <channel> <message>將致使集羣中全部節點都向channel頻道發送message消息。
要讓集羣全部節點都執行相同命令,能夠廣播,但還要用PUBLISH發是由於直接廣播這種作法,不符合redis集羣的「各個節點經過發送和接收消息來進行通訊」這一規則。
clusterMsgDataPublish

原生Gossip過程是由種子節點發起,當一個種子節點有狀態須要更新到網絡中的其餘節點時,它會隨機的選擇周圍幾個節點散播消息,收到消息的節點也會重複該過程,直至最終網絡中全部的節點都收到了消息。這個過程可能須要必定的時間,因爲不能保證某個時刻全部節點都收到消息,可是理論上最終全部節點都會收到消息,所以它是一個最終一致性協議。每次散播消息都選擇還沒有發送過的節點進行散播(有冗餘)

codis

介紹codis的架構組件,可用性,一致性,擴展性

架構組件

clipboard.png

Codis 3.x 由如下組件組成:

  • Codis Server:基於 redis-3.2.8 分支開發。增長了額外的數據結構,以支持 slot 有關的操做以及數據遷移指令。具體的修改能夠參考文檔 redis 的修改。
  • Codis Proxy:客戶端鏈接的 Redis 代理服務, 實現了 Redis 協議。 除部分命令不支持之外(不支持的命令列表),表現的和原生的 Redis 沒有區別(就像 Twemproxy)。
    對於同一個業務集羣而言,能夠同時部署多個 codis-proxy 實例;
    不一樣 codis-proxy 之間由 codis-dashboard 保證狀態同步。
  • Codis Dashboard:集羣管理工具,支持 codis-proxy、codis-server 的添加、刪除,以及據遷移等操做。在集羣狀態發生改變時,codis-dashboard 維護集羣下全部 codis-proxy 的狀態的一致性。這裏和zk交互保證一致,數據遷移,主從增長等都經過這個
    對於同一個業務集羣而言,同一個時刻 codis-dashboard 只能有 0個或者1個;
    全部對集羣的修改都必須經過 codis-dashboard 完成。
  • Codis Admin:集羣管理的命令行工具。
    可用於控制 codis-proxy、codis-dashboard 狀態以及訪問外部存儲。
  • Codis FE:集羣管理界面。
    多個集羣實例共享能夠共享同一個前端展現頁面;
    經過配置文件管理後端 codis-dashboard 列表,配置文件可自動更新。
  • Storage:爲集羣狀態提供外部存儲。
    提供 Namespace 概念,不一樣集羣的會按照不一樣 product name 進行組織;
    目前僅提供了 Zookeeper、Etcd、Fs 三種實現,可是提供了抽象的 interface 可自行擴展。

擴展性

  • slot
    整個集羣總共 1024 個 slot (槽)
    每一個 redis group 對應着slot range (如: 0~127), slot range 存儲在 zk 中
    每一個 key 請求時,crc32(key) % 1024 映射到 個 slot_id,proxy 經過 slot_id 找到對應的 redis group 讀寫數據
    集羣的擴容縮容,都是經過 slot 遷移來實現,兩段式提交的過程。遷移時並不影響線上單 key 讀寫訪問
  • 1.數據遷移migrateslot(slotid,from,dest)
    一臺機器上有不少redis實例(一個實例一個groupid),遷移:sid=>gid,多個遷移會建立任務隊列放zk
    zookeeper同步配置,放遷移隊列,提供可靠的一致性變動,每一個codis-proxy中有配置,sid=>gid/gid=>sid的,dash專門開啓一個協程從zk的任務隊列中獲取slot遷移,一個一個進行
    遷移有兩個階段,第一階段狀態改成pre_m。若proxy都確認,將狀態改m。向所在的redis-server發送遷移命
    dash修改到zk,proxy監聽回覆到zk,dash監聽zk進行狀態機的變動,同步配置作配置的下發同步(zk 除了存儲路由信息,同時還做爲一個事件同步的媒介服務,好比變動master 或者數據遷移這樣的事情,須要全部的proxy 經過監聽特定zk 事件來實現。)
    clipboard.png
    三次配置更改同步:
    第一次fillslot將BackendAddr由OriginGroupMaster改寫爲TargetGroupMaster,這一步操做至關於讀寫臨界區資源BackendAddr,因此必須帶寫鎖,而MigrateFrom只是順便改了而已
    第二次fillslot至關於取消了第一次的寫鎖,可是若是Promoting在執行的話,不該該取消Promoting設置的鎖
    第三次fillslot取消了MigrateFrom
  • 遷移過程當中讀寫請求:
    分發請求時存在一個prepare方法,這一步會獲取到該key對應的slot是否有MigrateFrom
    若是有的話,會使用SLOTSMGRTTAGONE將這個key從MigrateFrom表明的redis強制遷移到Backend表明的redis裏去,遷移完成之後再去訪問Backend得到這個ke
    這樣就能解決,若是被遷移的slot中的key,恰好被訪問時,產生的一致性問題了
  • 遷移與主從切換的衝突:
    migrate基本上不依賴lock,當發生數據衝突時,由強制遷移這個key來解決一致性問題,和lock基本上沒太大關係,lock主要是針對promote設計的
  • 自平衡算法
    clipboard.png

    1.使用自動負載均衡須要知足一個前提:全部codis-server的分組master必須配置maxmemory。
    2.各組codis-server分配多少個slot是由其maxmemory決定。好比:A組maxmemory爲10G, B組maxmory爲1G,進行自動均衡處理後,A組分配的slot會是B組的10倍。
    3.自動負載均衡並不會達到絕對意義上的均衡,其只作到maxmemory與分配的slot個數的比例均衡。沒法達到操做次數的均衡。
    4.自動負載均衡的處理過程當中,若是發現存在maxmemory與分配的slot個數比例不均衡時,則會進行發起slot遷移的操做。達到均衡目的的前提下,此過程當中會作到儘可能減小slot的遷移。

可用性

  • 複製
    主從切換:哨兵模式
  • proxy下線變動等
    zk

codis和twemproxy

codis和twemproxy最大的區別有兩個:
一個是codis支持動態水平擴展,對client徹底透明不影響服務的狀況下能夠完成增減redis實例的操做;
一個是codis是用go語言寫的並支持多線程而twemproxy用C並只用單線程。
後者又意味着:codis在多核機器上的性能會好於twemproxy;codis的最壞響應時間可能會由於GC的STW而變大,不過go1.5發佈後會顯著下降STW的時間;若是隻用一個CPU的話go語言的性能不如C,所以在一些短鏈接而非長鏈接的場景中,整個系統的瓶頸可能變成accept新tcp鏈接的速度,這時codis的性能可能會差於twemproxy。

某自研集羣

自研緣由:

數據量的限制。1024.
遷移比想象的頻繁
zk依賴,zk出問題,路由錯誤沒法發現,redis沒有路由信息

思路

  • 基於redis集羣,基本都靠redis-cluster
  • 加一層proxy,客戶端與集羣鏈接,單進程單線程(mget豈不是很慢=》用了異步 epoll的模式,發完了不等)
    1.創建連接模塊。與客戶端和redis-clsuster創建連接。
    2.命令處理模塊。區分單次操做命令和屢次操做命令(Mget/Mset)。
    3.路由模塊。維護redis-cluster的nodes路由信息。(原來client隨便打,錯了moved這記錄了下)

關於雙活

方案1:
clipboard.png
方案2:
codis收到命令後發送給兩個機房的redis
方案3:
clipboard.png

redis一些優化和問題

  1. 白名單動能能夠用Bit-array/bitmaps代替set實現
  2. setex代替set,expire,有兩次網絡
  3. 內存提出策略:近似lru,對性能妥協,qps太大時也淘汰不過來,可能會有一些寫不進來
  4. 大key沒法分片致使沒法水平擴容來解決問題。因此一個key不要存特別大的數據,原則上一個key<100k
  5. 同一個實例,同一個Key,QPS特別大的時候,仍然扛不住,要避免熱key,靜態配置類的Key遷出redis放代碼裏,其餘熱key要考慮高併發寫入的問題。4萬的pks針對同一key扛不住,能夠轉爲雙緩存或者改一個有常駐內存的語言寫。Php的雙緩存能夠經過fpm的apcu,redis,mysql來作。
  6. 應用redis時要預測qps,數據容量,內存中的量和磁盤代碼可能還不一致。

7.redis影響性能的命令:(執行時間長,傳輸數據多)
key*,sort(非要單獨機器
smembers 控制集合的數量,分子集,srandmember,
save bgsave/afo啓動時/master首次收到slave同步請求等時fork進程(fork時雖然數據寫時複製,但仍是會複製頁表,大頁能夠減小頁表,但改就會複製)
appendfsync everysec 子進程持久化和主進程 IO阻塞
bgrewriteaof AOF buffer和文件合併時阻塞的

熱key

問題:請求多,部分key集中於同一機器,沒法經過增長機器解決,源於redis的從都是備份恢復做用,codis等集羣也是
解決方案:

  • 1
    讀:本地緩存(客戶端本地:redis/程序/mysql,服務端本地 就是副本擴展多份多機器)
    缺點:須要提早獲取熱點,容量有限,不一致時間長,熱點key遺漏。
    寫:取租約,限流
  • 2 讀寫分離。讀複製多份,負載均衡

    clipboard.png

  • 熱點key的採樣。(未知熱key)
    線程搶鎖開始採樣,外層N次,分1024個桶,看每一個桶中是否有熱點,用標準差。再對桶進行M此採樣,選出熱點key.客戶端從服務器獲取這批key進行本地緩存
  • 失效擊穿
    寫操做刪除本地緩存。根據容量或者過時淘汰,在過時淘汰時爲防止擊穿,首次發現過時後延長一點過時時間,只有首次的去獲取新的key更新。

redis集羣的分佈式鎖redlock

1.同時刻只有一個獲取鎖,2.多數節點可用則能夠獲取鎖,3.不會死鎖,4.鎖按期間有效1/2 :同時多個master上請求鎖,超過一半則成功3:master有時間自動釋放,監控每隔時間檢查釋放等4:釋放鎖須要密鑰,保證不釋放別人的鎖步驟:一、獲取當前時間(單位是毫秒)。二、輪流用相同的key和隨機值在N個節點上請求鎖,在這一步裏,客戶端在每一個master上請求鎖時,會有一個和總的鎖釋放時間相比小的多的超時時間。好比若是鎖自動釋放時間是10秒鐘,那每一個節點鎖請求的超時時間多是5-50毫秒的範圍,這個能夠防止一個客戶端在某個宕掉的master節點上阻塞過長時間,若是一個master節點不可用了,咱們應該儘快嘗試下一個master節點。三、客戶端計算第二步中獲取鎖所花的時間,只有當客戶端在大多數master節點上成功獲取了鎖(在這裏是3個),並且總共消耗的時間不超過鎖釋放時間,這個鎖就認爲是獲取成功了。四、若是鎖獲取成功了,那如今鎖自動釋放時間就是最初的鎖釋放時間減去以前獲取鎖所消耗的時間。五、若是鎖獲取失敗了,無論是由於獲取成功的鎖不超過一半(N/2+1)仍是由於總消耗時間超過了鎖釋放時間,客戶端都會到每一個master節點上釋放鎖,即使是那些他認爲沒有獲取成功的鎖。

相關文章
相關標籤/搜索