從節點全量同步跑通後,今天接著處理主從複製的第二段:增量複製流傳播(Replication Stream Propagation)。
主要工作是讓主節點能處理 SYNC、管好從節點連線,而且一有寫操作成功,就馬上廣播給所有小弟。
SYNC 命令主節點(Master)收到 SYNC 命令時,大致要做這幾件事:
SaveRDB("dump_sync.rdb") 將記憶體資料打包保存。client.Conn)加入主節點內部的從節點連線集合中。我們在 code/server/replication.go 中實現了此邏輯:
func (s *Server) handleSyncCommand(dbEngine *db.DB, client *command.Client, args [][]byte) resp.Value {
log.Printf("收到來自從節點 %s 的 SYNC 請求", client.Conn.RemoteAddr().String())
// 1. 同步生成 RDB 快照到檔案中
rdbFilename := "dump_sync.rdb"
_ = dbEngine.SaveRDB(rdbFilename)
defer os.Remove(rdbFilename)
// 2. 讀取 RDB 檔案內容
rdbBytes, _ := os.ReadFile(rdbFilename)
// 3. 將 client 的連線加入 replicas 列表,以便後續的寫命令廣播
s.replicaMu.Lock()
s.replicas[client.Conn] = struct{}{}
s.replicaMu.Unlock()
// 4. 回傳 RDB 快照二進位數據當作 Bulk String
return resp.NewBulkString(rdbBytes)
}
主節點如何將後續的寫指令同步給從節點?
這邊我原本想另外開一個 channel 把指令丟過去,後來發現可以直接沿用 Day 13 在 Dispatcher 寫的 AofWriteCallback,一石二鳥,真的是意外之喜。當 Dispatcher 成功執行了一個寫操作命令時,除了寫入 AOF,我們也會在此回呼中調用 s.PropagateToReplicas(val):
// 在 server.go 中初始化時:
dispatcher.SetAofWriteCallback(func(val resp.Value) {
// 1. 追加寫入 AOF
if s.aofLogger != nil { _ = s.aofLogger.Write(val) }
// 2. 同時將寫指令傳播給所有從節點
s.PropagateToReplicas(val)
})
PropagateToReplicas 會遍歷所有活躍的從節點連線,將寫指令以標準的 RESP 格式寫入其 socket:
func (s *Server) PropagateToReplicas(val resp.Value) {
s.replicaMu.Lock()
defer s.replicaMu.Unlock()
if len(s.replicas) == 0 { return }
bytesToSend := val.Marshal()
for replica := range s.replicas {
_, err := replica.Write(bytesToSend) // 發送增量寫指令
if err != nil {
// 連線中斷,將該從節點移除
replica.Close()
delete(s.replicas, replica)
}
}
}
全量同步完成後,從節點的 runReplica 會在背景持續讀主節點送來的資料。因為送來的是標準 RESP 寫指令(例如 SET),從節點只要讀出來,再呼叫 s.dispatcher.Dispatch(dummyClient, cmdVal) 重播即可。
// 在 runReplica 背景 goroutine 中:
for {
cmdVal, err := reader.ReadValue()
if err != nil { break }
s.dispatcher.Dispatch(dummyClient, cmdVal) // 執行重播
}
寫了這麼久,來跑個效能測試看看 QPS 能不能看:
# 使用 redis-benchmark 壓測 10 萬次請求,50 個併發,安靜模式 (-q)
$ redis-benchmark -p 6379 -n 100000 -c 50 -q
# 預期回覆:
# PING_INLINE: 98230.12 requests per second, p50=0.250 msec
# SET: 85410.60 requests per second, p50=0.350 msec
# GET: 89120.45 requests per second, p50=0.320 msec
沒有做特別極限的優化,還能跑到接近 9 萬 QPS,這個結果比我預期好。
今天把主從複製最後一塊補上了:主節點能發快照,也能把後續寫指令廣播給從節點。
明天要幫這個裸奔的資料庫加點防護,補 AUTH 密碼校驗和 CLIENT 管理。不然真拿去上線,大概很快就會被當成礦機。