Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitec02c37

Browse files
authored
Revertible Migration (#1607)
This PR introduces revertible migrations, following the approach suggested by@jonahberquist. In case a migration causes production impact after the cut-over, it can be reverted quickly while preserving the writes that happened after the cut-over.When gh-ost is invoked with the --checkpoint flag and the migration completes, the migration can be reverted by invoking gh-ost again with the --revert flag and the --old-table flag specifying the name of the "old" table from the first migration e.g. _mytable_del. Also see docs/revert.md.
1 parent215dee4 commitec02c37

File tree

15 files changed

+483
-35
lines changed

15 files changed

+483
-35
lines changed

‎doc/hooks.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ The following variables are available on all hooks:
7676
-`GH_OST_HOOKS_HINT_OWNER` - copy of`--hooks-hint-owner` value
7777
-`GH_OST_HOOKS_HINT_TOKEN` - copy of`--hooks-hint-token` value
7878
-`GH_OST_DRY_RUN` - whether or not the`gh-ost` run is a dry run
79+
-`GH_OST_REVERT` - whether or not`gh-ost` is running in revert mode
7980

8081
The following variable are available on particular hooks:
8182

‎doc/resume.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- The first`gh-ost` process was invoked with`--checkpoint`
55
- The first`gh-ost` process had at least one successful checkpoint
66
- The binlogs from the last checkpoint's binlog coordinates still exist on the replica gh-ost is inspecting (specified by`--host`)
7+
- The checkpoint table (name ends with`_ghk`) still exists
78

89
To resume, invoke`gh-ost` again with the same arguments with the`--resume` flag.
910

‎doc/revert.md‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#Reverting Migrations
2+
3+
`gh-ost` can attempt to revert a previously completed migration if the follow conditions are met:
4+
- The first`gh-ost` process was invoked with`--checkpoint`
5+
- The checkpoint table (name ends with`_ghk`) still exists
6+
- The binlogs from the time of the migration's cut-over still exist on the replica gh-ost is inspecting (specified by`--host`)
7+
8+
To revert, find the name of the "old" table from the original migration e.g.`_mytable_del`. Then invoke`gh-ost` with the same arguments and the flags`--revert` and`--old-table="_mytable_del"`.
9+
gh-ost will read the binlog coordinates of the original cut-over from the checkpoint table and bring the old table up to date. Then it performs another cut-over to complete the reversion.
10+
Note that the checkpoint table (name ends with_ghk) will not be automatically dropped unless`--ok-to-drop-table` is provided.
11+
12+
>[!WARNING]
13+
>It is recommended use`--checkpoint` with`--gtid` enabled so that checkpoint binlog coordinates store GTID sets rather than file positions. In that case,`gh-ost` can revert using a different replica than it originally attached to.
14+
15+
###❗ Note ❗
16+
Reverting is roughly equivalent to applying the "reverse" migration._Before attempting to revert you should determine if the reverse migration is possible and does not involve any unacceptable data loss._
17+
18+
For example: if the original migration drops a`NOT NULL` column that has no`DEFAULT` then the reverse migration adds the column. In this case, the reverse migration is impossible if rows were added after the original cut-over and the revert will fail.
19+
Another example: if the original migration modifies a`VARCHAR(32)` column to`VARCHAR(64)`, the reverse migration truncates the`VARCHAR(64)` column to`VARCHAR(32)`. If values were inserted with length > 32 after the cut-over then the revert will fail.
20+
21+
22+
##Example
23+
The migration starts with a`gh-ost` invocation such as:
24+
```shell
25+
gh-ost \
26+
--chunk-size=100 \
27+
--host=replica1.company.com \
28+
--database="mydb" \
29+
--table="mytable" \
30+
--alter="drop key idx1"
31+
--gtid \
32+
--checkpoint \
33+
--checkpoint-seconds=60 \
34+
--execute
35+
```
36+
37+
In this example`gh-ost` writes a cut-over checkpoint to`_mytable_ghk` after the cut-over is successful. The original table is renamed to`_mytable_del`.
38+
39+
Suppose that dropping the index causes problems, the migration can be revert with:
40+
```shell
41+
# revert migration
42+
gh-ost \
43+
--chunk-size=100 \
44+
--host=replica1.company.com \
45+
--database="mydb" \
46+
--table="mytable" \
47+
--old-table="_mytable_del"
48+
--gtid \
49+
--checkpoint \
50+
--checkpoint-seconds=60 \
51+
--revert \
52+
--execute
53+
```
54+
55+
gh-ost then reconnects at the binlog coordinates stored in the cut-over checkpoint and applies DMLs until the old table is up-to-date.
56+
Note that the "reverse" migration is`ADD KEY idx(...)` so there is no potential data loss to consider in this case.

‎go/base/context.go‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ type MigrationContext struct {
104104
AzureMySQLbool
105105
AttemptInstantDDLbool
106106
Resumebool
107+
Revertbool
108+
OldTableNamestring
107109

108110
// SkipPortValidation allows skipping the port validation in `ValidateConnection`
109111
// This is useful when connecting to a MySQL instance where the external port
@@ -348,6 +350,10 @@ func getSafeTableName(baseName string, suffix string) string {
348350
// GetGhostTableName generates the name of ghost table, based on original table name
349351
// or a given table name
350352
func (this*MigrationContext)GetGhostTableName()string {
353+
ifthis.Revert {
354+
// When reverting the "ghost" table is the _del table from the original migration.
355+
returnthis.OldTableName
356+
}
351357
ifthis.ForceTmpTableName!="" {
352358
returngetSafeTableName(this.ForceTmpTableName,"gho")
353359
}else {
@@ -364,14 +370,18 @@ func (this *MigrationContext) GetOldTableName() string {
364370
tableName=this.OriginalTableName
365371
}
366372

373+
suffix:="del"
374+
ifthis.Revert {
375+
suffix="rev_del"
376+
}
367377
ifthis.TimestampOldTable {
368378
t:=this.StartTime
369379
timestamp:=fmt.Sprintf("%d%02d%02d%02d%02d%02d",
370380
t.Year(),t.Month(),t.Day(),
371381
t.Hour(),t.Minute(),t.Second())
372-
returngetSafeTableName(tableName,fmt.Sprintf("%s_del",timestamp))
382+
returngetSafeTableName(tableName,fmt.Sprintf("%s_%s",timestamp,suffix))
373383
}
374-
returngetSafeTableName(tableName,"del")
384+
returngetSafeTableName(tableName,suffix)
375385
}
376386

377387
// GetChangelogTableName generates the name of changelog table, based on original table name

‎go/cmd/gh-ost/main.go‎

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ func main() {
148148
flag.BoolVar(&migrationContext.Checkpoint,"checkpoint",false,"Enable migration checkpoints")
149149
flag.Int64Var(&migrationContext.CheckpointIntervalSeconds,"checkpoint-seconds",300,"The number of seconds between checkpoints")
150150
flag.BoolVar(&migrationContext.Resume,"resume",false,"Attempt to resume migration from checkpoint")
151+
flag.BoolVar(&migrationContext.Revert,"revert",false,"Attempt to revert completed migration")
152+
flag.StringVar(&migrationContext.OldTableName,"old-table","","The name of the old table when using --revert, e.g. '_mytable_del'")
151153

152154
maxLoad:=flag.String("max-load","","Comma delimited status-name=threshold. e.g: 'Threads_running=100,Threads_connected=500'. When status exceeds threshold, app throttles writes")
153155
criticalLoad:=flag.String("critical-load","","Comma delimited status-name=threshold, same format as --max-load. When status exceeds threshold, app panics and quits")
@@ -206,12 +208,35 @@ func main() {
206208

207209
migrationContext.SetConnectionCharset(*charset)
208210

209-
ifmigrationContext.AlterStatement=="" {
211+
ifmigrationContext.AlterStatement==""&&!migrationContext.Revert{
210212
log.Fatal("--alter must be provided and statement must not be empty")
211213
}
212214
parser:=sql.NewParserFromAlterStatement(migrationContext.AlterStatement)
213215
migrationContext.AlterStatementOptions=parser.GetAlterStatementOptions()
214216

217+
ifmigrationContext.Revert {
218+
ifmigrationContext.Resume {
219+
log.Fatal("--revert cannot be used with --resume")
220+
}
221+
ifmigrationContext.OldTableName=="" {
222+
migrationContext.Log.Fatalf("--revert must be called with --old-table")
223+
}
224+
225+
// options irrelevant to revert mode
226+
ifmigrationContext.AlterStatement!="" {
227+
log.Warning("--alter was provided with --revert, it will be ignored")
228+
}
229+
ifmigrationContext.AttemptInstantDDL {
230+
log.Warning("--attempt-instant-ddl was provided with --revert, it will be ignored")
231+
}
232+
ifmigrationContext.IncludeTriggers {
233+
log.Warning("--include-triggers was provided with --revert, it will be ignored")
234+
}
235+
ifmigrationContext.DiscardForeignKeys {
236+
log.Warning("--discard-foreign-keys was provided with --revert, it will be ignored")
237+
}
238+
}
239+
215240
ifmigrationContext.DatabaseName=="" {
216241
ifparser.HasExplicitSchema() {
217242
migrationContext.DatabaseName=parser.GetExplicitSchema()
@@ -347,7 +372,14 @@ func main() {
347372
acceptSignals(migrationContext)
348373

349374
migrator:=logic.NewMigrator(migrationContext,AppVersion)
350-
iferr:=migrator.Migrate();err!=nil {
375+
varerrerror
376+
ifmigrationContext.Revert {
377+
err=migrator.Revert()
378+
}else {
379+
err=migrator.Migrate()
380+
}
381+
382+
iferr!=nil {
351383
migrator.ExecOnFailureHook()
352384
migrationContext.Log.Fatale(err)
353385
}

‎go/logic/applier.go‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -437,25 +437,20 @@ func (this *Applier) CreateCheckpointTable() error {
437437
"`gh_ost_chk_iteration` bigint",
438438
"`gh_ost_rows_copied` bigint",
439439
"`gh_ost_dml_applied` bigint",
440+
"`gh_ost_is_cutover` tinyint(1) DEFAULT '0'",
440441
}
441442
for_,col:=rangethis.migrationContext.UniqueKey.Columns.Columns() {
442443
ifcol.MySQLType=="" {
443444
returnfmt.Errorf("CreateCheckpoinTable: column %s has no type information. applyColumnTypes must be called",sql.EscapeName(col.Name))
444445
}
445446
minColName:=sql.TruncateColumnName(col.Name,sql.MaxColumnNameLength-4)+"_min"
446447
colDef:=fmt.Sprintf("%s %s",sql.EscapeName(minColName),col.MySQLType)
447-
if!col.Nullable {
448-
colDef+=" NOT NULL"
449-
}
450448
colDefs=append(colDefs,colDef)
451449
}
452450

453451
for_,col:=rangethis.migrationContext.UniqueKey.Columns.Columns() {
454452
maxColName:=sql.TruncateColumnName(col.Name,sql.MaxColumnNameLength-4)+"_max"
455453
colDef:=fmt.Sprintf("%s %s",sql.EscapeName(maxColName),col.MySQLType)
456-
if!col.Nullable {
457-
colDef+=" NOT NULL"
458-
}
459454
colDefs=append(colDefs,colDef)
460455
}
461456

@@ -627,7 +622,7 @@ func (this *Applier) WriteCheckpoint(chk *Checkpoint) (int64, error) {
627622
iferr!=nil {
628623
returninsertId,err
629624
}
630-
args:=sqlutils.Args(chk.LastTrxCoords.String(),chk.Iteration,chk.RowsCopied,chk.DMLApplied)
625+
args:=sqlutils.Args(chk.LastTrxCoords.String(),chk.Iteration,chk.RowsCopied,chk.DMLApplied,chk.IsCutover)
631626
args=append(args,uniqueKeyArgs...)
632627
res,err:=this.db.Exec(query,args...)
633628
iferr!=nil {
@@ -645,7 +640,7 @@ func (this *Applier) ReadLastCheckpoint() (*Checkpoint, error) {
645640

646641
varcoordStrstring
647642
vartimestampint64
648-
ptrs:= []interface{}{&chk.Id,&timestamp,&coordStr,&chk.Iteration,&chk.RowsCopied,&chk.DMLApplied}
643+
ptrs:= []interface{}{&chk.Id,&timestamp,&coordStr,&chk.Iteration,&chk.RowsCopied,&chk.DMLApplied,&chk.IsCutover}
649644
ptrs=append(ptrs,chk.IterationRangeMin.ValuesPointers...)
650645
ptrs=append(ptrs,chk.IterationRangeMax.ValuesPointers...)
651646
err:=row.Scan(ptrs...)

‎go/logic/applier_test.go‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ func (suite *ApplierTestSuite) SetupSuite() {
214214
testmysql.WithUsername(testMysqlUser),
215215
testmysql.WithPassword(testMysqlPass),
216216
testcontainers.WithWaitStrategy(wait.ForExposedPort()),
217+
testmysql.WithConfigFile("my.cnf.test"),
217218
)
218219
suite.Require().NoError(err)
219220

@@ -272,7 +273,7 @@ func (suite *ApplierTestSuite) TestInitDBConnections() {
272273
mysqlVersion,_:=strings.CutPrefix(testMysqlContainerImage,"mysql:")
273274
suite.Require().Equal(mysqlVersion,migrationContext.ApplierMySQLVersion)
274275
suite.Require().Equal(int64(28800),migrationContext.ApplierWaitTimeout)
275-
suite.Require().Equal("SYSTEM",migrationContext.ApplierTimeZone)
276+
suite.Require().Equal("+00:00",migrationContext.ApplierTimeZone)
276277

277278
suite.Require().Equal(sql.NewColumnList([]string{"id","item_id"}),migrationContext.OriginalTableColumnsOnApplier)
278279
}
@@ -702,6 +703,7 @@ func (suite *ApplierTestSuite) TestWriteCheckpoint() {
702703
Iteration:2,
703704
RowsCopied:100000,
704705
DMLApplied:200000,
706+
IsCutover:true,
705707
}
706708
id,err:=applier.WriteCheckpoint(chk)
707709
suite.Require().NoError(err)
@@ -716,6 +718,7 @@ func (suite *ApplierTestSuite) TestWriteCheckpoint() {
716718
suite.Require().Equal(chk.IterationRangeMax.String(),gotChk.IterationRangeMax.String())
717719
suite.Require().Equal(chk.RowsCopied,gotChk.RowsCopied)
718720
suite.Require().Equal(chk.DMLApplied,gotChk.DMLApplied)
721+
suite.Require().Equal(chk.IsCutover,gotChk.IsCutover)
719722
}
720723

721724
funcTestApplier(t*testing.T) {

‎go/logic/checkpoint.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ type Checkpoint struct {
2828
Iterationint64
2929
RowsCopiedint64
3030
DMLAppliedint64
31+
IsCutoverbool
3132
}

‎go/logic/hooks.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ func (this *HooksExecutor) applyEnvironmentVariables(extraVariables ...string) [
6969
env=append(env,fmt.Sprintf("GH_OST_HOOKS_HINT_OWNER=%s",this.migrationContext.HooksHintOwner))
7070
env=append(env,fmt.Sprintf("GH_OST_HOOKS_HINT_TOKEN=%s",this.migrationContext.HooksHintToken))
7171
env=append(env,fmt.Sprintf("GH_OST_DRY_RUN=%t",this.migrationContext.Noop))
72+
env=append(env,fmt.Sprintf("GH_OST_REVERT=%t",this.migrationContext.Revert))
7273

7374
env=append(env,extraVariables...)
7475
returnenv

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp