You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Parallel row-copy, DML merge, frontier filter, and heartbeat lag throttle (#2)
Performance optimizations for gh-ost that significantly speed up row-copy
under high write load while keeping binlog lag bounded:
- Parallel row-copy with dedicated connection pool and time-bounded drain
- DML event merging within batches (INSERT/DELETE cancellation, UPDATE folding)
- Frontier filter to skip DML events beyond copy frontier
- Heartbeat lag throttle (--copy-max-lag-millis) for row-copy pacing
- Adaptive drain budget and auto-tuning chunk size
- Runtime-changeable --copy-concurrency and --copy-max-lag-millis
- Fix multithreaded replication data inconsistency
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy file name to clipboardExpand all lines: doc/command-line-flags.md
+64Lines changed: 64 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -92,6 +92,30 @@ password=123456
92
92
93
93
Defaults to `true`. See [`exact-rowcount`](#exact-rowcount)
94
94
95
+
### copy-concurrency
96
+
97
+
Number of concurrent row-copy goroutines. Default: `1` (original single-threaded behavior). Range: `1-32`.
98
+
99
+
Higher values speed up row-copy under heavy write load by using parallel `INSERT...SELECT` operations on non-overlapping ranges. Each worker copies a different chunk concurrently, and a bounded drain budget ensures DML events are still processed between copy batches.
100
+
101
+
When `--copy-concurrency` > 1, the [heartbeat lag throttle](#copy-max-lag-millis) is automatically active to prevent unbounded binlog lag growth.
102
+
103
+
This value can be changed at runtime via [interactive commands](interactive-commands.md).
Maximum allowed *internal* binlog processing lag (HeartbeatLag) in milliseconds before row-copy is paused. Default: `60000` (60 seconds). Set to `0` to disable.
110
+
111
+
This is only active when `--copy-concurrency` > 1. When HeartbeatLag exceeds this threshold, row-copy pauses and gh-ost drains the binlog event queue exclusively until lag drops to half the threshold (hysteresis prevents oscillation).
112
+
113
+
**Differs from [`--max-lag-millis`](#max-lag-millis):**`--max-lag-millis` measures *replica* replication lag and throttles the entire migration. `--copy-max-lag-millis` measures gh-ost's *own* binlog processing lag and only pauses row-copy while DML apply continues at full speed.
114
+
115
+
This value can be changed at runtime via [interactive commands](interactive-commands.md).
116
+
117
+
See [Throttle: Copy-specific lag throttle](throttle.md#copy-specific-lag-throttle---copy-max-lag-millis) for a detailed comparison.
118
+
95
119
### critical-load
96
120
97
121
Comma delimited status-name=threshold, same format as [`--max-load`](#max-load).
@@ -200,6 +224,16 @@ See [`initially-drop-ghost-table`](#initially-drop-ghost-table)
200
224
201
225
Default False. Should `gh-ost` forcibly delete an existing socket file. Be careful: this might drop the socket file of a running migration!
202
226
227
+
### copy-max-lag-millis
228
+
229
+
When using parallel row-copy (`--copy-concurrency` > 1), this flag sets the maximum allowed *internal* binlog processing lag (HeartbeatLag) before row-copy is paused. Unlike `--max-lag-millis` which measures replica lag and throttles the entire migration, `--copy-max-lag-millis` only pauses row-copy while DML event processing continues at full speed.
230
+
231
+
Default: `60000` (60 seconds). Set to `0` to disable (maximum copy speed, unbounded lag).
232
+
233
+
Row-copy resumes when HeartbeatLag drops to half the threshold (hysteresis prevents oscillation).
234
+
235
+
See [Throttle: Copy-specific lag throttle](throttle.md#copy-specific-lag-throttle---copy-max-lag-millis) for a detailed comparison with `--max-lag-millis`.
236
+
203
237
### max-lag-millis
204
238
205
239
On a replication topology, this is perhaps the most important migration throttling factor: the maximum lag allowed for migration to work. If lag exceeds this value, migration throttles.
@@ -252,6 +286,28 @@ Defaults to an auto-determined and advertised upon startup file. Defines Unix so
252
286
253
287
By default `gh-ost` verifies no foreign keys exist on the migrated table. On servers with large number of tables this check can take a long time. If you're absolutely certain no foreign keys exist (table does not reference other table nor is referenced by other tables) and wish to save the check time, provide with `--skip-foreign-key-checks`.
254
288
289
+
### skip-dml-frontier-filter
290
+
291
+
Disable the frontier-based DML skip optimization. Default: `false` (optimization is enabled).
292
+
293
+
When enabled (default), gh-ost skips DML events targeting rows that have not yet been copied — since row-copy will capture the latest value when it reaches those rows. This reduces redundant DML apply work during the copy phase.
294
+
295
+
**Safety constraints** — the frontier filter is automatically disabled in two situations regardless of this flag:
296
+
297
+
1.**When `--copy-concurrency` > 1**: With parallel row-copy, multiple chunks are being copied concurrently. The frontier (last completed chunk boundary) is not a reliable boundary because in-flight chunks may not have committed yet. A DML event targeting a row in an in-flight chunk could be incorrectly skipped, causing data loss.
298
+
299
+
2.**In replica modes** (`--test-on-replica`, `--migrate-on-replica`): In replica mode, gh-ost reads binlog events from the replica's relay log. These events may be ahead of the replica's SQL thread apply position — meaning row-copy `SELECT` queries may not yet see the data from the skipped events. This would cause silent data loss since neither the DML apply nor the row-copy would capture those changes.
300
+
301
+
Use `--skip-dml-frontier-filter` for benchmarking or if you suspect the optimization is causing issues.
302
+
303
+
### skip-dml-merge
304
+
305
+
Disable DML event merging within batches. Default: `false` (merging is enabled).
306
+
307
+
When enabled (default), gh-ost merges redundant DML events for the same row before applying them. For example, an INSERT followed by multiple UPDATEs to the same row becomes a single INSERT with the final values. Under high write load, this can reduce applied statements by ~36%.
308
+
309
+
Use `--skip-dml-merge` for benchmarking or if you suspect merging is causing issues.
310
+
255
311
### skip-metadata-lock-check
256
312
257
313
By default `gh-ost` performs a check before the cut-over to ensure the rename session holds the exclusive metadata lock on the table. In case `performance_schema.metadata_locks` cannot be enabled on your setup, this check can be skipped with `--skip-metadata-lock-check`.
@@ -333,3 +389,11 @@ Makes the _old_ table include a timestamp value. The _old_ table is what the ori
333
389
### tungsten
334
390
335
391
See [`tungsten`](cheatsheet.md#tungsten) on the cheatsheet.
392
+
393
+
### workers
394
+
395
+
Number of concurrent workers for applying DML events. Default: `8`. Each worker uses one goroutine and its own database connection to apply binlog events in parallel.
396
+
397
+
This controls the parallelism of the multithreaded replication (MTR) subsystem which processes binlog DML events. Higher values can improve DML throughput under heavy write load, but beyond 8-16 workers the gains diminish as contention on the ghost table increases.
Copy file name to clipboardExpand all lines: doc/interactive-commands.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -24,6 +24,8 @@ Both interfaces may serve at the same time. Both respond to simple text command,
24
24
-`chunk-size=<newsize>`: modify the `chunk-size`; applies on next running copy-iteration
25
25
-`dml-batch-size=<newsize>`: modify the `dml-batch-size`; applies on next applying of binary log events
26
26
-`max-lag-millis=<max-lag>`: modify the maximum replication lag threshold (milliseconds, minimum value is `100`, i.e. `0.1` second)
27
+
-`copy-concurrency=<N>`: modify the number of parallel row-copy workers (range 1-32). Setting to `1` switches to single-threaded legacy behavior. Takes effect on the next copy iteration.
28
+
-`copy-max-lag-millis=<millis>`: modify the heartbeat lag threshold for copy throttling (milliseconds). Set `0` to disable. Takes effect immediately on the next drain check.
27
29
-`max-load=<max-load-thresholds>`: modify the `max-load` config; applies on next running copy-iteration
28
30
- The `max-load` format must be: `some_status=<numeric-threshold>[,some_status=<numeric-threshold>...]`'
29
31
- For example: `Threads_running=50,threads_connected=1000`, and you would then write/echo `max-load=Threads_running=50,threads_connected=1000` to the socket.
### Copy-specific lag throttle (--copy-max-lag-millis)
80
+
81
+
When using parallel row-copy (`--copy-concurrency` > 1), gh-ost uses a bounded drain strategy that gives row-copy more execution turns. This can cause the binlog processing to fall behind, resulting in increasing "HeartbeatLag" — the time since the last heartbeat event was *processed* from the binlog stream.
82
+
83
+
`--copy-max-lag-millis` (default: `60000`, i.e. 60 seconds) sets a threshold for how far behind binlog processing may fall during the row-copy phase. When HeartbeatLag exceeds this threshold, row-copy is paused and gh-ost drains the binlog event queue exclusively until lag drops to half the threshold (hysteresis to prevent oscillation).
84
+
85
+
**How it differs from `--max-lag-millis`:**
86
+
87
+
||`--max-lag-millis`|`--copy-max-lag-millis`|
88
+
|---|---|---|
89
+
|**What it measures**| Replication lag on downstream replicas (via heartbeat table queries or `SHOW SLAVE STATUS`) | Binlog processing lag *within gh-ost itself* (wall-clock time since last heartbeat event was read from the binlog stream) |
90
+
|**What it throttles**| The entire migration — all reads and writes pause | Only row-copy — DML event processing continues at full speed |
91
+
|**When it matters**| Always — protects replicas from falling behind | Only with parallel row-copy (`--copy-concurrency` > 1) |
92
+
|**Default**| 1500ms (strict — sub-second replica lag) | 60000ms (permissive — allows some internal lag buildup for speed) |
93
+
|**Resume behavior**| Resumes immediately when lag drops below threshold | Resumes when lag drops to threshold/2 (hysteresis) |
94
+
95
+
**Why both are needed:**`--max-lag-millis` protects *downstream replicas* from excessive replication lag. `--copy-max-lag-millis` protects against *gh-ost's own internal lag* — which only occurs with parallel row-copy because the bounded drain budget gives copy more turns at the expense of binlog processing. With single-copy mode (the default), `ProcessEventsUntilDrained()` blocks indefinitely so internal lag never accumulates.
96
+
97
+
Set `--copy-max-lag-millis=0` to disable the throttle entirely (maximum copy speed, unbounded lag growth).
98
+
79
99
### Throttle precedence
80
100
81
101
Any single factor in the above that suggests the migration should throttle - causes throttling. That is, once some component decides to throttle, you cannot override it; you cannot force continued execution of the migration.
Copy file name to clipboardExpand all lines: go/cmd/gh-ost/main.go
+5Lines changed: 5 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -112,6 +112,11 @@ func main() {
112
112
flag.BoolVar(&migrationContext.PanicOnWarnings, "panic-on-warnings", false, "Panic when SQL warnings are encountered when copying a batch indicating data loss")
113
113
cutOverLockTimeoutSeconds:=flag.Int64("cut-over-lock-timeout-seconds", 3, "Max number of seconds to hold locks on tables while attempting to cut-over (retry attempted when lock exceeds timeout) or attempting instant DDL")
114
114
niceRatio:=flag.Float64("nice-ratio", 0, "force being 'nice', imply sleep time per chunk time; range: [0.0..100.0]. Example values: 0 is aggressive. 1: for every 1ms spent copying rows, sleep additional 1ms (effectively doubling runtime); 0.7: for every 10ms spend in a rowcopy chunk, spend 7ms sleeping immediately after")
115
+
flag.IntVar(&migrationContext.NumWorkers, "workers", 8, "Number of concurrent workers for applying DML events. Each worker uses one goroutine.")
116
+
flag.Int64Var(&migrationContext.CopyConcurrency, "copy-concurrency", 1, "Number of concurrent row-copy goroutines. Higher values speed up row-copy under write load by using parallel INSERT...SELECT operations on non-overlapping ranges. (range 1-32)")
flag.Int64Var(&migrationContext.CopyMaxLagMillis, "copy-max-lag-millis", 60000, "Heartbeat lag (ms) at which row-copy pauses to let binlog catch up. Only active with --copy-concurrency > 1. Set 0 to disable. Default 60s balances copy speed vs lag growth.")
115
120
116
121
maxLagMillis:=flag.Int64("max-lag-millis", 1500, "replication lag at which to throttle operation")
117
122
replicationLagQuery:=flag.String("replication-lag-query", "", "Deprecated. gh-ost uses an internal, subsecond resolution query")
0 commit comments