与Redis类似,Memcached也是基于内存的KV缓存系统,与Redis的不同之处主要有以下几点:
在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函数写数据。