diff --git a/go/base/context.go b/go/base/context.go index 270b7a0..6032a93 100644 --- a/go/base/context.go +++ b/go/base/context.go @@ -101,6 +101,7 @@ type MigrationContext struct { AliyunRDS bool GoogleCloudPlatform bool AzureMySQL bool + AttemptInstantDDL bool config ContextConfig configMutex *sync.Mutex diff --git a/go/cmd/gh-ost/main.go b/go/cmd/gh-ost/main.go index b99e70b..660c492 100644 --- a/go/cmd/gh-ost/main.go +++ b/go/cmd/gh-ost/main.go @@ -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") diff --git a/go/logic/applier.go b/go/logic/applier.go index 50fd9bd..b9313a3 100644 --- a/go/logic/applier.go +++ b/go/logic/applier.go @@ -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`, diff --git a/go/logic/migrator.go b/go/logic/migrator.go index b443d69..40415d0 100644 --- a/go/logic/migrator.go +++ b/go/logic/migrator.go @@ -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: