Memcached源码解析(一)—网络模型

与Redis类似,Memcached也是基于内存的KV缓存系统,与Redis的不同之处主要有以下几点:

  • Redis支持的key种类丰富,Memcached只支持简单的string类型的KV对
  • Memcached是单机系统,Redis是分布式系统,支持分片和复制
  • Memcached是纯内存缓存系统,宕机后数据会丢失,Redis支持数据持久化
  • 网络模型不同,Memcached是多线程模型,Redis是单线程模型,同等配置下,Memcached开启4个工作线程的吞吐约为Redis的3倍
  • 内存管理方式不同,Redis内存完全交由操作系统管理,Memcached的内存又自己管理

在Redis出现之后,Memcached的地位逐渐被Redis取代,现在使用Memcached的系统已经比较少了,但是Memcached的代码小巧精美,通过阅读Memcached的代码,能够学到较多知识,下面就分成两节,学习Memcached的网络模型和内存管理方式

网络模型

Memcached是基于Libevent的事件库来实现网络线程模型,与Redis类似,Memchached也是Reactor事件模型。

主线程

在Memchached启动时,会在主线程中创建main_base,用来listen和accept用户的连接。

int main(int argc, char **argv) {
	...
	// 初始化event事件循坏main_base
    struct event_config *ev_config;
    ev_config = event_config_new();
    event_config_set_flag(ev_config, EVENT_BASE_FLAG_NOLOCK);
    main_base = event_base_new_with_config(ev_config);
    event_config_free(ev_config);
		
		// 创建socket用来listen和accept用户连接
		if (settings.port && server_sockets(settings.port, tcp_transport,
                                           portnumber_file)) {
            vperror("failed to listen on TCP port %d", settings.port);
            exit(EX_OSERR);
        }
	...
}

server_sockets通过conn_new将listen fd加入main_base的事件循环中用来accept用户连接:

conn *conn_new(const int sfd, enum conn_states init_state,
                const int event_flags,
                const int read_buffer_size, enum network_transport transport,
                struct event_base *base, void *ssl) {
	// 创建conn对象,并初始化
	// 加入event管理,设置epoll回调函数
    event_set(&c->event, sfd, event_flags, event_handler, (void *)c);
    event_base_set(base, &c->event);
    c->ev_flags = event_flags;

    if (event_add(&c->event, 0) == -1) {
        perror("event_add");
        return NULL;
    }
     ...          
}

事件触发函数event_handler,通过当前conn的状态判断,调用不同的事件处理函数,比如对于主进程中的conn_listening状态,会accept用户连接,然后把新建的连接分发给工作线程:

void event_handler(const evutil_socket_t fd, const short which, void *arg) {
	...
	// 进行状态机转化处理
    drive_machine(c);
	...
}

static void drive_machine(conn *c) {
	...
	switch(c->state) {
        case conn_listening:    //连接请求
        	// accept连接
            sfd = accept(c->sfd, (struct sockaddr *)&addr, &addrlen);
            // 分发请求
            dispatch_conn_new(sfd, conn_new_cmd, EV_READ | EV_PERSIST,
                                 READ_BUFFER_CACHED, c->transport, ssl_v);
	...
}

工作线程

工作线程用来处理用户的读写请求,在Memcached启动时,会创建N个worker thread,每个worker thread的信息通过LIBEVENT_THREAD保存,worker thread与main thread之间通过pipe通信

int main(int argc, char** argv) {
	...
	memcached_thread_init(settings.num_threads, NULL);
	...
}

void memcached_thread_init(int nthreads, void *arg) {
	...
	// 为worker线程分配锁,每个桶一个锁
    item_locks = calloc(item_lock_count, sizeof(pthread_mutex_t));
    ...
    /* Create threads after we've done all the libevent setup. */
    for (i = 0; i < nthreads; i++) {
        create_worker(worker_libevent, &threads[i]);
    }

    /* Wait for all the threads to set themselves up before returning. */
    // 主线程等待worker线程初始化完毕
    pthread_mutex_lock(&init_lock);
    wait_for_thread_registration(nthreads);
    pthread_mutex_unlock(&init_lock);
}

// worker线程主循环
static void *worker_libevent(void *arg) {
    LIBEVENT_THREAD *me = arg;
	...
    register_thread_initialized();

    // 等待读写事件发生
    event_base_loop(me->base, 0);

    // same mechanism used to watch for all threads exiting.
    register_thread_initialized();

    event_base_free(me->base);
    return NULL;
}

主线程accept用户连接之后,把用户连接信息放到CQ_ITEM中,通过写入pipe的方式通知work thread来处理用户连接:

// 主线程将accept之后的连接分发给worker线程
void dispatch_conn_new(int sfd, enum conn_states init_state, int event_flags,
                       int read_buffer_size, enum network_transport transport, void *ssl) {
    //创建一个item                       
    CQ_ITEM *item = cqi_new();
    char buf[1];
    if (item == NULL) {
        close(sfd);
        /* given that malloc failed this may also fail, but let's try */
        fprintf(stderr, "Failed to allocate memory for connection object\n");
        return;
    }

    //轮询选择thread
    int tid = (last_thread + 1) % settings.num_threads;

    LIBEVENT_THREAD *thread = threads + tid;

    last_thread = tid;

    item->sfd = sfd;
    item->init_state = init_state;
    item->event_flags = event_flags;
    item->read_buffer_size = read_buffer_size;
    item->transport = transport;
    item->mode = queue_new_conn;
    item->ssl = ssl;

    // 将消息放入对应线程队列中
    cq_push(thread->new_conn_queue, item);

    MEMCACHED_CONN_DISPATCH(sfd, (int64_t)thread->thread_id);
    //写入一个'c',通知工作线程,准备接受套接字
    buf[0] = 'c';
    if (write(thread->notify_send_fd, buf, 1) != 1) {
        perror("Writing to thread notify pipe");
    }
}

触发work thread的事件处理函数thread_libevent_process,将用户连接加入自己的event_base中,并监听读写请求。

用户请求处理

work thread的事件处理函数同样是event_handler,然后通过drive_machine处理,当用户有请求时,进入conn_read状态,开始读取socket:

//状态机转换
static void drive_machine(conn *c) {
	...
	case conn_read: //开始读
		res = try_read_network(c);
		case READ_DATA_RECEIVED:
                conn_set_state(c, conn_parse_cmd);
	...
}

try_read_network会把用户数据读取到conn的缓存区中,接着设置状态为conn_parse_cmd,开始解析数据:

//状态机转换
static void drive_machine(conn *c) {
	...
	case conn_parse_cmd:    //解析命令
            c->noreply = false;
            if (c->try_read_command(c) == 0) {  //尝试进行解析
	...
}

try_read_command用来读取coon缓存区中的命令,然后调用process_command处理命令,根据不同的命令,调用命令处理函数,例如get命令:process_get_command;set命令:process_update_command等
命令处理完成之后,如果需要向用户返回响应,则设置状态为conn_write,调用transmit函数写数据。

你可能感兴趣的:(Memcached,memcached,网络,redis,nosql,缓存)