predixy源码学习

Predixy是一个代理,代理本质上就是用来转发请求的。其主要功能就是接收客户端的请求,然后把客户端请求转发给redis服务端,在redis服务端处理完消息请求后,接收它的响应,并把这个响应返回给客户端。

1.整体架构
Predixy的架构比较简单,它采用多线程的模式。入口代码逻辑也比较清晰:

Main函数里创建一个代理,然后在init方法里面创建多个处理线程Handler,handler的个数是由配置文件配置,这个一般可以根据CPU的核数来配置。Proxy初始化完成之后,就在run里面起handler线程,Proxy里面有一个用来保存handler的vector,创建的多个handler之间基本上是相互独立。整个predixy的流程基本就在handler的run方法中,入口是Handler::run()

Handler是一个事件循环。mEventLoop是创建Handler时初始化的Multiplexor(后续的Multiplexor我们统一以epoll为例),在mEventLoop->wait里面处理注册到epoll上的fd事件。
注册到epoll里面的事件主要有:
1、连接事件:在创建Handler的时候,创建mEventLoop,并把监听事件加入到epoll里面
2、已经建立连接的客户端的收包和回包
3、到redis服务端的请求转发和回包消息转发给客户端
主要的两个函数是wait和postEvent:wait里面主要是处理客户端请求读取,redis回包读取,postEvent主要是处理客户端请求转发给redis和redis回包转发给客户端;在wait中收到的包,会在接下来的postEvent中立马尽量发出去,如果一次发送不完,会注册写事件到epoll中,等可写了会进行再次发送。

2.事件循环
上面介绍了大体的逻辑,下面我们来仔细看看事件循环里两个主要的函数wait和postEvent。
Wait的入口在EpollMultiplexor::wait,有事件来了之后,就循环调用handler的handlerEvent方法,入口在Handler::handleEvent:

这里我们看到有3种连接的类型
1、ListenType
监听的socket,在创建监听socket的时候指定的类型
2、AcceptType
连接建立后,就会创建一个AcceptConnection,类型转为AcceptType
3、ConnectType
客户端有消息过来时,连接是AcceptConnection,读取了客户端消息,在转发时,需要拿到predixy和redis的连接来做这个转发的动作,这时候的连接就是ConnectConnection。
handleListenEvent:主要是处理接收连接,把新来的连接加入到epoll
handleAcceptConnectionEvent:读取客户端的消息AcceptConnection::readEvent,如果有可写事件,则会addPostEvent,使得前面没发完的消息可以再次发送。
readEvent里面会读消息–>AcceptConnection::parse解析请求,把请求放入mRequests队列—>Handler::handleRequest

然后根据路由,选择server,getConnectConnection,把消息加入到mSendRequests队列。
handleConnectConnectionEvent:和handleAcceptConnectionEvent是类似的,不过处理的是响应的消息,读从redis到predixy的回包,然后ConnectConnection::handleResponse
PostEvent入口是Handler::postEvent(),这里会统一处理写,postConnectConnectionEvent是predixy往redis写消息,写了之后会把请求放到mSentRequests队列;postAcceptConnectionEvent是predixy往客户端写。

这里的写操作都会先进行一个合包fill,然后掉接口写,如果写完了,会把写事件删掉,如果一次没写完,会把写事件加到epoll里面,等epoll可写了就会返回写事件。

3.排队机制
从上面我们可以看到predixy的处理流程涉及到几个队列,主要是下面3个:
AcceptConnection::mRequests : 客户端连接的请求队列
ConnectConncetion::mSendRequests : 到redis端的待发送队列
ConnectConncetion::mSentRequests : 到redis端的已发送队列
这几个队列的配合关系如下图所示:

1、客户端发送请求给predixy,predixy先将req消息放到AcceptConnection::mRequests队列
2、解析消息后,将req放到对应redis连接的ConnectConncetion::mSendRequests队列里面,这里可能有多个redis,所以这一步需要先路由到具体的server
3、Predixy转发请求给redis
4、将req从ConnectConncetion::mSendRequests 转移到ConnectConncetion::mSentRequests
5、Redis处理消息完毕回响应给predixy
6、Predixy将响应消息对应的req从ConnectConncetion::mSentRequests移除,并通知对应的AcceptConnection
7、AcceptConnection接收到响应后吧消息返回给客户端并出队
这里有一个细节,predixy需要保证同一个连接先发送的请求需要先回复,因此predixy收到redis回包后,从mSentRequests中取出头部请求,只有当mRequests队首的请求完成了才能回,否则在mRequests队列里面等待

postEvent在回包的时候,则会从队首开始,对所有done为true的请求处理回包。

4.路由选择
在predixy向redis转发请求时,需要先根据路由策略取到server,得知是向哪个redis转发,然后从server获取ConnectConncetion进行操作。

在Proxy::init中会先根据配置初始化ServerPool,ServerPool里面有一个个ServerGroup,ServerGroup里面是Server

StandaloneServerPool和ClusterServerPool中serverGroup的组织方式不同:StandaloneServerPool中的mGroupPool是配置文件指定哪些server属于同一个ServerGroup,
此外,ClusterServerPool中,每个ServerGroup负责若干个slots,而StandaloneServerPool,每个group也负责一部分数据,负责的数据的方式取决于mDist的值,有modula和random 2种。StandaloneServerPool和ClusterServerPool都有std::vector mServerPool,用于实现randomkey。
从ServerPool中获取server分2个步骤:
1、找到ServerGroup,如何寻找取决于数据分布方式
2、从ServerGroup中找一个server,如何寻找取决于命令的读写属性,节点的角色和权重
每种ServerPool需要实现下面几个接口:
GetServerFunc mGetServerFunc; //从ServerPool获取server
RefreshRequestFunc mRefreshrequestFunc; //发送请求,刷新server信息
HandleResponseFunc mHandleResponseFunc; //处理刷新server请求的回包
Server中只有server的ip和port信息,具体的连接由每个handler自己维护连接池:mConnectionPool

5.共享连接和独占连接
Predixy有2种使用到后端连接的方式
SharedConnection :无上下文的请求使用
PrivateConnection :有上下文的请求使用
每个handler为每个Server都维护2类连接池mPrivateConns和mShareConns。PrivateConnection是每个带Context的请求独占一个,一般这类请求通过长连接发起,predixy上维护的请求数量也是有限的。

6.多key命令的拆分
多key命令,例如mget、mset、de、unlink这类命令在parse的时候,会拆开。例如:mget a b c会拆分成mget a,mget b。Mget c;一个请求被拆分成多个请求依次入队列,会将第一个设置为leader,后续的设置为follower,在处理回包的时候,对后端返回的请求进行调整。