代码全景
代码规模不大只有1w多行,而且功能划分的比较清晰,包括:
- 事件处理: event/nc_epoll.c、event/nc_event.h、event/nc_evport.c、event/nc_kqueue.c
- 各种Hash函数: hashkit/nc_crc16.c、hashkit/nc_crc32.c、hashkit/nc_fnv.c、hashkit /nc_hashkit.h、hashkit/nc_hsieh.c、hashkit/nc_jenkins.c、hashkit /nc_ketama.c、hashkit/nc_md5.c、hashkit/nc_modula.c、hashkit/nc_murmur.c、 -hashkit/nc_one_at_a_time.c、hashkit/nc_random.c
- 协议: proto/nc_memcache.c、proto/nc_proto.h、proto/nc_redis.c
- 自定义的数据类型: nc_array.c、nc_array.h、nc_string.c、nc_string.h
- 网络通信相关: nc_connection.c、nc_connection.h、nc_client.c、nc_client.h、nc_proxy.c、nc_proxy.h
- 信号处理: nc_signal.c、nc_signal.h
- 关键数据结构和算法: nc_rbtree.h、nc_rbtree.c、nc_queue.h、nc_request.c、nc_response.c、nc_mbuf.c、 nc_mbuf.h、- nc_message.c、nc_message.h、nc_server.c、nc_server.h
- 统计、日志和工具: nc_stats.c、nc_stats.h、nc_log.c、nc_log.h、nc_util.c、nc_util.h
- 配置文件: nc_conf.c、nc_conf.h
- 主程序: nc.c、nc_core.c、nc_core.h
一些技术实现
- 采用epoll + 回调的单线程工作模式,单线程epoll监听,每个事件触发则调用相应的回调函数
- 可监听多个地址,客户端向这些地址发起连接请求,每个地址将请求转发到后端的server池
实现上是通过创建多个socket,监听多个地址,然后加入epoll监听中,单线程epoll_wait监听请求
- 三种连接:proxy:监听客户端连接、client:处理客户端数据连接、server:后端server的连接,每种连接都会保存起读写回调函数,每个连接会用一个conn数据结构表示,相关的读写回调函数会记录在此,每新建一个连接时会加入到时间管理器的监听事件中(每个连接有一个fd),并将该conn赋值给epoll_event.data.ptr,以便事件触发时得到该conn,从而调用到相应的回调函数,proxy连接的回调函数是在conn_get_proxy函数中初始化的,client和server连接的回调函数是在conn_get函数中初始化的
- 事件驱动消息流转:多个in/out队列,消息根据当前的处理流程会进入到某个队列中,从后端server得到回包后,会将请求的msg和回包的msg关联起来,然后client连接找到相应的回包并发送给客户端,整个流程是事件驱动的
- 总共2个线程,主线程和stats线程
- stats线程实现原理:一个简单的http服务,即请求一个http的url,返回stats数据,实现原理:创建一个新的stats线程,epoll_create创建新的事件管理器,创建监听fd,加入epoll的监听事件中并设置回调函数,有请求来了调用相应的回调函数进行处理,默认30秒触发一次
- 超时处理:请求后端server如果配置了超时,则会进行超时处理,具体是发送给后端请求后,如果规定时间没返回,会超时关闭客户端连接,实现上是通过红黑树实现,key是超时的时间,value是连接,每次epoll从红黑树得到最小的时间(最左节点)赋值给ctx->timeout,然后传入epoll_wait的第四个参数,epoll_wait(ep, event, nevent, timeout),到达规定时间后epoll_wait会返回,实现对超时事件的处理
在redis中,超时事件的实现是通过一个双向链表,每次遍历得到最早超时事件,然后传入epoll_wait的参数中的
- 事件驱动实现:提供了统一的上层接口,底层封装了epoll、kqueue、evport实现,跟redis事件驱动实现类似
- 实现了一些核心数据结构和算法:红黑树、一致性hash算法(用来选择后端server,将请求路由到后端某个server去)、变长字符串、变长数组、队列(实现来源于github.com/freebsd/fre…
- 守护进程实现:nc_daemonize函数
- 连接缓冲池技术&连接的LRU管理:每个后端server维护一个s_conn_q连接队列,建立的连接放到该队列中,每次需要建立该server的连接就判断是否超过最大连接数(pool->server_connections,默认为1),没有则建立新的连接插入队列,超过则从队头取连接conn并插入到队尾(即LRU算法),然后返回这个连接,完成对连接的复用,代码见conn_get函数
- 空闲连接队列free_connq,三种连接:proxy,client,server在释放连接时会将连接放到空闲连接队列中,需要建立连接时从该空闲连接队列取连接,而不是重新分配内存,相当于一个空闲连接池,三种连接复用该空闲连接池,简单理解就是一个连接数据结构的缓存池,不用每次重新分配
- 消息结构的缓存池技术:读取消息需要先分配msg结构,优先从空闲msg队列free_msgq中获取,相当于一个msg缓冲池
一次请求的处理流程
三种连接
- proxy connection, 用来监听用户建立连接的请求,建立连接成功后会对应产生一个客户端连接
- client connection,由建连成功后产生,用户读写数据都是通过 client connection 解析请求后,根据 key 和哈希规则选择一个 server 进行转发
- server connection,转发用户请求到缓存资源并接收和解析响应数据转回 client connection,client connection 将响应返回到用户
三个队列
上图的 client connection 之所以没有 imsgq 是因为请求解析完可以直接进入 server 的 imsgq
- 用户通过 proxy connection 建立连接,产生一个 client connection
- client connection 开始读取用户的请求数据,并将完整的请求根据 key 和设置的哈希规则选择 server, 然后将这个请求存放到 server 的 imsgq
- 接着 server connection 发送 imsgq 请求到远程资源,发送完成之后(写 tcp buffer) 就会将 msg 从 imsgq 迁移到 omsgq,响应回来之后从 omsgq 队列里面找到这个对应的 msg 以及 client connection
- 最后将响应内容放到 client connection 的 omsgq,由 client connection 将数据发送回客户端。
连接的回调函数
proxy连接:
conn->recv = proxy_recv;
conn->recv_next = NULL;
conn->recv_done = NULL;
conn->send = NULL;
conn->send_next = NULL;
conn->send_done = NULL;
conn->close = proxy_close;
conn->active = NULL;
conn->ref = proxy_ref;
conn->unref = proxy_unref;
client连接:
conn->recv = msg_recv;
conn->recv_next = req_recv_next;
conn->recv_done = req_recv_done;
conn->send = msg_send;
conn->send_next = rsp_send_next;
conn->send_done = rsp_send_done;
conn->close = client_close;
conn->active = client_active;
conn->ref = client_ref;
conn->unref = client_unref;
conn->enqueue_inq = NULL;
conn->dequeue_inq = NULL;
conn->enqueue_outq = req_client_enqueue_omsgq;
conn->dequeue_outq = req_client_dequeue_omsgq;
server连接:
conn->recv = msg_recv;
conn->recv_next = rsp_recv_next;
conn->recv_done = rsp_recv_done;
conn->send = msg_send;
conn->send_next = req_send_next;
conn->send_done = req_send_done;
conn->close = server_close;
conn->active = server_active;
conn->ref = server_ref;
conn->unref = server_unref;
conn->enqueue_inq = req_server_enqueue_imsgq;
conn->dequeue_inq = req_server_dequeue_imsgq;
conn->enqueue_outq = req_server_enqueue_omsgq;
conn->dequeue_outq = req_server_dequeue_omsgq;
* Client+ Proxy Server+
* (nutcracker)
* .
* msg_recv {read event} . msg_recv {read event}
* + . +
* | . |
* \ . /
* req_recv_next . rsp_recv_next
* + . +
* | . | Rsp
* req_recv_done . rsp_recv_done <===
* + . +
* | . |
* Req \ . /
* ===> req_filter* . *rsp_filter
* + . +
* | . |
* \ . /
* req_forward-// (a) . (c) \-rsp_forward
* .
* .
* msg_send {write event} . msg_send {write event}
* + . +
* | . |
* Rsp' \ . / Req'
* <=== rsp_send_next . req_send_next ===>
* + . +
* | . |
* \ . /
* rsp_send_done-// (d) . (b) //-req_send_done
*
*
* (a) -> (b) -> (c) -> (d) is the normal flow of transaction consisting
* of a single request response, where (a) and (b) handle request from
* client, while (c) and (d) handle the corresponding response from the
* server.
请求处理流转
前文说过三种连接,都注册到了事件管理器中,并保存了读写回调函数(proxy连接只有读函数,因为只需要accept接收client连接请求),当有相应的事件触发,就会调用相应的回调函数进行处理,一个请求的处理流程为:client发起连接请求 -> client发起命令请求 -> server连接可写 -> server连接可读 -> client连接可写,下面按这些事件依次介绍,可以看到msg使用到了三个队列(颜色标识):
client发起连接请求
client发起命令请求
server连接可写事件触发
server返回回包触发可读事件
client连接可写事件触发
参考文章
| 文章地址 | 说明 |
|---|---|
| www.cnblogs.com/foxmailed/p… | 源码解析,请求消息的流转过程和处理过程 |
| idning.github.io/twemproxy-s… | 内容很全,各个介绍,源码分析 |
| hulkdev.com/posts-meitu… | 美图多进程 twemproxy 开源实现:github.com/bitleak/twe… |
| juejin.cn/post/684490… | twemproxy源码分析,核心流程介绍 |
| github.com/joeylichang… | Twemproxy介绍 |
| xie.infoq.cn/article/2ee… | 结合nginx架构优化twemproxy |
| xie.infoq.cn/article/2ee… | 二次开发,Nginx master-worker机制引入到twemproxy |
| github.com/twitter/twe… | twemproxy添加ping命令修改 |