Skip to content

Commit bea5323

Browse files
Merge branch 'master' into master
2 parents b538733 + 7320fda commit bea5323

File tree

25 files changed

+210
-31
lines changed

25 files changed

+210
-31
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on: [pull_request]
55
jobs:
66
build:
77

8-
runs-on: ubuntu-latest
8+
runs-on: ubuntu-20.04
99

1010
steps:
1111
- uses: actions/checkout@v2

.github/workflows/replica-tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ on: [pull_request]
55
jobs:
66
build:
77

8-
runs-on: ubuntu-latest
8+
runs-on: ubuntu-20.04
99
strategy:
1010
matrix:
11-
version: [mysql-5.7.25,mysql-8.0.16]
11+
version: [mysql-5.7.25,mysql-8.0.16,PerconaServer-8.0.21]
1212

1313
steps:
1414
- uses: actions/checkout@v2

doc/command-line-flags.md

Lines changed: 28 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:
@@ -230,6 +246,18 @@ Allows `gh-ost` to connect to the MySQL servers using encrypted connections, but
230246

231247
`--ssl-key=/path/to/ssl-key.key`: SSL private key file (in PEM format).
232248

249+
### storage-engine
250+
Default is `innodb`, and `rocksdb` support is currently experimental. InnoDB and RocksDB are both transactional engines, supporting both shared and exclusive row locks.
251+
252+
But RocksDB currently lacks a few features support compared to InnoDB:
253+
- Gap Locks
254+
- Foreign Key
255+
- Generated Columns
256+
- Spatial
257+
- Geometry
258+
259+
When `--storage-engine=rocksdb`, `gh-ost` will make some changes necessary (e.g. sets isolation level to `READ_COMMITTED`) to support RocksDB.
260+
233261
### test-on-replica
234262

235263
Issue the migration on a replica; do not modify data on master. Useful for validating, testing and benchmarking. See [`testing-on-replica`](testing-on-replica.md)

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: 18 additions & 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
@@ -289,6 +290,19 @@ func NewMigrationContext() *MigrationContext {
289290
}
290291
}
291292

