Skip to content

Commit 45cf63c

Browse files
authored
Merge branch 'master' into ensure-test-errors-detected
2 parents 8abd584 + 3613f22 commit 45cf63c

File tree

9 files changed

+89
-1
lines changed

9 files changed

+89
-1
lines changed

doc/command-line-flags.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,22 @@ If you happen to _know_ your servers use RBR (Row Based Replication, i.e. `binlo
4545
Skipping this step means `gh-ost` would not need the `SUPER` privilege in order to operate.
4646
You may want to use this on Amazon RDS.
4747

48+
### attempt-instant-ddl
49+
50+
MySQL 8.0 supports "instant DDL" for some operations. If an alter statement can be completed with instant DDL, only a metadata change is required internally. Instant operations include:
51+
52+
- Adding a column
53+
- Dropping a column
54+
- Dropping an index
55+
- Extending a varchar column
56+
- Adding a virtual generated column
57+
58+
It is not reliable to parse the `ALTER` statement to determine if it is instant or not. This is because the table might be in an older row format, or have some other incompatibility that is difficult to identify.
59+
60+
`--attempt-instant-ddl` is disabled by default, but the risks of enabling it are relatively minor: `gh-ost` may need to acquire a metadata lock at the start of the operation. This is not a problem for most scenarios, but it could be a problem for users that start the DDL during a period with long running transactions.
61+
62+
`gh-ost` will automatically fallback to the normal DDL process if the attempt to use instant DDL is unsuccessful.
63+
4864
### conf
4965

5066
`--conf=/path/to/my.cnf`: file where credentials are specified. Should be in (or contain) the following format:

doc/shared-key.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ CREATE TABLE tbl (
2929

3030
(This is also the definition of the _ghost_ table, except that that table would be called `_tbl_gho`).
3131

32-
In this migration, the _before_ and _after_ versions contain the same unique not-null key (the PRIMARY KEY). To run this migration, `gh-ost` would iterate through the `tbl` table using the primary key, copy rows from `tbl` to the _ghost_ table `_tbl_gho` in primary key order, while also applying the binlog event writes from `tble` onto `_tbl_gho`.
32+
In this migration, the _before_ and _after_ versions contain the same unique not-null key (the PRIMARY KEY). To run this migration, `gh-ost` would iterate through the `tbl` table using the primary key, copy rows from `tbl` to the _ghost_ table `_tbl_gho` in primary key order, while also applying the binlog event writes from `tbl` onto `_tbl_gho`.
3333

3434
The applying of the binlog events is what requires the shared unique key. For example, an `UPDATE` statement to `tbl` translates to a `REPLACE` statement which `gh-ost` applies to `_tbl_gho`. A `REPLACE` statement expects to insert or replace an existing row based on its row's values and the table's unique key constraints. In particular, if inserting that row would result in a unique key violation (e.g., a row with that primary key already exists), it would _replace_ that existing row with the new values.
3535

go/base/context.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ type MigrationContext struct {
101101
AliyunRDS bool
102102
GoogleCloudPlatform bool
103103
AzureMySQL bool
104+
AttemptInstantDDL bool
104105

105106
config ContextConfig
106107
configMutex *sync.Mutex

go/cmd/gh-ost/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ func main() {
6767
flag.StringVar(&migrationContext.DatabaseName, "database", "", "database name (mandatory)")
6868
flag.StringVar(&migrationContext.OriginalTableName, "table", "", "table name (mandatory)")
6969
flag.StringVar(&migrationContext.AlterStatement, "alter", "", "alter statement (mandatory)")
70+
flag.BoolVar(&migrationContext.AttemptInstantDDL, "attempt-instant-ddl", false, "Attempt to use instant DDL for this migration first")
71+
7072
flag.BoolVar(&migrationContext.CountTableRows, "exact-rowcount", false, "actually count table rows as opposed to estimate them (results in more accurate progress estimation)")
7173
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")
7274
flag.BoolVar(&migrationContext.AllowedRunningOnMaster, "allow-on-master", false, "allow this migration to run directly on master. Preferably it would run on a replica")

go/logic/applier.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,16 @@ func (this *Applier) generateSqlModeQuery() string {
135135
return fmt.Sprintf("sql_mode = %s", sqlModeQuery)
136136
}
137137

138+
// generateInstantDDLQuery returns the SQL for this ALTER operation
139+
// with an INSTANT assertion (requires MySQL 8.0+)
140+
func (this *Applier) generateInstantDDLQuery() string {
141+
return fmt.Sprintf(`ALTER /* gh-ost */ TABLE %s.%s %s, ALGORITHM=INSTANT`,
142+
sql.EscapeName(this.migrationContext.DatabaseName),
143+
sql.EscapeName(this.migrationContext.OriginalTableName),
144+
this.migrationContext.AlterStatementOptions,
145+
)
146+
}
147+
138148
// readTableColumns reads table columns on applier
139149
func (this *Applier) readTableColumns() (err error) {
140150
this.migrationContext.Log.Infof("Examining table structure on applier")
@@ -188,6 +198,27 @@ func (this *Applier) ValidateOrDropExistingTables() error {
188198
return nil
189199
}
190200

201+
// AttemptInstantDDL attempts to use instant DDL (from MySQL 8.0, and earlier in Aurora and some others).
202+
// If successful, the operation is only a meta-data change so a lot of time is saved!
203+
// The risk of attempting to instant DDL when not supported is that a metadata lock may be acquired.
204+
// This is minor, since gh-ost will eventually require a metadata lock anyway, but at the cut-over stage.
205+
// Instant operations include:
206+
// - Adding a column
207+
// - Dropping a column
208+
// - Dropping an index
209+
// - Extending a VARCHAR column
210+
// - Adding a virtual generated column
211+
// It is not reliable to parse the `alter` statement to determine if it is instant or not.
212+
// This is because the table might be in an older row format, or have some other incompatibility
213+
// that is difficult to identify.
214+
func (this *Applier) AttemptInstantDDL() error {
215+
query := this.generateInstantDDLQuery()
216+
this.migrationContext.Log.Infof("INSTANT DDL query is: %s", query)
217+
// We don't need a trx, because for instant DDL the SQL mode doesn't matter.
218+
_, err := this.db.Exec(query)
219+
return err
220+
}
221+
191222
// CreateGhostTable creates the ghost table on the applier host
192223
func (this *Applier) CreateGhostTable() error {
193224
query := fmt.Sprintf(`create /* gh-ost */ table %s.%s like %s.%s`,

go/logic/applier_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,16 @@ func TestApplierBuildDMLEventQuery(t *testing.T) {
170170
test.S(t).ExpectEquals(res[0].args[3], 42)
171171
})
172172
}
173+
174+
func TestApplierInstantDDL(t *testing.T) {
175+
migrationContext := base.NewMigrationContext()
176+
migrationContext.DatabaseName = "test"
177+
migrationContext.OriginalTableName = "mytable"
178+
migrationContext.AlterStatementOptions = "ADD INDEX (foo)"
179+
applier := NewApplier(migrationContext)
180+
181+
t.Run("instantDDLstmt", func(t *testing.T) {
182+
stmt := applier.generateInstantDDLQuery()
183+
test.S(t).ExpectEquals(stmt, "ALTER /* gh-ost */ TABLE `test`.`mytable` ADD INDEX (foo), ALGORITHM=INSTANT")
184+
})
185+
}

go/logic/migrator.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,17 @@ func (this *Migrator) Migrate() (err error) {
360360
if err := this.createFlagFiles(); err != nil {
361361
return err
362362
}
363+
// In MySQL 8.0 (and possibly earlier) some DDL statements can be applied instantly.
364+
// Attempt to do this if AttemptInstantDDL is set.
365+
if this.migrationContext.AttemptInstantDDL {
366+
this.migrationContext.Log.Infof("Attempting to execute alter with ALGORITHM=INSTANT")
367+
if err := this.applier.AttemptInstantDDL(); err == nil {
368+
this.migrationContext.Log.Infof("Success! table %s.%s migrated instantly", sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName))
369+
return nil
370+
} else {
371+
this.migrationContext.Log.Infof("ALGORITHM=INSTANT not supported for this operation, proceeding with original algorithm: %s", err)
372+
}
373+
}
363374

364375
initialLag, _ := this.inspector.getReplicationLag()
365376
this.migrationContext.Log.Infof("Waiting for ghost table to be migrated. Current lag is %+v", initialLag)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
drop table if exists gh_ost_test;
2+
create table gh_ost_test (
3+
id int auto_increment,
4+
i int not null,
5+
color varchar(32),
6+
primary key(id)
7+
) auto_increment=1;
8+
9+
drop event if exists gh_ost_test;
10+
11+
insert into gh_ost_test values (null, 11, 'red');
12+
insert into gh_ost_test values (null, 13, 'green');
13+
insert into gh_ost_test values (null, 17, 'blue');
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
--attempt-instant-ddl

0 commit comments

Comments
 (0)