# 慢查詢日志
[TOC=2,3]
慢查詢日志是 Redis 提供的一個用于觀察系統性能的功能,這個功能的實現非常簡單,這里我們也簡單地講解一下。
本章先介紹和慢查詢功能相關的數據結構和變量,然后介紹 Redis 是如何記錄命令的執行時間,以及如何為執行超過限制事件的命令記錄慢查詢日志的。
### 相關數據結構
每條慢查詢日志都以一個 `slowlog.h/slowlogEntry` 結構定義:
~~~
typedef struct slowlogEntry {
// 命令參數
robj **argv;
// 命令參數數量
int argc;
// 唯一標識符
long long id; /* Unique entry identifier. */
// 執行命令消耗的時間,以納秒(1 / 1,000,000,000 秒)為單位
long long duration; /* Time spent by the query, in nanoseconds. */
// 命令執行時的時間
time_t time; /* Unix time at which the query was executed. */
} slowlogEntry;
~~~
記錄服務器狀態的 `redis.h/redisServer` 結構里保存了幾個和慢查詢有關的屬性:
~~~
struct redisServer {
// ... other fields
// 保存慢查詢日志的鏈表
list *slowlog; /* SLOWLOG list of commands */
// 慢查詢日志的當前 id 值
long long slowlog_entry_id; /* SLOWLOG current entry ID */
// 慢查詢時間限制
long long slowlog_log_slower_than; /* SLOWLOG time limit (to get logged) */
// 慢查詢日志的最大條目數量
unsigned long slowlog_max_len; /* SLOWLOG max number of items logged */
// ... other fields
};
~~~
`slowlog` 屬性是一個鏈表,鏈表里的每個節點保存了一個慢查詢日志結構,所有日志按添加時間從新到舊排序,新的日志在鏈表的左端,舊的日志在鏈表的右端。
`slowlog_entry_id` 在創建每條新的慢查詢日志時增一,用于產生慢查詢日志的 ID (這個 ID 在執行 `SLOWLOG RESET` 之后會被重置)。
`slowlog_log_slower_than` 是用戶指定的命令執行時間上限,執行時間大于等于這個值的命令會被慢查詢日志記錄。
`slowlog_max_len` 慢查詢日志的最大數量,當日志數量等于這個值時,添加一條新日志會造成最舊的一條日志被刪除。
下圖展示了一個 `slowlog` 屬性的實例:
![digraph g { rankdir = LR; node [shape = record, style = filled]; edge [style = bold]; // nodes redisServer [label = "redisServer | ... |<slowlog> slowlog | ... ", fillcolor = "#A8E270"]; slowlog_1 [label = "<head>slowlogEntry | argv | argc | id | duration | time", fillcolor = "#95BBE3"]; slowlog_2 [label = "<head>slowlogEntry | argv | argc | id | duration | time", fillcolor = "#95BBE3"]; slowlog_3 [label = "<head>slowlogEntry | argv | argc | id | duration | time", fillcolor = "#95BBE3"]; // edges redisServer:slowlog -> slowlog_1; slowlog_1 -> slowlog_2; slowlog_2 -> slowlog_3;}](https://box.kancloud.cn/2015-09-13_55f4effdcfdd6.svg)
### 慢查詢日志的記錄
在每次執行命令之前,Redis 都會用一個參數記錄命令執行前的時間,在命令執行完之后,再計算一次當前時間,然后將兩個時間值相減,得出執行命令所耗費的時間值 `duration` ,并將 `duration` 傳給 `slowlogPushEntryIfNeed` 函數。
如果 `duration` 超過服務器設置的執行時間上限 `server.slowlog_log_slower_than` 的話,`slowlogPushEntryIfNeed` 就會創建一條新的慢查詢日志,并將它加入到慢查詢日志鏈表里。
可以用一段偽代碼來表示這個過程:
~~~
def execute_redis_command_with_slowlog():
# 命令執行前的時間
start = ustime()
# 執行命令
execute_command(argv, argc)
# 計算命令執行所耗費的時間
duration = ustime() - start
if slowlog_is_enabled:
slowlogPushEntryIfNeed(argv, argc, duration)
def slowlogPushEntryIfNeed(argv, argc, duration)
# 如果執行命令耗費的時間超過服務器設置命令執行時間上限
# 那么創建一條新的 slowlog
if duration > server.slowlog_log_slower_than:
# 創建新 slowlog
log = new slowlogEntry()
# 設置各個域
log.argv = argv
log.argc = argc
log.duration = duration
log.id = server.slowlog_entry_id
log.time = now()
# 將新 slowlog 追加到日志鏈表末尾
server.slowlog.append(log)
# 更新服務器 slowlog
server.slowlog_entry_id += 1
~~~
### 慢查詢日志的操作
針對慢查詢日志有三種操作,分別是查看、清空和獲取日志數量:
- 查看日志:在日志鏈表中遍歷指定數量的日志節點,復雜度為 \(O(N)\) 。
- 清空日志:釋放日志鏈表中的所有日志節點,復雜度為 \(O(N)\) 。
- 獲取日志數量:獲取日志的數量等同于獲取 `server.slowlog` 鏈表的數量,復雜度為 \(O(1)\) 。
### 小結
- Redis 用一個鏈表以 FIFO 的順序保存著所有慢查詢日志。
- 每條慢查詢日志以一個慢查詢節點表示,節點中記錄著執行超時的命令、命令的參數、命令執行時的時間,以及執行命令所消耗的時間等信息。