Redis 是單線程模型?
一、背景
二、Redis6.0多線程IO概述
1. 參數(shù)與配置
2. 執(zhí)行流程概述
三、源碼分析
1. 初始化
2. 讀數(shù)據(jù)流程
3. 寫數(shù)據(jù)流程
4. 多線程IO動(dòng)態(tài)暫停與開啟
四、性能對(duì)比
1. 測(cè)試環(huán)境
2. Redis版本
3. 壓測(cè)命令
4. 統(tǒng)計(jì)結(jié)果
5. 結(jié)論
五、6.0多線程IO不足
六、總結(jié)
一、背景
使用過Redis的同學(xué)肯定都了解過一個(gè)說法,說Redis是單線程模型,那么實(shí)際情況是怎樣的呢?
其實(shí),我們常說Redis是單線程模型,是指Redis采用單線程的事件驅(qū)動(dòng)模型,只有并且只會(huì)在一個(gè)主線程中執(zhí)行Redis命令操作,這意味著它在處理請(qǐng)求時(shí)不使用復(fù)雜的上下文切換或鎖機(jī)制。盡管只是單線程的架構(gòu),但Redis通過非阻塞的I/O操作和高效的事件循環(huán)來處理大量的并發(fā)連接,性能仍然非常高。
然而在Redis4.0開始也引入了一些后臺(tái)線程執(zhí)行異步淘汰、異步刪除過期key、異步執(zhí)行大key刪除等任務(wù),然后,在Redis6.0中引入了多線程IO特性,將Redis單節(jié)點(diǎn)訪問請(qǐng)求從10W提升到20W。
而在去年Valkey社區(qū)發(fā)布的Valkey8.0版本,在I/O線程系統(tǒng)上進(jìn)行了重大升級(jí),特別是異步I/O線程的引入,使主線程和I/O線程能夠并行工作,可實(shí)現(xiàn)最大化服務(wù)吞吐量并減少瓶頸,使得Valkey單節(jié)點(diǎn)訪問請(qǐng)求可以提升到100W。
那么在Redis6.0和Valkey8.0中多線程IO是怎么回事呢?是否改變了Redis原有單線程模型?
- 2024年,Redis商業(yè)支持公司Redis Labs宣布Redis核心代碼的許可證從BSD變更為RSALv2,明確禁止云廠商提供Redis托管服務(wù),這一決定直接導(dǎo)致社區(qū)分裂。
- 為維護(hù)開源自由,Linux基金會(huì)聯(lián)合多家科技公司(包括AWS、Google、Cloud、Oracle等)宣布支持Valkey,作為Redis的替代分支。
- Valkey8.0系Valkey社區(qū)發(fā)布的首個(gè)主要大版本。
- 最新消息,在Redis項(xiàng)目創(chuàng)始人antirez今年加入Redis商業(yè)公司5個(gè)月后,Redis宣傳從Redis8開始,Redis項(xiàng)目重新開源。
本篇文章主要介紹Redis6.0多線程IO特性。
二、Redis6.0 多線程 IO 概述
Redis6.0引入多線程IO,但多線程部分只是用來處理網(wǎng)絡(luò)數(shù)據(jù)的讀寫和協(xié)議解析,執(zhí)行命令仍然是單線程。默認(rèn)是不開啟的,需要進(jìn)程啟動(dòng)前開啟配置,并且在運(yùn)行期間無法通過 config set 命令動(dòng)態(tài)修改。
參數(shù)與配置
多線程IO涉及下面兩個(gè)配置參數(shù):
# io-threads 4 IO 線程數(shù)量
# io-threads-do-reads no 讀數(shù)據(jù)及數(shù)據(jù)解析是否也用 IO 線程- io-threads 表示IO線程數(shù)量, io-threads 設(shè)置為1時(shí)(代碼中默認(rèn)值),表示只使用主線程,不開啟多線程IO。因此,若要配置開啟多線程IO,需要設(shè)置 io-threads 大于1,但不可以超過最大值128。
- 但在默認(rèn)情況下,Redis只將多線程IO用于向客戶端寫數(shù)據(jù),因?yàn)樽髡哒J(rèn)為通常使用多線程執(zhí)行讀數(shù)據(jù)的操作幫助不是很大。如果需要使用多線程用于讀數(shù)據(jù)和解析數(shù)據(jù),則需要將參數(shù) io-threads-do-reads 設(shè)置為 yes 。
- 此兩項(xiàng)配置參數(shù)在Redis運(yùn)行期間無法通過 config set 命令修改,并且開啟SSL時(shí),不支持多線程IO特性。
- 若機(jī)器CPU將至少超過4核時(shí),則建議開啟,并且至少保留一個(gè)備用CPU核,使用超過8個(gè)線程可能并不會(huì)有多少幫助。
執(zhí)行流程概述
Redis6.0引入多線程IO后,讀寫數(shù)據(jù)執(zhí)行流程如下所示:
圖片
流程簡(jiǎn)述
- 主線程負(fù)責(zé)接收建立連接請(qǐng)求,獲取socket放入全局等待讀處理隊(duì)列。
- 主線程處理完讀事件之后,通過RR(Round Robin)將這些連接分配給這些IO線程,也會(huì)分配給主線程自己。
- 主線程先讀取分配給自己的客戶端數(shù)據(jù),然后阻塞等待其他IO線程讀取socket完畢。
- IO線程將請(qǐng)求數(shù)據(jù)讀取并解析完成(這里只是讀數(shù)據(jù)和解析、并不執(zhí)行)。
- 主線程通過單線程的方式執(zhí)行請(qǐng)求命令。
- 主線程通過RR(Round Robin)將回寫客戶端事件分配給這些IO線程,也會(huì)分配給主線程自己。
- 主線程同樣執(zhí)行部分寫數(shù)據(jù)到客戶端,然后阻塞等待IO線程將數(shù)據(jù)回寫socket完畢。
設(shè)計(jì)特點(diǎn)
- IO線程要么同時(shí)在讀socket,要么同時(shí)在寫,不會(huì)同時(shí)讀和寫。
- IO線程只負(fù)責(zé)讀寫socket解析命令,不負(fù)責(zé)命令執(zhí)行。
- 主線程也會(huì)參與數(shù)據(jù)的讀寫。
三、源碼分析
多線程IO相關(guān)源代碼都在源文件networking.c中最下面。
初始化
主線程在main函數(shù)中調(diào)用InitServerLast函數(shù),InitServerLast函數(shù)中調(diào)用initThreadedIO函數(shù),在initThreadedIO函數(shù)中根據(jù)配置文件中的線程數(shù)量,創(chuàng)建對(duì)應(yīng)數(shù)量的IO工作線程數(shù)量。
/* Initialize the data structures needed for threaded I/O. */
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;
}
}- 如果 io_threads_num 的數(shù)量為1,則只運(yùn)行主線程, io_threads_num 的IO線程數(shù)量不允許超過 128。
- 序號(hào)為0的線程是主線程,因此實(shí)際的工作線程數(shù)目是io-threads - 1。
初始化流程
- 為包括主線程在內(nèi)的每個(gè)線程分配list列表,用于后續(xù)保存待處理的客戶端。
- 為主線程以外的其他IO線程初始化互斥對(duì)象mutex,但是立即調(diào)用pthread_mutex_lock占有互斥量,將io_threads_pending[i]設(shè)置為0,接著創(chuàng)建對(duì)應(yīng)的IO工作線程。
- 占用互斥量是為了創(chuàng)建IO工作線程后,可暫時(shí)等待后續(xù)啟動(dòng)IO線程的工作,因?yàn)镮OThreadMain函數(shù)在io_threads_pending[id] == 0時(shí)也調(diào)用了獲取mutex,所以此時(shí)無法繼續(xù)向下運(yùn)行,等待啟動(dòng)。
- 在startThreadedIO函數(shù)中會(huì)釋放mutex來啟動(dòng)IO線程工作。何時(shí)調(diào)用startThreadedIO打開多線程IO,具體見下文的「多線程IO動(dòng)態(tài)暫停與開啟」。
IO 線程主函數(shù)
IO線程主函數(shù)代碼如下所示:
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);
redisSetCpuAffinity(server.server_cpulist);
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);
}
}從IO線程主函數(shù)邏輯可以看到:
- 如果IO線程等待處理任務(wù)數(shù)量為0,則IO線程一直在空循環(huán),因此后面主線程給IO線程分發(fā)任務(wù)后,需要設(shè)置IO線程待處理任務(wù)數(shù) io_threads_pending[id] ,才會(huì)觸發(fā)IO線程工作。
- 如果IO線程等待處理任務(wù)數(shù)量為0,并且未獲取到mutex鎖,則會(huì)等待獲取鎖,暫停運(yùn)行,由于主線程在創(chuàng)建IO線程之前先獲取了鎖,因此IO線程剛啟動(dòng)時(shí)是暫停運(yùn)行狀態(tài),需要等待主線程釋放鎖,啟動(dòng)IO線程。
- IO線程待處理任務(wù)數(shù)為0時(shí),獲取到鎖并再次釋放鎖,是為了讓主線程可以暫停IO線程。
- 只有io_threads_pending[id]不為0時(shí),則繼續(xù)向下執(zhí)行操作,根據(jù)io_threads_op決定是讀客戶端還是寫客戶端,從這里也可以看出IO線程要么同時(shí)讀,要么同時(shí)寫。
讀數(shù)據(jù)流程
主線程將待讀數(shù)據(jù)客戶端加入隊(duì)列
當(dāng)客戶端連接有讀事件時(shí),會(huì)觸發(fā)調(diào)用readQueryFromClient函數(shù),在該函數(shù)中會(huì)調(diào)用postponeClientRead。
void readQueryFromClient(connection *conn) {
client *c = connGetPrivateData(conn);
int nread, readlen;
size_t qblen;
/* Check if we want to read from the client later when exiting from
* the event loop. This is the case if threaded I/O is enabled. */
if (postponeClientRead(c)) return;
......以下省略
}
/* Return 1 if we want to handle the client read later using threaded I/O.
* This is called by the readable handler of the event loop.
* As a side effect of calling this function the client is put in the
* pending read clients and flagged as such. */
int postponeClientRead(client *c) {
if (io_threads_active &&
server.io_threads_do_reads &&
!ProcessingEventsWhileBlocked &&
!(c->flags & (CLIENT_MASTER|CLIENT_SLAVE|CLIENT_PENDING_READ)))
{
c->flags |= CLIENT_PENDING_READ;
listAddNodeHead(server.clients_pending_read,c);
return 1;
} else {
return 0;
}
}如果開啟多線程,并且開啟多線程讀(io_threads_do_reads 為 yes),則將客戶端標(biāo)記為CLIENT_PENDING_READ,并且加入clients_pending_read列表。
然后readQueryFromClient函數(shù)中就立即返回,主線程沒有執(zhí)行從客戶端連接中讀取的數(shù)據(jù)相關(guān)邏輯,讀取了客戶端數(shù)據(jù)行為等待后續(xù)各個(gè)IO線程執(zhí)行。
主線程分發(fā)并阻塞等待
主線程在beforeSleep函數(shù)中會(huì)調(diào)用handleClientsWithPendingReadsUsingThreads函數(shù)。
/* When threaded I/O is also enabled for the reading + parsing side, the
* readable handler will just put normal clients into a queue of clients to
* process (instead of serving them synchronously). This function runs
* the queue using the I/O threads, and process them in order to accumulate
* the reads in the buffers, and also parse the first command available
* rendering it in the client structures. */
int handleClientsWithPendingReadsUsingThreads(void) {
if (!io_threads_active || !server.io_threads_do_reads) return 0;
int processed = listLength(server.clients_pending_read);
if (processed == 0) return 0;
if (tio_debug) printf("%d TOTAL READ pending clients\n", processed);
/* Distribute the clients across N different lists. */
listIter li;
listNode *ln;
listRewind(server.clients_pending_read,&li);
int item_id = 0;
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
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_READ;
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);
readQueryFromClient(c->conn);
}
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 READ All threads finshed\n");
/* Run the list of clients again to process the new buffers. */
while(listLength(server.clients_pending_read)) {
ln = listFirst(server.clients_pending_read);
client *c = listNodeValue(ln);
c->flags &= ~CLIENT_PENDING_READ;
listDelNode(server.clients_pending_read,ln);
if (c->flags & CLIENT_PENDING_COMMAND) {
c->flags &= ~CLIENT_PENDING_COMMAND;
if (processCommandAndResetClient(c) == C_ERR) {
/* If the client is no longer valid, we avoid
* processing the client later. So we just go
* to the next. */
continue;
}
}
processInputBuffer(c);
}
return processed;
}- 先檢查是否開啟多線程,以及是否開啟多線程讀數(shù)據(jù)(io_threads_do_reads),未開啟直接返回。
- 檢查隊(duì)列clients_pending_read長(zhǎng)度,為0直接返回,說明沒有待讀事件。
- 遍歷clients_pending_read隊(duì)列,通過RR算法,將隊(duì)列中的客戶端循環(huán)分配給各個(gè)IO線程,包括主線程本身。
- 設(shè)置io_threads_op = IO_THREADS_OP_READ,并且將io_threads_pending數(shù)組中各個(gè)位置值設(shè)置為對(duì)應(yīng)各個(gè)IO線程分配到的客戶端數(shù)量,如上面介紹,目的是為了使IO線程工作。
- 主線程開始讀取客戶端數(shù)據(jù),因?yàn)橹骶€程也分配了任務(wù)。
- 主線程阻塞等待,直到所有的IO線程都完成讀數(shù)據(jù)工作。
- 主線程執(zhí)行命令。
IO 線程讀數(shù)據(jù)
在IO線程主函數(shù)中,如果 io_threads_op == IO_THREADS_OP_READ ,則調(diào)用readQueryFromClient從網(wǎng)絡(luò)中讀取數(shù)據(jù)。
IO 線程讀取數(shù)據(jù)后,不會(huì)執(zhí)行命令。
在readQueryFromClient函數(shù)中,最后會(huì)執(zhí)行processInputBuffer函數(shù),在processInputBuffe函數(shù)中,如IO線程檢查到客戶端設(shè)置了CLIENT_PENDING_READ標(biāo)志,則不執(zhí)行命令,直接返回。
......省略
/* If we are in the context of an I/O thread, we can't really
* execute the command here. All we can do is to flag the client
* as one that needs to process the command. */
if (c->flags & CLIENT_PENDING_READ) {
c->flags |= CLIENT_PENDING_COMMAND;
break;
}
...... 省略寫數(shù)據(jù)流程
命令處理完成后,依次調(diào)用:
addReply-->prepareClientToWrite-->clientInstallWriteHandler,將待寫客戶端加入隊(duì)列clients_pending_write。
void clientInstallWriteHandler(client *c) {
/* Schedule the client to write the output buffers to the socket only
* if not already done and, for slaves, if the slave can actually receive
* writes at this stage. */
if (!(c->flags & CLIENT_PENDING_WRITE) &&
(c->replstate == REPL_STATE_NONE ||
(c->replstate == SLAVE_STATE_ONLINE && !c->repl_put_online_on_ack)))
{
/* Here instead of installing the write handler, we just flag the
* client and put it into a list of clients that have something
* to write to the socket. This way before re-entering the event
* loop, we can try to directly write to the client sockets avoiding
* a system call. We'll only really install the write handler if
* we'll not be able to write the whole reply at once. */
c->flags |= CLIENT_PENDING_WRITE;
listAddNodeHead(server.clients_pending_write,c);
}
}在beforeSleep函數(shù)中調(diào)用handleClientsWithPendingWritesUsingThreads。
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;
}- 判斷clients_pending_write隊(duì)列的長(zhǎng)度,如果為0則直接返回。
- 判斷是否開啟了多線程,若只有很少的客戶端需要寫,則不使用多線程IO,直接在主線程完成寫操作。
- 如果使用多線程IO來完成寫數(shù)據(jù),則需要判斷是否先開啟多線程IO(因?yàn)闀?huì)動(dòng)態(tài)開啟與暫停)。
- 遍歷clients_pending_write隊(duì)列,通過RR算法,循環(huán)將所有客戶端分配給各個(gè)IO線程,包括主線程自身。
- 設(shè)置io_threads_op = IO_THREADS_OP_WRITE,并且將io_threads_pending數(shù)組中各個(gè)位置值設(shè)置為對(duì)應(yīng)的各個(gè)IO線程分配到的客戶端數(shù)量,目的是為了使IO線程工作。
- 主線程開始寫客戶端數(shù)據(jù),因?yàn)橹骶€程也分配了任務(wù),寫完清空任務(wù)隊(duì)列。
- 阻塞等待,直到所有IO線程完成寫數(shù)據(jù)工作。
- 再次遍歷所有客戶端,如果有需要,為客戶端在事件循環(huán)上安裝寫句柄函數(shù),等待事件回調(diào)。
多線程 IO 動(dòng)態(tài)暫停與開啟
從上面的寫數(shù)據(jù)的流程中可以看到,在Redis運(yùn)行過程中多線程IO是會(huì)動(dòng)態(tài)暫停與開啟的。
在上面的寫數(shù)據(jù)流程中,先調(diào)用stopThreadedIOIfNeeded函數(shù)判斷是否需要暫停多線程IO,當(dāng)?shù)却龑懙目蛻舳藬?shù)量低于線程數(shù)的2倍時(shí),會(huì)暫停多線程IO,否則就會(huì)打開多線程。
int stopThreadedIOIfNeeded(void) {
int pending = listLength(server.clients_pending_write);
/* Return ASAP if IO threads are disabled (single threaded mode). */
if (server.io_threads_num == 1) return 1;
if (pending < (server.io_threads_num*2)) {
if (io_threads_active) stopThreadedIO();
return 1;
} else {
return 0;
}
}在寫數(shù)據(jù)流程handleClientsWithPendingWritesUsingThreads函數(shù)中,stopThreadedIOIfNeeded返回0的話,就會(huì)執(zhí)行下面的startThreadedIO函數(shù),開啟多線程IO。
void startThreadedIO(void) {
serverAssert(server.io_threads_active == 0);
for (int j = 1; j < server.io_threads_num; j++)
pthread_mutex_unlock(&io_threads_mutex[j]);
server.io_threads_active = 1;
}
void stopThreadedIO(void) {
/* We may have still clients with pending reads when this function
* is called: handle them before stopping the threads. */
handleClientsWithPendingReadsUsingThreads();
serverAssert(server.io_threads_active == 1);
for (int j = 1; j < server.io_threads_num; j++)
pthread_mutex_lock(&io_threads_mutex[j]);
server.io_threads_active = 0;
}從上面的代碼中可以看出:
- 開啟多線程IO是通過釋放mutex鎖來讓IO線程開始執(zhí)行讀數(shù)據(jù)或者寫數(shù)據(jù)動(dòng)作。
- 暫停多線程IO則是通過加鎖來讓IO線程暫時(shí)不執(zhí)行讀數(shù)據(jù)或者寫數(shù)據(jù)動(dòng)作,此處加鎖后,IO線程主函數(shù)由于無法獲取到鎖,因此會(huì)暫時(shí)阻塞。
四、性能對(duì)比
測(cè)試環(huán)境
兩臺(tái)物理機(jī)配置:CentOS Linux release 7.3.1611(Core) ,12核CPU1.5GHz,256G內(nèi)存(free 128G)。
Redis版本
使用Redis6.0.6,多線程IO模式使用線程數(shù)量為4,即 io-threads 4 ,參數(shù) io-threads-do-reads 分別設(shè)置為 no 和 yes ,進(jìn)行對(duì)比測(cè)試。
壓測(cè)命令
redis-benchmark -h 172.xx.xx.xx -t set,get -n 1000000 -r 100000000 --threads ${threadsize} -d ${datasize} -c ${clientsize}
單線程 threadsize 為 1,多線程 threadsize 為 4
datasize為value 大小,分別設(shè)置為 128/512/1024
clientsize 為客戶端數(shù)量,分別設(shè)置為 256/2000
如:./redis-benchmark -h 172.xx.xx.xx -t set,get -n 1000000 -r 100000000 --threads 4 -d 1024 -c 256統(tǒng)計(jì)結(jié)果
當(dāng) io-threads-do-reads 為 no 時(shí),統(tǒng)計(jì)圖表如下所示(c 2000表示客戶端數(shù)量為2000)。
圖片
當(dāng) io-threads-do-reads 為 yes 時(shí),統(tǒng)計(jì)圖表如下所示(c 256表示客戶端數(shù)量為256)。
圖片
結(jié)論
使用redis-benchmark做Redis6單線程和多線程簡(jiǎn)單SET/GET命令性能測(cè)試:
- 從上面可以看到GET/SET命令在設(shè)置4個(gè)IO線程時(shí),QPS相比于大部分情況下的單線程,性能幾乎是翻倍了。
- 連接數(shù)越多,多線程優(yōu)勢(shì)越明顯。
- value值越小,多線程優(yōu)勢(shì)越明顯。
- 使用多線程讀命令比寫命令優(yōu)勢(shì)更加明顯,當(dāng)value越大,寫命令越發(fā)沒有明顯的優(yōu)勢(shì)。
- 參數(shù) io-threads-do-reads 為yes,性能有微弱的優(yōu)勢(shì),不是很明顯。
- 總體來說,以上結(jié)果基本符合預(yù)期,結(jié)果僅作參考。
五、6.0 多線程 IO 不足
盡管引入多線程IO大幅提升了Redis性能,但是Redis6.0的多線程IO仍然存在一些不足:
- CPU核心利用率不足:當(dāng)前主線程仍負(fù)責(zé)大部分的IO相關(guān)任務(wù),并且當(dāng)主線程處理客戶端的命令時(shí),IO線程會(huì)空閑相當(dāng)長(zhǎng)的時(shí)間,同時(shí)值得注意的是,主線程在執(zhí)行IO相關(guān)任務(wù)期間,性能受到最慢IO線程速度的限制。
- IO線程執(zhí)行的任務(wù)有限:目前,由于主線程同步等待IO線程,線程僅執(zhí)行讀取解析和寫入操作。如果線程可以異步工作,我們可以將更多工作卸載到IO線程上,從而減少主線程的負(fù)載。
- 不支持帶有TLS的IO線程。
最新的Valkey8.0版本中,通過引入異步IO線程,將更多的工作轉(zhuǎn)移到IO線程執(zhí)行,同時(shí)通過批量預(yù)讀取內(nèi)存數(shù)據(jù)減少內(nèi)存訪問延遲,大幅提高Valkey單節(jié)點(diǎn)訪問QPS,單個(gè)實(shí)例每秒可處理100萬個(gè)請(qǐng)求。我們后續(xù)再詳細(xì)介紹Valkey8.0異步IO特性。
六、總結(jié)
Redis6.0引入多線程IO,但多線程部分只是用來處理網(wǎng)絡(luò)數(shù)據(jù)的讀寫和協(xié)議解析,執(zhí)行命令仍然是單線程。通過開啟多線程IO,并設(shè)置合適的CPU數(shù)量,可以提升訪問請(qǐng)求一倍以上。
Redis6.0多線程IO仍然存在一些不足,沒有充分利用CPU核心,在最新的Valkey8.0版本中,引入異步IO將進(jìn)一步大幅提升Valkey性能。



























