polardbxoperator/pkg/binlogtool/cmd/seekcp.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
}),
}