Memcached 源码分析--网络模型流程分析

一、功能介绍

Memcached 是一个高性能的分布式内存对象缓存系统,用于动态Web应用以减轻数据库负载。
它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高动态、数据库驱动网站的速度。
Memcached基于一个存储键/值对的hashmap。其守护进程(daemon)是用C写的,但是客户端可以用任何语言来编写,
并通过memcached协议与守护进程通信。

memcached缺乏认证以及安全管制,这代表应该将memcached服务器放置在防火墙后。

memcached的API使用32位元的循环冗余校验(CRC-32)计算键值后,将资料分散在不同的机器上。
当表格满了以后,接下来新增的资料会以LRU机制替换掉。

memcached是一款非常普及的服务器端缓存软件,memcached主要是基于Libevent库进行开发的

memcached服务器端并没有分布式功能、各个memcached不会互相通信以共享信息、完全取决于客户端的实现达到分布式。

研究版本:memcached-1.4.31

二、网络线程模型

主要涉及两个主要文件:memcached.c 和thread.c文件


主线程主要用于监听accpet客户端的Socket连接,而工作线程主要用于接管具体的客户端连接。
主线程和工作线程之间主要通过基于Libevent的pipe的读写事件来监听,当有连接连接上来的时候,主线程会将连接交个某一个工作线程去接管,后期客户端和服务端的读写工作都会在这个工作线程中进行。
工作线程也是基于Libevent的事件的,当有读或者写的事件进来的时候,就会触发事件的回调函数。


分析后的整体流程图解:


主线程负责客户端的套接字监听、当有新的连接到来时则均衡选择工作线程、工作线程负责读写并处理相应的命令。



main函数@memcached.c 文件了解整个构建逻辑


int main (int argc, char **argv) {
	/* handle SIGINT and SIGTERM */
    signal(SIGINT, sig_handler);
    signal(SIGTERM, sig_handler);

    /* init settings */
    settings_init();

    /* Run regardless of initializing it later */
    init_lru_crawler();
    init_lru_maintainer();

	hash_init(hash_type);
	
	/* initialize main thread libevent instance */
    main_base = event_init();

	/* start up worker threads if MT mode */
	// 这个方法主要用来创建工作线程,默认会创建8个工作线程
    memcached_thread_init(settings.num_threads);

	// 根据启动配置可以创建TCP/UDP协议套接字
	server_sockets(settings.port, tcp_transport, portnumber_file);
	server_sockets(settings.udpport, udp_transport, portnumber_file);

    /* enter the event loop */
    // 进入主线程的事件循环
	event_base_loop(main_base, 0);	
}


先看主线程的监听工作:

<pre name="code" class="cpp">server_sockets(settings.port, tcp_transport, portnumber_file);
-> server_socket(settings.inter, port, transport, portnumber_file);

/**
 * Create a socket and bind it to a specific port number
 */
static int server_socket(const char *interface,
                         int port,
                         enum network_transport transport,
                         FILE *portnumber_file) {
   
   // socket的bind、listen、setopt 等操作创建socket server
   socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
   fcntl(sfd, F_SETFL, flags | O_NONBLOCK);
   setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, (void *)&flags, sizeof(flags));
   setsockopt(sfd, SOL_SOCKET, SO_KEEPALIVE, (void *)&flags, sizeof(flags));
   setsockopt(sfd, SOL_SOCKET, SO_LINGER, (void *)&ling, sizeof(ling));
   setsockopt(sfd, IPPROTO_TCP, TCP_NODELAY, (void *)&flags, sizeof(flags));
   bind(sfd, next->ai_addr, next->ai_addrlen);
   
   // state的类型为:conn_listening 监听类型套接字、那么在哪里进行accept.
   listen_conn_add = conn_new(sfd, conn_listening, EV_READ | EV_PERSIST, 1,transport, main_base);                
}

conn *conn_new(const int sfd, enum conn_states init_state,...){
	// 这里首先会将监听套接字加入到libevent体系中,并设定event_handler事件回调函数
    event_set(&c->event, sfd, event_flags, event_handler, (void *)c);
    event_base_set(base, &c->event);
    c->ev_flags = event_flags;
	event_add(&c->event, 0);
}

void event_handler(const int fd, const short which, void *arg) {
	drive_machine(c); // 异步fd编程最重要的状态机
}

// 
static void drive_machine(conn *c) {
	while (!stop) {
		case conn_listening: // 调用accept接受客户端的socket连接代码 
			sfd = accept(c->sfd, (struct sockaddr *)&addr, &addrlen);
			
			// 客户端用socket连接上来,则会调用这个分发逻辑的函数、分发到工作线程接管具体的读写操作
			dispatch_conn_new(sfd, conn_new_cmd, EV_READ | EV_PERSIST, DATA_BUFFER_SIZE, c->transport);
			break;
			
		case conn_read:
			IS_UDP(c->transport) ? try_read_udp(c) : try_read_network(c);
			break;
		case conn_parse_cmd :
			try_read_command(c);
			break;
		case conn_write:
			add_iov(c, c->wcurr, c->wbytes);
			break;
		...			
	}
}


 
dispatch_conn_new函数@thread.c 

