github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/pkg/checker/binlog.go (about)

     1  // Copyright 2021 PingCAP, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package checker
    15  
    16  import (
    17  	"context"
    18  	"crypto/tls"
    19  	"database/sql"
    20  	"fmt"
    21  	"os"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/go-mysql-org/go-mysql/mysql"
    26  	"github.com/go-mysql-org/go-mysql/replication"
    27  	"github.com/pingcap/tidb/pkg/util"
    28  	"github.com/pingcap/tidb/pkg/util/dbutil"
    29  	"github.com/pingcap/tiflow/dm/config"
    30  	"github.com/pingcap/tiflow/dm/config/dbconfig"
    31  	"github.com/pingcap/tiflow/dm/pkg/conn"
    32  	tcontext "github.com/pingcap/tiflow/dm/pkg/context"
    33  	"github.com/pingcap/tiflow/dm/pkg/gtid"
    34  	"github.com/pingcap/tiflow/dm/pkg/log"
    35  	"github.com/pingcap/tiflow/dm/pkg/terror"
    36  	"github.com/pingcap/tiflow/dm/pkg/utils"
    37  	"github.com/pingcap/tiflow/pkg/errors"
    38  )
    39  
    40  // MySQLBinlogEnableChecker checks whether `log_bin` variable is enabled in MySQL.
    41  type MySQLBinlogEnableChecker struct {
    42  	db     *sql.DB
    43  	dbinfo *dbutil.DBConfig
    44  }
    45  
    46  // NewMySQLBinlogEnableChecker returns a RealChecker.
    47  func NewMySQLBinlogEnableChecker(db *sql.DB, dbinfo *dbutil.DBConfig) RealChecker {
    48  	return &MySQLBinlogEnableChecker{db: db, dbinfo: dbinfo}
    49  }
    50  
    51  // Check implements the RealChecker interface.
    52  func (pc *MySQLBinlogEnableChecker) Check(ctx context.Context) *Result {
    53  	result := &Result{
    54  		Name:  pc.Name(),
    55  		Desc:  "check whether mysql binlog is enabled",
    56  		State: StateFailure,
    57  		Extra: fmt.Sprintf("address of db instance - %s:%d", pc.dbinfo.Host, pc.dbinfo.Port),
    58  	}
    59  
    60  	value, err := dbutil.ShowLogBin(ctx, pc.db)
    61  	if err != nil {
    62  		markCheckError(result, err)
    63  		return result
    64  	}
    65  	if strings.ToUpper(value) != "ON" {
    66  		result.Errors = append(result.Errors, NewError("log_bin is %s, and should be ON", value))
    67  		result.Instruction = "MySQL as source: please refer to the document to enable the binlog https://dev.mysql.com/doc/refman/5.7/en/replication-howto-masterbaseconfig.html"
    68  		return result
    69  	}
    70  	result.State = StateSuccess
    71  	return result
    72  }
    73  
    74  // Name implements the RealChecker interface.
    75  func (pc *MySQLBinlogEnableChecker) Name() string {
    76  	return "mysql_binlog_enable"
    77  }
    78  
    79  /*****************************************************/
    80  
    81  // MySQLBinlogFormatChecker checks mysql binlog_format.
    82  type MySQLBinlogFormatChecker struct {
    83  	db     *sql.DB
    84  	dbinfo *dbutil.DBConfig
    85  }
    86  
    87  // NewMySQLBinlogFormatChecker returns a RealChecker.
    88  func NewMySQLBinlogFormatChecker(db *sql.DB, dbinfo *dbutil.DBConfig) RealChecker {
    89  	return &MySQLBinlogFormatChecker{db: db, dbinfo: dbinfo}
    90  }
    91  
    92  // Check implements the RealChecker interface.
    93  func (pc *MySQLBinlogFormatChecker) Check(ctx context.Context) *Result {
    94  	result := &Result{
    95  		Name:  pc.Name(),
    96  		Desc:  "check whether mysql binlog_format is ROW",
    97  		State: StateFailure,
    98  		Extra: fmt.Sprintf("address of db instance - %s:%d", pc.dbinfo.Host, pc.dbinfo.Port),
    99  	}
   100  
   101  	value, err := dbutil.ShowBinlogFormat(ctx, pc.db)
   102  	if err != nil {
   103  		markCheckError(result, err)
   104  		return result
   105  	}
   106  	if strings.ToUpper(value) != "ROW" {
   107  		result.Errors = append(result.Errors, NewError("binlog_format is %s, and should be ROW", value))
   108  		result.Instruction = "MySQL as source: please execute 'set global binlog_format=ROW;'; AWS Aurora (MySQL)/RDS MySQL as source: please refer to the document to create a new DB parameter group and set the binlog_format=row: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_WorkingWithDBInstanceParamGroups.html. Then modify the instance to use the new DB parameter group and restart the instance to take effect."
   109  		return result
   110  	}
   111  	result.State = StateSuccess
   112  
   113  	return result
   114  }
   115  
   116  // Name implements the RealChecker interface.
   117  func (pc *MySQLBinlogFormatChecker) Name() string {
   118  	return "mysql_binlog_format"
   119  }
   120  
   121  /*****************************************************/
   122  
   123  var (
   124  	mysqlBinlogRowImageRequired   MySQLVersion = [3]uint{5, 6, 2}
   125  	mariaDBBinlogRowImageRequired MySQLVersion = [3]uint{10, 1, 6}
   126  )
   127  
   128  // MySQLBinlogRowImageChecker checks mysql binlog_row_image.
   129  type MySQLBinlogRowImageChecker struct {
   130  	db     *sql.DB
   131  	dbinfo *dbutil.DBConfig
   132  }
   133  
   134  // NewMySQLBinlogRowImageChecker returns a RealChecker.
   135  func NewMySQLBinlogRowImageChecker(db *sql.DB, dbinfo *dbutil.DBConfig) RealChecker {
   136  	return &MySQLBinlogRowImageChecker{db: db, dbinfo: dbinfo}
   137  }
   138  
   139  // Check implements the RealChecker interface.
   140  // 'binlog_row_image' is introduced since mysql 5.6.2, and mariadb 10.1.6.
   141  // > In MySQL 5.5 and earlier, full row images are always used for both before images and after images.
   142  // So we need check 'binlog_row_image' after mysql 5.6.2 version and mariadb 10.1.6.
   143  // ref:
   144  // - https://dev.mysql.com/doc/refman/5.6/en/replication-options-binary-log.html#sysvar_binlog_row_image
   145  // - https://mariadb.com/kb/en/library/replication-and-binary-log-server-system-variables/#binlog_row_image
   146  func (pc *MySQLBinlogRowImageChecker) Check(ctx context.Context) *Result {
   147  	result := &Result{
   148  		Name:  pc.Name(),
   149  		Desc:  "check whether mysql binlog_row_image is FULL",
   150  		State: StateFailure,
   151  		Extra: fmt.Sprintf("address of db instance - %s:%d", pc.dbinfo.Host, pc.dbinfo.Port),
   152  	}
   153  
   154  	// check version firstly
   155  	value, err := dbutil.ShowVersion(ctx, pc.db)
   156  	if err != nil {
   157  		markCheckError(result, err)
   158  		return result
   159  	}
   160  
   161  	version, err := toMySQLVersion(value)
   162  	if err != nil {
   163  		markCheckError(result, err)
   164  		return result
   165  	}
   166  
   167  	// for mysql.version < 5.6.2,  we don't need to check binlog_row_image.
   168  	if !version.Ge(mysqlBinlogRowImageRequired) {
   169  		result.State = StateSuccess
   170  		return result
   171  	}
   172  
   173  	// for mariadb.version < 10.1.6.,  we don't need to check binlog_row_image.
   174  	if conn.IsMariaDB(value) && !version.Ge(mariaDBBinlogRowImageRequired) {
   175  		result.State = StateSuccess
   176  		return result
   177  	}
   178  
   179  	value, err = dbutil.ShowBinlogRowImage(ctx, pc.db)
   180  	if err != nil {
   181  		markCheckError(result, err)
   182  		return result
   183  	}
   184  	if strings.ToUpper(value) != "FULL" {
   185  		result.Errors = append(result.Errors, NewError("binlog_row_image is %s, and should be FULL", value))
   186  		result.Instruction = "MySQL as source: please execute 'set global binlog_row_image = FULL;'; AWS Aurora (MySQL)/RDS MySQL as source: please refer to the document to create a new DB parameter group and set the binlog_row_image = FULL: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_WorkingWithDBInstanceParamGroups.html Then modify the instance to use the new DB parameter group and restart the instance to take effect."
   187  		return result
   188  	}
   189  	result.State = StateSuccess
   190  	return result
   191  }
   192  
   193  // Name implements the RealChecker interface.
   194  func (pc *MySQLBinlogRowImageChecker) Name() string {
   195  	return "mysql_binlog_row_image"
   196  }
   197  
   198  // BinlogDBChecker checks if migrated dbs are in binlog_do_db or binlog_ignore_db.
   199  type BinlogDBChecker struct {
   200  	db            *conn.BaseDB
   201  	dbinfo        *dbutil.DBConfig
   202  	schemas       map[string]struct{}
   203  	caseSensitive bool
   204  }
   205  
   206  // NewBinlogDBChecker returns a RealChecker.
   207  func NewBinlogDBChecker(db *conn.BaseDB, dbinfo *dbutil.DBConfig, schemas map[string]struct{}, caseSensitive bool) RealChecker {
   208  	newSchemas := make(map[string]struct{}, len(schemas))
   209  	for schema := range schemas {
   210  		newSchemas[schema] = struct{}{}
   211  	}
   212  	return &BinlogDBChecker{db: db, dbinfo: dbinfo, schemas: newSchemas, caseSensitive: caseSensitive}
   213  }
   214  
   215  // Check implements the RealChecker interface.
   216  func (c *BinlogDBChecker) Check(ctx context.Context) *Result {
   217  	result := &Result{
   218  		Name:  c.Name(),
   219  		Desc:  "check whether migrated dbs are in binlog_do_db/binlog_ignore_db",
   220  		State: StateFailure,
   221  		Extra: fmt.Sprintf("address of db instance - %s:%d", c.dbinfo.Host, c.dbinfo.Port),
   222  	}
   223  
   224  	flavor, err := conn.GetFlavor(ctx, c.db)
   225  	if err != nil {
   226  		markCheckError(result, err)
   227  		return result
   228  	}
   229  	tctx := tcontext.NewContext(ctx, log.L())
   230  	binlogDoDB, binlogIgnoreDB, err := conn.GetBinlogDB(tctx, c.db, flavor)
   231  	if err != nil {
   232  		markCheckError(result, err)
   233  		return result
   234  	}
   235  	if !c.caseSensitive {
   236  		binlogDoDB = strings.ToLower(binlogDoDB)
   237  		binlogIgnoreDB = strings.ToLower(binlogIgnoreDB)
   238  	}
   239  	binlogDoDBs := strings.Split(binlogDoDB, ",")
   240  	binlogIgnoreDBs := strings.Split(binlogIgnoreDB, ",")
   241  	// MySQL will check –binlog-do-db first, if there are any options,
   242  	// it will apply this one and ignore –binlog-ignore-db. If the
   243  	// –binlog-do-db is NOT set, then mysql will check –binlog-ignore-db.
   244  	// If both of them are empty, it will log changes for all DBs.
   245  	if len(binlogDoDB) != 0 {
   246  		for _, doDB := range binlogDoDBs {
   247  			delete(c.schemas, doDB)
   248  		}
   249  		if len(c.schemas) > 0 {
   250  			dbs := utils.SetToSlice(c.schemas)
   251  			result.Errors = append(result.Errors, NewWarn("these dbs [%s] are not in binlog_do_db[%s]", strings.Join(dbs, ","), binlogDoDB))
   252  			result.Instruction = "Ensure that the do_dbs contains the dbs you want to migrate"
   253  			return result
   254  		}
   255  	} else {
   256  		ignoreDBs := []string{}
   257  		for _, ignoreDB := range binlogIgnoreDBs {
   258  			if _, ok := c.schemas[ignoreDB]; ok {
   259  				ignoreDBs = append(ignoreDBs, ignoreDB)
   260  			}
   261  		}
   262  		if len(ignoreDBs) > 0 {
   263  			result.Errors = append(result.Errors, NewWarn("these dbs [%s] are in binlog_ignore_db[%s]", strings.Join(ignoreDBs, ","), binlogIgnoreDB))
   264  			result.Instruction = "Ensure that the ignore_dbs does not contain the dbs you want to migrate"
   265  			return result
   266  		}
   267  	}
   268  	result.State = StateSuccess
   269  	return result
   270  }
   271  
   272  // Name implements the RealChecker interface.
   273  func (c *BinlogDBChecker) Name() string {
   274  	return "binlog_do_db/binlog_ignore_db check"
   275  }
   276  
   277  // MetaPositionChecker checks if meta position for given source database is valid.
   278  type MetaPositionChecker struct {
   279  	db         *conn.BaseDB
   280  	sourceCfg  dbconfig.DBConfig
   281  	enableGTID bool
   282  	meta       *config.Meta
   283  }
   284  
   285  // NewBinlogDBChecker returns a RealChecker.
   286  func NewMetaPositionChecker(db *conn.BaseDB, sourceCfg dbconfig.DBConfig, enableGTID bool, meta *config.Meta) RealChecker {
   287  	return &MetaPositionChecker{db: db, sourceCfg: sourceCfg, enableGTID: enableGTID, meta: meta}
   288  }
   289  
   290  // Check implements the RealChecker interface.
   291  func (c *MetaPositionChecker) Check(ctx context.Context) *Result {
   292  	result := &Result{
   293  		Name:  c.Name(),
   294  		Desc:  "check whether meta position is valid for db",
   295  		State: StateFailure,
   296  		Extra: fmt.Sprintf("address of db instance - %s:%d", c.sourceCfg.Host, c.sourceCfg.Port),
   297  	}
   298  
   299  	var tlsConfig *tls.Config
   300  	var err error
   301  	if c.sourceCfg.Security != nil {
   302  		if loadErr := c.sourceCfg.Security.LoadTLSContent(); loadErr != nil {
   303  			markCheckError(result, loadErr)
   304  			result.Instruction = "please check upstream tls config"
   305  			return result
   306  		}
   307  		tlsConfig, err = util.NewTLSConfig(
   308  			util.WithCAContent(c.sourceCfg.Security.SSLCABytes),
   309  			util.WithCertAndKeyContent(c.sourceCfg.Security.SSLCertBytes, c.sourceCfg.Security.SSLKeyBytes),
   310  			util.WithVerifyCommonName(c.sourceCfg.Security.CertAllowedCN),
   311  			util.WithMinTLSVersion(tls.VersionTLS10),
   312  		)
   313  		if err != nil {
   314  			markCheckError(result, err)
   315  			result.Instruction = "please check upstream tls config"
   316  			return result
   317  		}
   318  	}
   319  
   320  	flavor, err := conn.GetFlavor(ctx, c.db)
   321  	if err != nil {
   322  		markCheckError(result, err)
   323  		result.Instruction = "please check upstream database config"
   324  		return result
   325  	}
   326  
   327  	// always use a new random serverID
   328  	randomServerID, err := conn.GetRandomServerID(tcontext.NewContext(ctx, log.L()), c.db)
   329  	if err != nil {
   330  		// should never happened unless the master has too many slave
   331  		markCheckError(result, terror.Annotate(err, "fail to get random server id for relay reader"))
   332  		return result
   333  	}
   334  
   335  	h, _ := os.Hostname()
   336  	h = "dm-checker-" + h
   337  	// https://github.com/mysql/mysql-server/blob/1bfe02bdad6604d54913c62614bde57a055c8332/include/my_hostname.h#L33-L42
   338  	if len(h) > 60 {
   339  		h = h[:60]
   340  	}
   341  
   342  	syncCfg := replication.BinlogSyncerConfig{
   343  		ServerID:  randomServerID,
   344  		Flavor:    flavor,
   345  		Host:      c.sourceCfg.Host,
   346  		Port:      uint16(c.sourceCfg.Port),
   347  		User:      c.sourceCfg.User,
   348  		Password:  c.sourceCfg.Password,
   349  		TLSConfig: tlsConfig,
   350  		Localhost: h,
   351  	}
   352  
   353  	syncer := replication.NewBinlogSyncer(syncCfg)
   354  	defer syncer.Close()
   355  	var streamer *replication.BinlogStreamer
   356  	if c.enableGTID {
   357  		gtidSet, err2 := gtid.ParserGTID(flavor, c.meta.BinLogGTID)
   358  		if err2 != nil {
   359  			markCheckError(result, err2)
   360  			result.Instruction = "you should check your BinlogGTID's format, "
   361  			if flavor == mysql.MariaDBFlavor {
   362  				result.Instruction += "it should consist of three numbers separated with dashes '-', see https://mariadb.com/kb/en/gtid/"
   363  			} else {
   364  				result.Instruction += "it should be any combination of single GTIDs and ranges of GTID, see https://dev.mysql.com/doc/refman/8.0/en/replication-gtids-concepts.html"
   365  			}
   366  			return result
   367  		}
   368  		streamer, err = syncer.StartSyncGTID(gtidSet)
   369  	} else {
   370  		streamer, err = syncer.StartSync(mysql.Position{Name: c.meta.BinLogName, Pos: c.meta.BinLogPos})
   371  	}
   372  	if err != nil {
   373  		markCheckError(result, err)
   374  		result.Instruction = "you should make sure your meta's binlog position is valid and not purged, and the user has REPLICATION SLAVE privilege"
   375  		return result
   376  	}
   377  	// if we don't get a new event after 15s, it means there is no new event in the binlog
   378  	ctx2, cancel := context.WithTimeout(ctx, 15*time.Second)
   379  	defer cancel()
   380  	_, err = streamer.GetEvent(ctx2)
   381  	if err != nil && errors.Cause(err) != context.DeadlineExceeded {
   382  		markCheckError(result, err)
   383  		result.Instruction = "you should make sure your meta's binlog position is valid and not purged, and the user has REPLICATION SLAVE privilege"
   384  		return result
   385  	}
   386  
   387  	result.State = StateSuccess
   388  	return result
   389  }
   390  
   391  // Name implements the RealChecker interface.
   392  func (c *MetaPositionChecker) Name() string {
   393  	return "meta position check"
   394  }