1、概述
redis6.0 新增了 多線程機制。一直存在兩個疑問:
1)、 爲什麼要加入多線程
2)、多線程機制,爲什麼不設計成跟memcache 一樣
2、解析
首先第一個問題,redis 區別於memcache 的一點是,redis是單線程。但是性能也很好,主要得益於他的 IO多路複用,還有單線程省了多線程切換上下文的開銷。線程上下文切換,可能會造成CPU CACHE MISS, CPU的一級緩存和三級緩存的 速度不是一個量級的。這次redis6.0 開啓了IO多線程的機制,主要是考慮,redis的讀寫網絡的read/write系統調用,在redis執行期間佔用了大量的CPU時間。如果把網絡IO做成多線程會提升很大性能。另一方面,如果是比較耗時的操作(比如lrange,del 大key),避免阻塞主線程太多時間,開線線程異步處理 不失爲一個好的選擇。
第二個問題,redis選擇多線程模型的時候,爲什麼不選擇跟memcache一樣的?redis的作者Antirez 曾經說過:
Redis 支持多線程有 2 種可行的方式:
第一種就是像“Memcached”那樣,一個 Redis 實例開啓多個線程,從而提升GET/SET等簡單命令中每秒可以執行的操作。這涉及到 I/O、命令解析等多線程處理,因此,我們將其稱之爲“I/O threading”。
另一種就是允許在不同的線程中執行較耗時較慢的命令,以確保其它客戶端不被阻塞,我們將這種線程模型稱爲“Slow commands threading”。
經過深思熟慮,Redis 不會採用“I/O threading”,Redis 在運行時主要受制於網絡和內存,所以提升 Redis 性能主要是通過在多個 Redis 實例,特別是 Redis 集羣。
接下來我們主要會考慮改進兩個方面:
Redis 集羣的多個實例通過編排能夠合理地使用本地實例的磁盤,避免同時重寫 AOF。
提供一個 Redis 集羣代理,便於用戶在沒有較好的集羣協議客戶端時抽象出一個集羣。
補充說明一下,Redis 和 Memcached 一樣是一個內存系統,但不同於 Memcached。
多線程是複雜的,必須考慮使用簡單的數據模型,執行 LPUSH 的線程需要服務其他執行 LPOP 的線程。
我真正期望的實際是“slow operations threading”,在 Redis 6 或 Redis 7 中,將提供“key-level locking”,使得線程可以完全獲得對鍵的控制以處理緩慢的操作
簡單的說就是,redis目前性能還是受限於網絡和內存。所以採用多線程來處理網絡IO讀寫和解析,執行命令依然是交給主線程來做。多線程的併發控制又是複雜的(比如事務,LPUSH/LPOP的併發控制),爲了保持redis簡單的設計原則,採用IO多線程是比較合適的。
3、 源碼剖析
3.1、關鍵變量
io_threads_active:是否開始 IO 多線程
io_threads_op :操作類型 讀寫IO_THREADS_OP_WRITE or IO_THREADS_OP_READ
io_threads_list[IO_THREADS_MAX_NUM] :每個線程的 等待客戶端列表
io_threads_pending:每個線程的 等待客戶端個數
3.1、初始化線程,註冊回調
第25行,調用pthread_create 庫函數創建線程,並且註冊線程回調函數IOThreadMain。線程TID 綁定線程ID io_threads[i] = tid
void initThreadedIO(void) {
io_threads_active = 0; /* We start with threads not active. */
/* Don't spawn any thread if the user selected a single thread:
* we'll handle I/O directly from the main thread. */
if (server.io_threads_num == 1) return;
if (server.io_threads_num > IO_THREADS_MAX_NUM) {
serverLog(LL_WARNING,"Fatal: too many I/O threads configured. "
"The maximum number is %d.", IO_THREADS_MAX_NUM);
exit(1);
}
/* Spawn and initialize the I/O threads. */
for (int i = 0; i < server.io_threads_num; i++) {
/* Things we do for all the threads including the main thread. */
io_threads_list[i] = listCreate();
if (i == 0) continue; /* Thread 0 is the main thread. */
/* Things we do only for the additional threads. */
pthread_t tid;
pthread_mutex_init(&io_threads_mutex[i],NULL);
io_threads_pending[i] = 0;
pthread_mutex_lock(&io_threads_mutex[i]); /* Thread will be stopped. */
if (pthread_create(&tid,NULL,IOThreadMain,(void*)(long)i) != 0) {
serverLog(LL_WARNING,"Fatal: Can't initialize IO thread.");
exit(1);
}
io_threads[i] = tid;
}
}
3.2、回調函數
第12 行其實是一個循環等待的實現,這裏不用sleep,是爲了避免設置sleep 的時間不合適造成性能的損失, 但是循環等待 也會 佔用CPU,也是一個開銷。
第17 行其實是允許主線程來關閉其他線程的操作,只要將某個線程i的io_threads_pending[id] 設置爲0
第32行是將當前線程等待隊列裏所有的請求client,依次取出處理, 讀操作通過 readQueryFromClient 處理, 寫操作通過writeToClient 處理。
void *IOThreadMain(void *myid) {
/* The ID is the thread number (from 0 to server.iothreads_num-1), and is
* used by the thread to just manipulate a single sub-array of clients. */
long id = (unsigned long)myid;
char thdname[16];
snprintf(thdname, sizeof(thdname), "io_thd_%ld", id);
redis_set_thread_title(thdname);
while(1) {
/* Wait for start */
for (int j = 0; j < 1000000; j++) {
if (io_threads_pending[id] != 0) break;
}
/* Give the main thread a chance to stop this thread. */
if (io_threads_pending[id] == 0) {
pthread_mutex_lock(&io_threads_mutex[id]);
pthread_mutex_unlock(&io_threads_mutex[id]);
continue;
}
serverAssert(io_threads_pending[id] != 0);
if (tio_debug) printf("[%ld] %d to handle\n", id, (int)listLength(io_threads_list[id]));
/* Process: note that the main thread will never touch our list
* before we drop the pending count to 0. */
listIter li;
listNode *ln;
listRewind(io_threads_list[id],&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
if (io_threads_op == IO_THREADS_OP_WRITE) {
writeToClient(c,0);
} else if (io_threads_op == IO_THREADS_OP_READ) {
readQueryFromClient(c->conn);
} else {
serverPanic("io_threads_op value is unknown");
}
}
listEmpty(io_threads_list[id]);
io_threads_pending[id] = 0;
if (tio_debug) printf("[%ld] Done\n", id);
}
}
3.3、分配待處理任務
下面函數的主要功能是將IO請求,分配給不同的IO線程去處理。
第24行可以看出,分配策略是RR,int target_id = item_id % server.io_threads_num; 是用id做的hash取模。
第25行是選出IO線程後,將請求client,加入到他的隊列尾部。
第49行是更新標識,記錄各個線程隊列的長度,來通知回調函數執行。
第50行,是等所有線程都執行完成後,再回歸主線程。
第56行,是主線程執行接下來的命令操作。
int handleClientsWithPendingWritesUsingThreads(void) {
int processed = listLength(server.clients_pending_write);
if (processed == 0) return 0; /* Return ASAP if there are no clients. */
/* If I/O threads are disabled or we have few clients to serve, don't
* use I/O threads, but thejboring synchronous code. */
if (server.io_threads_num == 1 || stopThreadedIOIfNeeded()) {
return handleClientsWithPendingWrites();
}
/* Start threads if needed. */
if (!io_threads_active) startThreadedIO();
if (tio_debug) printf("%d TOTAL WRITE pending clients\n", processed);
/* Distribute the clients across N different lists. */
listIter li;
listNode *ln;
listRewind(server.clients_pending_write,&li);
int item_id = 0;
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
c->flags &= ~CLIENT_PENDING_WRITE;
int target_id = item_id % server.io_threads_num;
listAddNodeTail(io_threads_list[target_id],c);
item_id++;
}
/* Give the start condition to the waiting threads, by setting the
* start condition atomic var. */
io_threads_op = IO_THREADS_OP_WRITE;
for (int j = 1; j < server.io_threads_num; j++) {
int count = listLength(io_threads_list[j]);
io_threads_pending[j] = count;
}
/* Also use the main thread to process a slice of clients. */
listRewind(io_threads_list[0],&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
writeToClient(c,0);
}
listEmpty(io_threads_list[0]);
/* Wait for all the other threads to end their work. */
while(1) {
unsigned long pending = 0;
for (int j = 1; j < server.io_threads_num; j++)
pending += io_threads_pending[j];
if (pending == 0) break;
}
if (tio_debug) printf("I/O WRITE All threads finshed\n");
/* Run the list of clients again to install the write handler where
* needed. */
listRewind(server.clients_pending_write,&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
/* Install the write handler if there are pending writes in some
* of the clients. */
if (clientHasPendingReplies(c) &&
connSetWriteHandler(c->conn, sendReplyToClient) == AE_ERR)
{
freeClientAsync(c);
}
}
listEmpty(server.clients_pending_write);
return processed;
}