500 lines
13 KiB
Go
500 lines
13 KiB
Go
//go:build polardbx
|
|
|
|
/*
|
|
Copyright 2022 Alibaba Group Holding Limited.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"bufio"
|
|
"compress/gzip"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/alibaba/polardbx-operator/pkg/binlogtool/algo"
|
|
"github.com/alibaba/polardbx-operator/pkg/binlogtool/binlog"
|
|
"github.com/alibaba/polardbx-operator/pkg/binlogtool/binlog/meta"
|
|
"github.com/alibaba/polardbx-operator/pkg/binlogtool/tx"
|
|
"github.com/alibaba/polardbx-operator/pkg/binlogtool/utils"
|
|
)
|
|
|
|
func parseOffset(s string) (*binlog.EventOffset, error) {
|
|
if s == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
colonIdx := strings.IndexRune(s, ':')
|
|
if colonIdx < 0 {
|
|
return nil, errors.New("invalid binlog offset")
|
|
}
|
|
file, offsetStr := s[:colonIdx], s[colonIdx+1:]
|
|
offset, err := strconv.ParseUint(offsetStr, 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &binlog.EventOffset{File: file, Offset: offset}, nil
|
|
}
|
|
|
|
type binlogRange struct {
|
|
Start *binlog.EventOffset
|
|
End *binlog.EventOffset
|
|
}
|
|
|
|
func parseBinlogRange(s string) (binlogRange, error) {
|
|
seps := strings.Split(s, ",")
|
|
if len(seps) > 2 {
|
|
return binlogRange{}, errors.New("more than 2")
|
|
}
|
|
|
|
if len(seps) == 1 {
|
|
startOff, err := parseOffset(seps[0])
|
|
if err != nil {
|
|
return binlogRange{}, err
|
|
}
|
|
return binlogRange{Start: startOff}, nil
|
|
} else {
|
|
startOff, err := parseOffset(seps[0])
|
|
if err != nil {
|
|
return binlogRange{}, err
|
|
}
|
|
endOff, err := parseOffset(seps[0])
|
|
if err != nil {
|
|
return binlogRange{}, err
|
|
}
|
|
if strings.Split(startOff.File, ".")[0] != strings.Split(endOff.File, ".")[0] {
|
|
return binlogRange{}, errors.New("range invalid: basename not match")
|
|
}
|
|
if endOff.File < startOff.File || (startOff.File == endOff.File && startOff.Offset > endOff.Offset) {
|
|
return binlogRange{}, errors.New("range invalid: end < start")
|
|
}
|
|
return binlogRange{Start: startOff, End: endOff}, nil
|
|
}
|
|
}
|
|
|
|
type binlogRangesValue struct {
|
|
lastDir string
|
|
ranges map[string]binlogRange
|
|
}
|
|
|
|
func (v *binlogRangesValue) Set(s string) error {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
|
|
if v.lastDir == "" {
|
|
return errors.New("directory is not provided")
|
|
}
|
|
|
|
r, err := parseBinlogRange(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if r.Start != nil || r.End != nil {
|
|
v.ranges[v.lastDir] = r
|
|
}
|
|
|
|
v.lastDir = ""
|
|
return nil
|
|
}
|
|
|
|
func (v *binlogRangesValue) String() string {
|
|
return ""
|
|
}
|
|
|
|
func (v *binlogRangesValue) Type() string {
|
|
return ""
|
|
}
|
|
|
|
type binlogFileValue binlogRangesValue
|
|
|
|
func (v *binlogFileValue) Set(s string) error {
|
|
v.lastDir = s
|
|
return nil
|
|
}
|
|
|
|
func (v *binlogFileValue) String() string {
|
|
return ""
|
|
}
|
|
|
|
func (v *binlogFileValue) Type() string {
|
|
return ""
|
|
}
|
|
|
|
var (
|
|
seekCpDirectory string
|
|
seekCpBinlogRanges map[string]binlogRange
|
|
seekCpHeartbeatTxid uint64
|
|
seekCpBinEvents bool
|
|
seekCpVerbose bool
|
|
seekCpOutput string
|
|
)
|
|
|
|
func init() {
|
|
seekCpBinlogRanges = make(map[string]binlogRange)
|
|
brv := &binlogRangesValue{ranges: seekCpBinlogRanges}
|
|
|
|
seekCpCmd.Flags().Var((*binlogFileValue)(brv), "binlog-stream", "target binlog stream, must be followed with --binlog-range")
|
|
seekCpCmd.Flags().Var(brv, "binlog-range", `binlog range with format [[file1:start_index],[file2:end_index], e.g.
|
|
"mysql-bin.000000:123" -- [mysql-bin.000000:123, EOF]
|
|
"mysql-bin.000000:123,mysql-bin.000001:456" -- [mysql-bin.000000:123, mysql-bin.000001:456]
|
|
",mysql-bin.000001:456" -- [BEGIN, mysql-bin.000002:456]`)
|
|
seekCpCmd.Flags().BoolVar(&seekCpBinEvents, "binary-events", false, "use binary format events instead (end with .evs)")
|
|
seekCpCmd.Flags().Uint64Var(&seekCpHeartbeatTxid, "heartbeat-txid", 0, "heartbeat transaction id")
|
|
seekCpCmd.Flags().BoolVarP(&seekCpVerbose, "verbose", "v", false, "verbose mode")
|
|
seekCpCmd.Flags().StringVar(&seekCpOutput, "output", "", "output file (binary format)")
|
|
|
|
rootCmd.AddCommand(seekCpCmd)
|
|
}
|
|
|
|
var sortedBinaryLogsByStream map[string][]meta.BinlogFile
|
|
|
|
func printBinlogFiles() {
|
|
for streamName, binaryLogs := range sortedBinaryLogsByStream {
|
|
r := seekCpBinlogRanges[streamName]
|
|
fmt.Printf(" %s: ", streamName)
|
|
if len(binaryLogs) > 1 {
|
|
if r.Start != nil && r.End != nil {
|
|
fmt.Printf("%s-%s\n", r.Start.String(), r.End.String())
|
|
} else if r.Start != nil {
|
|
fmt.Printf("%s-%s\n", r.Start.String(), binaryLogs[len(binaryLogs)-1].String())
|
|
} else if r.End != nil {
|
|
fmt.Printf("%s-%s\n", binaryLogs[0].String(), r.End.String())
|
|
} else {
|
|
fmt.Printf("%s-%s\n", binaryLogs[0].String(), binaryLogs[len(binaryLogs)-1].String())
|
|
}
|
|
} else {
|
|
if r.Start != nil && r.End != nil {
|
|
fmt.Printf("%s:[%d-%d]\n", binaryLogs[0].String(), r.Start.Offset, r.End.Offset)
|
|
} else if r.Start != nil {
|
|
fmt.Printf("%s:[%d-]\n", binaryLogs[0].String(), r.Start.Offset)
|
|
} else if r.End != nil {
|
|
fmt.Printf("%s:[-%d]\n", binaryLogs[0].String(), r.End.Offset)
|
|
} else {
|
|
fmt.Printf("%s\n", binaryLogs[0].String())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func buildBinlogFiles() map[string][]meta.BinlogFile {
|
|
dir, err := os.Open(seekCpDirectory)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
defer dir.Close()
|
|
|
|
m := make(map[string][]meta.BinlogFile)
|
|
entries, err := dir.ReadDir(-1)
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
files, err := listBinlogFiles(path.Join(seekCpDirectory, entry.Name()))
|
|
if err != nil {
|
|
fmt.Println("List binlog files in " + entry.Name() + " failed: " + err.Error())
|
|
os.Exit(1)
|
|
}
|
|
if len(files) == 0 {
|
|
continue
|
|
}
|
|
m[entry.Name()] = files
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
func writeRecoverableConsistentPoint(recoverableTxs []uint64, borders map[string]binlog.EventOffset) error {
|
|
if len(seekCpOutput) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Layout
|
|
// TXID LENGTH -- 4 bytes
|
|
// REPEAT
|
|
// TXID -- 8 bytes
|
|
// STREAM LENGTH -- 2 bytes
|
|
// REPEAT
|
|
// STREAM NAME LEN -- 1 byte
|
|
// STREAM NAME -- len bytes
|
|
// OFFSET BINLOG FILE NAME LEN -- 1 byte
|
|
// OFFSET BINLOG FILE NAME -- len bytes
|
|
// OFFSET -- 8 bytes
|
|
|
|
f, err := os.OpenFile(seekCpOutput, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
w := bufio.NewWriter(f)
|
|
defer w.Flush()
|
|
|
|
gw, err := gzip.NewWriterLevel(w, gzip.BestSpeed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer gw.Close()
|
|
bytes, err := algo.SerializeCpResult(recoverableTxs, borders)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gw.Write(bytes)
|
|
return nil
|
|
}
|
|
|
|
func doSeekConsistentPoint(txParsers map[string]tx.TransactionEventParser, heartbeatTxid uint64) {
|
|
fmt.Println("=================== SEEK CONSISTENT POINTS ===================")
|
|
|
|
recoverableTxs, borders, err := algo.NewSeekConsistentPoint(txParsers, heartbeatTxid).Perform()
|
|
if err != nil {
|
|
_, _ = fmt.Fprintln(os.Stderr, "ERROR: "+err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("TOTAL RECOVERABLE TRANSACTIONS: %d\n", len(recoverableTxs))
|
|
if seekCpVerbose {
|
|
for i := 0; i < len(recoverableTxs); i += 5 {
|
|
r := i + 5
|
|
if r > len(recoverableTxs) {
|
|
r = len(recoverableTxs)
|
|
}
|
|
fmt.Printf(" %s\n", utils.JoinIntegerSequence(recoverableTxs[i:r], ", "))
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
fmt.Println("BINLOG INDEXES: ")
|
|
for name, border := range borders {
|
|
fmt.Printf(" %s: %s\n", name, border.String())
|
|
}
|
|
fmt.Println()
|
|
|
|
if err := writeRecoverableConsistentPoint(recoverableTxs, borders); err != nil {
|
|
_, _ = fmt.Fprintln(os.Stderr, "ERROR: "+err.Error())
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func listBinlogFiles(dir string) ([]meta.BinlogFile, error) {
|
|
d, err := os.Open(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
filenames, err := d.Readdirnames(-1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := 0; i < len(filenames); i++ {
|
|
filenames[i] = path.Join(dir, filenames[i])
|
|
}
|
|
return meta.BinaryLogFilesConsistentAndContinuousInName(filenames)
|
|
}
|
|
|
|
func buildTransactionParsers(name string, binlogFiles []meta.BinlogFile) (tx.TransactionEventParser, error) {
|
|
basename := binlogFiles[0].Basename
|
|
r := seekCpBinlogRanges[name]
|
|
startBinlogOffset, endBinlogOffset := r.Start, r.End
|
|
var startBf, endBf meta.BinlogFile
|
|
var err error
|
|
if startBinlogOffset != nil {
|
|
if strings.Split(startBinlogOffset.File, ".")[0] != basename {
|
|
return nil, errors.New("start offset invalid: basename not match")
|
|
}
|
|
if startBinlogOffset.File < binlogFiles[0].String() ||
|
|
startBinlogOffset.File > binlogFiles[len(binlogFiles)-1].String() {
|
|
return nil, errors.New("start offset invalid: range exceeds")
|
|
}
|
|
startBf, err = meta.ParseBinlogFile(startBinlogOffset.File)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if endBinlogOffset != nil {
|
|
if strings.Split(endBinlogOffset.File, ".")[0] != basename {
|
|
return nil, errors.New("end offset invalid: basename not match")
|
|
}
|
|
if endBinlogOffset.File < binlogFiles[0].String() ||
|
|
endBinlogOffset.File > binlogFiles[len(binlogFiles)-1].String() {
|
|
return nil, errors.New("end offset invalid: range exceeds")
|
|
}
|
|
endBf, err = meta.ParseBinlogFile(endBinlogOffset.File)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
scanners := make([]binlog.LogEventScanner, 0, len(binlogFiles))
|
|
for _, file := range binlogFiles {
|
|
if startBinlogOffset != nil && file.Index < startBf.Index {
|
|
continue
|
|
}
|
|
if endBinlogOffset != nil && file.Index > endBf.Index {
|
|
continue
|
|
}
|
|
|
|
startOffset := uint64(0)
|
|
opts := []binlog.LogEventScannerOption{
|
|
binlog.WithBinlogFile(file.String()),
|
|
binlog.WithLogEventHeaderFilter(tx.TransactionEventParserHeaderFilter()),
|
|
binlog.EnableChecksumValidation,
|
|
}
|
|
if startBinlogOffset != nil && file.Index == startBf.Index {
|
|
startOffset = startBinlogOffset.Offset
|
|
}
|
|
if endBinlogOffset != nil && file.Index == endBf.Index {
|
|
opts = append(opts, binlog.WithEndPos(endBinlogOffset.Offset))
|
|
}
|
|
|
|
filePath := file.File
|
|
scanners = append(scanners, binlog.NewLazyLogEventScanCloser(
|
|
func() (io.ReadCloser, error) {
|
|
f, err := os.Open(filePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return utils.NewSeekableBufferReader(f), nil
|
|
},
|
|
startOffset,
|
|
opts...))
|
|
}
|
|
|
|
return tx.NewTransactionEventParser(binlog.NewMultiLogEventScanner(scanners...)), nil
|
|
}
|
|
|
|
func buildAllTransactionParsers(binlogFiles map[string][]meta.BinlogFile) (map[string]tx.TransactionEventParser, error) {
|
|
txParsers := make(map[string]tx.TransactionEventParser)
|
|
for name, files := range binlogFiles {
|
|
p, err := buildTransactionParsers(name, files)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
txParsers[name] = p
|
|
}
|
|
|
|
return txParsers, nil
|
|
}
|
|
|
|
func findAndCollectAllBinlogFiles() {
|
|
fmt.Println("==================== COLLECT BINARY LOGS =====================")
|
|
|
|
sortedBinaryLogsByStream = buildBinlogFiles()
|
|
|
|
if len(sortedBinaryLogsByStream) == 0 {
|
|
_, _ = fmt.Fprintln(os.Stderr, "ERROR: no binlog files found")
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("TOTAL BINLOG DIRECTORIES: %d\n", len(sortedBinaryLogsByStream))
|
|
|
|
printBinlogFiles()
|
|
|
|
fmt.Println()
|
|
|
|
fileCnt := 0
|
|
for _, files := range sortedBinaryLogsByStream {
|
|
fileCnt += len(files)
|
|
}
|
|
|
|
fmt.Printf("TOTAL BINARY LOG FILES: %d\n", fileCnt)
|
|
fmt.Println()
|
|
}
|
|
|
|
func buildBinaryEventFiles() map[string]string {
|
|
fmt.Println("=================== COLLECT BINARY EVENTS ====================")
|
|
|
|
dir, err := os.Open(seekCpDirectory)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
defer dir.Close()
|
|
|
|
m := make(map[string]string)
|
|
entries, err := dir.ReadDir(-1)
|
|
if err != nil {
|
|
_, _ = fmt.Fprintln(os.Stderr, "ERROR: unable to list dir, "+err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
for _, e := range entries {
|
|
if strings.HasSuffix(e.Name(), ".evs") {
|
|
streamName := strings.Split(e.Name(), ".")[0]
|
|
m[streamName] = path.Join(seekCpDirectory, e.Name())
|
|
}
|
|
}
|
|
|
|
fmt.Printf("TOTAL BINARY EVENT STREAMS: %d\n", len(m))
|
|
|
|
fmt.Println()
|
|
|
|
return m
|
|
}
|
|
|
|
var seekCpCmd = &cobra.Command{
|
|
Use: "seekcp [flags] directory",
|
|
Short: "Seek consistent point from binary logs / transaction events",
|
|
Long: "Seek consistent point from binary logs / transaction events",
|
|
Args: func(cmd *cobra.Command, args []string) error {
|
|
if len(args) != 1 {
|
|
return errors.New("please specify the directory")
|
|
}
|
|
seekCpDirectory = args[0]
|
|
return nil
|
|
},
|
|
Run: wrap(func(cmd *cobra.Command, args []string) error {
|
|
var txParsers map[string]tx.TransactionEventParser
|
|
if seekCpBinEvents {
|
|
// Build from binary events.
|
|
binEventFiles := buildBinaryEventFiles()
|
|
|
|
txParsers = make(map[string]tx.TransactionEventParser)
|
|
for streamName, eventFile := range binEventFiles {
|
|
f, err := os.Open(eventFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
//goland:noinspection ALL
|
|
defer f.Close()
|
|
p, err := tx.NewBinaryTransactionEventParser(bufio.NewReader(f))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
txParsers[streamName] = p
|
|
}
|
|
} else {
|
|
// Build from binary logs.
|
|
findAndCollectAllBinlogFiles()
|
|
|
|
var err error
|
|
txParsers, err = buildAllTransactionParsers(sortedBinaryLogsByStream)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
doSeekConsistentPoint(txParsers, seekCpHeartbeatTxid)
|
|
return nil
|
|
}),
|
|
}
|