void dispatch_conn_new(int sfd, enum conn_states init_state, int event_flags...){
	// 申请一块CQ_ITEM的内存块,用于存储连接的基本信息
	CQ_ITEM *item = cqi_new();
	
	// 通过求余数的方法来得到当前的连接需要哪个工作线程接管、且记录每次最后一次使用的工作线程
	// 通过最后记录就可以让工作线程进入一个轮询,保证了每个工作线程处理的连接数的平衡-相同连接数
	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;
	// 工作线程的队列中放入CQ_ITEM
    cq_push(thread->new_conn_queue, item);
	
	// 向工作线程的pipe中写入数据,只为唤醒工作线程
	buf[0] = 'c';
	write(thread->notify_send_fd, buf, 1);
}


总结一下主线程工作:
1、memcached首先在主线程中会创建main_base,memcached的主线程的主要工作就是监听和接收listen和accpet新进入的连接。
2、当用户有连接进来的时候,main thread主线程会通过libevent驱动在状态机中accept新的连接。
3、main thread主线程会通过求余的方式选择一个worker thread工作线程。

主线程处程流程图:




再看工作线程的工作:

在main中调用 memcached_thread_init 创建工作线程的函数:

<pre name="code" class="cpp">/*
 * Initializes the thread subsystem, creating various worker threads.
 *
 * nthreads  Number of worker event handler threads to spawn
 */
void memcached_thread_init(int nthreads) {
	for (i = 0; i < nthreads; i++) {
        int fds[2];
        // 创建pipe,主要用于主线程和工作线程之间的通信
        if (pipe(fds)) {
            perror("Can't create notify pipe");
            exit(1);
        }

		// 每个线程的LIBEVENT_THREAD基本数据结构
        threads[i].notify_receive_fd = fds[0];
        threads[i].notify_send_fd = fds[1];

		// 主要是创建每个线程自己的libevent的event_base 
        setup_thread(&threads[i]);
        /* Reserve three fds for the libevent base, and two for the pipe */
        stats_state.reserved_fds += 5;
    }
    
    /* Create threads after we've done all the libevent setup. */
    for (i = 0; i < nthreads; i++) {
        create_worker(worker_libevent, &threads[i]);
    }
}

/*
 * Set up a thread's information.
 */
static void setup_thread(LIBEVENT_THREAD *me) {
	/* Listen for notifications from other threads */
    event_set(&me->notify_event, me->notify_receive_fd, EV_READ | EV_PERSIST, thread_libevent_process, me);
    event_base_set(me->base, &me->notify_event);
	event_add(&me->notify_event, 0) ;
	
	// 初始化一个工作队列
	me->new_conn_queue = malloc(sizeof(struct conn_queue));
	cq_init(me->new_conn_queue);
}


 
在些注册回调函数 thread_libevent_process 用于数据读事件监控。create_worker@thread.c 就是调用 pthread_create 创建线程、执行worker_libevent函数实体。 

/*
 * Worker thread: main event loop
 */
static void *worker_libevent(void *arg) {
	register_thread_initialized();

	// 每个线程中都会有自己独立的event_base和事件的循环机制、独立处理自己接管的连接。
    event_base_loop(me->base, 0);
}


创建工作线程的工作结束后、那么工作线程如何配合主线程工作的。主线程接收到accept新的连接后,
调用 dispatch_conn_new 分配一个工作线程并往队列中写入数据,利用管道唤醒工作线程函数 thread_libevent_process。

<pre name="code" class="cpp">/*
 * Processes an incoming "handle a new connection" item. This is called when
 * input arrives on the libevent wakeup pipe.
 */
static void thread_libevent_process(int fd, short which, void *arg) {
	// 主线程中如果有新的连接,会向其中一个线程的pipe中写入1
	read(fd, buf, 1);
	
	//从工作线程的队列中获取一个CQ_ITEM连接信息、如果item不为空,则需要进行连接后的接管。
    item = cq_pop(me->new_conn_queue); 
    if(item != NULL){
    	//conn_new这个方法非常重要,主要是创建socket的读写等监听事件。  
        //init_state 为初始化的类型,主要在drive_machine中通过这个状态类判断处理类型  
        conn *c = conn_new(item->sfd, item->init_state, item->event_flags,  
                           item->read_buffer_size, item->transport, me->base);  
    	...
    }
}


 

又通过conn_new函数介入到event_handler->drive_machine状态机中,如此就形成由数据事件驱动的完整运行态。


总结一下工作线程工作:
1、memcached启动的时候会初始化N个worker thread工作线程、默认是8个可配置。
2、worker thread工作线程和main thread主线程之间主要通过pipe来进行通信。
3、main thread会将当前用户的连接信息放入一个CQ_ITEM,并且将CQ_ITEM放入这个线程的conn_queue处理队列,然后主线程会通过写入pipe的方式来通知worker thread工作线程。
4、工作线程得到主线程通知,从自已的conn_queue队列中取得一条连接信息进行处理,创建libevent的socket读写事件
5、工作线程会监听用户的socket,如果用户有消息传递过来,则会进行消息解析和处理,返回相应的结果。

工作线程数据处理流程:




  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值