Attempt instant DDL if supported

This commit is contained in:
Morgan Tocker 2022-11-09 20:11:49 -07:00
parent 9b3fa793ac
commit 1be6a4c082
4 changed files with 76 additions and 0 deletions

View File

@ -101,6 +101,7 @@ type MigrationContext struct {
AliyunRDS bool
GoogleCloudPlatform bool
AzureMySQL bool
AttemptInstantDDL bool
config ContextConfig
configMutex *sync.Mutex

View File

@ -67,6 +67,8 @@ func main() {
flag.StringVar(&migrationContext.DatabaseName, "database", "", "database name (mandatory)")
flag.StringVar(&migrationContext.OriginalTableName, "table", "", "table name (mandatory)")
flag.StringVar(&migrationContext.AlterStatement, "alter", "", "alter statement (mandatory)")
flag.BoolVar(&migrationContext.AttemptInstantDDL, "attempt-instant-ddl", true, "Attempt to use instant DDL for this migration first.")
flag.BoolVar(&migrationContext.CountTableRows, "exact-rowcount", false, "actually count table rows as opposed to estimate them (results in more accurate progress estimation)")
flag.BoolVar(&migrationContext.ConcurrentCountTableRows, "concurrent-rowcount", true, "(with --exact-rowcount), when true (default): count rows after row-copy begins, concurrently, and adjust row estimate later on; when false: first count rows, then start row copy")
flag.BoolVar(&migrationContext.AllowedRunningOnMaster, "allow-on-master", false, "allow this migration to run directly on master. Preferably it would run on a replica")

View File

@ -188,6 +188,54 @@ func (this *Applier) ValidateOrDropExistingTables() error {
return nil
}
// AttemptInstantDDL attempts to use instant DDL (from MySQL 8.0, and earlier in Aurora and some others.)
// to apply the ALTER statement immediately. If it errors, the original
// gh-ost algorithm can be used. However, if it's successful -- a lot
// of time can potentially be saved. Instant operations include:
// - Adding a column
// - Dropping a column
// - Dropping an index
// - Extending a varchar column
// It is safer to attempt the change than try and parse the DDL, since
// there might be specifics about the table which make it not possible to apply instantly.
func (this *Applier) AttemptInstantDDL() error {
query := fmt.Sprintf(`ALTER /* gh-ost */ TABLE %s.%s %s, ALGORITHM=INSTANT`,
sql.EscapeName(this.migrationContext.DatabaseName),
sql.EscapeName(this.migrationContext.OriginalTableName),
this.migrationContext.AlterStatementOptions,
)
this.migrationContext.Log.Infof("INSTANT DDL Query is: %s", query)
err := func() error {
tx, err := this.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
sessionQuery := fmt.Sprintf(`SET SESSION time_zone = '%s'`, this.migrationContext.ApplierTimeZone)
sessionQuery = fmt.Sprintf("%s, %s", sessionQuery, this.generateSqlModeQuery())
if _, err := tx.Exec(sessionQuery); err != nil {
return err
}
if _, err := tx.Exec(query); err != nil {
this.migrationContext.Log.Infof("INSTANT DDL failed: %s", err)
return err
}
if err := tx.Commit(); err != nil {
// Neither SET SESSION nor ALTER are really transactional, so strictly speaking
// there's no need to commit; but let's do this the legit way anyway.
return err
}
return nil
}()
return err
}
// CreateGhostTable creates the ghost table on the applier host
func (this *Applier) CreateGhostTable() error {
query := fmt.Sprintf(`create /* gh-ost */ table %s.%s like %s.%s`,

View File

@ -351,12 +351,26 @@ func (this *Migrator) Migrate() (err error) {
if err := this.initiateInspector(); err != nil {
return err
}
// In MySQL 8.0 (and possibly earlier) some DDL statements can be applied instantly.
// As just a metadata change. We can't detect this unless we attempt the statement
// (i.e. there is no explain for DDL).
if this.migrationContext.AttemptInstantDDL {
this.migrationContext.Log.Infof("Attempting to execute ALTER TABLE as INSTANT DDL")
if err := this.attemptInstantDDL(); err == nil {
this.migrationContext.Log.Infof("Success! Table %s.%s migrated instantly", sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName))
return nil
} else {
this.migrationContext.Log.Infof("INSTANT DDL failed, will proceed with original algorithm: %s", err)
}
}
if err := this.initiateStreaming(); err != nil {
return err
}
if err := this.initiateApplier(); err != nil {
return err
}
if err := this.createFlagFiles(); err != nil {
return err
}
@ -734,6 +748,17 @@ func (this *Migrator) initiateServer() (err error) {
return nil
}
// attemptInstantDDL tries to apply the DDL statement directly to the table
// using a ALGORITHM=INSTANT assertion. If this fails, it will return an error,
// in which case the original algorithm should be used.
func (this *Migrator) attemptInstantDDL() (err error) {
this.applier = NewApplier(this.migrationContext)
if err := this.applier.InitDBConnections(); err != nil {
return err
}
return this.applier.AttemptInstantDDL()
}
// initiateInspector connects, validates and inspects the "inspector" server.
// The "inspector" server is typically a replica; it is where we issue some
// queries such as: