1. 準備收官
RadonDB作爲一款優秀的分佈式數據庫產品,離不開這幾大組件:
- 存儲
- 計算
RadonDB的存儲由MySQL存儲節點提供,這很正常,這些存儲節點還負責了計算工作。從這個角度上來說,RadonDB還不是很典型的分佈式數據庫系統。這種架構上,計算實際上並不是由RadonDB來提供的,而是由MySQL提供的。
RadonDB的設計上還有一個精巧的地方,就是利用了TokuDB存儲了全量數據,對比較複雜的計算,需要join的地方,都由這個全量存儲來提供。因此RadonDB看着不太像一個典型的分佈式存儲系統,不過青雲實現了MySQL集羣的Raft協議,這一點還是很值得稱讚的。
昨天夜裏和今天白天我讀了很多有關Raft協議的資料,那麼今天還是來看看RadonDB是如何實現MySQL的Raft的。
2. Xenon源碼解讀
RadonDB的Raft組件叫做Xenon,在github上也有源代碼。先來看看整體的架構圖:
一個Raft集羣應該有三種狀態:Leader,Candidate和Follower。Xenon的代碼是這樣實現的:
// Leader tuple.
type Leader struct {
*Raft
// the smallest binlog which slaves executed by SQL-Thread
relayMasterLogFile string
// leader degrade to follower
isDegradeToFollower bool
// Used to wait for the async job done.
wg sync.WaitGroup
// the binlog which we should purge to
nextPuregeBinlog string
purgeBinlogTick *time.Ticker
checkSemiSyncTick *time.Ticker
// leader process heartbeat request handler
processHeartbeatRequestHandler func(*model.RaftRPCRequest) *model.RaftRPCResponse
// leader process voterequest request handler
processRequestVoteRequestHandler func(*model.RaftRPCRequest) *model.RaftRPCResponse
// leader send heartbeat request to other followers
sendHeartbeatHandler func(*bool, chan *model.RaftRPCResponse)
// leader process send heartbeat response
processHeartbeatResponseHandler func(*int, *model.RaftRPCResponse)
}
Leader要做的事情有兩件:
- 向集羣內部發送心跳
- 處理來自Client的請求
這些都是通過RPC來實現的。代碼中首先去新建了一個Leader:
// NewLeader creates new Leader.
func NewLeader(r *Raft) *Leader {
L := &Leader{
Raft: r,
}
L.initHandlers()
return L
}
現在看看這些都是幹什麼的吧。
當選舉成功以後,一個節點成爲Leader,這個時候就會去調度這個方法來新建一個Leader角色。其實新建Leader也無非就做了這麼幾件事:
// leader handlers
func (r *Leader) initHandlers() {
// heartbeat request
r.setProcessHeartbeatRequestHandler(r.processHeartbeatRequest)
// vote request
r.setProcessRequestVoteRequestHandler(r.processRequestVoteRequest)
// send heartbeat
r.setSendHeartbeatHandler(r.sendHeartbeat)
r.setProcessHeartbeatResponseHandler(r.processHeartbeatResponse)
}
這段代碼讓我看着有點恍惚,很像Java的setter方法,這些方法將各種request發送了出去,但是我有點不知道爲什麼要處理投票的請求,按理說這個時候投票都已經結束了,開始了自己的任期,以後如果有機會調試代碼再看看。
今天先挑一個發送心跳的方法來看看:
// leaderSendHeartbeatHandler
// broadcast hearbeat requests to other peers of the cluster
func (r *Leader) sendHeartbeat(mysqlDown *bool, c chan *model.RaftRPCResponse) {
// check MySQL down
if r.mysql.GetState() == mysql.MysqlDead {
*mysqlDown = true
return
}
// broadcast heartbeat
r.mutex.RLock()
defer r.mutex.RUnlock()
for _, peer := range r.peers {
r.wg.Add(1)
go func(peer *Peer) {
defer r.wg.Done()
peer.sendHeartbeat(c)
}(peer)
}
}
因爲是基於MySQL節點做的Raft,因此首先要檢查mysqld是否存活,如果死掉了也就沒有發送心跳的意義了。這裏要說明的是,不是mysql向外發送心跳,是Xenon,因此一定要檢查mysqld的狀態先,mysqld掛掉了,就不發心跳了,就可以觸發新一輪選舉了。
發送心跳的時候要遍歷所有節點,因爲要給所有節點發心跳,每遍歷一個節點,就要給WaitGroup中增加1,waitGroup可以保證goroutine安全的結束。
這之後自然就是調用發送的方法,向集羣內廣播心跳了。
3. Go語言相關
Go語言有個很有意思的地方叫做函數類型。也就是說可以把變量指定成函數類型,比如這樣:
package main
import "fmt"
func test(param string) {
fmt.Println(param)
}
func main() {
t := test
t("quan")
}
我暫時還沒有想明白函數變量的好處,都說是讓代碼變得更靈活。
這段代碼中還有一個waitGroup,參考一下下面的代碼:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func test(param int) {
defer wg.Done()
fmt.Println(param)
}
func main() {
for i := 1; i < 10; i++ {
wg.Add(1)
go test(i)
}
wg.Wait()
}
注意,Add和Done一定要配對,不然一定報deadlock錯誤。
現在畫張圖看看原理:
所有的現成完成之後都會去等待,並在此時將wg的計數器減1,計數器被減到0時,等待結束。
4. 小結
其實要是學習Raft的實現的話,我這個系列可能還得再寫一段時間。