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 }