293+
func (this *MigrationContext) SetConnectionConfig(storageEngine string) error {
294+
var transactionIsolation string
295+
switch storageEngine {
296+
case "rocksdb":
297+
transactionIsolation = "READ-COMMITTED"
298+
default:
299+
transactionIsolation = "REPEATABLE-READ"
300+
}
301+
this.InspectorConnectionConfig.TransactionIsolation = transactionIsolation
302+
this.ApplierConnectionConfig.TransactionIsolation = transactionIsolation
303+
return nil
304+
}
305+
292306
func getSafeTableName(baseName string, suffix string) string {
293307
name := fmt.Sprintf("_%s_%s", baseName, suffix)
294308
if len(name) <= mysql.MaxTableNameLength {
@@ -427,6 +441,10 @@ func (this *MigrationContext) IsTransactionalTable() bool {
427441
{
428442
return true
429443
}
444+
case "rocksdb":
445+
{
446+
return true
447+
}
430448
}
431449
return false
432450
}

go/cmd/gh-ost/main.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ 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+
storageEngine := flag.String("storage-engine", "innodb", "Specify table storage engine (default: 'innodb'). When 'rocksdb': the session transaction isolation level is changed from REPEATABLE_READ to READ_COMMITTED.")
72+
7073
flag.BoolVar(&migrationContext.CountTableRows, "exact-rowcount", false, "actually count table rows as opposed to estimate them (results in more accurate progress estimation)")
7174
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")
7275
flag.BoolVar(&migrationContext.AllowedRunningOnMaster, "allow-on-master", false, "allow this migration to run directly on master. Preferably it would run on a replica")
@@ -180,6 +183,10 @@ func main() {
180183
migrationContext.Log.SetLevel(log.ERROR)
181184
}
182185

186+
if err := migrationContext.SetConnectionConfig(*storageEngine); err != nil {
187+
migrationContext.Log.Fatale(err)
188+
}
189+
183190
if migrationContext.AlterStatement == "" {
184191
log.Fatal("--alter must be provided and statement must not be empty")
185192
}
@@ -245,6 +252,9 @@ func main() {
245252
if *replicationLagQuery != "" {
246253
migrationContext.Log.Warning("--replication-lag-query is deprecated")
247254
}
255+
if *storageEngine == "rocksdb" {
256+
migrationContext.Log.Warning("RocksDB storage engine support is experimental")
257+
}
248258

249259
switch *cutOver {
250260
case "atomic", "default", "":

go/logic/applier.go

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

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

200+
// AttemptInstantDDL attempts to use instant DDL (from MySQL 8.0, and earlier in Aurora and some others).
201+
// If successful, the operation is only a meta-data change so a lot of time is saved!
202+
// The risk of attempting to instant DDL when not supported is that a metadata lock may be acquired.
203+
// This is minor, since gh-ost will eventually require a metadata lock anyway, but at the cut-over stage.
204+
// Instant operations include:
205+
// - Adding a column
206+
// - Dropping a column
207+
// - Dropping an index
208+
// - Extending a VARCHAR column
209+
// - Adding a virtual generated column
210+
// It is not reliable to parse the `alter` statement to determine if it is instant or not.
211+
// This is because the table might be in an older row format, or have some other incompatibility
212+
// that is difficult to identify.
213+
func (this *Applier) AttemptInstantDDL() error {
214+
query := this.generateInstantDDLQuery()
215+
this.migrationContext.Log.Infof("INSTANT DDL query is: %s", query)
216+
// We don't need a trx, because for instant DDL the SQL mode doesn't matter.
217+
_, err := this.db.Exec(query)
218+
return err
219+
}
220+
190221
// CreateGhostTable creates the ghost table on the applier host
191222
func (this *Applier) CreateGhostTable() error {
192223
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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,17 @@ func (this *Migrator) Migrate() (err error) {
359359
if err := this.createFlagFiles(); err != nil {
360360
return err
361361
}
362+
// In MySQL 8.0 (and possibly earlier) some DDL statements can be applied instantly.
363+
// Attempt to do this if AttemptInstantDDL is set.
364+
if this.migrationContext.AttemptInstantDDL {
365+
this.migrationContext.Log.Infof("Attempting to execute alter with ALGORITHM=INSTANT")
366+
if err := this.applier.AttemptInstantDDL(); err == nil {
367+
this.migrationContext.Log.Infof("Success! table %s.%s migrated instantly", sql.EscapeName(this.migrationContext.DatabaseName), sql.EscapeName(this.migrationContext.OriginalTableName))
368+
return nil
369+
} else {
370+
this.migrationContext.Log.Infof("ALGORITHM=INSTANT not supported for this operation, proceeding with original algorithm: %s", err)
371+
}
372+
}
362373

363374
initialLag, _ := this.inspector.getReplicationLag()
364375
this.migrationContext.Log.Infof("Waiting for ghost table to be migrated. Current lag is %+v", initialLag)
@@ -1031,6 +1042,7 @@ func (this *Migrator) printStatus(rule PrintStatusRule, writers ...io.Writer) {
10311042
)
10321043
w := io.MultiWriter(writers...)
10331044
fmt.Fprintln(w, status)
1045+
this.migrationContext.Log.Infof(status)
10341046

10351047
hooksStatusIntervalSec := this.migrationContext.HooksStatusIntervalSec
10361048
if hooksStatusIntervalSec > 0 && elapsedSeconds%hooksStatusIntervalSec == 0 {

go/mysql/connection.go

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,18 @@ import (
1818
)
1919

2020
const (
21-
transactionIsolation = "REPEATABLE-READ"
22-
TLS_CONFIG_KEY = "ghost"
21+
TLS_CONFIG_KEY = "ghost"
2322
)
2423

2524
// ConnectionConfig is the minimal configuration required to connect to a MySQL server
2625
type ConnectionConfig struct {
27-
Key InstanceKey
28-
User string
29-
Password string
30-
ImpliedKey *InstanceKey
31-
tlsConfig *tls.Config
32-
Timeout float64
26+
Key InstanceKey
27+
User string
28+
Password string
29+
ImpliedKey *InstanceKey
30+
tlsConfig *tls.Config
31+
Timeout float64
32+
TransactionIsolation string
3333
}
3434

3535
func NewConnectionConfig() *ConnectionConfig {
@@ -43,11 +43,12 @@ func NewConnectionConfig() *ConnectionConfig {
4343
// DuplicateCredentials creates a new connection config with given key and with same credentials as this config
4444
func (this *ConnectionConfig) DuplicateCredentials(key InstanceKey) *ConnectionConfig {
4545
config := &ConnectionConfig{
46-
Key: key,
47-
User: this.User,
48-
Password: this.Password,
49-
tlsConfig: this.tlsConfig,
50-
Timeout: this.Timeout,
46+
Key: key,
47+
User: this.User,
48+
Password: this.Password,
49+
tlsConfig: this.tlsConfig,
50+
Timeout: this.Timeout,
51+
TransactionIsolation: this.TransactionIsolation,
5152
}
5253
config.ImpliedKey = &config.Key
5354
return config
@@ -126,7 +127,7 @@ func (this *ConnectionConfig) GetDBUri(databaseName string) string {
126127
"charset=utf8mb4,utf8,latin1",
127128
"interpolateParams=true",
128129
fmt.Sprintf("tls=%s", tlsOption),
129-
fmt.Sprintf("transaction_isolation=%q", transactionIsolation),
130+
fmt.Sprintf("transaction_isolation=%q", this.TransactionIsolation),
130131
fmt.Sprintf("timeout=%fs", this.Timeout),
131132
fmt.Sprintf("readTimeout=%fs", this.Timeout),
132133
fmt.Sprintf("writeTimeout=%fs", this.Timeout),

0 commit comments

Comments
 (0)