Redis基礎
- 1 Redis
- 2 Redis twemproxy 叢集
- 3 redis cluster
- 4 原理說明
- 5 其他相關
- 6 資料遷移
- 7 redis 服務准入
1 Redis
1.1 持久化
1.1.1 AOF 重寫機制
AOF 重寫觸發條件
AOF 重寫可以由使用者通過呼叫 BGREWRITEAOF 手動觸發。 伺服器在 AOF 功能開啟的情況下,會維持以下三個變數:
- 記錄當前 AOF 檔案大小的變數 aof_current_size。
- 記錄最後一次 AOF 重寫之後,AOF 檔案大小的變數 aof_rewrite_base_size。
- 增長百分比變數 aof_rewrite_perc。
每次當 serverCron(伺服器週期性操作函式,在 src/redis.c 中)函式執行時,它會檢查以下條件是否全部滿足,如果全部滿足的話,就觸發自動的 AOF 重寫操作:
- 沒有 BGSAVE 命令(RDB 持久化)/AOF 持久化在執行;
- 沒有 BGREWRITEAOF 在進行;
- auto-aof-rewrite-percentage 引數不為 0
- 當前 AOF 檔案大小要大於 server.aof_rewrite_min_size(預設為 1MB)
- 當前 AOF 檔案大小和最後一次重寫後的大小之間的比率等於或者等於指定的增長百分比(在配置檔案設定了 auto-aof-rewrite-percentage 引數,不設定預設為 100%)
如果前面四個條件都滿足,並且當前 AOF 檔案大小比最後一次 AOF 重寫時的大小要大於指定的百分比,那麼觸發自動 AOF 重寫。
原始碼如下:
/* Trigger an AOF rewrite if needed */
// 觸發 BGREWRITEAOF
if (server.rdb_child_pid == -1 &&
server.aof_child_pid == -1 &&
server.aof_rewrite_perc &&
// AOF 檔案的當前大小大於執行 BGREWRITEAOF 所需的最小大小
server.aof_current_size > server.aof_rewrite_min_size)
{
// 上一次完成 AOF 寫入之後,AOF 檔案的大小
long long base = server.aof_rewrite_base_size ?
server.aof_rewrite_base_size : 1;
// AOF 檔案當前的體積相對於 base 的體積的百分比
long long growth = (server.aof_current_size*100/base) - 100;
// 如果增長體積的百分比超過了 growth ,那麼執行 BGREWRITEAOF
if (growth >= server.aof_rewrite_perc) {
redisLog(REDIS_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth);
// 執行 BGREWRITEAOF
rewriteAppendOnlyFileBackground();
}
}
複製程式碼
1.2 主從同步
主從同步相關引數
- repl-backlog-size: 增量重傳 buf
- repl-timeout: 主動超時
- client-output-buffer-limit(和寫入量有關)
- 這個引數分為 3 部分,第二部分涉及 slave
- slave 部分預設值:256M 64M 60 秒
- output-buffer 緩衝區裡放的是主庫待同步給從庫的操作資料。
- 如果 output-buffer>256M 則從節點需要重新全同步,如果 256>output-buffer>64 且持續時間 60 秒,則從節點需要重新全同步。
主從同步
分別啟動 master 和 slave 後,會自動啟動同步 slave 出現如下類似日誌,則同步已完成:
[4611] 24 Aug 19:11:46.843 * MASTER <-> SLAVE sync started
[4611] 24 Aug 19:11:46.844 * Non blocking connect for SYNC fired the event.
[4611] 24 Aug 19:11:46.844 * Master replied to PING,replication can continue...
[4611] 24 Aug 19:11:46.844 * Partial resynchronization not possible (no cached master)
[4611] 24 Aug 19:11:46.844 * Full resync from master: 0629e2e6e79c13c21ff38b638b6009183140939a:1
[4611] 24 Aug 19:13:55.662 * MASTER <-> SLAVE sync: receiving 5774276835 bytes from master
[4611] 24 Aug 19:14:45.578 * MASTER <-> SLAVE sync: Flushing old data
[4611] 24 Aug 19:16:57.509 * MASTER <-> SLAVE sync: Loading DB in memory
[4611] 24 Aug 19:19:44.191 * MASTER <-> SLAVE sync: Finished with success
複製程式碼
1.2.1 repl-timeout
若 slave 日誌出現如下行:
# Timeout receiving bulk data from MASTER... If the problem persists try to set the 'repl-timeout' parameter in redis.conf to a larger value.
複製程式碼
調整 slave 的 redis.conf 引數:
repl-timeout 60 # 將數值設得更大
如:config set repl-timeout 600
複製程式碼
1.2.2 寫入量太大超出 output-buffer
若 slave 日誌出現如下行:
# I/O error reading bulk count from MASTER: Resource temporarily unavailable
# I/O error trying to sync with MASTER: connection lost
複製程式碼
調整 master 分配給 slave client buffer:
client-output-buffer-limit slave 256mb 64mb 60
# 256mb 是一個硬性限制,當 output-buffer 的大小大於 256mb 之後就會斷開連線
# 64mb 60 是一個軟限制,當 output-buffer 的大小大於 64mb 並且超過了 60 秒的時候就會斷開連線
# 或者全部設為 0,取消限制。
如:config set client-output-buffer-limit "slave 0 0 0"
複製程式碼
1.2.3 repl-backlog-size 太小導致失敗
當 master-slave 複製連線斷開,server 端會釋放連線相關的資料結構。replication buffer 中的資料也就丟失,當斷開的 slave 重新連線上 master 的時候,slave 將會傳送 psync 命令(包含複製的偏移量 offset),請求 partial resync。如果請求的 offset 不存在,那麼執行全量的 sync 操作,相當於重新建立主從複製。
Unable to partial resync with slave $slaveip:6379 for lack of backlog (Slave request was: 5974421660).
複製程式碼
調整 repl-backlog-size 大小
1.2.4 主庫磁碟故障
觸發全量同步時,主庫磁碟故障,主庫 RDB 無法落盤,導致全量同步失敗
* replication.c: 1646 Full resync from master: 1a0b22011aff6ea5d53710acff4ee32adde636ec:399255497708
# replication.c: 1262 I/O error reading bulk count from MASTER: Operation now in progress
複製程式碼
repl-diskless-sync no #是否使用無盤複製 Diskless replication,預設是 no
對 master 進行操作
config set repl-diskless-sync yes
複製程式碼
1.3 Redis bug
1.3.1 AOF 控制程式碼洩露 bug
表現
日誌中提示
* Residual parent diff successfully flushed to the rewritten AOF (329.83 MB)
* Background AOF rewrite finished successfully
* Starting automatic rewriting of AOF on 100% growth
# Can't rewrite append only file in background: fork: Cannot allocate memory
* Starting automatic rewriting of AOF on 100% growth
# Can't rewrite append only file in background: fork: Cannot allocate memory
* Starting automatic rewriting of AOF on 100% growth
# Can't rewrite append only file in background: fork: Cannot allocate memory
* Starting automatic rewriting of AOF on 100% growth
# Error opening /setting AOF rewrite IPC pipes: Numerical result out of range
* Starting automatic rewriting of AOF on 100% growth
# Error opening /setting AOF rewrite IPC pipes: Numerical result out of range
# Error registering fd event for the new client: Numerical result out of range (fd=10128)
# Error registering fd event for the new client: Numerical result out of range (fd=10128)
複製程式碼
使用 lsof 命令檢查 fd 數,發現當時程式開啟的 fd 數已經達到 10128 個,而其中大部分基本都是 pipe. 在 Redis 中,pipe 主要用於父子程式間通訊,如 AOF 重寫、基於 socket 的 RDB 持久化等場景。
分析
fd 限制
首先,我們定位到 client 連線報錯的主要呼叫鏈為 networking.c/acceptCommonHandler => networking.c/createClient => ae.c/aeCreateFileEvent:
static void acceptCommonHandler(int fd,int flags,char *ip) {
client *c;
if ((c = createClient(fd)) == NULL) {
serverLog(LL_WARNING,"Error registering fd event for the new client: %s (fd=%d)",strerror(errno),fd);
close(fd); /* May be already closed,just ignore errors */
return;
}
//……
}
int aeCreateFileEvent(aeEventLoop *eventLoop,int fd,int mask,aeFileProc *proc,void *clientData)
{
if (fd >= eventLoop->setsize) {
errno = ERANGE;
return AE_ERR;
}
//……
}
複製程式碼
而 eventLoop->setsize 則是在 server.c/initServer 中被初始化和設定的,大小為 maxclient+128 個。而我們 maxclient 採用 Redis 預設配置 10000 個,所以當 fd=10128 時就出錯了。
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
複製程式碼
aof 重寫子程式啟動失敗為何不關閉 pipe
aof 重寫過程由 server.c/serverCron 定時時間事件處理函式觸發,呼叫 aof.c/rewriteAppendOnlyFileBackground 啟動 aof 重寫子程式。在 rewriteAppendOnlyFileBackground 方法中我們注意到如果 fork 失敗,過程就直接退出了。
int rewriteAppendOnlyFileBackground(void) {
//……
if (aofCreatePipes() != C_OK) return C_ERR; // 建立 pipe
//……
if ((childpid = fork()) == 0) {
/* Child */
//……
} else {
/* Parent */
// 子程式啟動出錯處理
if (childpid == -1) {
serverLog(LL_WARNING,"Can't rewrite append only file in background: fork: %s",strerror(errno)); // 最初記憶體不足正是這裡打出的錯誤 log
return C_ERR;
}
//……
}
}
複製程式碼
而關閉 pipe 的方法,是在 server.c/serverCron => aof.c/backgroundRewriteDoneHandler 中發現 AOF 重寫子程式退出後被呼叫:
//……
/* Check if a background saving or AOF rewrite in progress terminated. */
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||
ldbPendingChildren())
{
//……
// 任意子程式退出時執行
if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
//……
if (pid == -1) {
serverLog(……);
} else if (pid == server.rdb_child_pid) {
backgroundSaveDoneHandler(exitcode,bysignal);
} else if (pid == server.aof_child_pid) { // 發現是 aof 重寫子程式完成
backgroundRewriteDoneHandler(exitcode,bysignal); // 執行後續工作,包括關閉 pipe
}
//……
}
}
//……
複製程式碼
由此可見,如果 aof 重寫子程式沒有啟動,則 pipe 將不會被關閉。而下次嘗試啟動 aof 重寫時,又會呼叫 aof.c/aofCreatePipes 建立新的 pipe。
解決
- 2015 年就被兩次在社群上報(參考 github.com/antirez/red…
- 2016 年有開發者提交程式碼修復此問題,直至 2017 年 2 月相關修復才被合入主幹(參考 github.com/antirez/red…
- 這隻長壽的 bug 在 3.2.9 版本已修復
1.3.2 在 AOF 檔案 rewrite 期間如果設定 config set appendonly no,會導致 redis 程式一直死迴圈不間斷觸發 rewrite AOF
此 BUG 在 4.0.7 版本修復 (2018.1 月)
根因
redis 在 AOF rewrite 期間設定了 appendonly no,會 kill 子程式,設定 server.aof_fd = -1,但是並未更新 server.aof_rewrite_base_size。
在 serverCron 中觸發 AOF rewrite 時未判斷當前 appendonly 是否為 yes,只判斷了 server.aof_current_size 和 server.aof_rewrite_base_size 增長是否超過閾值
AOF rewrite 重寫完成後發現 server.aof_fd=-1 也未更新 server.aof_rewrite_base_size,導致 serverCron 中一直觸發 AOF rewrite。
1.3.3 redis slots 遷移的時候,永不過期的 key 因為 ttl>0 而過期,導致遷移丟失資料
詳細見部落格 blog.csdn.net/doc_sgl/art…
對應 PR: github.com/antirez/red…
在 4.0rc2 版本中進行修復
根因
所有丟失 key 的 ttl 因為沒有處理而使用了前一個 key 的 ttl!
問題出在下面程式碼的 for 迴圈,對於不過期的 key,ttl 應該是 0,但是如果前面有過期的 key,ttl>0. 那麼在下一個處理不過期 key 時,expireat=-1,不會進入 if,ttl 還是使用前一個 ttl,導致一個永不過期的 key 因為 ttl>0 而過期。
/* MIGRATE host port key dbid timeout [COPY | REPLACE]
*
* On in the multiple keys form:
*
* MIGRATE host port "" dbid timeout [COPY | REPLACE] KEYS key1 key2 ... keyN */
void migrateCommand(client *c) {
long long ttl,expireat;
ttl = 0;
...
/* Create RESTORE payload and generate the protocol to call the command. */
/*
問題出在這個 for 迴圈,對於不過期的 key,ttl>0. 在處理不過期 key 時,expireat=-1,導致 ttl 還是使用前一個 ttl.
導致一個永不過期的 key 因為 ttl>0 而過期。
*/
for (j = 0; j < num_keys; j++) {
/
expireat = getExpire(c->db,kv[j]);
if (expireat != -1) {
ttl = expireat-mstime();
if (ttl < 1) ttl = 1;
}
serverAssertWithInfo(c,NULL,rioWriteBulkCount(&cmd,'*',replace ? 5 : 4));
if (server.cluster_enabled)
serverAssertWithInfo(c,rioWriteBulkString(&cmd,"RESTORE-ASKING",14));
else
serverAssertWithInfo(c,"RESTORE",7));
serverAssertWithInfo(c,sdsEncodedObject(kv[j]));
serverAssertWithInfo(c,kv[j]->ptr,sdslen(kv[j]->ptr)));
serverAssertWithInfo(c,rioWriteBulkLongLong(&cmd,ttl));
/* Emit the payload argument,that is the serialized object using
* the DUMP format. */
createDumpPayload(&payload,ov[j]);
serverAssertWithInfo(c,payload.io.buffer.ptr,sdslen(payload.io.buffer.ptr)));
sdsfree(payload.io.buffer.ptr);
/* Add the REPLACE option to the RESTORE command if it was specified
* as a MIGRATE option. */
if (replace)
serverAssertWithInfo(c,"REPLACE",7));
}
複製程式碼
1.4 redis 日誌
1.4.1 日常日誌
DB 0: 1 keys (0 volatile) in 4 slots HT
複製程式碼
- Redis 中的 DB 是相互獨立存在的,所以可以出現重複的 key。好處一是,對小型專案可以做如下設定: 1 號 DB 做開發,2 號 DB 做測試等等。
- Redis Cluster 方案只允許使用 0 號資料庫
- 0 volatile: 目前 0 號 DB 中沒有 volatile key,volatile key 的意思是 過特定的時間就被 REDIS 自動刪除,在做快取時有用。
- 4 slots HT: 目前 0 號 DB 的 hash table 只有 4 個 slots(buckets)
- //todo
1.5 redis 協議說明
Redis 的客戶端和服務端之間採取了一種獨立名為 RESP(REdis Serialization Protocol) 的協議
Redis 協議在以下幾點之間做出了折衷:
- 簡單的實現
- 快速地被計算機解析
- 簡單得可以能被人工解析
注意:RESP 雖然是為 Redis 設計的,但是同樣也可以用於其他 C/S 的軟體。
1.5.1 網路層
Redis 在 TCP 埠 6379 上監聽到來的連線,客戶端連線到來時,Redis 伺服器為此建立一個 TCP 連線。
在客戶端與伺服器端之間傳輸的每個 Redis 命令或者資料都以、r\n 結尾。
1.5.2 請求
Redis 接收由不同引陣列成的命令。一旦收到命令,將會立刻被處理,並回復給客戶端。
1.5.3 新的統一請求協議
新的統一協議已在 Redis 1.2 中引入,但是在 Redis 2.0 中,這就成為了與 Redis 伺服器通訊的標準方式。
在這個統一協議裡,傳送給 Redis 服務端的所有引數都是二進位制安全的。以下是通用形式:
*<number of arguments> CR LF
$<number of bytes of argument 1> CR LF
<argument data> CR LF
...
$<number of bytes of argument N> CR LF
<argument data> CR LF
複製程式碼
例子如下:
*3
$3
SET
$5
mykey
$7
myvalue
複製程式碼
上面的命令看上去像是單引號字串,所以可以在查詢中看到每個位元組的準確值:
"*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n"
複製程式碼
在 Redis 的回覆中也使用這樣的格式。批量回復時,這種格式用於每個引數 $6\r\nmydata\r\n。
實際的統一請求協議是 Redis 用於返回列表項,並呼叫 Multi-bulk 回覆。 僅僅是 N 個以以*\r\n
為字首的不同批量回復,是緊隨的引數(批量回復)數目。
1.5.4 回覆
Redis 用不同的回覆型別回覆命令。它可能從伺服器傳送的第一個位元組開始校驗回覆型別:
用單行回覆,回覆的第一個位元組將是“+”
錯誤訊息,回覆的第一個位元組將是“-”
整型數字,回覆的第一個位元組將是“:”
批量回復,回覆的第一個位元組將是“$”
多個批量回復,回覆的第一個位元組將是“*”
複製程式碼
通俗點講,則如下
- (+) 表示一個正確的狀態資訊,具體資訊是當前行 + 後面的字元。
- (-) 表示一個錯誤資訊,具體資訊是當前行-後面的字元。
- (
*
) 表示訊息體總共有多少行,不包括當前行,*
後面是具體的行數。- ( 後面則是對應的長度的資料。
- (:) 表示返回一個數值,:後面是相應的數字節符。
(1)Simple Strings
狀態回覆(或者單行回覆)以“+”開始以“\r\n”結尾的單行字串形式。例如:
"+OK\r\n"
127.0.0.1:6379> set name meetbill
+OK\r\n # 服務端實際返回
-------------------
OK # redis-cli 客戶端顯示
複製程式碼
客戶端庫將在“+”後面返回所有資料,正如上例中字串“OK”一樣。
(2)Errors
錯誤回覆傳送類似於狀態回覆。唯一的不同是第一個位元組用“-”代替“+”。
錯誤回覆僅僅在一些意料之外的事情發生時傳送,例如:如果你試圖執行一個操作來應付錯誤的資料型別,或者如果命令不存在等等。所以當收到一個錯誤回覆時,客戶端將會出現一個異常。
127.0.0.1:6379> meetbill
-ERR unknown command 'meetbill'\r\n # 服務端實際返回,下同
---
(error) ERR unknown command 'meetbill' # redis-cli 客戶端顯示,下同
複製程式碼
(3)Integers
這種回覆型別只是用 CRLF 結尾字串來表示整型,用一個位元組的“:”作為字首。例如:“:0\r\n”,或者“:1000\r\n”是整型回覆。
像 INCR 或者 LASTAVE 命令用整型回覆作為實際回覆值,此時對於返回的整型沒有特殊的意思。它僅僅是為 INCR、LASTSAVE 的 UNIX 時間等增加數值。
一些命令像 EXISTS 將為 true 返回 1,為 false 返回 0。
其它命令像 SADD、SREM 和 SETNX 如果操作實際完成了的話將返回 1,否則返回 0。
接下來的命令將回復一個整型回覆:SETNX、DEL、EXISTS、INCR、INCRBY、DECR、DECRBY、DBSIZE、LASTSAVE、RENAMENX、MOVE、LLEN、SADD、SREM、SISMEMBER、SCARD。
27.0.0.1:6379> LPUSH info meetbill hello
:2\r\n # 服務端實際返回,下同
---
(integer) 2 # redis-cli 客戶端顯示,下同
127.0.0.1:6379> LLEN info
:2\r\n
---
(integer) 2
127.0.0.1:6379> EXISTS info
:1\r\n
---
(integer) 1
127.0.0.1:6379> DEL info
:1\r\n
---
(integer) 1
127.0.0.1:6379> EXISTS info
:0\r\n
---
(integer) 0
複製程式碼
(4)Bulk Strings
批量回覆被伺服器用於返回一個單二進位制安全字串。
C: GET mykey
S: $6\r\nfoobar\r\n
複製程式碼
伺服器傳送第一行回覆,該行以“$”開始後面跟隨實際要傳送的位元組數,隨後是 CRLF,然後傳送實際資料,隨後是 2 個位元組的額外資料用於最後的 CRLF。伺服器傳送的準確序列如下:
"$6\r\nfoobar\r\n"
複製程式碼
如果請求的值不存在,批量回復將使用特殊的值 -1 來作為資料長度,例如:
C: GET nonexistingkey
S: $-1
複製程式碼
當請求的物件不存在時,客戶端庫 API 不會返回空字串,而會返回空物件。例如:Ruby 庫返回‘nil’,而 C 庫返回 NULL(或者在回覆的物件裡設定指定的標誌)等等。
127.0.0.1:6379> set site moelove.info
+OK\r\n # 服務端實際返回,下同
---
OK # redis-cli 客戶端顯示,下同
127.0.0.1:6379> get site
$12\r\nmoelove.info\r\n
---
"moelove.info"
127.0.0.1:6379> del site
:1\r\n
---
(integer) 1
127.0.0.1:6379> get site
$-1\r\n
---
(nil)
127.0.0.1:6379> set site ''
+OK\r\n
---
OK
127.0.0.1:6379> get site
$0\r\n\r\n
---
""
複製程式碼
(5)Arrays
像命令 LRNGE 需要返回多個值(列表的每個元素是一個值,而 LRANGE 需要返回多於一個單元素)。使用多批量寫是有技巧的,用一個初始行作為字首來指示多少個批量寫緊隨其後。
批量回復的第一個位元組總是*
,例如:
C: LRANGE mylist 0 3
s: *4
s: $3
s: foo
s: $3
s: bar
s: $5
s: Hello
s: $5
s: World
複製程式碼
正如您可以看到的多批量回復是以完全相同的格式使用 Redis 統一協議將命令傳送給伺服器。
伺服器傳送的第一行是*4\r\n
,用於指定緊隨著 4 個批量回復。然後傳送每個批量寫。
如果指定的鍵不存在,則該鍵被認為是持有一個空的列表,且數值 0 被當作多批量計數值來傳送,例如:
C: LRANGE nokey 0 1
S: *0
複製程式碼
當 BLPOP 命令超時時,它返回 nil 多批量回復。這種型別多批量回復的計數器是 -1,且值被當作 nil 來解釋。例如:
C: BLPOP key 1
S: *-1
複製程式碼
當這種情況發生時,客戶端庫 API 將返回空 nil 物件,且不是一個空列表。這必須有別於空列表和錯誤條件(例如:BLPOP 命令的超時條件)。
127.0.0.1:6379> LPUSH info TaoBeier moelove.info
:2\r\n # 服務端實際返回,下同
---
(integer) 2 # redis-cli 客戶端顯示,下同
127.0.0.1:6379> LRANGE info 0 -1
*2\r\n$12\r\nmoelove.info\r\n$8\r\nTaoBeier\r\n
---
1) "moelove.info"
2) "TaoBeier"
127.0.0.1:6379> LPOP info
$12\r\nmoelove.info\r\n
---
"moelove.info"
127.0.0.1:6379> LPOP info
$8\r\nTaoBeier\r\n
---
"TaoBeier"
127.0.0.1:6379> LRANGE info 0 -1
*0\r\n
---
(empty list or set)
複製程式碼
1.5.6 多批量回復中的 Nil 元素
多批量回復的單元素長度可能是 -1,為了發出訊號這個元素被丟失且不是空字串。這種情況傳送在 SORT 命令時,此時使用 GET 模式選項且指定的鍵丟失。一個多批量回復包含一個空元素的例子如下:
S: *3
S: $3
S: foo
S: $-1
S: $3
S: bar
複製程式碼
第二個元素是空。客戶端庫返回如下:
["foo",nil,"bar"]
複製程式碼
1.5.7 多命令和管道
客戶端能使用同樣條件為了發出多個命令。管道用於支援多命令能夠被客戶端用單寫操作來傳送,它不需要為了傳送下一條命令而讀取伺服器的回覆。所有回覆都能在最後被讀出。
通常 Redis 伺服器和客戶端擁有非常快速的連線,所以在客戶端的實現中支援這個特性不是那麼重要,如果一個應用需要在短時間內發出大量的命令,管道仍然會非常快。
1.5.8 舊協議傳送命令
在統一請求協議出現前,Redis 用不同的協議傳送命令,現在仍然支援,它簡單通過手動 telnet。在這種協議中,有兩種型別的命令:
- 內聯命令:簡單命令其引數用空格分割字串。非二進位制安全。
- 批量命令:批量命令準確如內聯命令,但是最後的引數用特殊方式來處理用於保證最後引數二進位制安全。 內聯命令
最簡單的傳送 Redis 命令的方式是通過內聯命令。下面是一個使用內聯命令聊天的伺服器 / 客戶端的例子(伺服器聊天用 S: 開始,客戶端聊天用 C: 開始)。
C: PING
S: +PONG
複製程式碼
下面是另外一個內聯命令返回整數的例子:
C: EXISTS somekey
S: :0
複製程式碼
因為‘somekey’不存在,所以伺服器返回‘:0’。
注意:EXISTS 命令帶有一個引數。引數用空格分割。
批量命令
一些命令當用內聯命令傳送時需要一種特殊的格式用於支援最後引數二進位制安全。這種命令用最後引數作為“位元組計數器”,然後傳送批量資料(因為伺服器知道讀取多少個位元組,所以是二進位制安全的)。
請看下面的例子:
C: SET mykey 6
C: foobar
S: +OK
複製程式碼
這條命令的最後一個引數是‘6’。這用於指定隨後資料的位元組數,即字串“foobar”。注意:雖然這個位元組流是以額外的兩個 CRLF 位元組結尾的。
所有批量命令都是用這種準確的格式:用隨後資料的位元組數代替最後一個引數,緊跟著後面是組成引數本身的位元組和 CRLF。為了更清楚程式,下面是通過客戶端傳送字串的例子:
"SET mykey 6\r\nfoobar\r\n"
複製程式碼
Redis 有一個內部列表,用於表示哪些命令是內聯,哪些命令是批量,所以你不得不傳送相應的命令。強烈建議使用新的統一請求協議來代替老的協議。
1.6 Redis RDB 檔案格式
Redis *.rdb
檔案是一個記憶體記憶體儲的二進位製表示法。這個二進位制檔案足以完全恢復 Redis 的狀態。
rdb 檔案格式為快速讀和寫優化。LZF 壓縮可以用來減少檔案大小。通常,物件前面有它們的長度,這樣,在讀取物件之前,你可以準確地分配記憶體大小。
為快速讀 / 寫優化意味著磁碟上的格式應該儘可能接近於在記憶體裡的表示法。這種方式正是 rdb 檔案採用的。 導致的結果是,在不瞭解 Redis 在記憶體裡表示資料的資料結構的情況下,你沒法解析 rdb 檔案。
1.6.1 解析 RDB 的高層演演算法
在高層層面看,RDB 檔案有下面的格式:
----------------------------# RDB 是一個二進位制檔案。檔案裡沒有新行或空格。
52 45 44 49 53 # 魔術字串 "REDIS"
30 30 30 33 # RDB 版本號,高位優先。在這種情況下,版本是 0003 = 3
----------------------------
FE 00 # FE = code 指出資料庫選擇器。資料庫號 = 00
----------------------------# 鍵值對開始
FD $unsigned int # FD 指出 "有效期限時間是秒為單位". 在這之後,讀取 4 位元組無符號整數作為有效期限時間。
$value-type # 1 位元組標記指出值的型別 - set,map,sorted set 等。
$string-encoded-key # 鍵,編碼為一個 redis 字串。
$encoded-value # 值,編碼取決於 $value-type.
----------------------------
FC $unsigned long # FC 指出 "有效期限時間是豪秒為單位". 在這之後,讀取 8 位元組無符號長整數作為有效期限時間。
$value-type # 1 位元組標記指出值的型別 - set,map,sorted set 等。
$string-encoded-key # 鍵,編碼為一個 redis 字串。
$encoded-value # 值,編碼取決於 $value-type.
----------------------------
$value-type # 這個鍵值對沒有有效期限。$value_type 保證 != to FD,FC,FE and FF
$string-encoded-key
$encoded-value
----------------------------
FE $length-encoding # 前一個資料庫結束,下一個資料庫開始。資料庫號用長度編碼讀取。
----------------------------
... # 這個資料庫的鍵值對,另外的資料庫。
FF ## RDB 檔案結束指示器
8 byte checksum ## 整個檔案的 CRC32 校驗和。
複製程式碼
魔術數
檔案開始於魔術字串 REDIS
。這是一個快速明智的檢查是否正在處理一個 redis rdb 檔案。
52 45 44 49 53 # "REDIS"
RDB 版本號
接下來 4
個位元組儲存了 rdb 格式的版本號。這 4
個位元組解釋為 ascii 字元,然後使用字串到整數的轉換法轉換為一個整數。
00 00 00 03 # Version = 3
資料庫選擇器
一個 Redis 例項可以有多個資料庫。
單一位元組 0xFE
標記資料庫選擇器的開始。在這個位元組之後,一個可變長度的欄位指出資料庫序號。
見“長度編碼”章節來瞭解如何讀取資料庫序號。
鍵值對
在資料庫選擇器之後,檔案包含了一序列的鍵值對。
每個鍵值對有 4 部分:
- 鍵儲存期限時間戳。這是可選的。
- 一個位元組標記值的型別。
- 鍵編碼為 Redis 字串。見“Redis 字串編碼”。
- 值根據值型別進行編碼。見“Redis 值編碼”。
鍵儲存期限時間戳
這個區塊開始於一位元組標記。值 FD
指出儲存期限是以秒為單位指定。值 FC
指出有效期限是以毫秒為單位指定。
如果時間指定為毫秒,接下來 8
個位元組表示 unix 時間。這個數字是 unix 時間戳,精確到秒或毫秒,表示這個鍵的有效期限。
數字如何編碼見“Redis 長度編碼”章節。
在匯入過程中,已經過期的鍵將必須丟棄。
值型別
一個位元組標記指示用於儲存值的編碼。
0
= “String 編碼”1
= “ List 編碼”2
= “Set 編碼”3
= “Sorted Set 編碼”4
= “Hash 編碼”9
= “Zipmap 編碼”10
= “Ziplist 編碼”11
= “IntSet 編碼”12
= “以 Ziplist 編碼的 Sorted Set”13
= “以 Ziplist 編碼的 Hashmap” (在 rdb 版本 4 中引入)
鍵
鍵簡單地編碼為 Redis 字串。見“字串編碼”章節瞭解鍵如何被編碼。
值
值的編碼取決於值型別標記。
- 當值型別等於
0
,值是簡單字串。 - 當值型別是
9
,10
,11
或12
中的一個,值被包裝為字串。讀取字串後,它必須進一步解析。 - 當值型別是
1
,2
,3
或4
中的一個,值是一序列字串。這個序列字串用於構造 list,set,sorted set 或 hashmap。
1.6.2 長度編碼
長度編碼用於儲存流中接下來物件的長度。長度編碼是一個可變位元組編碼,為儘可能少用位元組而設計。
這是長度編碼如何工作:
- 從流中讀取一個位元組,最高
2
bit 被讀取。- 如果開始 bit 是
00
,接下來6
bit 表示長度。- 如果開始 bit 是
01
,從流再讀取額外一個位元組。這組合的的14
bit 表示長度。- 如果開始 bit 是
10
,那麼剩餘的6
bit 丟棄,從流中讀取額外的4
位元組,這4
個位元組表示長度。- 如果開始 bit 是
11
,那麼接下來的物件是以特殊格式編碼的。剩餘6
bit 指示格式。這種編碼通常用於把數字作為字串儲存或儲存編碼後的字串。見字串編碼。
作為這種編碼的結果:
- 數字
[0 - 63]
可以在1
個位元組裡儲存- 數字
[0 - 16383]
可以在2
個位元組裡儲存- 數字
[0 - (2^32 - 1)]
可以在4
個位元組裡儲存
1.6.3 字串編碼
Redis 字串是二進位制安全的--這意味著你可以在這裡儲存任何東西。它們沒有任何特殊的字串結束記號。 最好認為 Redis 字串是一個位元組陣列。
Redis 裡有三種型別的字串:
- 長度字首字串。
- 一個
8
,16
或32
bit 整數。- LZF 壓縮的字串。
長度字首字串
長度前置字串是很簡單的。字串位元組的長度首先編碼為“長度編碼”,在這之後儲存字串的原始位元組。
整數作為字串
首先讀取“長度編碼”塊,特別是第一個 2
bit 是 11
。在這種情況下,讀取剩餘的 6
bit。如果這 6
bit 的值是:
0
表示接下來是8
bit 整數1
表示接下來是16
bit 整數2
表示接下來是32
bit 整數
這些整數都是以 little endian 格式編碼的。
壓縮字串
首先讀取“長度編碼”,特別是第一個 2
bit 是 11
. 在這種情況下,讀取剩餘 6
bit。如果這 6
bit 值是 3
,它表示接下來是一個壓縮字串。
壓縮字串按如下讀取:
- 從流中讀取壓縮後的長度
clen
,按“長度編碼”。- 從流中讀取未壓縮長度,按“長度編碼”。
- 接下來從流中讀取
clen
個位元組。- 最後,這些位元組按 LZF 演演算法解壓。
1.6.4 List 編碼
一個 Redis list 表示為一序列字串。
- 首先,從流中讀取 list 的大小:
size
,按“長度編碼”。- 然後,
size
個字串從流中讀取,按“字串編碼”。- 使用這些字串重新構建 list。
1.6.5 Set 編碼
Set 編碼與 list 完全類似。
1.6.6 Sorted Set 編碼
- 首先,從流中讀取 sorted set 大小
size
,按“長度編碼”- 先後讀取兩個字串作為 set 的元素和它的分值,作為一個元組。
- 一共讀取
size
個上面的元組作為 sorted set 集合。
1.6.7 Hash 編碼
- 首先,從流中讀取 hash 大小
size
,按“長度編碼”。- 下一步,從流中讀取
2 * size
個字串,按“字串編碼”。- 交替的字串是鍵和值。
- 例如,
2 us washington india delhi
表示 map{"us" => "washington","india" => "dlhi"}
。
1.6.8 Zipmap 編碼
注意:Zipmap 編碼從 Redis 2.6 開始已棄用。小的的 hashmap 編碼為 ziplist。
Zipmap 是一個被序列化為一個字串的 hashmap。本質上,鍵值對按順序儲存。在這種結構裡查詢一個鍵的複雜度是 O(N)
。
當鍵值對數量很少時,這個結構用於替代 dictionary。
為解析 zipmap,首先用“字串編碼”從流讀取一個字串。這個字串包裝了 zipmap。字串的內容表示了 zipmap。
字串裡的 zipmap 結構如下: <zmlen><len>"foo"<len><free>"bar"<len>"hello"<len><free>"world"<zmend>
-
zmlen
:1
位元組長,儲存 zipmap 的大小。如果大於等於254
,值不使用。將需要迭代整個 zipmap 來找出長度。 -
len
: 後續字串的長度,可以是鍵或值的。這個長度儲存為1
個或5
個位元組(與上面描述的“長度編碼”不同)。 如果第一個位元組位於0
到252
,那麼它是 zipmap 的長度。如果第一個位元組是253
,讀取下4
個位元組作為無符號整數來表示 zipmap 的長度。254
和255
對這個欄位是非法的。 -
free
: 總是1
位元組,指出值後面的空閒位元組數。例如,如果鍵的值是America
,更新為USA
後,將有4
個空閒的位元組。 -
zmend
: 總是255
. 指出zipmap
結束。
有效的例子:
18 02 06 4d 4b 44 31 47 36 01 00 32 05 59 4e 4e 58 4b 04 00 46 37 54 49 ff ..
- 從使用“字串編碼”開始解碼。你會注意到 18 是字串的長度。因此,我們將讀取下 24 個位元組,直到 ff。
- 現在,我們開始解析從 @02 06… @ 開始的字串,使用 “Zipmap 編碼”
- 02 是 hashmap 裡條目的數量。
- 06 是下一個字串的長度。因為長度小於 254,我們不需要讀取任何額外的位元組
- 我們讀取下 6 個位元組 4d 4b 44 31 47 36 來得到鍵 “MKD1G6”
- 01 是下一個字串的長度,這個字串應當是值
- 00 是空閒位元組的數量
- 讀取下一個位元組 0x32,得到值“2”
- 在這種情況下,空閒位元組是 0,所以不需要跳過任何東西
- 05 是下一個字串的長度,在這種情況下是鍵。
- 讀取下 5 個位元組 59 4e 4e 58 4b,得到鍵 “YNNXK”
- 04 是下一個字串的長度,這是一個值
- 00 是值後面的空閒位元組數
- 讀取下 4 個位元組 46 37 54 49 來得到值 “F7TI”
- 最終,遇到 FF,這表示這個 zipmap 的結束
- 因此,這個 zipmap 表示 hash {"MKD1G6" => "2","YNNXK" => "F7TI"}
1.6.9 Ziplist 編碼
一個 Ziplist 是一個序列化為一個字串的 list。本質上,list 的元素按順序地儲存,藉助於標記(flag
)和偏移(offset
)來達到高校地雙向遍歷 list。
為解析一個 ziplist,首先從流中讀取一個字串,按“字串編碼”。這個字串是 ziplist 的封裝。這個字串的內容表示了 ziplist。
字串裡的 ziplist 的結構如下:
zlbytes
:這是一個4
位元組無符號整數,表示 ziplist 的總位元組數。這4
位元組是 little endian 格式--最先出現的是最低有效位組。zltail
:這是一個4
位元組無符號整數,little endian 格式。它表示到 ziplist 的尾條目(tail entry)的偏移。zllen
:這是一個2
位元組無符號整數,little endian 格式。它表示 ziplist 的條目的數量entry
:一個條目表示 ziplist 的元素。細節在下面zlend
:總是等於255
。它表示 ziplist 的結束
ziplist 的每個條目有下面的格式:
<length-prev-entry><special-flag><raw-bytes-of-entry>
length-prev-enty
: 這個欄位儲存上一個條目的長度,如果是第一個條目則是 0。這允許容易地進行反向遍歷 list。這個長度儲存為 1 或 5 個位元組。 如果第一個位元組小於等於 253,它被認為是長度,如果第一個位元組是 254,接下來 4 個位元組用於儲存長度。4 位元組按無符號整數讀取。
special-flag
:這個標記指出條目是字串還是整數。它也指示字串長度或整數的大小。這個標記的可變編碼如下:
- |00pppppp| - 1 位元組:字串值長度小於等於 63 位元組(6 bit)
- |01pppppp|qqqqqqqq| - 2 位元組:字串值長度小於等於 16383 位元組(14 bit)
- |10______|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 位元組:字串值長度大於等於 16384 位元組
- |1100____| - 讀取後面 2 個位元組作為 16 bit 有符號整數
- |1101____| - 讀取後面 4 個位元組作為 32 bit 有符號整數
- |1110____| - 讀取後面 8 個位元組作為 64 bit 有符號整數
- |11110000| - 讀取後面 3 個位元組作為 2 4bit 有符號整數
- |11111110| - 讀取後面 1 個位元組作為 8 bit 有符號整數
- |1111xxxx| - (當 xxxx 位於 0000 到 1101)直接 4 bit 整數。0 到 12 的無符號整數。被編碼的實際值是從 1 到 13,因為 0000 和 1111 不能使用,所以應當從編碼的 4bit 值裡減去 1 來獲得正確的值。
Raw Bytes
:在special flag
後,是原始位元組。位元組的數字由前面的special flag
部分決定。舉例
23 23 00 00 00 1e 00 00 00 04 00 00 e0 ff ff ff ff ff ff ff 7f 0a d0 ff ff 00 00 06 c0 fc 3f 04 c0 3f 00 ff ...
| | | | | | | |
- 從使用“字串編碼”開始解碼。23 是字串的長度,然後讀取 35 個位元組直到 ff
- 使用“Ziplist 編碼”解析開始於 23 00 00 ... 的字串
- 前 4 個位元組 23 00 00 00 表示 Ziplis 長度的位元組總數。注意,這是 little endian 格式
- 接下來 4 個位元組 1e 00 00 00 表示到尾條目的偏移。 0x1e = 30,這是一個基於 0 的偏移。 0th position = 23,1st position = 00 and so on. It follows that the last entry starts at 04 c0 3f 00 .. 。
- 接下來 2 個位元組 04 00 表示 list 裡條目的數量。
- 從現在開始,讀取條目。
- 00 表示前一個條目的長度。0 表示這是第一個條目。
- e0 是特殊標記,因為它開始於位模式 1110____,讀取下 8 個位元組作為整數。這是 list 的第一個條目。
- 現在開始讀取第二個條目。
- 0a 是前一個條目的長度。10 位元組 = 1 位元組 prev 長度 + 1 位元組特殊標記長度 + 8 位元組整數
- d0 是特殊標記,因為它開始於位模式 1101____,讀取下 4 個位元組作為整數。這是 list 的第二個條目。
- 現在開始第二個條目。
- 06 是前一個條目的長度。 6 位元組 = 1 位元組 prev 長度 + 1 位元組特殊標記 + 4 位元組整數。
- c0 是特殊標記,因為它開始於位模式 1100____,讀取下 2 個位元組作為整數。這是 list 的第三個條目。
- 現在開始讀取第四個條目。
- 04 是前一個題目的長度。
- c0 指出是 2 位元組整數。
- 讀取下 2 個位元組,作為第四個條目。
- 最終遇到 ff,這表明已經讀取完 list 裡的所有元素。
- 因此,ziplist 儲存了值 [0×7fffffffffffffff,65535,16380,63]。
1.6.10 Intset 編碼
一個 Inset 是一個整數的二叉搜尋樹。這個二叉樹在一個整數陣列裡實現。intset 用於當 set 的所有元素都是整數時。Inset 支援達 64
位的整數。
作為一個優化,如果整數能用更少的位元組表示,整數陣列將由 16
位或 32
位整數構建。當一個新元素插入時,intset 實現在需要時將進行一次升級。
因為 Intset 是二叉搜尋樹,set 裡的數字總是有序的。
一個 Intset 有一個 Set 的外部介面。
為瞭解析 Inset,首先使用“字串編碼”從流中讀取一個字串。這個字串包含了 Intset。這個字串的內容表示了 Intset。
在字串裡,Intset 有一個非常簡單的佈局: <encoding><length-of-contents><contents>
encoding
:是一個32
位無符號整數。它有 3 個可能的值 -2
,4
或8
。它指出內容裡儲存的每個整數的位元組大小。嗯,是的,這是浪費的-可以在2
bit 裡儲存這些資訊。
length-of-contet
:是一個32
位無符號整數,指出內容陣列的長度。
contents
:是一個$length-of-content
個位元組的陣列。它包含了二叉搜尋樹。
舉例
14 04 00 00 00 03 00 00 00 fc ff 00 00 fd ff 00 00 fe ff 00 00 ...
- 使用“字串編碼”來開始。14 是字串的長度,讀取下 20 個位元組直到 00.
- 現在,開始解析開始於 04 00 00 .... 的字串。
- 前 4 個位元組 04 00 00 00 是編碼,因為它的值是 4,我們知道我們正在處理 32 位整數。
- 下 4 個位元組 03 00 00 00 是內容的長度。這樣,我們知道我們正在處理 3 個整數,每個 4 位元組長。
- 從現在開始,我們以 4 個位元組為一組讀取,再把它轉換為一個無符號整數。
- 這樣,我們的 intset 看起來是這樣的 - 0x0000FFFC,0x0000FFFD,0x0000FFFE。注意,這些整數是 little endian 格式的。首先出現的是最低有效位。
1.6.11 以 Ziplist 編碼的 Sorted Set
以 ziplist 編碼儲存的 sorted list 跟上面描述的 Ziplist 很像。在 ziplist 裡,sorted set 的每個元素後跟它的 score。
舉例
[‘Manchester City’,1,‘Manchester United’,2,‘Totenham’,3]
如你所見 score 跟在每個元素後面。
1.6.12 Ziplist 編碼的 Hashmap
在這裡,hashmap 的鍵值對是作為連續的條目儲存在 ziplist 裡。
注意:這是在 rdb 版本 4 引入,它廢棄了在先前版本里使用的 zipmap。
舉例
{"us" => “washington”,“india” => "delhi"}
儲存在 ziplist 裡是: [“us”,“washington”,“india”,“delhi”]
CRC32 校驗和
從 RDB 版本 5 開始,一個 8
位元組的 CRC32
校驗和被加到檔案結尾。可以通過 redis.conf 檔案的一個引數來作廢這個校驗和。
當校驗和被作廢時,這個欄位將是 0
。
2 Redis twemproxy 叢集
- Nutcracker,又稱 Twemproxy(讀音:"two-em-proxy")是支援 memcached 和 redis 協議的快速、輕量級代理;
- 它的建立旨在減少後端快取伺服器上的連線數量;
- 再結合管道技術(
pipelining*
)、及分片技術可以橫向擴充套件分散式快取架構;
- Redis pipelining(流式批處理、管道技術):將一系列請求連續傳送到 Server 端,不必每次等待 Server 端的返回,而 Server 端會將請求放進一個有序的管道中,在執行完成後,會一次性將結果返回(解決 Client 端和 Server 端的網路延遲造成的請求延遲)
2.1 Twemproxy 特性
twemproxy 的特性:
- 支援失敗節點自動刪除
- 可以設定重新連線該節點的時間
- 可以設定連線多少次之後刪除該節點
- 支援設定 HashTag
- 通過 HashTag 可以自己設定將兩個 key 雜湊到同一個例項上去
- 減少與 redis 的直接連線數
- 保持與 redis 的長連線
- 減少了客戶端直接與伺服器連線的連線數量
- 自動分片到後端多個 redis 例項上
- 多種 hash 演演算法:md5、crc16、crc32 、crc32a、fnv1_64、fnv1a_64、fnv1_32、fnv1a_32、hsieh、murmur、jenkins
- 多種分片演演算法:ketama(一致性 hash 演演算法的一種實現)、modula、random
- 可以設定後端例項的權重
- 避免單點問題
- 可以平行部署多個代理層,通過 HAProxy 做負載均衡,將 redis 的讀寫分散到多個 twemproxy 上。
- 支援狀態監控
- 可設定狀態監控 ip 和埠,訪問 ip 和埠可以得到一個 json 格式的狀態資訊串
- 可設定監控資訊重新整理間隔時間
- 使用 pipelining 處理請求和響應
- 連線複用,記憶體複用
- 將多個連線請求,組成 reids pipelining 統一向 redis 請求
- 並不是支援所有 redis 命令
- 不支援 redis 的事務操作
- 使用 SIDFF,SDIFFSTORE,SINTER,SINTERSTORE,SMOVE,SUNION and SUNIONSTORE 命令需要保證 key 都在同一個分片上。
2.2 環境說明
4 臺 redis 伺服器
10.10.10.4:6379 - 1
10.10.10.5:6379 - 2
複製程式碼
2.2 安裝依賴
安裝 autoconf centos 7 yum 安裝既可, autoconf 版本必須 2.64 以上版本
yum -y install autoconf
複製程式碼
2.3 安裝 Twemproxy
git clone https://github.com/twitter/twemproxy.git
autoreconf -fvi #生成 configure 檔案
./configure --prefix=/opt/local/twemproxy/ --enable-debug=log
make && make install
mkdir -p /opt/local/twemproxy/{run,conf,logs}
ln -s /opt/local/twemproxy/sbin/nutcracker /usr/bin/
複製程式碼
2.4 配置 Twemproxy
cd /opt/local/twemproxy/conf/
vi nutcracker.yml #編輯配置檔案
meetbill:
listen: 10.10.10.4:6380 #監聽埠
hash: fnv1a_64 #key 值 hash 演演算法,預設 fnv1a_64
distribution: ketama #分佈演演算法
#ketama 一致性 hash 演演算法;modula 非常簡單,就是根據 key 值的 hash 值取模;random 隨機分佈
auto_eject_hosts: true #摘除後端故障節點
redis: true #是否是 redis 快取,預設是 false
timeout: 400 #代理與後端超時時間,毫秒
server_retry_timeout: 200000 #摘除故障節點後重新連線的時間,毫秒
server_failure_limit: 1 #故障多少次摘除
servers:
- 10.10.10.4:6379:1 server1
- 10.10.10.5:6379:1 server2
複製程式碼
檢查配置檔案是否正確
nutcracker -t -c /opt/local/twemproxy/conf/nutcracker.yml
複製程式碼
2.5 啟動 Twemproxy
2.5.1 啟動命令詳解
Usage: nutcracker [-?hVdDt] [-v verbosity level] [-o output file]
[-c conf file] [-s stats port] [-a stats addr]
[-i stats interval] [-p pid file] [-m mbuf size]
引數 釋義
-h,–help 檢視幫助檔案,顯示命令選項
-V,–version 檢視 nutcracker 版本
-t,–test-conf 測試配置指令碼的正確性
-d,–daemonize 以守護程式執行
-D,–describe-stats 列印狀態描述
-v,–verbosity=N 設定日誌級別 (default: 5,min: 0,max: 11)
-o,–output=S 設定日誌輸出路徑,預設為標準錯誤輸出 (default: stderr)
-c,–conf-file=S 指定配置檔案路徑 (default: conf/nutcracker.yml)
-s,–stats-port=N 設定狀態監控埠,預設 22222 (default: 22222)
-a,–stats-addr=S 設定狀態監控 IP,預設 0.0.0.0 (default: 0.0.0.0)
-i,–stats-interval=N 設定狀態聚合間隔 (default: 30000 msec)
-p,–pid-file=S 指定程式 pid 檔案路徑,預設關閉 (default: off)
-m,–mbuf-size=N 設定 mbuf 塊大小,以 bytes 單位 (default: 16384 bytes)
複製程式碼
2.5.2 啟動
nutcracker -d -c /opt/local/twemproxy/conf/nutcracker.yml -p /opt/local/twemproxy/run/redisproxy.pid -o /opt/local/twemproxy/logs/redisproxy.log
複製程式碼
2.6 檢視狀態
2.6.1 狀態引數
nutcracker --describe-stats
This is nutcracker-0.2.4
pool stats:
client_eof "# eof on client connections"
client_err "# errors on client connections"
client_connections "# active client connections"
server_ejects "# times backend server was ejected"
forward_error "# times we encountered a forwarding error"
fragments "# fragments created from a multi-vector request"
server stats:
server_eof "# eof on server connections"
server_err "# errors on server connections"
server_timedout "# timeouts on server connections"
server_connections "# active server connections"
requests "# requests"
request_bytes "total request bytes"
responses "# respones"
response_bytes "total response bytes"
in_queue "# requests in incoming queue"
in_queue_bytes "current request bytes in incoming queue"
out_queue "# requests in outgoing queue"
out_queue_bytes "current request bytes in outgoing queue"
複製程式碼
2.6.2 狀態例項
#curl -s http://127.0.0.1:22222|python -mjson.tool
{
"meetbill": { # 配置名稱
"client_connections": 0,# 當前活躍的客戶端連線數
"client_eof": 0,"client_err": 2,# 客戶端連線錯誤次數
"forward_error": 0,# 轉發錯誤次數
"fragments": 0,"server_ejects": 0 # 後端服務被踢出次數
"server1": {
"in_queue": 0,"in_queue_bytes": 0,"out_queue": 0,"out_queue_bytes": 0,"request_bytes": 0,# 已請求位元組數
"requests": 0,# 已請求次數
"response_bytes": 0,# 已相應位元組數
"responses": 0,# 已響應次數
"server_connections": 0,# 當前活躍的服務端連線數
"server_eof": 0,"server_err": 0,# 服務端連線錯誤次數
"server_timedout": 0 # 因連線超時的服務端錯誤次數
},"server2": {
"in_queue": 0,"requests": 0,"response_bytes": 0,"responses": 0,"server_connections": 0,"server_eof": 0,"server_timedout": 0
},},"service": "nutcracker","source": "meetbill",# 主機名
"timestamp": 1520780415,# 當前時間戳
"uptime": 3160,# 服務已經啟動的時間(單位:秒
"version": "0.2.4"
}
複製程式碼
2.6.3 獲取 Twemproxy 狀態
使用 curl 獲取 Twemproxy 狀態時,如果後端的 redis 或者 memcache 過多,將會導致獲取狀態內容失敗,這個是因為 proxy 的狀態埠返回的不是 HTTP 資料包,可以進行如下解決方法
Python 程式
def fetch_stats(ip,port):
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((ip,port))
raw = ""
while True:
data = s.recv(1024)
if len(data) == 0:
break
raw += data
s.close()
stats = json.loads(raw)
return stats
複製程式碼
nc
nc ip stat_port
複製程式碼
2.7 其他
2.7.1 傳送訊號修改日誌級別以及重新開啟日誌檔案
日誌只有在編譯安裝的時候啟用( --enable-debug=log),預設情況下日誌寫到 stderr. 可以使用 -o 或者 --output 命令指定輸出檔案,使用 -v 標記日誌級別
# 提高日誌級別(級別越高越詳細)
kill -SIGTTIN <pid>
# 降低日誌級別
kill -SIGTTOU <pid>
# 重新開啟日誌檔案
kill -SIGHUP <pid>
複製程式碼
3 redis cluster
3.1 cluster 命令
- 叢集 (cluster)
- cluster info 列印叢集的資訊
- cluster nodes 列出叢集當前已知的所有節點 (node),以及這些節點的相關資訊
- 節點 (node)
- cluster meet 將 ip 和 port 所指定的節點新增到叢集當中,讓它成為叢集的一份子
- cluster forget <node_id> 從叢集中移除 node_id 指定的節點
- cluster replicate <node_id> 將當前節點設定為 node_id 指定的節點的從節點
- cluster saveconfig 將節點的配置檔案儲存到硬碟裡面
- cluster slaves <node_id> 列出該 slave 節點的 master 節點
- cluster set-config-epoch 強制設定 configEpoch
- 槽 (slot)
- cluster addslots [slot ...] 將一個或多個槽 (slot) 指派 (assign) 給當前節點
- cluster delslots [slot ...] 移除一個或多個槽對當前節點的指派
- cluster flushslots 移除指派給當前節點的所有槽,讓當前節點變成一個沒有指派任何槽的節點
- cluster setslot node <node_id> 將槽 slot 指派給 node_id 指定的節點,如果槽已經指派給另一個節點,那麼先讓另一個節點刪除該槽,然後再進行指派
- cluster setslot migrating <node_id> 將本節點的槽 slot 遷移到 node_id 指定的節點中
- cluster setslot importing <node_id> 從 node_id 指定的節點中匯入槽 slot 到本節點
- cluster setslot stable 取消對槽 slot 的匯入 (import) 或者遷移 (migrate)
- 鍵 (key)
- cluster keyslot 計算鍵 key 應該被放置在哪個槽上
- cluster countkeysinslot 返回槽 slot 目前包含的鍵值對數量
- cluster getkeysinslot 返回 count 個 slot 槽中的鍵
- 其它
- cluster myid 返回節點的 ID
- cluster slots 返回節點負責的 slot
- cluster reset 重置叢集,慎用
3.2 redis cluster 配置
cluster-enabled yes
複製程式碼
如果配置 yes 則開啟叢集功能,此 redis 例項作為叢集的一個節點,否則,它是一個普通的單一的 redis 例項。
cluster-config-file nodes-6379.conf
複製程式碼
雖然此配置的名字叫"叢集配置檔案",但是此配置檔案不能人工編輯,它是叢集節點自動維護的檔案,主要用於記錄叢集中有哪些節點、他們的狀態以及一些持久化引數等,方便在重啟時恢復這些狀態。通常是在收到請求之後這個檔案就會被更新。
cluster-node-timeout 15000
複製程式碼
這是叢集中的節點能夠失聯的最大時間,超過這個時間,該節點就會被認為故障。如果主節點超過這個時間還是不可達,則用它的從節點將啟動故障遷移,升級成主節點。注意,任何一個節點在這個時間之內如果還是沒有連上大部分的主節點,則此節點將停止接收任何請求。一般設定為 15 秒即可。
cluster-slave-validity-factor 10
複製程式碼
如果設定成 0,則無論從節點與主節點失聯多久,從節點都會嘗試升級成主節點。如果設定成正數,則 cluster-node-timeout 乘以 cluster-slave-validity-factor 得到的時間,是從節點與主節點失聯後,此從節點資料有效的最長時間,超過這個時間,從節點不會啟動故障遷移。假設 cluster-node-timeout=5,cluster-slave-validity-factor=10,則如果從節點跟主節點失聯超過 50 秒,此從節點不能成為主節點。注意,如果此引數配置為非 0,將可能出現由於某主節點失聯卻沒有從節點能頂上的情況,從而導致叢集不能正常工作,在這種情況下,只有等到原來的主節點重新迴歸到叢集,叢集才恢復運作。
cluster-migration-barrier 1
複製程式碼
主節點需要的最小從節點數,只有達到這個數,主節點失敗時,它從節點才會進行遷移。更詳細介紹可以看本教程後面關於副本遷移到部分。
cluster-require-full-coverage yes
複製程式碼
在部分 key 所在的節點不可用時,如果此引數設定為"yes"(預設值),則整個叢集停止接受操作;如果此引數設定為”no”,則叢集依然為可達節點上的 key 提供讀操作。
3.3 redis cluster 狀態
127.0.0.1:8001> cluster info
- cluster_state:ok
- 如果當前 redis 發現有 failed 的 slots,預設為把自己 cluster_state 從 ok 個性為 fail,寫入命令會失敗。如果設定 cluster-require-full-coverage 為 no,則無此限制。
- cluster_slots_assigned:16384 #已分配的槽
- cluster_slots_ok:16384 #槽的狀態是 ok 的數目
- cluster_slots_pfail:0 #可能失效的槽的數目
- cluster_slots_fail:0 #已經失效的槽的數目
- cluster_known_nodes:6 #叢集中節點個數
- cluster_size:3 #叢集中設定的分片個數
- cluster_current_epoch:15 #叢集中的 currentEpoch 總是一致的,currentEpoch 越高,代表節點的配置或者操作越新,叢集中最大的那個 node epoch
- cluster_my_epoch:12 #當前節點的 config epoch,每個主節點都不同,一直遞增,其表示某節點最後一次變成主節點或獲取新 slot 所有權的邏輯時間。
- cluster_stats_messages_sent:270782059
- cluster_stats_messages_received:270732696
127.0.0.1:8001> cluster nodes
25e8c9379c3db621da6ff8152684dc95dbe2e163 192.168.64.102:8002 master - 0 1490696025496 15 connected 5461-10922
d777a98ff16901dffca53e509b78b65dd1394ce2 192.168.64.156:8001 slave 0b1f3dd6e53ba76b8664294af2b7f492dbf914ec 0 1490696027498 12 connected
8e082ea9fe9d4c4fcca4fbe75ba3b77512b695ef 192.168.64.108:8000 master - 0 1490696025997 14 connected 0-5460
0b1f3dd6e53ba76b8664294af2b7f492dbf914ec 192.168.64.170:8001 myself,master - 0 0 12 connected 10923-16383
eb8adb8c0c5715525997bdb3c2d5345e688d943f 192.168.64.101:8002 slave 25e8c9379c3db621da6ff8152684dc95dbe2e163 0 1490696027498 15 connected
4000155a787ddab1e7f12584dabeab48a617fc46 192.168.67.54:8000 slave 8e082ea9fe9d4c4fcca4fbe75ba3b77512b695ef 0 1490696026497 14 connected
複製程式碼
- 節點 ID:例如 25e8c9379c3db621da6ff8152684dc95dbe2e163
- ip:port:節點的 ip 地址和埠號,例如 192.168.64.102:8002
- flags:節點的角色 (master,slave,myself) 以及狀態 (pfail,fail)
- 如果節點是一個從節點的話,那麼跟在 flags 之後的將是主節點的節點 ID,例如 192.168.64.156:8001 主節點的 ID 就是 0b1f3dd6e53ba76b8664294af2b7f492dbf914ec
- 叢集最近一次向節點傳送 ping 命令之後,過了多長時間還沒接到回覆
- 節點最近一次返回 pong 回覆的時間
- 節點的配置紀元 (config epoch)
- 本節點的網路連線情況
- 節點目前包含的槽,例如 192.168.64.102:8002 目前包含的槽為 5461-10922
3.4 redis cluster 的 failover 機制
failover 是 redis cluster 提供的容錯機制,cluster 最核心的功能之一。failover 支援兩種模式:
- 故障 failover:自動恢復叢集的可用性
- 人為 failover:支援叢集的可運維操作
3.4.1 故障 failover
故障 failover 表現在一個 master 分片故障後,slave 接管 master 的過程。
分為如下 3 個階段:
- 探測階段
- 準備階段
- 執行階段
探測階段
叢集中的所有分片通過 gossip 協議傳遞。探測步驟為:
- (1)在 cron 中非遍歷 cluster nodes 做 ping 傳送,隨機從 5 個節點中選出最老 pong_recv 的節點傳送 ping,再遍歷節點中 pong_recv > timeout/2 的節點傳送 ping。
- (2)再遍歷每個節點從發出 ping 包後超時沒有收到 pong 包的時間,超時將對應的分片設定為 pfail 狀態,在跟其他節點的 gossip 包過程中,每個節點會帶上被標記為 pfail 狀態的包。
- (3)每個正常分片收到 ping 包後,統計叢集中 maste 分片將故障節點設定為 pfail, 超過一半以上的節點設定為 pfail, 則將節點設定為 fail 狀態。如果這個分片屬於故障節點的 slave 節點,則主動廣播故障節點為 fail 狀態。
準備階段
在 cron 函式中,slave 節點獲取到 master 節點狀態為 fail,主動發起一次 failover 操作,該操作並不是立即執行,而是設計了多個限制:
- (1)過期的超時不執行。如何判斷是夠過期?
- data_age = 當前時間點 - 上次 master 失聯的時間點 - 超時時間
- 如果 data_age >
master 到 slave 的 ping 間隔時間 + 超時時間*cluster_slave_validity_factor
, 則認為過期。cluster_slave_validity_factor 是一個配置項,cluster_slave_validity_factor 設定的越小越不容易觸發 failover。- (2)計算出一個延遲執行的時間 failover_auth_time, failover_auth_time = 當前時間 + 500ms + 0-500ms 的隨機值 + 當前 slave 的 rank * 1s,rank 按已同步的 offset 計算,offset 同步的越延遲,rank 值越大,該 slave 就越推遲觸發 failover 的時間,以此來避免多個 slave 同時 failover。只有當前時間到 failover_auth_time 的時間點才會執行 failover。
執行階段
- (1)將 currentEpoch 自增,再賦值給 failover_auth_epoch
- (2)向其他 master 分片發起 failover 投票,等待投票結果
- (3)其他 master 分片收到 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 請求後,會判斷是否符合以下情況:
- epoch 必須 >= 所有叢集檢視的 master 節點的 epoch
- 發起者是 slave
- slave 的 master 已是 fail 狀態
- 在相同 epoch 內只投票一次
- 在超時時間(cluster_node_timeout) * 2 的時間內只投票一次
- (4)其他 master 回覆 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK,slave 端收到後做統計
- (5)在 cron 中判斷統計超過一半以上 master 回覆,開始執行 failover
- (6)標記自身節點為 master
- (7)清理複製鏈路
- (8)重置叢集拓撲結構資訊
- (9)向叢集內所有節點廣播
3.4.2 人為 failover
人為 failover 支援三種模式的 failover:預設、force、takeover。
預設
(1)由 salve 給 master 傳送 CLUSTERMSG_TYPE_MFSTART
(2)master 收到後設定 clients_pause_end_time = 當前時間 + 5s*2,clients_paused =1,客戶端暫停所有請求,新建請求會被加到 block client list。
(3)master 在 ping 包中帶上 repl_offset 的資訊
(4)slave 檢查 master 的 repl_offset,確認同步已完成
(5)設定 mf_can_start = 1,在 cron 中開始正常的 failover 流程,不需要像故障 failover 設定推遲執行而是立即執行操作,而且其他 master 投票時不需要考慮 master 是否為 fail 狀態。
複製程式碼
日誌:如下為主例項日誌
5484:M 01 Apr 18:31:07.572 # Manual failover requested by slave db1e03f2158f48019cddd680764a17635b3901c5.
5484:M 01 Apr 18:31:07.796 # Failover auth granted to db1e03f2158f48019cddd680764a17635b3901c5 for epoch 122
5484:M 01 Apr 18:31:07.797 # Connection with slave 【slave1_ip:slave1_port】 lost.
5484:M 01 Apr 18:32:08.509 # Disconnecting timedout slave: 【slave2_ip:slave2_port】
5484:M 01 Apr 18:32:08.509 # Connection with slave 【slave2_ip:slave2_port】 lost.
5484:M 01 Apr 18:32:08.509 # Disconnecting timedout slave: 【slave3_ip:slave3_port】
5484:M 01 Apr 18:32:08.509 # Connection with slave 【slave3_ip:slave3_port】 lost.
複製程式碼
force
忽略主備同步的狀態,設定 mf_can_start = 1,標記 failover 開始。
takeover
直接執行故障 failover 的第 6-9 步,忽略主備同步,忽略叢集其他 master 的投票。
4 原理說明
4.1 一致性 hash
4.1.1 傳統的取模方式
例如 10 條資料,3 個節點,如果按照取模的方式,那就是
- node a: 0,3,6,9
- node b: 1,4,7
- node c: 2,5,8
當增加一個節點的時候,資料分佈就變更為
- node a:0,8
- node b:1,9
- node c: 2,6
- node d: 3,7
總結:資料 3,7,8,9 在增加節點的時候,都需要做搬遷,成本太高
4.1.2 一致性雜湊方式
最關鍵的區別就是,對節點和資料,都做一次雜湊運算,然後比較節點和資料的雜湊值,資料取和節點最相近的節點做為存放節點。這樣就保證當節點增加或者減少的時候,影響的資料最少。還是拿剛剛的例子,(用簡單的字串的 ascii 碼做雜湊 key):
十條資料,算出各自的雜湊值
- 0:192
- 1:196
- 2:200
- 3:204
- 4:208
- 5:212
- 6:216
- 7:220
- 8:224
- 9:228
有三個節點,算出各自的雜湊值
- node a: 203
- node g: 209
- node z: 228
這個時候比較兩者的雜湊值,如果大於 228,就歸到前面的 203,相當於整個雜湊值就是一個環,對應的對映結果:
- node a: 0,2
- node g: 3,4
- node z: 5,9
這個時候加入 node n,就可以算出 node n 的雜湊值:
- node n: 216
這個時候對應的資料就會做遷移:
- node a: 0,4
- node n: 5,6
- node z: 7,9
這個時候只有 5 和 6 需要做遷移
4.1.3 虛擬節點
另外,這個時候如果只算出三個雜湊值,那再跟資料的雜湊值比較的時候,很容易分得不均衡,因此就引入了虛擬節點的概念,通過把三個節點加上 ID 字尾等方式,每個節點算出 n 個雜湊值,均勻的放在雜湊環上,這樣對於資料算出的雜湊值,能夠比較雜湊的分佈(詳見下面程式碼中的 replica)
通過這種演演算法做資料分佈,在增減節點的時候,可以大大減少資料的遷移規模。
4.2 redis 過期資料儲存方式以及刪除方式
當你通過 expire 或者 pexpire 命令,給某個鍵設定了過期時間,那麼它在伺服器是怎麼儲存的呢?到達過期時間後,又是怎麼刪除的呢?
4.2.1 儲存方式
比如:
redis> EXPIRE book 5
(integer) 1
複製程式碼
首先我們知道,redis 維護了一個儲存了所有的設定的 key->value 的字典。但是其實不止一個字典的。
redis 有一個包含過期事件的字典
每當有設定過期事件的 key 後,redis 會用當前的事件,加上過期的時間段,得到過期的標準時間,儲存在 expires 字典中。
從上圖可以看出來,比如你給 book 設定過期事件,那麼 expires 字典的 key 也為 book,值是當前的時間 +5s 後的 unix time。
4.2.2 刪除方式
如果一個鍵已經過期了,那麼 redis 的如果刪除它呢?redis 採用了 2 種刪除方式;
惰性刪除
惰性刪除的原理是:放任鍵過期不管,但是每次從鍵空間獲取鍵的時候,如果該鍵存在,再去 expires 字典判斷這個鍵是不是過期。如果過期則返回空,並刪除該鍵。過程如下:
- 優點:惰性刪除對 cpu 是友好的。保證在鍵必須刪除的時候才會消耗 cpu
- 缺點:惰性刪除對記憶體特別不友好。雖然鍵過期,但是沒有使用則一直存在記憶體中。
定期刪除
redis 架構中的時間事件,每隔一段時間後,在規定的時間內,會主動去檢測 expires 字典中包含的 key 進行檢測,發現過期的則刪除。在 redis 的原始碼 redis.c/activeExpireCycle 函式中。 下面分別是這個函式的原始碼與虛擬碼:
void activeExpireCycle(int type) {
// 靜態變數,用來累積函式連續執行時的資料
static unsigned int current_db = 0; /* Last DB tested. */
static int timelimit_exit = 0; /* Time limit hit in previous call? */
static long long last_fast_cycle = 0; /* When last fast cycle ran. */
unsigned int j,iteration = 0;
// 預設每次處理的資料庫數量
unsigned int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL;
// 函式開始的時間
long long start = ustime(),timelimit;
// 快速模式
if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
// 如果上次函式沒有觸發 timelimit_exit ,那麼不執行處理
if (!timelimit_exit) return;
// 如果距離上次執行未夠一定時間,那麼不執行處理
if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
// 執行到這裡,說明執行快速處理,記錄當前時間
last_fast_cycle = start;
}
/*
* 一般情況下,函式只處理 REDIS_DBCRON_DBS_PER_CALL 個資料庫,
* 除非:
* 當前資料庫的數量小於 REDIS_DBCRON_DBS_PER_CALL
* 如果上次處理遇到了時間上限,那麼這次需要對所有資料庫進行掃描,
* 這可以避免過多的過期鍵佔用空間
*/
if (dbs_per_call > server.dbnum || timelimit_exit)
dbs_per_call = server.dbnum;
// 函式處理的微秒時間上限
// ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 預設為 25 ,也即是 25 % 的 CPU 時間
timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
timelimit_exit = 0;
if (timelimit <= 0) timelimit = 1;
// 如果是執行在快速模式之下
// 那麼最多隻能執行 FAST_DURATION 微秒
// 預設值為 1000 (微秒)
if (type == ACTIVE_EXPIRE_CYCLE_FAST)
timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */
// 遍歷資料庫
for (j = 0; j < dbs_per_call; j++) {
int expired;
// 指向要處理的資料庫
redisDb *db = server.db+(current_db % server.dbnum);
// 為 DB 計數器加一,如果進入 do 迴圈之後因為超時而跳出
// 那麼下次會直接從下個 DB 開始處理
current_db++;
do {
unsigned long num,slots;
long long now,ttl_sum;
int ttl_samples;
// 獲取資料庫中帶過期時間的鍵的數量
// 如果該數量為 0 ,直接跳過這個資料庫
if ((num = dictSize(db->expires)) == 0) {
db->avg_ttl = 0;
break;
}
// 獲取資料庫中鍵值對的數量
slots = dictSlots(db->expires);
// 當前時間
now = mstime();
// 這個資料庫的使用率低於 1% ,掃描起來太費力了(大部分都會 MISS)
// 跳過,等待字典收縮程式執行
if (num && slots > DICT_HT_INITIAL_SIZE &&
(num*100/slots < 1)) break;
// 已處理過期鍵計數器
expired = 0;
// 鍵的總 TTL 計數器
ttl_sum = 0;
// 總共處理的鍵計數器
ttl_samples = 0;
// 每次最多隻能檢查 LOOKUPS_PER_LOOP 個鍵
if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
// 開始遍歷資料庫
while (num--) {
dictEntry *de;
long long ttl;
// 從 expires 中隨機取出一個帶過期時間的鍵
if ((de = dictGetRandomKey(db->expires)) == NULL) break;
// 計算 TTL
ttl = dictGetSignedIntegerVal(de)-now;
// 如果鍵已經過期,那麼刪除它,並將 expired 計數器增一
if (activeExpireCycleTryExpire(db,de,now)) expired++;
if (ttl < 0) ttl = 0;
// 累積鍵的 TTL
ttl_sum += ttl;
// 累積處理鍵的個數
ttl_samples++;
}
// 為這個資料庫更新平均 TTL 統計資料
if (ttl_samples) {
// 計算當前平均值
long long avg_ttl = ttl_sum/ttl_samples;
// 如果這是第一次設定資料庫平均 TTL ,那麼進行初始化
if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
/* Smooth the value averaging with the previous one. */
// 取資料庫的上次平均 TTL 和今次平均 TTL 的平均值
db->avg_ttl = (db->avg_ttl+avg_ttl)/2;
}
// 我們不能用太長時間處理過期鍵,
// 所以這個函式執行一定時間之後就要返回
// 更新遍歷次數
iteration++;
// 每遍歷 16 次執行一次
if ((iteration & 0xf) == 0 && /* check once every 16 iterations. */
(ustime()-start) > timelimit)
{
// 如果遍歷次數正好是 16 的倍數
// 並且遍歷的時間超過了 timelimit
// 那麼斷開 timelimit_exit
timelimit_exit = 1;
}
// 已經超時了,返回
if (timelimit_exit) return;
// 如果已刪除的過期鍵佔當前總資料庫帶過期時間的鍵數量的 25 %
// 那麼不再遍歷
} while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
}
}
複製程式碼
虛擬碼是:
# 預設每次檢測的資料庫數量為 16
DEFAULT_DB_NUMBERS = 16
# 預設每次檢測的鍵的數量最大為 20
DEFAULT_KEY_NUMBERS = 20
# 全域性變數,記錄當前檢測的進度
current_db = 0
def activeExpireCycle():
# 初始化要檢測的資料庫數量
# 如果伺服器的資料庫數量小於 16,則以伺服器的為準
if server.dbnumbers < DEFAULT_DB_NUMBERS:
db_numbers = server.dbnumbers
else
db_numbers = DEFAULT_DB_NUMBERS
# 遍歷每次資料庫
for i in range(db_numbers):
# 如果 current_db 的值等於伺服器的數量,代表已經遍歷全,則重新開始
if current_db = db_numbers:
current_db = 0
# 獲取當前要處理的資料庫
redisDb = server.db[current_db]
# 將資料庫索引 +1,指向下一個資料庫
current_db++
do
# 檢測資料庫中的鍵
for j in range(DEFAULT_KEY_NUMBERS):
# 如果資料庫中沒有過期鍵則跳過這個庫
if redisDb.expires.size() == 0:break
# 隨機獲取一個帶有過期事件的鍵
key_with_ttl = redisDb.expires.get_random_key()
# 檢測鍵是不是過期了,如果過期則刪除
if is_expired(key_with_ttl):
delete_key(key_with_ttl)
# 已達到時間上限,則停止處理
if reach_time_limit(): retrun
while expired>ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4
複製程式碼
對 activeExpireCycle 進行總結:
- redis 預設 1s 呼叫 10 次,這個是 redis 的配置中的 hz 選項。hz 預設是 10,代表 1s 呼叫 10 次,每 100ms 呼叫一次。
- hz 不能太大,太大的話,cpu 會花大量的時間消耗在判斷過期的 key 上,對 cpu 不友好。但是如果你的 redis 過期資料過多,可以適當調大。
- hz 不能太小,因為太小的話,一旦過期的 key 太多可能會過濾不完。
- redis 執行定期刪除函式,必須在一定時間內,超過該時間就 return。事件定義為
timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100
可以看出該時間與 hz 成反比,hz 預設 10,timelimit 就為 25ms;hz 修改為 100,那麼 timelimit 就為 2.5ms。 - 抽取 20 個資料進行判斷刪除為一個輪訓,每經過 16 個輪訓才會去判斷一次時間是不是超時。
- 如果一個資料庫,使用率低於 1%,則不去進行定期刪除操作。
- 如果對一個資料庫,這次刪除操作,已經刪除了 25% 的過期 key,那麼就跳過這個庫。
4.2.3 redis 主從刪除過期 key 方式
當 redis 主從模型下,從伺服器的刪除過期 key 的動作是由主伺服器控制的。
- 1、主伺服器在惰性刪除、客戶端主動刪除、定期刪除一個 key 的時候,會向從伺服器傳送一個 del 的命令,告訴從伺服器需要刪除這個 key。
- 2、從伺服器在執行客戶端讀取 key 的時候,如果該 key 已經過期,也不會將該 key 刪除,而是返回一個 null
- 3、從伺服器只有在接收到主伺服器的 del 命令才會將一個 key 進行刪除。
4.2.4 總結
- 1、expires 字典的 key 指向資料庫中的某個 key,而值記錄了資料庫中該 key 的過期時間,過期時間是一個以毫秒為單位的 unix 時間戳;
- 2、redis 使用惰性刪除和定期刪除兩種策略來刪除過期的 key;惰性刪除只會在碰到過期 key 才會刪除;定期刪除則每隔一段時間主動查詢並刪除過期鍵;
- 3、當主伺服器刪除一個過期 key 後,會向所有的從伺服器傳送一條 del 命令,顯式的刪除過期 key;
- 4、從伺服器即使發現過期 key 也不會自作主張刪除它,而是等待主伺服器傳送 del 命令,這種統一、中心化的過期 key 刪除策略可以保證主從伺服器的資料一致性。
4.3 cluster 選舉演演算法 Raft
3 種狀態:
- Leader(領袖)
- Follower(群眾)
- Candidate(候選人)。
規則:群眾發起投票成為候選人,候選人得到大多數票至少 (n/2)+1,才能成為領導人,(自己可以投自己,當沒有接受到請求節點的選票時,發起投票節點才能自己選自己),領導人負責處理所有與客戶端互動,是資料唯一入口,協調指揮群眾節點。
選舉過程:考慮最簡單情況,abc 三個節點,每個節點只有一張票,當 N 個節點發出投票請求,其他節點必須投出自己的一票,不能棄票,最差的情況是每個人都有一票,那麼隨機設定一個 timeout 時間,就像加時賽一樣,這時同時的概率大大降低,誰最先恢復過來,就向其他兩個節點發出投票請求,獲得大多數選票,成為領導人。選出 Leader 後,Leader 通過定期向所有 Follower 傳送心跳資訊維持其統治。若 Follower 一段時間未收到 Leader 的心跳則認為 Leader 可能已經掛了再次發起選主過程。
5 其他相關
5.1 核心引數 overcommit
它是 記憶體分配策略,可選值:0、1、2。
- 0, 表示核心將檢查是否有足夠的可用記憶體供應用程式使用;如果有足夠的可用記憶體,記憶體申請允許;否則,記憶體申請失敗,並把錯誤返回給應用程式。
- 1, 表示核心允許分配所有的實體記憶體,而不管當前的記憶體狀態如何。
- 2, 表示核心允許分配超過所有實體記憶體和交換空間總和的記憶體
什麼是 Overcommit 和 OOM
Linux 對大部分申請記憶體的請求都回復"yes",以便能跑更多更大的程式。因為申請記憶體後,並不會馬上使用記憶體。這種技術叫做 Overcommit。當 linux 發現記憶體不足時,會發生 OOM killer(OOM=out-of-memory)。它會選擇殺死一些程式(使用者態程式,不是核心執行緒),以便釋放記憶體。 當 oom-killer 發生時,linux 會選擇殺死哪些程式?選擇程式的函式是 oom_badness 函式(在 mm/oom_kill.c 中),該函式會計算每個程式的點數 (0~1000)。點數越高,這個程式越有可能被殺死。每個程式的點數跟 oom_score_adj 有關,而且 oom_score_adj 可以被設定 (-1000 最低,1000 最高)。
解決方法:
很簡單,按提示的操作(將 vm.overcommit_memory 設為 1)即可:可以通過 cat /proc/sys/vm/overcommit_memory
和 sysctl -a | grep overcommit
檢視
有三種方式修改核心引數,但要有 root 許可權:
- (1)編輯 /etc/sysctl.conf ,改 vm.overcommit_memory=1,然後 sysctl -p 使配置檔案生效
- (2)sysctl vm.overcommit_memory=1
- (3)echo 1 > /proc/sys/vm/overcommit_memory
6 資料遷移
6.1 目標
從 A 叢集熱遷移到 B 叢集
6.2 怎麼實現
mysql 的主從同步是基於 binlog,redis 主從是一個 op buf,mongo 主從同步是 oplog.
redis 裡的 aof 就是類似 binlog,記錄每個操作的 log
所以,我們可以利用 aof,把它當作 binlog,用於做遷移,分三步:
- 遷移基準資料
- 追增量
- 追上後,上層切流量。
redis 的 aof 包含了基準資料和增量,所以我們只需要把舊叢集中 redis 例項上的 aof 重放到新叢集,重放追上時修改上層,把入口換為新叢集即可。
6.3 問題
6.3.1 aof 不是冪等的
aof 不像 binlog 那樣可以重做 redolog,binlog 記錄的操作是冪等 (idempotent) 的,意味著如果失敗了,可以重做一次。
這是因為 binlog 記錄的是操作的結果,比如:
op log
---------------------------
set x 0 x = 0
incr x x = 1
incr x x = 2
複製程式碼
但是 redis 的 aof 記錄的是操作:
op log
---------------------------
set x 0 x = 0
incr x incr x
incr x incr x
複製程式碼
這就是說,如果我們在重放 aof 的過程中出錯(比如網路中斷):
不能繼續(不好找到上次同步到哪),也不能重新重放一次,(incr 兩次,值就錯了) 只能清除目標叢集的資料重新遷移一次
不過,好在 redis 單例項的 afo 資料都不大,一般 10G 左右,重放大約 20min 就能完成,出錯的概率也很小。(這也是 redis 可以這樣做,而其他持久儲存比如 mysql,mongo 必須支援斷點同步的原因)
6.3.2 切流量時的不一致
前面說的步驟是:
- 追 aof
- 追上後,切流量。 追 aof 是一個動態的過程,追上後,新的寫操作馬上就來,所以這裡追上的意思是說,新的寫入馬上會被消化掉。
但是考慮這樣一種場景:
假設 client 上做對 x 做兩次 set(一個機器上做兩次,或者兩個 app-server 上分別做):
client old_cluster new_cluster
-----------------------------------------------
set x 1(a) set x 1(客戶操作)
-----------------------------------------------> 切流量到 new_cluster
set x 2(b) set x 2 (客戶操作)
set x 1 (b 操作被重放到 new_cluster)
複製程式碼
a 操作還沒同步到 new_cluster,流量就已經切到了 new_cluster,這時候對同一個 key 的更新,會被老叢集上的操作覆蓋掉。
解決:
這個短暫的不一致,對多數業務,是能容忍的(很少有業務會高速更新同一個 key) 如果非要達到一致,當追 aof 追上後,app-server 停寫,等待徹底追上(此時老叢集的 aof 不會有更新了),然後再切流量。
6.4 實現
7 redis 服務准入
准入的目的是為了讓大家,在合理的範圍內使用 Redis 以維持 Redis 的穩定及效能。以良好的設計、常規的用法,規避 Redis 使用過程中可能遇到的問題,從而令 redis 能夠更加穩定的發揮其服務特性。
7.1 資料設計
7.1.1 value 大小約束
對於 string 型別 key,單 key 對應 value 大小不超過 10k
對於複雜型別 key(hash、list、set、zset),單 key 對應 value 大小不超過 100k
單個 kv 過大對網路卡及 cpu 造成較大消耗
redis 及 proxy 使用單執行緒 epoll 模型處理訊息請求,在 kv 較小(100 位元組內)情況下,單程式都可以承受 3 萬以上 qps。但在 kv 較大情況下,qps 承壓能力受網路卡上限影響,同時大量資料在記憶體與網路卡驅動之間進行復制,對 cpu 也有較大的消耗。
對於大 key 寫請求,主要的壓力在於主從複製使用的出口頻寬,主節點下面帶的從節點越多,出口頻寬消耗越嚴重,同時主節點 cpu 消耗也越嚴重
單個 kv 過大會增加運維成本
叢集分片機制是一般是以 key 作為 hash 物件,當單個 kv 過大時,可能會出現 hash 不均的情況。hash 不均帶來的後果是,一個叢集下不同分片的記憶體使用量是有明顯區別的,這對於運維過程中的資源預留規劃是不利的
其次,單 key 過大在分片擴容場景下,可能造成資料遷移超時,導致擴容失敗
單個 kv 過大,在執行刪除操作時,會造成較長時間阻塞。
由於 redis 單執行緒執行的機制,一個操作阻塞主執行緒,會導致該時間段內所有請求都堆積在 tcp buffer 中,得不到及時的處理。如果較多大 kv 在短時間內密集的執行刪除或其他耗時操作,會導致該 redis 響應時間明顯升高,甚至超時
設計建議:
合理構造 reids 資料結構及請求內容
經過網路的請求,應當僅進行滿足當前需求的存取。
典型的優化 case 是:1 個大 json 存一個大 string,只關注 json 中某一個或某幾個屬性的讀,也要讀取全部 string;只修改 json 中一個屬性,也要將整個 string 重新覆蓋寫。優化成 hash 後,可大大降低對網路卡、cpu、記憶體容量的壓力,同時當 hash key 個數較少(512 內),value 不是很大(64 位元組),可以進行壓縮,降低 redis 自身的資料結構開銷。
儘量避免 key value 中重複的內容,比如 key 使用 id 進行索引話,value 中就可以不必再存放 id 欄位。
資料預熱
若一個流程需要多次讀取 redis 中相同內容,建議流程起始點一次讀取,多次使用,儘量減少與 redis 互動,減輕後端壓力
複製程式碼
7.1.2 value 複雜度約束
對於非 str 型別的複雜 key(hash、list、set、zset),需注意控制單個複雜 key 下屬 member 個數,單 key 下屬 member 不超過 1000
複雜 key 的操作有很多種,主要可分為 O(1) 操作(如 hget)及非 O(1) 操作(hgetall、smembers 等)。
當一個複雜 key 本身下屬的 member 較多時,對其進行非 O(1) 操作可能會造成 redis 執行緒嚴重阻塞,從而導致請求堆積、超時。
我們線上有過幾次類似的 case,一個新上的流程,set 中慢慢的積累 member,然後不停的對其進行 smembers,導致 redis 程式的 cpu 緩慢上漲,最終上游超時情況越來越嚴重。
因此,在設計上首先要儘量避免超大的複雜 key,其次,對於 member 數量可能會比較多的複雜 key,要嚴格審查其非 O(1) 操作。
複製程式碼
7.1.3 冷熱資料設計約束
不建議在 redis 中存放 1 天以上不訪問的資料,冷資料須考慮設定過期時間或使用 db 方式儲存
redis 作為全記憶體資料庫,使用其第一目的就是用成本換效能,記憶體儲存成本比 ssd 及 hdd 都要高很多
典型的伺服器有 128G 記憶體,若算上持久化對記憶體的額外消耗,常規情況下只能提供約 80G 的使用容量,因此對 redis 的儲存空間要格外的珍惜,設計上如果允許一個 key 進入記憶體長時間不使用,不做快取超時,就會造成資源上的浪費。
複製程式碼
7.1.4 分片資料量約束
單分片不超過 12G
單分片資料量大,會影響持久化的效率,全量資料同步時間過長,同步成功率下降。並且單次持久化過程消耗的記憶體、時間上升。
觸發持久化時 fork 子程式耗時增加,導致主執行緒 io 阻塞時間增加。主執行緒 io 阻塞意味著 redis 處於不可用狀態。目前超過 12G,單 fork 程式堵塞時間超過 3s 以上。
單分片資料量大,會增加讀資料時,hash 的時延。
單分片負責的資料量越大,單程式負擔的 io 壓力也越大
機器上 redis 業務是混布的,單分片資料量過大,會導致將機器記憶體使用率推向預警值,從而影響整個機器其他 redis 業務的使用。
減小運維成本,單分片資料量小,跑 kv 分離,資料分析的效率會快很多。
複製程式碼
7.2 執行時約束
7.2.1 多 key 命令注意事項
對於 mset、mget、del 的多 key 操作,建議每次批量操作小於 500 個 key
mset、mget、del 的多 key 操作,對於 proxy 會有額外的 cpu 消耗。
這三種特殊的操作,在後端做多分片時,proxy 需要將每個操作中的一批 key 按照後端分配規則,重組成 n 批 key 的組合,n 等於分片數量,然後分別將重組後的 n 個多 key 操作分片發給後端每一個分片;
回覆訊息時,也需要等待所有請求從後端回覆回來,在 proxy 層進行結果 merge,再返回給上層。因此這種操作在 key 數量上升時,對 proxy 的 cpu 會造成額外的壓力,
因此強烈建議控制批量操作的 key 數量,以及減少 mset、mget、del 等多 key 操作。
複製程式碼
7.2.2 pipline 命令注意事項
每個 pipeline 批次下 key 數量限制在 500 以內
一個 pipline 型別求情內容過多時,一次性打到 redis-proxy 時,會導致 proxy 申請記憶體數量暴漲,導致擠佔同一物理機上混布的其他服務的資源,嚴重時會導致伺服器重啟。
因此 pipline 型別請求需要嚴格限制單批次內的請求量。
複製程式碼
7.2.3 不支援的命令
由於分片原理完全是依據 key 進行路由,因此引入分片後,有少量原生命令在 twemproxy 下無法支援:
不帶 key 的命令,如 info、dbsize、keys、multi 等
阻塞連線的命令不支援,如 blpop 等
涉及多 key 聯合操作命令不支援,如 zunionstor 等
script 指令碼
複製程式碼
7.3 風險點
7.3.1 無協議保障的 io 流程
提交指資料庫承諾一條資料修改請求生效的時機
常規系統中,提交之前有各種協議流程,能夠保障提交後的一致性、永續性效果
常規的保障協議包括:事務日誌、持久化後提交、多副本(甚至全量副本)生效後再對使用者端進行承諾的二階段提交等
redis 為了追求效能,並未使用上述任何協議
redis 的提交僅僅是主節點記憶體提交,既不等待持久化,也不等待從節點,由此帶來的風險包括:
- 1,記憶體提交後主節點立刻 down 掉,可能導致提交的資料被丟失
- 2,提交後立刻讀從節點,可能讀取到提交前的舊資料
7.3.2 單執行緒帶來的風險
redis 使用單執行緒無鎖設計,這樣做的好處是能夠最大限度的發揮單執行緒效能,避免爭鎖帶來的額外開銷
缺陷是當一個請求操作比較重,耗時較長時,會將當前 redis 例項負責的所有請求都阻塞一小段時間,主執行緒阻塞甚至會影響主從探活
因此線上命令需要嚴格控制,避免發生長時間執行緒阻塞的慢請求