Redis网络模型
- 阅读源码的初衷
- Redis源码阅读
阅读源码的初衷
很多网上解释这个Redis为啥这么块?都会说Redis这么快的原因会有一个Redis才用了单线程&使用了多路io复用来检查io事件,单线程可以避免多线程对资源的竞争。如果我们使用了多线程那么就需要考虑加锁,如果要加锁那么这个锁的粒度如何考虑应该加什么样的锁?如果出现了死锁那么效率只会更低。但是各位老铁有没有相关单线程最怕的是什么?当然是最怕阻塞操作如果此时单线程阻塞了那么就无法处理其他客户端的连接而且当reator模型检测到有事情发送时,需要调用read这样的系统调用将客户端的数据读取上了,而read这个操作是阻塞的,将客户端的数据读取上来之后我们还需要对这个decode进行解码,如果客户端的数据很大那么无疑这两部是非常耗时的肯定会导致这个Redis的效率下降。带着这个因为博主开始查看Redis6.2的源代码。在这里提前告知一下博主阅读完Redis网络模型的感受只能说是牛逼
- 对于多线程来说最怕资源的竞争所以Redis的工作线程才用了多线程而对于IO线程会有一个自己的专属队列这样就避免了资源的竞争
- 对于单线程最怕的是阻塞,redis采用多线程来进行IO的读取但是这个命令的执行确实单线程的。
Redis源码阅读
首先在这里我先说一下博主阅读Redis源码的思路,首先博主是从这个main函数开始看的,这个main函数在networking.c这个文件当中。
我们看到这个initServer这个函数。下面我们看看这个函数具体干了些什么?main函数当中这个其他函数暂时不用管因为我们只是查看Redis的网络部门
void initServer(void) {
createSharedObjects();//创建共享对象
server.el = aeCreateEventLoop(server.maxclients + CONFIG_FDSET_INCR);//重要创建事件循环器
//多路io复用器
server.db = zmalloc(sizeof(redisDb) * server.dbnum);//创建Redis的默认的16个数据库
if (server.port != 0 &&
listenToPort(server.port, server.ipfd, &server.ipfd_count) == C_ERR)
//listenToPort主要干了3件事 socket创建套接字,bind,listen
exit(1);
if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
serverPanic("Can't create event loop timers.");
exit(1);
}
if (aeCreateFileEvent(server.el, server.module_blocked_pipe[0], AE_READABLE,
moduleBlockedClientPipeReadable, NULL) == AE_ERR) {
serverPanic(
"Error registering the readable event for the module "
"blocked clients subsystem.");
}
//开启事件循环需要先调用beforeSleep这个回调
aeSetBeforeSleepProc(server.el, beforeSleep);
aeSetAfterSleepProc(server.el, afterSleep);
//当事件循环处理完毕时调用的回调
}
在这里说明一下几个重要的函数。
- aeCreateEventLoop:创建事件循环器
- listenToPort:创建套接字,bind,listen.
- aeCreateFileEvent:创建文件事件并将监听套接字注册到事情循环器上。
下面我们看看这两个函数里面主要干了些啥事情
.........上面代码逻辑忽略
s = socket(p->ai_family,p->ai_socktype,p->ai_protocol;
if (anetListen(err,s,p->ai_addr,p->ai_addrlen,backlog) == ANET_ERR) s = ANET_ERR;
而anetListen主要干的是
bind(s,sa,len);
listen(s, backlog)
下面我们看看则个aeCreateFileEvent这个函数主要是干了些啥
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData)
{
if (fd >= eventLoop->setsize) {
errno = ERANGE;
return AE_ERR;
}
aeFileEvent *fe = &eventLoop->events[fd];//拿出当前文件描述符对于的文件事情对象
if (aeApiAddEvent(eventLoop, fd, mask) == -1)//将监听套接字注册到事情循环器当中
return AE_ERR;
fe->mask |= mask;//关系的是什么事件
if (mask & AE_READABLE) fe->rfileProc = proc;//并绑定对应的回调
if (mask & AE_WRITABLE) fe->wfileProc = proc;
fe->clientData = clientData;
if (fd > eventLoop->maxfd)
eventLoop->maxfd = fd;
return AE_OK;
}
在这里主要是将监听套接字注册到事件循环器当中,并设置当连接到来时需要执行的回调,当事情到来时只需要调用对应的回调即可,而这个监听套接字对应的回调是这个acceptTcpHandler这个回调函数下面我们一起来看看这个函数干了些什么
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
char cip[NET_IP_STR_LEN];
UNUSED(el);
UNUSED(mask);
UNUSED(privdata);
while(max--) {
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);//将客户端的连接通过accepter获取上来
.........
acceptCommonHandler(connCreateAcceptedSocket(cfd),0,cip);//给这个客户端的连接绑定对应的回调
}
}
注意我们需要重点看一下这个connCreateAcceptedSocket这个函数他干了什么非常的重要我们点进去进行查看。
connection *connCreateSocket() {
connection *conn = zcalloc(sizeof(connection));
conn->type = &CT_Socket;//尤其是这一步非常的主要负责后面你可能难懂获取到的连接时怎么注册的事情循环器当中的
conn->fd = -1;
return conn;
}
下面我们在看看这个CT_Socket这货是什么东西
ConnectionType CT_Socket = {
.ae_handler = connSocketEventHandler,
.close = connSocketClose,
.write = connSocketWrite,
.read = connSocketRead,
.accept = connSocketAccept,
.connect = connSocketConnect,
.set_write_handler = connSocketSetWriteHandler,//用来设置写回调
.set_read_handler = connSocketSetReadHandler,//用来设置读回调
.get_last_error = connSocketGetLastError,
.blocking_connect = connSocketBlockingConnect,
.sync_write = connSocketSyncWrite,
.sync_read = connSocketSyncRead,
.sync_readline = connSocketSyncReadLine,
.get_type = connSocketGetType
};
我们点进去一看哦原来是一堆的回调函数,后面我们会见到这一堆回调函数的使用的请各位老铁重点关注一下。
下面我们继续往下面看
#define MAX_ACCEPTS_PER_CALL 1000
static void acceptCommonHandler(connection *conn, int flags, char *ip) {
client *c;
....................
/* Create connection and client */
if ((c = createClient(conn)) == NULL) {
//客户端的连接打包成为一个客户端
}
c->flags |= flags;//设置对应的标志位
}
下面我们重点看一下这个createClient这个函数,我们点进去进行查看。
client *createClient(connection *conn) {
client *c = zmalloc(sizeof(client));
if (conn) {
connNonBlock(conn);//设置非阻塞
connEnableTcpNoDelay(conn);
if (server.tcpkeepalive)//长连接
connKeepAlive(conn,server.tcpkeepalive);
connSetReadHandler(conn, readQueryFromClient);
//当客户端有事件到来时会调用readQueryFromClient这个函数
}
}
这个readQueryFromClient函数非常的有意思,后面我们重点来解释这个函数。下面我们来看看这个事情循环的过程在networking.c的最后部门 aeMain这个函数。
下面我们点进去看看这个函数
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {//开启事件循环也就是开始一直转圈了
aeProcessEvents(eventLoop, AE_ALL_EVENTS|
AE_CALL_BEFORE_SLEEP|
AE_CALL_AFTER_SLEEP);
}
}
下面我们看看这个aeProcessEvents事情循环器。
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
int processed = 0, numevents;
if (eventLoop->beforesleep != NULL && flags & AE_CALL_BEFORE_SLEEP)
eventLoop->beforesleep(eventLoop);
//在进行epoll_wait之前需要处理一些事情很重要
numevents = aeApiPoll(eventLoop, tvp);
//进行epoll_wait等待客户端的事件到来
/* After sleep callback. */
if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)
eventLoop->aftersleep(eventLoop);//醒来之后需要处理的一些回调
for (j = 0; j < numevents; j++) {
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
int mask = eventLoop->fired[j].mask;
int fd = eventLoop->fired[j].fd;
int fired = 0;
int invert = fe->mask & AE_BARRIER;
if (!invert && fe->mask & mask & AE_READABLE) {//读事情到来
fe->rfileProc(eventLoop,fd,fe->clientData,mask);//调用对应的读回调
fired++;
fe = &eventLoop->events[fd];
}
/* Fire the writable event. */
if (fe->mask & mask & AE_WRITABLE) {//写事情到来
if (!fired || fe->wfileProc != fe->rfileProc) {
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
//调用对应的写回调
fired++;
}
}
processed++;
}
}
.......
return processed; /* return the number of processed file/time events */
}
而这个读回调就是这个readQueryFromClient这个函数,这个函数非常的优秀一共会进行三次。
其主要功能主要是图上的这三个功能。可能有老铁一直关系的多线程在哪里了,其实还是在networking.c里面博主给忘记了,下面我们可以看看他里面的initLastServer.
下面我们点进去看看
void InitServerLast() {
bioInit();
initThreadedIO();//开始创建线程了
set_jemalloc_bg_thread(server.jemalloc_bg_thread);
server.initial_memory_usage = zmalloc_used_memory();
}
然后就是这个initThreadedIO
void initThreadedIO(void) {
server.io_threads_active = 0; /* We start with threads not active. */
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. */
pthread_t tid;
pthread_mutex_init(&io_threads_mutex[i],NULL);
setIOPendingCount(i, 0);//设置每个线程的队列长度为0,线程启动后会判断队列的长度是否大于0
pthread_mutex_lock(&io_threads_mutex[i]);
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;
}
}
而这个线程的入口函数是这个IOThreadMain.然后我们在回到那个读连接到来时我们需要调用的回调函数readQueryFromClient。
void readQueryFromClient(connection *conn) {
...............
/* 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;
//将客户端的连接分发到这个全局队列当中clients_pending_read这个全局队列当中
............
nread = connRead(c->conn, c->querybuf+qblen, readlen);
//读取客户端的连接
............
processInputBuffer(c);//开始去执行命令
}
下面我们一起看看这个postponeClientRead这个函数
int postponeClientRead(client *c) {
if (server.io_threads_active &&
server.io_threads_do_reads &&
!ProcessingEventsWhileBlocked &&
!(c->flags & (CLIENT_MASTER|CLIENT_SLAVE|CLIENT_PENDING_READ)))
//一开始c->flag是这个CONN_STATE_ACCEPTING所有会进来
{
c->flags |= CLIENT_PENDING_READ;
listAddNodeHead(server.clients_pending_read,c);//加入到全局队列当中
return 1;
} else {
return 0;
}
}
而一开始客户端的这个状态是这个
当加入到全局队列当中之后main线程就会通过取轮询的方式将全局队列当中的客户端交给每一个线程去处理。那么在哪里开始进行分配的了?在这里博主就直接说了,在beforeSleep这里会调用分发的函数
void beforeSleep(struct aeEventLoop *eventLoop) {
UNUSED(eventLoop);
................
handleClientsWithPendingReadsUsingThreads();
//将客户端的分发到每个线程的队列当中只不过是读事情
.......................
handleClientsWithPendingWritesUsingThreads();
将客户端的分发到每个线程的队列当中只不过是写事情
......................
}
也就是在进行epoll_wait之前会将client通过轮询的方式分发到每个线程的队列当中去。并更新每个线程的队列长度。那么每个线程在主线程没有分发给他们任务时他们在干什么了?
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);
makeThreadKillable();
while(1) {
//死循环的空转
for (int j = 0; j < 1000000; j++) {
if (getIOPendingCount(id) != 0) break;//判断自己的队列当中有没有东西
}
/* Give the main thread a chance to stop this thread. */
if (getIOPendingCount(id) == 0) {
pthread_mutex_lock(&io_threads_mutex[id]);
pthread_mutex_unlock(&io_threads_mutex[id]);
continue;
}
serverAssert(getIOPendingCount(id) != 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]);
setIOPendingCount(id, 0);
}
}
当然我们可以在redis.conf里面进行配置这个线程的数量
在main线程没有给每个线程的队列当中分配client时,其他线程一直在死循环的空转,并且不会让出CPU,直到main线程给他分配了client之后开始根据时读事情还是写事件执行对应的回调。我们惊奇的发现IO线程当中也再次调用了这个readQueryFromClient但是这次进入到这个函数当中在执行postponeClient的时候不会直接return 因为在第一次放入到全局队列当中c的flag已经被置为这个client_pending_read,就会继续执行下面的代码。而下面的代码时这个读取客户端的数据上面,并进行解码。但是不会执行这个命令
以为此时客户端的状态是这个CLIENT_PENDID_READ.直接break了并不会执行这下面的状态而当每个线程将客户端处理完成之后main线程会同一进行处理。
当命令执行完成之后,又会将结果放入到全局队列当中
函数:
void clientInstallWriteHandler(client *c) {
if (!(c->flags & CLIENT_PENDING_WRITE) &&
(c->replstate == REPL_STATE_NONE ||
(c->replstate == SLAVE_STATE_ONLINE && !c->repl_put_online_on_ack)))
{
c->flags |= CLIENT_PENDING_WRITE;
listAddNodeHead(server.clients_pending_write,c);//放入全局队列当中
}
}
而在这个函数当中进行分发 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 the boring synchronous code. */
if (server.io_threads_num == 1 || stopThreadedIOIfNeeded()) {
return handleClientsWithPendingWrites();
}
/* Start threads if needed. */
if (!server.io_threads_active) startThreadedIO();
/* 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;
/* Remove clients from the list of pending writes since
* they are going to be closed ASAP. */
if (c->flags & CLIENT_CLOSE_ASAP) {
listDelNode(server.clients_pending_write, ln);
continue;
}
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]);
setIOPendingCount(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 += getIOPendingCount(j);
if (pending == 0) break;
}
/* 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);
/* Update processed count on server */
server.stat_io_writes_processed += processed;
return processed;
}
和读取的过程查不多。
总结:
- Redis通过io多路复用让一个线程能够高效的处理多个客户端的请求减少了网络IO的开销
- 工作线程采用单线程,单线程避免了多线程对资源的竞争,因为多线程我们不得不面对的问题就是资源竞争,资源竞争就意味着需要加锁那么加什么样的锁,锁的粒度如何把握,出现了死锁这样返回会降低Redis的效率
- 但是当客户端的请求数据很大时,通过read将客户端的数据读取上了和decode操作会很耗时,因此redis引入了这个多线程,但是多线程我们说过最怕的就是这个资源的竞争,因此redis为每一个线程分配了一个自己的本地队列,这样就不存在资源竞争问题
- io线程只负责将读取客户端的发送过来的数据读取上了并进行decode和将结果发送给客户端,并不会参与命令的执行。