package pitr import ( "compress/gzip" "context" "encoding/json" "errors" "fmt" "github.com/alibaba/polardbx-operator/pkg/binlogtool/algo" "github.com/alibaba/polardbx-operator/pkg/binlogtool/tx" "github.com/alibaba/polardbx-operator/pkg/hpfs/common" "github.com/alibaba/polardbx-operator/pkg/hpfs/config" hpfs "github.com/alibaba/polardbx-operator/pkg/hpfs/proto" "github.com/go-logr/logr" "github.com/google/uuid" pkgErrors "github.com/pkg/errors" "golang.org/x/exp/slices" "google.golang.org/grpc" "io" "math" "net/http" "os" "path/filepath" "sort" "strconv" "strings" "time" ) const ( CopyBufferSize = 1 << 20 ) type Step func(pCtx *Context) error func MustMarshalJSON(obj interface{}) string { b, err := json.Marshal(obj) if err != nil { panic(err) } return string(b) } func LoadAllBinlog(pCtx *Context) error { pCtx.Logger.Info("LoadAllBinlog...") taskConfig := pCtx.TaskConfig if taskConfig == nil { return errors.New("taskConfig must not be nil") } hpfsClient, err := NewHpfsClient(taskConfig.HpfsEndpoint) if err != nil { return pkgErrors.Wrap(err, fmt.Sprintf("failed to get hpfs client, endpoint=%s", taskConfig.HpfsEndpoint)) } filestreamIp, filestreamPort := common.ParseNetAddr(taskConfig.FsEndpoint) restoreBinlogs := make([]RestoreBinlog, 0) for _, xStore := range taskConfig.XStores { if xStore.GlobalConsistent { pCtx.ConsistentXStoreCount += 1 } for _, pod := range xStore.Pods { if pod.Host == "" { continue } var binlogSources []BinlogSource pCtx.Logger.Info("list local binlog list", "pod", pod.PodName, "host", pod.Host, "logDir", pod.LogDir) resp, err := hpfsClient.ListLocalBinlogList(context.Background(), &hpfs.ListLocalBinlogListRequest{ Host: &hpfs.Host{NodeName: pod.Host}, LogDir: pod.LogDir, }) if err != nil { pCtx.Logger.Error(err, "failed to list local binlog list", "pod", pod.PodName, "host", pod.Host, "logDir", pod.LogDir) return err } if resp.Version != "" { for _, binlogFile := range resp.GetBinlogFiles() { binlogFilename := filepath.Base(binlogFile) absoluteFilepath := filepath.Join(pod.LogDir, binlogFilename) binlogSources = append(binlogSources, BinlogSource{ Filename: binlogFilename, LSource: &LocalSource{ FsIp: filestreamIp, FsPort: filestreamPort, NodeName: pod.Host, DataFilepath: absoluteFilepath, }, BinlogChecksum: taskConfig.BinlogChecksum, Version: resp.Version, Timestamp: taskConfig.Timestamp, StartIndex: xStore.BackupSetStartIndex, }) } } pCtx.Logger.Info("finish list local binlog list", "pod", pod.PodName, "host", pod.Host, "logDir", pod.LogDir, "response", MustMarshalJSON(resp)) pCtx.Logger.Info("remote binlog list", "pod", pod.PodName, "host", pod.Host, "logDir", pod.LogDir) remoteResp, err := hpfsClient.ListRemoteBinlogList(context.Background(), &hpfs.ListRemoteBinlogListRequest{ Namespace: taskConfig.Namespace, PxcName: taskConfig.PxcName, PxcUid: taskConfig.PxcUid, XStoreName: xStore.XStoreName, XStoreUid: xStore.XStoreUid, SinkName: taskConfig.SinkName, SinkType: taskConfig.SinkType, PodName: pod.PodName, }) if err != nil { pCtx.Logger.Error(err, "failed to list local binlog list", "pod", pod.PodName, "host", pod.Host, "logDir", pod.LogDir) return err } pCtx.Logger.Info("finish remote binlog list", "pod", pod.PodName, "host", pod.Host, "logDir", pod.LogDir, "resp", MustMarshalJSON(remoteResp)) versionFilesMap := map[string]map[string][]string{} for _, file := range remoteResp.GetFiles() { elements := strings.Split(file, "/") if len(elements) < 4 { err := errors.New(fmt.Sprintf("invaid filepath = %s", file)) pCtx.Logger.Error(err, "failed to get version") return err } version := elements[len(elements)-4] _, ok := versionFilesMap[version] if !ok { versionFilesMap[version] = map[string][]string{} } filename := filepath.Base(file) index := 0 if strings.HasSuffix(filename, ".txt") { filename = filename[:len(filename)-4] index = 1 } if versionFilesMap[version][filename] == nil { versionFilesMap[version][filename] = make([]string, 2) } versionFilesMap[version][filename][index] = file } for version, versionVal := range versionFilesMap { for filename, vals := range versionVal { dataFilepath := vals[0] metaFilepath := vals[1] if dataFilepath == "" || metaFilepath == "" { err := fmt.Errorf("invalid filepath, dataFilepath = %s, metaFilepath = %s", dataFilepath, metaFilepath) return err } binlogSources = append(binlogSources, BinlogSource{ Filename: filename, RSource: &RemoteSource{ FsIp: filestreamIp, FsPort: filestreamPort, Sink: &config.Sink{ Name: taskConfig.SinkName, Type: taskConfig.SinkType, }, DataFilepath: dataFilepath, MetaFilepath: metaFilepath, }, BinlogChecksum: taskConfig.BinlogChecksum, Version: version, Timestamp: taskConfig.Timestamp, StartIndex: xStore.BackupSetStartIndex, }) } } groupedBinlogSources := groupBinlogSources(binlogSources) for k, bs := range groupedBinlogSources { version, err := strconv.ParseInt(k, 10, 64) if err != nil { return fmt.Errorf("failed to parse version = %s", k) } restoreBinlogs = append(restoreBinlogs, RestoreBinlog{ GlobalConsistent: xStore.GlobalConsistent, PxcName: taskConfig.PxcName, PxcUid: taskConfig.PxcUid, XStoreName: xStore.XStoreName, XStoreUid: xStore.XStoreUid, PodName: pod.PodName, StartIndex: xStore.BackupSetStartIndex, Timestamp: taskConfig.Timestamp, Version: uint64(version), HeartbeatSname: xStore.HeartbeatSname, Sources: bs, spillFilepath: filepath.Join(taskConfig.SpillDirectory, uuid.New().String()), }) } } } pCtx.RestoreBinlogs = restoreBinlogs return nil } func groupBinlogSources(sources []BinlogSource) map[string][]BinlogSource { result := map[string][]BinlogSource{} for _, source := range sources { _, ok := result[source.Version] if !ok { result[source.Version] = []BinlogSource{} } result[source.Version] = append(result[source.Version], source) } for k := range result { sort.Slice(result[k], func(i, j int) bool { return result[k][i].GetBinlogFileNum() < result[k][j].GetBinlogFileNum() }) tmpSources := make([]BinlogSource, 0) for _, source := range result[k] { if len(tmpSources) > 0 && tmpSources[len(tmpSources)-1].Filename == source.Filename { if tmpSources[len(tmpSources)-1].RSource == nil { tmpSources[len(tmpSources)-1].RSource = source.RSource } else if tmpSources[len(tmpSources)-1].LSource == nil { tmpSources[len(tmpSources)-1].LSource = source.LSource } continue } tmpSources = append(tmpSources, source) } result[k] = tmpSources } return result } func NewHpfsClient(endpoint string) (hpfs.HpfsServiceClient, error) { hpfsConn, err := grpc.Dial(endpoint, grpc.WithInsecure()) if err != nil { return nil, err } return hpfs.NewHpfsServiceClient(hpfsConn), nil } func PrepareBinlogMeta(pCtx *Context) error { pCtx.Logger.Info("PrepareBinlogMeta...") flags := map[string]bool{} newRestoreBinlogs := make([]RestoreBinlog, 0, len(pCtx.RestoreBinlogs)/2) for _, restoreBinlog := range pCtx.RestoreBinlogs { _, ok := flags[restoreBinlog.XStoreName] if !ok { err := restoreBinlog.SearchByTimestamp() if err != nil { pCtx.Logger.Error(err, "failed to SearchByTimestamp", "pxcName", restoreBinlog.PxcName, "xStoreName", restoreBinlog.XStoreName, "podName", restoreBinlog.PodName, "version", restoreBinlog.Version) return err } if restoreBinlog.CheckValid() { err := restoreBinlog.SpillResultSources() if err != nil { pCtx.Logger.Error(err, fmt.Sprintf("failed to spill result sources of podName = %s, version = %d", restoreBinlog.PodName, restoreBinlog.Version)) return err } newRestoreBinlogs = append(newRestoreBinlogs, restoreBinlog) flags[restoreBinlog.XStoreName] = true } else { pCtx.Logger.Info("invalid searchByTimestamp result", "pxcName", restoreBinlog.PxcName, "xStoreName", restoreBinlog.XStoreName, "podName", restoreBinlog.PodName, "version", restoreBinlog.Version) } } } pCtx.RestoreBinlogs = newRestoreBinlogs if len(pCtx.RestoreBinlogs) != len(pCtx.TaskConfig.XStores) { // failed. some xStore binlog does not exist between the backup set start index and the timestamp restoreBinlogXstores := make([]string, 0, len(pCtx.RestoreBinlogs)) for _, restoreBinlog := range pCtx.RestoreBinlogs { restoreBinlogXstores = append(restoreBinlogXstores, restoreBinlog.XStoreName) } slices.Sort(restoreBinlogXstores) configXStores := make([]string, 0, len(pCtx.TaskConfig.XStores)) for xStoreName, _ := range pCtx.TaskConfig.XStores { configXStores = append(configXStores, xStoreName) } slices.Sort(configXStores) err := errors.New("failed get proper binlogs") pCtx.Logger.Error(err, "", "expect", configXStores, "actual", restoreBinlogXstores) return err } return nil } func CollectInterestedTxEvents(pCtx *Context) error { pCtx.Logger.Info("CollectInterestedTxEvents...") if !pCtx.NeedConsistentPoint() { for i, _ := range pCtx.RestoreBinlogs { pCtx.RestoreBinlogs[i].LoadResultSources() } pCtx.Logger.Info("Skip CollectInterestedTxEvents...") return nil } xidCntMap := map[uint64]int{} xidTsMap := map[uint64]tx.Event{} for k, rb := range pCtx.RestoreBinlogs { if !rb.GlobalConsistent { continue } rb.LoadResultSources() xidCntMapInner := map[uint64]bool{} for _, rs := range rb.ResultSources { for _, ht := range rs.HeartbeatTxEvents { if ht.Type == tx.Commit && ht.Ts > 0 { xid := ht.XID xidTsMap[xid] = ht if _, innerOk := xidCntMapInner[xid]; !innerOk { _, ok := xidCntMap[xid] if !ok { xidCntMap[xid] = 0 } xidCntMap[xid] = xidCntMap[xid] + 1 } } } } rb.SpillResultSources() pCtx.RestoreBinlogs[k] = rb } candidateHeartbeatTxEvents := make([]tx.Event, 0) for xid, cnt := range xidCntMap { // ignore meta db if cnt == len(pCtx.RestoreBinlogs)-1 { candidateHeartbeatTxEvents = append(candidateHeartbeatTxEvents, xidTsMap[xid]) } } sort.Slice(candidateHeartbeatTxEvents, func(i, j int) bool { return candidateHeartbeatTxEvents[i].Ts < candidateHeartbeatTxEvents[j].Ts }) if len(candidateHeartbeatTxEvents) < 2 { return errors.New(fmt.Sprintf("heartbeat count expect >= 2, actual = %d", len(candidateHeartbeatTxEvents))) } beginXid := candidateHeartbeatTxEvents[len(candidateHeartbeatTxEvents)-2].XID endXid := candidateHeartbeatTxEvents[len(candidateHeartbeatTxEvents)-1].XID pCtx.CpHeartbeatXid = endXid for j, rb := range pCtx.RestoreBinlogs { err := rb.LoadResultSources() pCtx.RestoreBinlogs[j] = rb if err != nil { return err } if !rb.GlobalConsistent { continue } for k, rs := range rb.ResultSources { if len(rs.RangeTxEvents) > 0 { newRangeTxEvents := make([]tx.Event, 0) startIndex := math.MaxInt endIndex := math.MaxInt for i, txEvent := range rs.RangeTxEvents { switch txEvent.XID { case beginXid: if txEvent.Type == tx.Prepare { startIndex = i } case endXid: if txEvent.Type == tx.Commit { endIndex = i } } if i >= startIndex && i <= endIndex { newRangeTxEvents = append(newRangeTxEvents, txEvent) } } rs.RangeTxEvents = newRangeTxEvents rb.ResultSources[k] = rs } } pCtx.RestoreBinlogs[j] = rb } return nil } type myTransactionParser struct { sources []BinlogSource } func (tp *myTransactionParser) Parse(h tx.EventHandler) error { for _, source := range tp.sources { for _, txEvent := range source.RangeTxEvents { h(&txEvent) } } return nil } func Checkpoint(pCtx *Context) error { pCtx.Logger.Info("Checkpoint...") if !pCtx.NeedConsistentPoint() { pCtx.Logger.Info("Skip Checkpoint...") return nil } txParsers := map[string]tx.TransactionEventParser{} for _, restoreBinlog := range pCtx.RestoreBinlogs { if !restoreBinlog.GlobalConsistent { continue } txParsers[restoreBinlog.XStoreName] = &myTransactionParser{ sources: restoreBinlog.ResultSources, } } recoverableTxs, borders, err := algo.NewSeekConsistentPoint(txParsers, pCtx.CpHeartbeatXid).Perform() if err != nil { return err } pCtx.RecoverTxsBytes, err = algo.SerializeCpResult(recoverableTxs, borders) if err != nil { return err } pCtx.Borders = borders for i := 0; i < len(pCtx.RestoreBinlogs); i++ { restoreBinlog := pCtx.RestoreBinlogs[i] if !restoreBinlog.GlobalConsistent { continue } eOffset, ok := pCtx.Borders[restoreBinlog.XStoreName] if !ok { err := fmt.Errorf("failed to get event offset of xstore name = %s", restoreBinlog.XStoreName) pCtx.Logger.Error(err, "") return err } var found bool for j := 0; j < len(restoreBinlog.ResultSources); j++ { if restoreBinlog.ResultSources[j].getBinlogFilename() == eOffset.File { offset := eOffset.Offset restoreBinlog.ResultSources[j].TruncateLength = &offset pCtx.RestoreBinlogs[i] = restoreBinlog found = true break } } if !found { err := fmt.Errorf("impossible. event offset = %s", eOffset.String()) pCtx.Logger.Error(err, "failed to find binlog source") return err } } return nil } type httpLogger struct { mux *http.ServeMux logger logr.Logger } func (h *httpLogger) ServeHTTP(w http.ResponseWriter, r *http.Request) { requestInfo := map[string]string{} requestInfo["host"] = r.Host requestInfo["path"] = r.URL.Path vals := r.URL.Query() for k, val := range vals { requestInfo[k] = fmt.Sprintf("%+v", val) } requestId := vals.Get("requestid") h.logger.Info(MustMarshalJSON(requestInfo), "requestid", requestId) begin := time.Now().UnixMilli() h.mux.ServeHTTP(w, r) h.logger.Info(fmt.Sprintf("time cost %d ms", time.Now().UnixMilli()-begin), "requestid", requestId) } func FinishAndStartHttpServer(pCtx *Context) error { pCtx.Logger.Info("FinishAndStartHttpServer...") mux := &http.ServeMux{} mux.HandleFunc("/status", func(writer http.ResponseWriter, request *http.Request) { if pCtx.LastErr == nil { writer.WriteHeader(http.StatusOK) writer.Write([]byte("success")) } else { writer.WriteHeader(http.StatusInternalServerError) writer.Write([]byte(fmt.Sprintf("%+v", pCtx.LastErr))) } }) mux.HandleFunc("/lastErr", func(writer http.ResponseWriter, request *http.Request) { if pCtx.LastErr != nil { writer.Write([]byte(fmt.Sprintf("%+v", pCtx.LastErr))) } writer.Write([]byte("")) }) mux.HandleFunc("/context", func(writer http.ResponseWriter, request *http.Request) { writer.Write([]byte(MustMarshalJSON(pCtx))) }) mux.HandleFunc("/binlogs", func(writer http.ResponseWriter, request *http.Request) { query := request.URL.Query() xStore := query.Get("xstore") if xStore == "" { writer.Write([]byte("xstore param is required")) return } for _, restoreBinlog := range pCtx.RestoreBinlogs { if restoreBinlog.XStoreName == xStore { files := make([]HttpBinlogFileInfo, 0, len(restoreBinlog.ResultSources)) for _, source := range restoreBinlog.ResultSources { trueLength := *source.GetTrueLength() files = append(files, HttpBinlogFileInfo{ Filename: source.getBinlogFilename(), Length: &trueLength, }) } writer.Write([]byte(MustMarshalJSON(files))) return } } writer.Write([]byte("[]")) }) mux.HandleFunc("/download/binlog", func(writer http.ResponseWriter, request *http.Request) { query := request.URL.Query() xStore := query.Get("xstore") if xStore == "" { writer.WriteHeader(http.StatusNotFound) writer.Write([]byte("xstore param is required")) return } filename := query.Get("filename") if filename == "" { writer.WriteHeader(http.StatusNotFound) writer.Write([]byte("filename param is required")) } onlyMeta := query.Get("only_meta") for _, restoreBinlog := range pCtx.RestoreBinlogs { if restoreBinlog.XStoreName == xStore { for _, source := range restoreBinlog.ResultSources { if filename == source.getBinlogFilename() { if strings.EqualFold(onlyMeta, "true") { binlogSourceJsonBytes := []byte(MustMarshalJSON(source)) writer.Write(binlogSourceJsonBytes) return } reader, err := source.OpenStream() if err != nil { pCtx.Logger.Error(err, "failed to open stream", "xstoreName", xStore, "filename", filename) writer.WriteHeader(http.StatusInternalServerError) return } length := int64(*source.GetTrueLength()) writer.Header().Add("Content-Length", strconv.FormatInt(length, 10)) writer.WriteHeader(http.StatusAccepted) io.CopyBuffer(writer, io.LimitReader(reader, length), make([]byte, CopyBufferSize)) reader.Close() return } } } } writer.WriteHeader(http.StatusNotFound) }) mux.HandleFunc("/download/recovertxs", func(writer http.ResponseWriter, request *http.Request) { if pCtx.RecoverTxsBytes != nil { writer.WriteHeader(http.StatusOK) gw, _ := gzip.NewWriterLevel(writer, gzip.BestSpeed) defer gw.Close() gw.Write(pCtx.RecoverTxsBytes) } else { writer.WriteHeader(http.StatusNotFound) } }) mux.HandleFunc("/exit", func(writer http.ResponseWriter, request *http.Request) { if !pCtx.Closed.CAS(false, true) { return } exitCode := 0 if pCtx.LastErr != nil { exitCode = 1 } go func() { time.Sleep(2 * time.Second) os.Exit(exitCode) }() }) listenAddr := fmt.Sprintf(":%d", pCtx.TaskConfig.HttpServerPort) pCtx.Logger.Info("start http server, listen " + listenAddr) err := http.ListenAndServe(listenAddr, &httpLogger{mux: mux, logger: pCtx.Logger}) return err }