Verifiers
Verifiers in Ghostferry are designed to ensure that Ghostferry did not corrupt/miss data. There are two independent mechanisms:
- A data verifier, selected with
VerifierTypein the embeddedghostferry.Config:"ChecksumTable","Inline", the deprecated"Iterative", or"NoVerification". LeavingVerifierTypeempty means no data verifier in copydb; in library code, an emptyVerifierTypelets you supply your ownFerry.Verifierinstead. - The
TargetVerifier, which monitors the target’s binlog for writes that did not come from Ghostferry. It is enabled by default (SkipTargetVerification: false) whichever data verifier is chosen, includingNoVerification. It does not compare row contents and is not a substitute for a final data verification.
A comparison of the ChecksumTableVerifier and InlineVerifier data verifiers is given below:
| ChecksumTableVerifier | InlineVerifier | |
|---|---|---|
| Mechanism | CHECKSUM TABLE | Verify row after insert; Reverify changed rows before and during cutover. |
| Impacts on Cutover Time | Linear w.r.t data size | Linear w.r.t. change rate 1 |
| Impacts on Copy Time 2 | None | Linear w.r.t data size |
| Memory Usage | Minimal | Linear w.r.t rows changed |
| Partial table copy | Not supported | Supported |
| Worst Case Scenario | Large databases causes unacceptable downtime | Verification is slower than the change rate of the DB |
ChecksumTable is a simple choice for small whole-table copies such as the tutorial, because it scans every copied table during cutover. Inline is the non-deprecated incremental option for larger datasets or partial copies. A successful verification in a rehearsal does not verify the data of a later run; see Running ghostferry-copydb in production.
Note that the InlineVerifier on its own may potentially miss some cases, and keeping the TargetVerifier enabled is recommended if these cases are possible. In the table below, “Yes” means the condition is detected within the scope of the selected verifier: the copied tables, and for the Inline verifier the compared columns (columns configured in IgnoredColumnsForVerification are skipped, and columns listed in CompressedColumnsForVerification are compared after decompression).
| Conditions | ChecksumTable | Inline | Inline + Target |
|---|---|---|---|
| Data inconsistency due to Ghostferry issuing an incorrect UPDATE on the target database (example: encoding-type issues). | Yes 3 | Yes | Yes |
| Data inconsistency due to Ghostferry failing to INSERT on the target database. | Yes | Yes | Yes |
| Data inconsistency due to Ghostferry failing to DELETE on the target database. | Yes | Yes | Yes |
| Data inconsistency due to rogue application issuing writes (INSERT/UPDATE/DELETE) against the target database. | Yes | Sometimes 4 | Yes |
| Data inconsistency due to missing binlog events when Ghostferry is resumed from the wrong binlog coordinates. | Yes | Sometimes 5 | Sometimes 5 |
| Data inconsistency if Ghostferry’s Binlog writing implementation is incorrect and modified the wrong row on the target (example, an UPDATE is supposed to go to id = 1 but Ghostferry instead issued a query for id = 2). This is an unrealistic scenario, but is included for illustrative purposes. | Yes | Probably not 6 | Probably not 6 |
IterativeVerifier (Deprecated)
NOTE! This is a deprecated verifier. Use the InlineVerifier instead.
IterativeVerifier verifies the source and target in a couple of steps:
-
After the data copy, it first compares the hashes of each applicable rows of the source and the target together to make sure they are the same. This is known as the initial verification.
- If they are the same: the verification for that row is complete.
- If they are not the same: add it into a reverify queue.
-
For any rows changed during the initial verification process, add it into the reverify queue.
-
After the initial verification, verify the rows’ hashes in the reverification queue again. This is done to reduce the time needed to reverify during the cutover as we assume the reverification queue will become smaller during this process.
-
During the cutover stage, verify all rows’ hashes in the reverify queue.
- If they are the same: the verification for that row is complete.
- If they are not the same: the verification fails.
-
If no verification failure occurs, the compared rows of the source and the target are identical within the scope of the verifier. If verification failure does occur (4b), then the source and target are not identical.
A proof of concept TLA+ verification of this algorithm is done in https://github.com/Shopify/ghostferry/tree/iterative-verifier-tla.
InlineVerifier
InlineVerifier verifies the source and target inline with the other components with a few slight differences from the IterativeVerifier above. The primary difference being that this verification process happens while the data is being copied by the DataIterator instead of after the fact.
With regards to the DataIterator and BatchWriter:
-
While selecting the data in the
DataIterator, a fingerprint is appended to the end of the statement thatSELECTs data from the source asSELECT *, MD5(...) FROM ... -
The fingerprint, gathered from the
MD5(...)of the query above is stored on theRowBatchto be used in the next verification step. -
The
BatchWriterthen writes theRowBatchas follows:- A transaction is opened on the target.
- The data contained in the
RowBatchis inserted. - The pagination key and fingerprint of these rows are then
SELECTed from the target in the same transaction. - The target fingerprints are compared with the source fingerprints stored on the
RowBatch. The pagination keys of mismatched rows are added to thereverifyStoreto be verified again later. - The transaction is committed.
Query and write failures are retried (with a limit) and fail the run if the retry limit is exceeded. A mismatch alone does not abort the copy: it is enqueued for reverification. The exception is when
EnforceInlineVerificationis set on theBatchWriter(used for standalone copies without binlog streaming, such asFerry.RunStandaloneDataCopy), where a mismatch fails the batch.
With regards to the BinlogStreamer:
-
As DMLs are observed by the
BinlogStreamer, the pagination keys of the changed rows are placed into thereverifyStoreto be periodically verified for correctness, everyInlineVerifierConfig.VerifyBinlogEventsInterval(default"1s"). -
This continues to happen in the background until cutover is allowed.
-
If a row is found not to match, its pagination key is added back into the
reverifyStoreto be verified again. -
VerifyBeforeCutoverreverifies thereverifyStorein at most 30 passes, stopping early once at most 1000 rows remain queued or a pass no longer shrinks the queue. IfInlineVerifierConfig.MaxExpectedDowntimeis set (non-empty and non-zero) and the last pass took longer, the run fails. This is an estimate, not a hard guarantee on downtime. -
When
VerifyDuringCutoverbegins, all of the remaining rows in thereverifyStoreare verified. If any mismatch remains, the result hasDataCorrect: falseand a message listing the mismatched pagination keys.VerifyDuringCutovercan only be started once, and any source binlog event received after it started is an error.
VerifyDuringCutover must be called after binlog streaming has stopped and before the target receives application writes. ghostferry-copydb does not call it automatically: the operator must click Run Verification in the web UI after cutover and check that Verified Correct is true and no error is shown before letting applications write to the target. The copydb state done only means that copying and streaming have finished. Custom applications must call VerifyDuringCutover themselves.
TargetVerifier
TargetVerifier detects writes to the copied tables on the target that were not made by Ghostferry during the move process. It is enabled by default and is meant to be used in conjunction with one of the data verifiers above; it does not compare row contents.
Ghostferry prepends an SQL annotation (Target.Marginalia, default application:ghostferry) to its statements on the target. The TargetVerifier checks for this expected annotation:
-
A BinlogStreamer is created and attached to the Target. This requires the target to have binary logging with
binlog_rows_query_log_events=ON, and the target user to have replication privileges. -
As this BinlogStreamer receives DML events for the copied tables, it extracts the annotation from the query event preceding each
RowsEvent. -
If no annotation is found for the DML, or the extracted annotation text does not match
Target.Marginalia, an error is returned and the run fails.
This detects unexpected writers; it is not cryptographic authentication and does not protect against a writer that deliberately uses the same annotation.
The TargetVerifier must be stopped (Ferry.StopTargetVerifier) after source binlog processing has finished and before the target is opened to application writes; otherwise it would fail the run on the application’s writes. Stopping it also lets it process all target binlog events up to the stop point. ghostferry-copydb does this automatically after the source binlog streaming stops and before calling CutoverUnlock. Custom applications must do the equivalent themselves.
-
Additional improvements could be made to reduce this as long as Ghostferry is faster than the rate of change. See https://github.com/Shopify/ghostferry/issues/13. ↩
-
Increase in copy time does not increase downtime. Downtime occurs only in cutover. ↩
-
Note that the CHECKSUM TABLE statement is broken in MySQL 5.7 for tables with JSON columns. These tables will result in a false positive event: even if two tables are identical, they can emit different checksums. See https://bugs.mysql.com/bug.php?id=87847. This applies to every row in this table. ↩
-
If the rows modified by the rogue application are modified again on the source after Ghostferry starts, the InlineVerifier’s binlog tailer should pick up that row and attempt to reverify it. ↩
-
If the rows missed after resume are modified again on the source after Ghostferry starts, the InlineVerifier’s binlog tailer should pick up that row and attempt to reverify it. ↩ ↩2
-
If the implementation of the Ghostferry algorithm is so broken, chances are the InlineVerifier won’t catch it either as it relies on the same algorithm to enumerate the table and tail the binlogs. ↩ ↩2