From ec5593026c59f080f6562407ddde89e38d1f984b Mon Sep 17 00:00:00 2001 From: Yan Barkovskiy <12791394+yan-bar@users.noreply.github.com> Date: Tue, 3 Nov 2026 19:42:27 -0600 Subject: [PATCH 1/2] Increase default DML batch size to 100 Keep the migration context and flag documentation consistent, and cover the new default with a regression test. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- doc/command-line-flags.md | 4 ++-- go/base/context.go | 2 +- go/base/context_test.go | 5 +++++ go/cmd/gh-ost/main.go | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/doc/command-line-flags.md b/doc/command-line-flags.md index ff781b250..f08befd60 100644 --- a/doc/command-line-flags.md +++ b/doc/command-line-flags.md @@ -175,9 +175,9 @@ See also: [`skip-foreign-key-checks`](#skip-foreign-key-checks) `gh-ost` reads event from the binary log and applies them onto the _ghost_ table. It does so in batched writes: grouping multiple events to apply in a single transaction. This gives better write throughput as we don't need to sync the transaction log to disk for each event. -The `--dml-batch-size` flag controls the size of the batched write. Allowed values are `1 - 1000`, where `1` means no batching (every event from the binary log is applied onto the _ghost_ table on its own transaction). Default value is `10`. +The `--dml-batch-size` flag controls the size of the batched write. Allowed values are `1 - 1000`, where `1` means no batching (every event from the binary log is applied onto the _ghost_ table on its own transaction). Default value is `100`. -Why is this behavior configurable? Different workloads have different characteristics. Some workloads have very large writes, such that aggregating even `50` writes into a transaction makes for a significant transaction size. On other workloads write rate is high such that one just can't allow for a hundred more syncs to disk per second. The default value of `10` is a modest compromise that should probably work very well for most workloads. Your mileage may vary. +Why is this behavior configurable? Different workloads have different characteristics. Some workloads have very large writes, such that aggregating even `50` writes into a transaction makes for a significant transaction size. On other workloads write rate is high such that one just can't allow for a hundred more syncs to disk per second. The default value is `100`, but the appropriate batch size depends on the workload. Your mileage may vary. Noteworthy is that setting `--dml-batch-size` to higher value _does not_ mean `gh-ost` blocks or waits on writes. The batch size is an upper limit on transaction size, not a minimal one. If `gh-ost` doesn't have "enough" events in the pipe, it does not wait on the binary log, it just writes what it already has. This conveniently suggests that if write load is light enough for `gh-ost` to only see a few events in the binary log at a given time, then it is also light enough for `gh-ost` to apply a fraction of the batch size. diff --git a/go/base/context.go b/go/base/context.go index b2ec8ff1e..7bae13bbc 100644 --- a/go/base/context.go +++ b/go/base/context.go @@ -323,7 +323,7 @@ func NewMigrationContext() *MigrationContext { ApplierConnectionConfig: mysql.NewConnectionConfig(), MaxLagMillisecondsThrottleThreshold: 1500, CutOverLockTimeoutSeconds: 3, - DMLBatchSize: 10, + DMLBatchSize: 100, etaNanoseonds: ETAUnknown, maxLoad: NewLoadMap(), criticalLoad: NewLoadMap(), diff --git a/go/base/context_test.go b/go/base/context_test.go index a9f62150d..d3ddcb12e 100644 --- a/go/base/context_test.go +++ b/go/base/context_test.go @@ -21,6 +21,11 @@ func init() { log.SetLevel(log.ERROR) } +func TestNewMigrationContextDMLBatchSize(t *testing.T) { + context := NewMigrationContext() + require.Equal(t, int64(100), context.DMLBatchSize) +} + func TestGetTableNames(t *testing.T) { { context := NewMigrationContext() diff --git a/go/cmd/gh-ost/main.go b/go/cmd/gh-ost/main.go index cd1f5993f..dda25890e 100644 --- a/go/cmd/gh-ost/main.go +++ b/go/cmd/gh-ost/main.go @@ -123,7 +123,7 @@ func main() { flag.BoolVar(&migrationContext.CutOverExponentialBackoff, "cut-over-exponential-backoff", false, "Wait exponentially longer intervals between failed cut-over attempts. Wait intervals obey a maximum configurable with 'exponential-backoff-max-interval').") exponentialBackoffMaxInterval := flag.Int64("exponential-backoff-max-interval", 64, "Maximum number of seconds to wait between attempts when performing various operations with exponential backoff.") chunkSize := flag.Int64("chunk-size", 1000, "amount of rows to handle in each iteration (allowed range: 10-100,000)") - dmlBatchSize := flag.Int64("dml-batch-size", 10, "batch size for DML events to apply in a single transaction (range 1-1000)") + dmlBatchSize := flag.Int64("dml-batch-size", 100, "batch size for DML events to apply in a single transaction (range 1-1000)") defaultRetries := flag.Int64("default-retries", 60, "Default number of retries for various operations before panicking") flag.BoolVar(&migrationContext.PanicOnWarnings, "panic-on-warnings", false, "Panic when SQL warnings are encountered when copying a batch indicating data loss") 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") From 3dbfb47269a84fdf9b8157a28da1220657202d04 Mon Sep 17 00:00:00 2001 From: Yan Barkovskiy <12791394+yan-bar@users.noreply.github.com> Date: Tue, 3 Nov 2026 19:59:36 -0600 Subject: [PATCH 2/2] Document packet-size risk of larger DML batches Explain unchanged retries of oversized batches and recommend a smaller batch size, including 10 to preserve the previous default limit. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- doc/command-line-flags.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/command-line-flags.md b/doc/command-line-flags.md index f08befd60..c9e44b18d 100644 --- a/doc/command-line-flags.md +++ b/doc/command-line-flags.md @@ -179,6 +179,8 @@ The `--dml-batch-size` flag controls the size of the batched write. Allowed valu Why is this behavior configurable? Different workloads have different characteristics. Some workloads have very large writes, such that aggregating even `50` writes into a transaction makes for a significant transaction size. On other workloads write rate is high such that one just can't allow for a hundred more syncs to disk per second. The default value is `100`, but the appropriate batch size depends on the workload. Your mileage may vary. +For workloads with large rows, the combined DML statements in a batch can exceed MySQL's `max_allowed_packet` limit. `gh-ost` retries the same batch without reducing its size, so a persistently oversized batch eventually aborts the migration when retries are exhausted. Use a smaller `--dml-batch-size` for these workloads; `--dml-batch-size=10` preserves the previous default batch limit. + Noteworthy is that setting `--dml-batch-size` to higher value _does not_ mean `gh-ost` blocks or waits on writes. The batch size is an upper limit on transaction size, not a minimal one. If `gh-ost` doesn't have "enough" events in the pipe, it does not wait on the binary log, it just writes what it already has. This conveniently suggests that if write load is light enough for `gh-ost` to only see a few events in the binary log at a given time, then it is also light enough for `gh-ost` to apply a fraction of the batch size. ### exact-rowcount