Skip to content

Commit c2186db

Browse files
author
Shlomi Noach
authored
Merge branch 'master' into touch-postpone-flag-file
2 parents 0db9510 + dfc9f41 commit c2186db

File tree

49 files changed

+508
-106
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+508
-106
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ But then a rare genetic mutation happened, and the `c` transformed into `t`. And
8484

8585
We develop `gh-ost` at GitHub and for the community. We may have different priorities than others. From time to time we may suggest a contribution that is not on our immediate roadmap but which may appeal to others.
8686

87-
Please see [Coding gh-ost](https://github.com/github/gh-ost/blob/develdocs/doc/coding-ghost.md) for a guide to getting started developing with gh-ost.
87+
Please see [Coding gh-ost](doc/coding-ghost.md) for a guide to getting started developing with gh-ost.
8888

8989
## Download/binaries/source
9090

RELEASE_VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.0.36
1+
1.0.42

doc/cheatsheet.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
# Cheatsheet
22

3+
### Operation modes
4+
35
![operation modes](images/gh-ost-operation-modes.png)
46

57

68
`gh-ost` operates by connecting to potentially multiple servers, as well as imposing itself as a replica in order to streamline binary log events directly from one of those servers. There are various operation modes, which depend on your setup, configuration, and where you want to run the migration.
79

8-
### a. Connect to replica, migrate on master
10+
#### a. Connect to replica, migrate on master
911

1012
This is the mode `gh-ost` expects by default. `gh-ost` will investigate the replica, crawl up to find the topology's master, and will hook onto it as well. Migration will:
1113

@@ -47,7 +49,7 @@ gh-ost \
4749
With `--execute`, migration actually copies data and flips tables. Without it this is a `noop` run.
4850

4951

50-
### b. Connect to master
52+
#### b. Connect to master
5153

5254
If you don't have replicas, or do not wish to use them, you are still able to operate directly on the master. `gh-ost` will do all operations directly on the master. You may still ask it to be considerate of replication lag.
5355

@@ -80,7 +82,7 @@ gh-ost \
8082
[--execute]
8183
```
8284

83-
### c. Migrate/test on replica
85+
#### c. Migrate/test on replica
8486

8587
This will perform a migration on the replica. `gh-ost` will briefly connect to the master but will thereafter perform all operations on the replica without modifying anything on the master.
8688
Throughout the operation, `gh-ost` will throttle such that the replica is up to date.
@@ -146,7 +148,7 @@ gh-ost --allow-master-master --assume-master-host=a.specific.master.com
146148

147149
Topologies using _tungsten replicator_ are peculiar in that the participating servers are not actually aware they are replicating. The _tungsten replicator_ looks just like another app issuing queries on those hosts. `gh-ost` is unable to identify that a server participates in a _tungsten_ topology.
148150

149-
If you choose to migrate directly on master (see above), there's nothing special you need to do.
151+
If you choose to migrate directly on master (see above), there's nothing special you need to do.
150152

151153
If you choose to migrate via replica, then you need to make sure Tungsten is configured with log-slave-updates parameter (note this is different from MySQL's own log-slave-updates parameter), otherwise changes will not be in the replica's binlog, causing data to be corrupted after table swap. You must also supply the identity of the master, and indicate this is a tungsten setup, as follows:
152154

@@ -155,3 +157,15 @@ gh-ost --tungsten --assume-master-host=the.topology.master.com
155157
```
156158

157159
Also note that `--switch-to-rbr` does not work for a Tungsten setup as the replication process is external, so you need to make sure `binlog_format` is set to ROW before Tungsten Replicator connects to the server and starts applying events from the master.
160+
161+
### Concurrent migrations
162+
163+
It is possible to run concurrent `gh-ost` migrations.
164+
165+
- Never on the exact same table.
166+
- If running on different replicas, (e.g. `table1` on `replica1` and `table2` on `replica2`) then no further configuration required.
167+
- If running from same server (binaries run on same server, regardless of which replica/replicas are used):
168+
- Make sure not to specify same `-serve-socket-file` (or let `gh-ost` pick one for you).
169+
- You may choose to use same `-throttle-flag-file` (preferably use `-throttle-additional-flag-file`, this is exactly the reason there's two, this latter file is for sharing).
170+
- You may choose to use same `-panic-flag-file`. This all depends on your flow and how you'd like to control your migrations.
171+
- If using same inspected box (either master or replica, `--host=everyone.uses.this.host`) then for each `gh-ost` process you must also provide a different, unique `--replica-server-id`. Optionally use process ID (`$$` in shell) ; but it's on you to choose a number that does not collide with another `gh-ost` or another running replica.

doc/interactive-commands.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Both interfaces may serve at the same time. Both respond to simple text command,
1919
- `sup`: returns a brief status summary of migration progress
2020
- `coordinates`: returns recent (though not exactly up to date) binary log coordinates of the inspected server
2121
- `chunk-size=<newsize>`: modify the `chunk-size`; applies on next running copy-iteration
22+
- `dml-batch-size=<newsize>`: modify the `dml-batch-size`; applies on next applying of binary log events
2223
- `max-lag-millis=<max-lag>`: modify the maximum replication lag threshold (milliseconds, minimum value is `100`, i.e. `0.1` second)
2324
- `max-load=<max-load-thresholds>`: modify the `max-load` config; applies on next running copy-iteration
2425
- The `max-load` format must be: `some_status=<numeric-threshold>[,some_status=<numeric-threshold>...]`'
@@ -52,7 +53,7 @@ While migration is running:
5253
$ echo status | nc -U /tmp/gh-ost.test.sample_data_0.sock
5354
# Migrating `test`.`sample_data_0`; Ghost table is `test`.`_sample_data_0_gst`
5455
# Migration started at Tue Jun 07 11:45:16 +0200 2016
55-
# chunk-size: 200; max lag: 1500ms; max-load: map[Threads_connected:20]
56+
# chunk-size: 200; max lag: 1500ms; dml-batch-size: 10; max-load: map[Threads_connected:20]
5657
# Throttle additional flag file: /tmp/gh-ost.throttle
5758
# Serving on unix socket: /tmp/gh-ost.test.sample_data_0.sock
5859
# Serving on TCP port: 10001
@@ -63,7 +64,7 @@ Copy: 0/2915 0.0%; Applied: 0; Backlog: 0/100; Elapsed: 40s(copy), 41s(total); s
6364
$ echo "chunk-size=250" | nc -U /tmp/gh-ost.test.sample_data_0.sock
6465
# Migrating `test`.`sample_data_0`; Ghost table is `test`.`_sample_data_0_gst`
6566
# Migration started at Tue Jun 07 11:56:03 +0200 2016
66-
# chunk-size: 250; max lag: 1500ms; max-load: map[Threads_connected:20]
67+
# chunk-size: 250; max lag: 1500ms; dml-batch-size: 10; max-load: map[Threads_connected:20]
6768
# Throttle additional flag file: /tmp/gh-ost.throttle
6869
# Serving on unix socket: /tmp/gh-ost.test.sample_data_0.sock
6970
# Serving on TCP port: 10001

doc/questions.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,8 @@ At this time there is no equivalent to `ALTER IGNORE`, where duplicates are impl
2323
2424
It is therefore unlikely that `gh-ost` will support this behavior.
2525

26+
### Run concurrent migrations?
27+
28+
Yes. TL;DR if running all on same replica/master, make sure to provide `--replica-server-id`. [Read more](cheatsheet.md#concurrent-migrations)
29+
2630
# Why

doc/requirements-and-limitations.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ The `SUPER` privilege is required for `STOP SLAVE`, `START SLAVE` operations. Th
2828

2929
- MySQL 5.7 generated columns are not supported. They may be supported in the future.
3030

31-
- MySQL 5.7 `JSON` columns are not supported. They are likely to be supported shortly.
31+
- MySQL 5.7 `POINT` column type is not supported.
32+
33+
- MySQL 5.7 `JSON` columns are supported but not as part of `PRIMARY KEY`
3234

3335
- The two _before_ & _after_ tables must share a `PRIMARY KEY` or other `UNIQUE KEY`. This key will be used by `gh-ost` to iterate through the table rows when copying. [Read more](shared-key.md)
3436
- The migration key must not include columns with NULL values. This means either:

go/base/context.go

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ const (
4646
)
4747

4848
const (
49-
HTTPStatusOK = 200
49+
HTTPStatusOK = 200
50+
MaxEventsBatchSize = 1000
5051
)
5152

5253
var (
@@ -191,6 +192,7 @@ type MigrationContext struct {
191192
Iteration int64
192193
MigrationIterationRangeMinValues *sql.ColumnValues
193194
MigrationIterationRangeMaxValues *sql.ColumnValues
195+
ForceTmpTableName string
194196

195197
recentBinlogCoordinates mysql.BinlogCoordinates
196198

@@ -242,26 +244,52 @@ func GetMigrationContext() *MigrationContext {
242244
return context
243245
}
244246

247+
func getSafeTableName(baseName string, suffix string) string {
248+
name := fmt.Sprintf("_%s_%s", baseName, suffix)
249+
if len(name) <= mysql.MaxTableNameLength {
250+
return name
251+
}
252+
extraCharacters := len(name) - mysql.MaxTableNameLength
253+
return fmt.Sprintf("_%s_%s", baseName[0:len(baseName)-extraCharacters], suffix)
254+
}
255+
245256
// GetGhostTableName generates the name of ghost table, based on original table name
257+
// or a given table name
246258
func (this *MigrationContext) GetGhostTableName() string {
247-
return fmt.Sprintf("_%s_gho", this.OriginalTableName)
259+
if this.ForceTmpTableName != "" {
260+
return getSafeTableName(this.ForceTmpTableName, "gho")
261+
} else {
262+
return getSafeTableName(this.OriginalTableName, "gho")
263+
}
248264
}
249265

250266
// GetOldTableName generates the name of the "old" table, into which the original table is renamed.
251267
func (this *MigrationContext) GetOldTableName() string {
268+
var tableName string
269+
if this.ForceTmpTableName != "" {
270+
tableName = this.ForceTmpTableName
271+
} else {
272+
tableName = this.OriginalTableName
273+
}
274+
252275
if this.TimestampOldTable {
253276
t := this.StartTime
254277
timestamp := fmt.Sprintf("%d%02d%02d%02d%02d%02d",
255278
t.Year(), t.Month(), t.Day(),
256279
t.Hour(), t.Minute(), t.Second())
257-
return fmt.Sprintf("_%s_%s_del", this.OriginalTableName, timestamp)
280+
return getSafeTableName(tableName, fmt.Sprintf("%s_del", timestamp))
258281
}
259-
return fmt.Sprintf("_%s_del", this.OriginalTableName)
282+
return getSafeTableName(tableName, "del")
260283
}
261284

262285
// GetChangelogTableName generates the name of changelog table, based on original table name
286+
// or a given table name.
263287
func (this *MigrationContext) GetChangelogTableName() string {
264-
return fmt.Sprintf("_%s_ghc", this.OriginalTableName)
288+
if this.ForceTmpTableName != "" {
289+
return getSafeTableName(this.ForceTmpTableName, "ghc")
290+
} else {
291+
return getSafeTableName(this.OriginalTableName, "ghc")
292+
}
265293
}
266294

267295
// GetVoluntaryLockName returns a name of a voluntary lock to be used throughout
@@ -441,8 +469,8 @@ func (this *MigrationContext) SetDMLBatchSize(batchSize int64) {
441469
if batchSize < 1 {
442470
batchSize = 1
443471
}
444-
if batchSize > 100 {
445-
batchSize = 100
472+
if batchSize > MaxEventsBatchSize {
473+
batchSize = MaxEventsBatchSize
446474
}
447475
atomic.StoreInt64(&this.DMLBatchSize, batchSize)
448476
}
@@ -672,7 +700,7 @@ func (this *MigrationContext) ReadConfigFile() error {
672700
gcfg.RelaxedParserMode = true
673701
gcfgscanner.RelaxedScannerMode = true
674702
if err := gcfg.ReadFileInto(&this.config, this.ConfigFile); err != nil {
675-
return err
703+
return fmt.Errorf("Error reading config file %s. Details: %s", this.ConfigFile, err.Error())
676704
}
677705

678706
// We accept user & password in the form "${SOME_ENV_VARIABLE}" in which case we pull

go/base/context_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
Copyright 2016 GitHub Inc.
3+
See https://github.com/github/gh-ost/blob/master/LICENSE
4+
*/
5+
6+
package base
7+
8+
import (
9+
"testing"
10+
"time"
11+
12+
"github.com/outbrain/golib/log"
13+
test "github.com/outbrain/golib/tests"
14+
)
15+
16+
func init() {
17+
log.SetLevel(log.ERROR)
18+
}
19+
20+
func TestGetTableNames(t *testing.T) {
21+
{
22+
context = newMigrationContext()
23+
context.OriginalTableName = "some_table"
24+
test.S(t).ExpectEquals(context.GetOldTableName(), "_some_table_del")
25+
test.S(t).ExpectEquals(context.GetGhostTableName(), "_some_table_gho")
26+
test.S(t).ExpectEquals(context.GetChangelogTableName(), "_some_table_ghc")
27+
}
28+
{
29+
context = newMigrationContext()
30+
context.OriginalTableName = "a123456789012345678901234567890123456789012345678901234567890"
31+
test.S(t).ExpectEquals(context.GetOldTableName(), "_a1234567890123456789012345678901234567890123456789012345678_del")
32+
test.S(t).ExpectEquals(context.GetGhostTableName(), "_a1234567890123456789012345678901234567890123456789012345678_gho")
33+
test.S(t).ExpectEquals(context.GetChangelogTableName(), "_a1234567890123456789012345678901234567890123456789012345678_ghc")
34+
}
35+
{
36+
context = newMigrationContext()
37+
context.OriginalTableName = "a123456789012345678901234567890123456789012345678901234567890123"
38+
oldTableName := context.GetOldTableName()
39+
test.S(t).ExpectEquals(oldTableName, "_a1234567890123456789012345678901234567890123456789012345678_del")
40+
}
41+
{
42+
context = newMigrationContext()
43+
context.OriginalTableName = "a123456789012345678901234567890123456789012345678901234567890123"
44+
context.TimestampOldTable = true
45+
longForm := "Jan 2, 2006 at 3:04pm (MST)"
46+
context.StartTime, _ = time.Parse(longForm, "Feb 3, 2013 at 7:54pm (PST)")
47+
oldTableName := context.GetOldTableName()
48+
test.S(t).ExpectEquals(oldTableName, "_a1234567890123456789012345678901234567890123_20130203195400_del")
49+
}
50+
{
51+
context = newMigrationContext()
52+
context.OriginalTableName = "foo_bar_baz"
53+
context.ForceTmpTableName = "tmp"
54+
test.S(t).ExpectEquals(context.GetOldTableName(), "_tmp_del")
55+
test.S(t).ExpectEquals(context.GetGhostTableName(), "_tmp_gho")
56+
test.S(t).ExpectEquals(context.GetChangelogTableName(), "_tmp_ghc")
57+
}
58+
}

go/cmd/gh-ost/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func main() {
6363
flag.BoolVar(&migrationContext.AllowedRunningOnMaster, "allow-on-master", false, "allow this migration to run directly on master. Preferably it would run on a replica")
6464
flag.BoolVar(&migrationContext.AllowedMasterMaster, "allow-master-master", false, "explicitly allow running in a master-master setup")
6565
flag.BoolVar(&migrationContext.NullableUniqueKeyAllowed, "allow-nullable-unique-key", false, "allow gh-ost to migrate based on a unique key with nullable columns. As long as no NULL values exist, this should be OK. If NULL values exist in chosen key, data may be corrupted. Use at your own risk!")
66-
flag.BoolVar(&migrationContext.ApproveRenamedColumns, "approve-renamed-columns", false, "in case your `ALTER` statement renames columns, gh-ost will note that and offer its interpretation of the rename. By default gh-ost does not proceed to execute. This flag approves that gh-ost's interpretation si correct")
66+
flag.BoolVar(&migrationContext.ApproveRenamedColumns, "approve-renamed-columns", false, "in case your `ALTER` statement renames columns, gh-ost will note that and offer its interpretation of the rename. By default gh-ost does not proceed to execute. This flag approves that gh-ost's interpretation is correct")
6767
flag.BoolVar(&migrationContext.SkipRenamedColumns, "skip-renamed-columns", false, "in case your `ALTER` statement renames columns, gh-ost will note that and offer its interpretation of the rename. By default gh-ost does not proceed to execute. This flag tells gh-ost to skip the renamed columns, i.e. to treat what gh-ost thinks are renamed columns as unrelated columns. NOTE: you may lose column data")
6868
flag.BoolVar(&migrationContext.IsTungsten, "tungsten", false, "explicitly let gh-ost know that you are running on a tungsten-replication based topology (you are likely to also provide --assume-master-host)")
6969
flag.BoolVar(&migrationContext.DiscardForeignKeys, "discard-foreign-keys", false, "DANGER! This flag will migrate a table that has foreign keys and will NOT create foreign keys on the ghost table, thus your altered table will have NO foreign keys. This is useful for intentional dropping of foreign keys")
@@ -120,6 +120,7 @@ func main() {
120120
help := flag.Bool("help", false, "Display usage")
121121
version := flag.Bool("version", false, "Print version & exit")
122122
checkFlag := flag.Bool("check-flag", false, "Check if another flag exists/supported. This allows for cross-version scripting. Exits with 0 when all additional provided flags exist, nonzero otherwise. You must provide (dummy) values for flags that require a value. Example: gh-ost --check-flag --cut-over-lock-timeout-seconds --nice-ratio 0")
123+
flag.StringVar(&migrationContext.ForceTmpTableName, "force-table-names", "", "table name prefix to be used on the temporary tables")
123124

124125
flag.Parse()
125126

go/logic/applier.go

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ func (this *Applier) CreateChangelogTable() error {
200200
id bigint auto_increment,
201201
last_update timestamp not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
202202
hint varchar(64) charset ascii not null,
203-
value varchar(255) charset ascii not null,
203+
value varchar(4096) charset ascii not null,
204204
primary key(id),
205205
unique key hint_uidx(hint)
206206
) auto_increment=256
@@ -398,35 +398,41 @@ func (this *Applier) CalculateNextIterationRangeEndValues() (hasFurtherRange boo
398398
if this.migrationContext.MigrationIterationRangeMinValues == nil {
399399
this.migrationContext.MigrationIterationRangeMinValues = this.migrationContext.MigrationRangeMinValues
400400
}
401-
query, explodedArgs, err := sql.BuildUniqueKeyRangeEndPreparedQuery(
402-
this.migrationContext.DatabaseName,
403-
this.migrationContext.OriginalTableName,
404-
&this.migrationContext.UniqueKey.Columns,
405-
this.migrationContext.MigrationIterationRangeMinValues.AbstractValues(),
406-
this.migrationContext.MigrationRangeMaxValues.AbstractValues(),
407-
atomic.LoadInt64(&this.migrationContext.ChunkSize),
408-
this.migrationContext.GetIteration() == 0,
409-
fmt.Sprintf("iteration:%d", this.migrationContext.GetIteration()),
410-
)
411-
if err != nil {
412-
return hasFurtherRange, err
413-
}
414-
rows, err := this.db.Query(query, explodedArgs...)
415-
if err != nil {
416-
return hasFurtherRange, err
417-
}
418-
iterationRangeMaxValues := sql.NewColumnValues(this.migrationContext.UniqueKey.Len())
419-
for rows.Next() {
420-
if err = rows.Scan(iterationRangeMaxValues.ValuesPointers...); err != nil {
401+
for i := 0; i < 2; i++ {
402+
buildFunc := sql.BuildUniqueKeyRangeEndPreparedQueryViaOffset
403+
if i == 1 {
404+
buildFunc = sql.BuildUniqueKeyRangeEndPreparedQueryViaTemptable
405+
}
406+
query, explodedArgs, err := buildFunc(
407+
this.migrationContext.DatabaseName,
408+
this.migrationContext.OriginalTableName,
409+
&this.migrationContext.UniqueKey.Columns,
410+
this.migrationContext.MigrationIterationRangeMinValues.AbstractValues(),
411+
this.migrationContext.MigrationRangeMaxValues.AbstractValues(),
412+
atomic.LoadInt64(&this.migrationContext.ChunkSize),
413+
this.migrationContext.GetIteration() == 0,
414+
fmt.Sprintf("iteration:%d", this.migrationContext.GetIteration()),
415+
)
416+
if err != nil {
421417
return hasFurtherRange, err
422418
}
423-
hasFurtherRange = true
424-
}
425-
if !hasFurtherRange {
426-
log.Debugf("Iteration complete: no further range to iterate")
427-
return hasFurtherRange, nil
419+
rows, err := this.db.Query(query, explodedArgs...)
420+
if err != nil {
421+
return hasFurtherRange, err
422+
}
423+
iterationRangeMaxValues := sql.NewColumnValues(this.migrationContext.UniqueKey.Len())
424+
for rows.Next() {
425+
if err = rows.Scan(iterationRangeMaxValues.ValuesPointers...); err != nil {
426+
return hasFurtherRange, err
427+
}
428+
hasFurtherRange = true
429+
}
430+
if hasFurtherRange {
431+
this.migrationContext.MigrationIterationRangeMaxValues = iterationRangeMaxValues
432+
return hasFurtherRange, nil
433+
}
428434
}
429-
this.migrationContext.MigrationIterationRangeMaxValues = iterationRangeMaxValues
435+
log.Debugf("Iteration complete: no further range to iterate")
430436
return hasFurtherRange, nil
431437
}
432438

0 commit comments

Comments
 (0)