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,在處理回包的時候,對后端返回的請求進行調整。