節選自《redis開發與運維》
先來看一段client list的執行結果
127.0.0.1:6379> client list
id=254487 addr=10.2.xx.234:60240 fd=1311 name= age=8888581 idle=8888581 flags=N
db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
id=300210 addr=10.2.xx.215:61972 fd=3342 name= age=8054103 idle=8054103 flags=N
db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
輸出結果的每一行代表一個客戶端的信息,可以看到每行包含了十幾個屬性,它們是每個客戶端的一些執行狀態,理解這些屬性對于Redis的開發和運維人員非常有幫助。下面將選擇幾個重要的屬性進行說明,其余通過表格的形式進行展示。
(1)標識:id、addr、fd、name
這四個屬性屬于客戶端的標識:
·id:客戶端連接的唯一標識,這個id是隨著Redis的連接自增的,重啟Redis后會重置為0。
·addr:客戶端連接的ip和端口。
·fd:socket的文件描述符,與lsof命令結果中的fd是同一個,如果fd=-1代表當前客戶端不是外部客戶端,而是Redis內部的偽裝客戶端。
·name:客戶端的名字,后面的client setName和client getName兩個命令會對其進行說明。
(2)輸入緩沖區:qbuf、qbuf-free
Redis為每個客戶端分配了輸入緩沖區,它的作用是將客戶端發送的命令臨時保存,同時Redis從會輸入緩沖區拉取命令并執行,輸入緩沖區為客戶端發送命令到Redis執行命令提供了緩沖功能,如圖4-5所示。
client list中qbuf和qbuf-free分別代表這個緩沖區的總容量和剩余容量,Redis沒有提供相應的配置來規定每個緩沖區的大小,輸入緩沖區會根據輸入內容大小的不同動態調整,只是要求每個客戶端緩沖區的大小不能超過1G,超過后客戶端將被關閉。下面是Redis源碼中對于輸入緩沖區的硬編碼:
[圖片上傳失敗...(image-e1b72f-1571740556050)]
/* Protocol and I/O related defines */
#define REDIS_MAX_QUERYBUF_LEN (102410241024) /* 1GB max query buffer. */
輸入緩沖使用不當會產生兩個問題:
·一旦某個客戶端的輸入緩沖區超過1G,客戶端將會被關閉。
·輸入緩沖區不受maxmemory控制,假設一個Redis實例設置了maxmemory為4G,已經存儲了2G數據,但是如果此時輸入緩沖區使用了3G,已經超過maxmemory限制,可能會產生數據丟失、鍵值淘汰、OOM等情況(如圖4-6所示)。
[圖片上傳失敗...(image-79c38d-1571740556050)]
執行效果如下:
127.0.0.1:6390> info memory
# Memory
used_memory_human:5.00G
...
maxmemory_human:4.00G
....
上面已經看到,輸入緩沖區使用不當造成的危害非常大,那么造成輸入緩沖區過大的原因有哪些?輸入緩沖區過大主要是因為Redis的處理速度跟不上輸入緩沖區的輸入速度,并且每次進入輸入緩沖區的命令包含了大量bigkey,從而造成了輸入緩沖區過大的情況。還有一種情況就是Redis發生了阻塞,短期內不能處理命令,造成客戶端輸入的命令積壓在了輸入緩沖區,造成了輸入緩沖區過大。
那么如何快速發現和監控呢?監控輸入緩沖區異常的方法有兩種:
·通過定期執行client list命令,收集qbuf和qbuf-free找到異常的連接記錄并分析,最終找到可能出問題的客戶端。
·通過info命令的info clients模塊,找到最大的輸入緩沖區,例如下面命令中的其中client_biggest_input_buf代表最大的輸入緩沖區,例如可以設置超過10M就進行報警:
127.0.0.1:6379> info clients
# Clients
connected_clients:1414
client_longest_output_list:0
client_biggest_input_buf:2097152
blocked_clients:0
這兩種方法各有自己的優劣勢,表4-3對兩種方法進行了對比。
表4-3 對比client list和info clients監控輸入緩沖區的優劣勢
[圖片上傳失敗...(image-dcc3de-1571740556050)]
輸入緩沖區問題出現概率比較低,但是也要做好防范,在開發中要減少bigkey、減少Redis阻塞、合理的監控報警。
(3)輸出緩沖區:obl、oll、omem
Redis為每個客戶端分配了輸出緩沖區,它的作用是保存命令執行的結果返回給客戶端,為Redis和客戶端交互返回結果提供緩沖,如圖4-7所示。
與輸入緩沖區不同的是,輸出緩沖區的容量可以通過參數client-output-buffer-limit來進行設置,并且輸出緩沖區做得更加細致,按照客戶端的不同分為三種:普通客戶端、發布訂閱客戶端、slave客戶端,如圖4-8所示。
[圖片上傳失敗...(image-5b93ed-1571740556050)]
[圖片上傳失敗...(image-320e63-1571740556050)]
應的配置規則是:
client-output-buffer-limit
·<class>:客戶端類型,分為三種。a)normal:普通客戶端;b)slave:slave客戶端,用于復制;c)pubsub:發布訂閱客戶端。
·<hard limit>:如果客戶端使用的輸出緩沖區大于<hard limit>,客戶端會被立即關閉。
·<soft limit>和<soft seconds>:如果客戶端使用的輸出緩沖區超過了<soft limit>并且持續了<soft limit>秒,客戶端會被立即關閉。
Redis的默認配置是:
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
和輸入緩沖區相同的是,輸出緩沖區也不會受到maxmemory的限制,如果使用不當同樣會造成maxmemory用滿產生的數據丟失、鍵值淘汰、OOM等情況。
實際上輸出緩沖區由兩部分組成:固定緩沖區(16KB)和動態緩沖區,其中固定緩沖區返回比較小的執行結果,而動態緩沖區返回比較大的結果,例如大的字符串、hgetall、smembers命令的結果等,通過Redis源碼中redis.h的redisClient結構體(Redis3.2版本變為Client)可以看到兩個緩沖區的實現細節:
typedef struct redisClient {
// 動態緩沖區列表 list *reply;
// 動態緩沖區列表的長度(對象個數)
unsigned long reply_bytes;
// 固定緩沖區已經使用的字節數 int bufpos;
// 字節數組作為固定緩沖區 char buf[REDIS_REPLY_CHUNK_BYTES];
} redisClient;
固定緩沖區使用的是字節數組,動態緩沖區使用的是列表。當固定緩沖區存滿后會將Redis新的返回結果存放在動態緩沖區的隊列中,隊列中的每個對象就是每個返回結果,如圖4-9所示。
[圖片上傳失敗...(image-24b35c-1571740556050)]
client list中的obl代表固定緩沖區的長度,oll代表動態緩沖區列表的長度,omem代表使用的字節數。例如下面代表當前客戶端的固定緩沖區的長度為0,動態緩沖區有4869個對象,兩個部分共使用了133081288字節=126M內存:
id=7 addr=127.0.0.1:56358 fd=6 name= age=91 idle=0 flags=O db=0 sub=0 psub=0 multi=-1
qbuf=0 qbuf-free=0 obl=0 oll=4869 omem=133081288 events=rw cmd=monitor
監控輸出緩沖區的方法依然有兩種:
·通過定期執行client list命令,收集obl、oll、omem找到異常的連接記錄并分析,最終找到可能出問題的客戶端。
·通過info命令的info clients模塊,找到輸出緩沖區列表最大對象數,例如:
127.0.0.1:6379> info clients
# Clients
connected_clients:502
client_longest_output_list:4869
client_biggest_input_buf:0
blocked_clients:0
其中,client_longest_output_list代表輸出緩沖區列表最大對象數,這兩種統計方法的優劣勢和輸入緩沖區是一樣的,這里就不再贅述了。相比于輸入緩沖區,輸出緩沖區出現異常的概率相對會比較大,那么如何預防呢?方法如下:
·進行上述監控,設置閥值,超過閥值及時處理。
·限制普通客戶端輸出緩沖區的,把錯誤扼殺在搖籃中,例如可以進行如下設置:
client-output-buffer-limit normal 20mb 10mb 120
·適當增大slave的輸出緩沖區的,如果master節點寫入較大,slave客戶端的輸出緩沖區可能會比較大,一旦slave客戶端連接因為輸出緩沖區溢出被kill,會造成復制重連。
·限制容易讓輸出緩沖區增大的命令,例如,高并發下的monitor命令就是一個危險的命令。
及時監控內存,一旦發現內存抖動頻繁,可能就是輸出緩沖區過大。
(4)客戶端的存活狀態
client list中的age和idle分別代表當前客戶端已經連接的時間和最近一次的空閑時間:
id=2232080 addr=10.16.xx.55:32886 fd=946 name= age=603382 idle=331060 flags=N db=0
sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
例如上面這條記錄代表當期客戶端連接Redis的時間為603382秒,其中空閑了331060秒:
id=254487 addr=10.2.xx.234:60240 fd=1311 name= age=8888581 idle=8888581 flags=N db=0
sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
例如上面這條記錄代表當期客戶端連接Redis的時間為8888581秒,其中空閑了8888581秒,實際上這種就屬于不太正常的情況,當age等于idle時,說明連接一直處于空閑狀態。
為了更加直觀地描述age和idle,下面用一個例子進行說明:
String key = "hello";
// 1) 生成jedis,并執行get操作 Jedis jedis = new Jedis("127.0.0.1", 6379);
System.out.println(jedis.get(key));
// 2) 休息10秒 TimeUnit.SECONDS.sleep(10);
// 3) 執行新的操作ping
System.out.println(jedis.ping());
// 4) 休息5秒 TimeUnit.SECONDS.sleep(5);
// 5) 關閉jedis連接 jedis.close();
下面對代碼中的每一步進行分析,用client list命令來觀察age和idle參數的相應變化。
為了與redis-cli的客戶端區分,本次測試客戶端IP地址:10.7.40.98。
1)在執行代碼之前,client list只有一個客戶端,也就是當前的redis-cli,下面為了節省篇幅忽略掉這個客戶端。
127.0.0.1:6379> client list
id=45 addr=127.0.0.1:55171 fd=6 name= age=2 idle=0 flags=N db=0 sub=0 psub=0
multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
2)使用Jedis生成了一個新的連接,并執行get操作,可以看到IP地址為10.7.40.98的客戶端,最后執行的命令是get,age和idle分別是1秒和0秒:
127.0.0.1:6379> client list
id=46 addr=10.7.40.98:62908 fd=7 name= age=1 idle=0 flags=N db=0 sub=0 psub=0
multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
3)休息10秒,此時Jedis客戶端并沒有關閉,所以age和idle一直在遞增:
27.0.0.1:6379> client list
id=46 addr=10.7.40.98:62908 fd=7 name= age=9 idle=9 flags=N db=0 sub=0 psub=0
multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
4)執行新的操作ping,發現執行后age依然在增加,而idle從0計算,也就是不再閑置:
127.0.0.1:6379> client list
id=46 addr=10.7.40.98:62908 fd=7 name= age=11 idle=0 flags=N db=0 sub=0 psub=0
multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ping
5)休息5秒,觀察age和idle增加:
127.0.0.1:6379> client list
id=46 addr=10.7.40.98:62908 fd=7 name= age=15 idle=5 flags=N db=0 sub=0 psub=0
multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ping
6)關閉Jedis,Jedis連接已經消失:
redis-cli client list | grep "10.7.40.98”為空
(5)客戶端的限制maxclients和timeout
Redis提供了maxclients參數來限制最大客戶端連接數,一旦連接數超過maxclients,新的連接將被拒絕。maxclients默認值是10000,可以通過info clients來查詢當前Redis的連接數:
127.0.0.1:6379> info clients
Clients
connected_clients:1414
...
可以通過config set maxclients對最大客戶端連接數進行動態設置:
127.0.0.1:6379> config get maxclients
"maxclients"
"10000"
127.0.0.1:6379> config set maxclients 50
OK
127.0.0.1:6379> config get maxclients
"maxclients"
"50"
一般來說maxclients=10000在大部分場景下已經絕對夠用,但是某些情況由于業務方使用不當(例如沒有主動關閉連接)可能存在大量idle連接,無論是從網絡連接的成本還是超過maxclients的后果來說都不是什么好事,因此Redis提供了timeout(單位為秒)參數來限制連接的最大空閑時間,一旦客戶端連接的idle時間超過了timeout,連接將會被關閉,例如設置timeout為30秒:
Redis默認的timeout是0,也就是不會檢測客戶端的空閑 127.0.0.1:6379> config set timeout 30
OK
下面繼續使用Jedis進行模擬,整個代碼和上面是一樣的,只不過第2)步驟休息了31秒:
String key = "hello";
// 1) 生成jedis,并執行get操作 Jedis jedis = new Jedis("127.0.0.1", 6379);
System.out.println(jedis.get(key));
// 2) 休息31秒 TimeUnit.SECONDS.sleep(31);
// 3) 執行get操作 System.out.println(jedis.get(key));
// 4) 休息5秒 TimeUnit.SECONDS.sleep(5);
// 5) 關閉jedis連接 jedis.close();
執行上述代碼可以發現在執行完第2)步之后,client list中已經沒有了Jedis的連接,也就是說timeout已經生效,將超過30秒空閑的連接關閉掉:
127.0.0.1:6379> client list
id=16 addr=10.7.40.98:63892 fd=6 name= age=19 idle=19 flags=N db=0 sub=0 psub=0
multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
超過timeout后,Jedis連接被關閉 redis-cli client list | grep “10.7.40.98”為空
同時可以看到,在Jedis代碼中的第3)步拋出了異常,因為此時客戶端已經被關閉,所以拋出的異常是JedisConnectionException,并且提示Unexpected end of stream:
stream: world
Exception in thread "main" redis.clients.jedis.exceptions.JedisConnectionException:
Unexpected end of stream.
如果將Redis的loglevel設置成debug級別,可以看到如下日志,也就是客戶端被Redis關閉的日志:
12885:M 26 Aug 08:46:40.085 - Closing idle client
Redis源碼中redis.c文件中clientsCronHandleTimeout函數就是針對timeout參數進行檢驗的,只不過在源碼中timeout被賦值給了server.maxidletime:
int clientsCronHandleTimeout(redisClient *c) {
// 當前時間 time_t now = server.unixtime;
// server.maxidletime就是參數timeout
if (server.maxidletime &&
// 很多客戶端驗證,這里就不占用篇幅,最重要的驗證是下面空閑時間超過了maxidletime就會 // 被關閉掉客戶端 (now - c->lastinteraction > server.maxidletime))
{
redisLog(REDIS_VERBOSE,"Closing idle client");
// 關閉客戶端 freeClient(c);
}
}
Redis的默認配置給出的timeout=0,在這種情況下客戶端基本不會出現上面的異常,這是基于對客戶端開發的一種保護。例如很多開發人員在使用JedisPool時不會對連接池對象做空閑檢測和驗證,如果設置了timeout>0,可能就會出現上面的異常,對應用業務造成一定影響,但是如果Redis的客戶端使用不當或者客戶端本身的一些問題,造成沒有及時釋放客戶端連接,可能會造成大量的idle連接占據著很多連接資源,一旦超過maxclients;后果也是不堪設想。所在在實際開發和運維中,需要將timeout設置成大于0,例如可以設置為300秒,同時在客戶端使用上添加空閑檢測和驗證等等措施,例如JedisPool使用common-pool提供的三個屬性:minEvictableIdleTimeMillis、testWhileIdle、timeBetweenEvictionRunsMillis,4.2節已經進行了說明,這里就不再贅述。
(6)客戶端類型
client list中的flag是用于標識當前客戶端的類型,例如flag=S代表當前客戶端是slave客戶端、flag=N代表當前是普通客戶端,flag=O代表當前客戶端正在執行monitor命令,表4-4列出了11種客戶端類型。
[圖片上傳失敗...(image-d18643-1571740556050)]
(7)其他
上面已經將client list中重要的屬性進行了說明,表4-5列出之前介紹過以及一些比較簡單或者不太重要的屬性。
表4-5 client list命令結果的全部屬性
[圖片上傳失敗...(image-bcff52-1571740556050)]
[圖片上傳失敗...(image-4a93ae-1571740556050)]
2.client setName和client getName
client setName xx
client getName
client setName用于給客戶端設置名字,這樣比較容易標識出客戶端的來源,例如將當前客戶端命名為test_client,可以執行如下操作:
127.0.0.1:6379> client setName test_client
OK
此時再執行client list命令,就可以看到當前客戶端的name屬性為test_client:
127.0.0.1:6379> client list
id=55 addr=127.0.0.1:55604 fd=7 name=test_client age=23 idle=0 flags=N db=0 sub=0
psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
如果想直接查看當前客戶端的name,可以使用client getName命令,例如下面的操作:
127.0.0.1:6379> client getName
"test_client"
client getName和setName命令可以做為標識客戶端來源的一種方式,但是通常來講,在Redis只有一個應用方使用的情況下,IP和端口作為標識會更加清晰。當多個應用方共同使用一個Redis,那么此時client setName可以作為標識客戶端的一個依據。
3.client kill
client kill ip:port
此命令用于殺掉指定IP地址和端口的客戶端,例如當前客戶端列表為:
127.0.0.1:6379> client list
id=49 addr=127.0.0.1:55593 fd=6 name= age=9 idle=0 flags=N db=0 sub=0 psub=0
multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
id=50 addr=127.0.0.1:52343 fd=7 name= age=4 idle=4 flags=N db=0 sub=0 psub=0
multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
如果想殺掉127.0.0.1:52343的客戶端,可以執行:
127.0.0.1:6379> client kill 127.0.0.1:52343
OK
執行命令后,client list結果只剩下了127.0.0.1:55593這個客戶端:
127.0.0.1:6379> client list
id=49 addr=127.0.0.1:55593 fd=6 name= age=9 idle=0 flags=N db=0 sub=0 psub=0
multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
由于一些原因(例如設置timeout=0時產生的長時間idle的客戶端),需要手動殺掉客戶端連接時,可以使用client kill命令。
4.client pause
client pause timeout(毫秒)
如圖4-10所示,client pause命令用于阻塞客戶端timeout毫秒數,在此期間客戶端連接將被阻塞。
[圖片上傳失敗...(image-8e7472-1571740556050)]
例如在一個客戶端執行:
127.0.0.1:6379> client pause 10000
OK
在另一個客戶端執行ping命令,發現整個ping命令執行了9.72秒(手動執行redis-cli,只為了演示,不代表真實執行時間):
127.0.0.1:6379> ping
PONG
(9.72s)
該命令可以在如下場景起到作用:
·client pause只對普通和發布訂閱客戶端有效,對于主從復制(從節點內部偽裝了一個客戶端)是無效的,也就是此期間主從復制是正常進行的,所以此命令可以用來讓主從復制保持一致。
·client pause可以用一種可控的方式將客戶端連接從一個Redis節點切換到另一個Redis節點。
需要注意的是在生產環境中,暫停客戶端成本非常高。
5.monitor
monitor命令用于監控Redis正在執行的命令,如圖4-11所示,我們打開了兩個redis-cli,一個執行set get ping命令,另一個執行monitor命令。可以看到monitor命令能夠監聽其他客戶端正在執行的命令,并記錄了詳細的時間戳。
[圖片上傳失敗...(image-c63b7-1571740556050)]
monitor的作用很明顯,如果開發和運維人員想監聽Redis正在執行的命令,就可以用monitor命令,但事實并非如此美好,每個客戶端都有自己的輸出緩沖區,既然monitor能監聽到所有的命令,一旦Redis的并發量過大,monitor客戶端的輸出緩沖會暴漲,可能瞬間會占用大量內存,圖4-12展示了monitor命令造成大量內存使用。
[圖片上傳失敗...(image-c88e14-1571740556050)]