Skip to main content

Data Sync Internals

Testudo data sync is not database-level real-time replication. It is a periodic backup plus target-side restore workflow built around Velero. DataSync schedules and orchestrates the run, AppBackup creates Velero Backup objects on the current primary cluster, and AppRestore creates Velero Restore objects on the secondary cluster. PVC data is moved through Velero file-system backup (FSB) and object storage.

This model fits Pilot Light DR: the secondary cluster keeps a standby resource skeleton, while data is refreshed on a schedule. During failover, the target side does not need to restore all data from scratch.

DataSync flow

Editable source: data-sync-flow.excalidraw

Mental Model

DisasterInstance
-> DataSync
-> AppBackup -> source Velero Backup -> object storage
-> AppRestore -> target Velero Restore -> target PVC
LayerObjectResponsibility
User intentDisasterInstanceDefines namespaces, selectors, restore policy, and primary/secondary roles
Sync controlDataSyncRegisters schedules, triggers syncs, connects backup and restore, records history
Backup wrapperAppBackupCreates and observes Velero Backup on the source cluster
Restore wrapperAppRestoreCreates and observes Velero Restore on the target cluster
Data planeVelero FSBReads mounted volumes from source Pods, uploads data, and restores it into target PVCs
StorageStorageRepository / BSLProvides Velero access to S3/MinIO-compatible object storage

Sync Direction

DataSync follows the runtime primary and secondary cluster roles.

If DisasterInstance.status.primaryCluster and secondaryCluster are set:

source = primaryCluster
target = secondaryCluster

If runtime roles are not set yet, the operator falls back to the static DisasterConfig:

source = DisasterConfig.spec.sourceCluster
target = DisasterConfig.spec.targetCluster

After failover or reprotect, DataSync does not need a new object name. The operator re-evaluates the current roles and changes the direction to the new primary-to-secondary path.

Triggers

DataSync has three trigger paths.

TriggerDescription
Initial syncIf status.lastSyncTime is empty, the operator starts a first sync so the instance can become protected
Scheduled syncspec.trigger.schedule is a standard five-field cron expression registered in the operator SyncScheduler
Manual syncUpdating spec.trigger.manual to a new RFC3339 timestamp triggers an immediate sync

When spec.paused=true, scheduled jobs are removed. A running sync continues to advance. Manual sync is used for operations such as creating a restore point before a release, validating a changed restore policy, or executing failover FinalSync.

If the previous sync is still InProgress, a new cron trigger is skipped to avoid concurrent writes to the same DataSync state.

Backup Phase

At the start of a sync, the operator first verifies that StorageRepository is available, then creates or reuses a long-lived AppBackup:

AppBackup name = ds-<DataSync name>

Each sync run writes a new backup action to this object:

spec:
action:
type: Backup
requestAt: "<current time>"

AppBackup then creates a Velero Backup in the source cluster velero namespace. The DataSync backup template is roughly:

includedNamespaces:
- <protected namespace>
excludedNamespaces:
- velero
- kube-system
includedResources:
- pods
- persistentvolumeclaims
- persistentvolumes
snapshotVolumes: false
defaultVolumesToFsBackup: true
storageLocation: <BSL derived from StorageRepository>

The important fields are:

FieldMeaning
includedResources includes pods/pvc/pvVelero FSB needs Pod and volume metadata to locate data
snapshotVolumes=falseCSI VolumeSnapshot is not the primary path
defaultVolumesToFsBackup=trueVelero file-system backup is used
storageLocationPoints to the object storage BSL

Runtime BSL names are suffixed by cluster:

<StorageRepository>-<sourceCluster>

The object storage prefix uses the source cluster name. This allows several clusters to share a bucket without overwriting each other.

Restore Phase

After the source Velero Backup completes, DataSync creates a target-side AppRestore:

AppRestore name = rec-ds-<DataSync name prefix>-<BackupName hash>

The target-side AppRestore first ensures that the target cluster can read the source backup through a BSL:

BSL name = <StorageRepository>-<sourceCluster>
prefix = <sourceCluster>

Then it creates a Velero Restore. The data restore template is roughly:

backupName: <current Velero Backup name>
includedNamespaces:
- <protected namespace>
includedResources:
- pods
- persistentvolumeclaims
- persistentvolumes
restorePVs: true
existingResourcePolicy: None
preserveNodePorts: true

Normal DataSync uses existingResourcePolicy=None so it does not overwrite standby resources that already exist. Drill paths may use Update when restoring into a drill namespace.

If the instance has a restore policy, the operator applies it while building AppRestore, including StorageClass mappings, IngressClass mappings, and modifier rules. The apply target is dataSync, so only rules appropriate for the data restore path should be configured there.

PVC volumeName Cleanup

During the first initializing data restore, DataSync also injects a system-level ResourceModifier that removes the static PVC volume binding:

resourceModifierRules:
- conditions:
groupResource: persistentvolumeclaims
namespaces:
- <protected namespace>
patches:
- operation: remove
path: /spec/volumeName

Velero backups may contain PVCs with the source cluster spec.volumeName. That field means the PVC is already bound to a specific PV name. Source PV names usually cannot be reused directly in the target cluster. Restoring that binding as-is can leave the target PVC pointing at a missing source-side PV, or conflict with the PV that the target cluster dynamically provisions.

The current implementation injects this cleanup rule only at this exact point:

DataSync.status.lastSyncTime == nil
AND DisasterInstance.status.fsmState == Initializing

In other words, it only affects the first DataSync restore during instance initialization. After the first sync completes, later scheduled syncs do not automatically remove volumeName, so the operator does not disturb PVC bindings that have already stabilized on the target side.

After volumeName is removed, the target PVC must bind again. The target cluster therefore needs a working StorageClass/provisioner, or pre-created static PVs that satisfy the PVC requirements. If the target side has neither dynamic provisioning nor a matching PV, the PVC stays Pending, the trafficless Pod cannot mount the volume, and Velero PodVolumeRestore / DataDownload cannot complete.

If the instance has no restorePolicy, the operator appends this rule directly to AppRestore.spec.resourceModifierRules for the DataSync restore. If the instance has a restorePolicy, the operator injects it as a system-protect rule during policy compilation, so user-defined modifier rules cannot override this safety patch.

Drill data restore has the same protection when namespaceMapping restores a source namespace into a different target namespace. In that case, the operator injects PVC volumeName cleanup for the mapped target namespace so drill PVCs do not carry source-side PV bindings into the new namespace.

Why Trafficless Restore Exists

Velero FSB restore needs a target Pod that mounts the PVC. In Pilot Light mode, target workloads usually stay at replicas=0:

The target cluster has StatefulSet / Deployment / PVC
but no business Pod is running

Without a Pod, Velero Node Agent has no mount point to write to. If the original business Pod is restored directly, it may match a Service selector and receive traffic too early, or be adopted by a Deployment/StatefulSet controller.

Trafficless Restore restores a temporary Pod whose only job is to mount the PVC and receive data.

How The Temporary Pod Avoids Traffic

DataSync injects a ResourceModifier rule for pods:

resourceModifierRules:
- conditions:
groupResource: pods
patches:
- operation: add
path: /metadata/labels
value: '{"trafficless": "true"}'
- operation: add
path: /metadata/ownerReferences
value: '[]'
- operation: replace
path: /spec/containers/0/image
value: busybox:1.36
- operation: add
path: /spec/containers/0/command
value: '["sleep","3600"]'
PatchPurpose
Replace labels with trafficless=truePrevents Service selectors from routing business traffic to this Pod
Clear ownerReferencesPrevents workload controllers or GC from immediately adopting or deleting it
Replace image with busybox:1.36Avoids starting the real business process
Add sleep 3600Keeps the Pod alive long enough for Velero Node Agent to restore volume data

The Pod name and volume mounts still come from backup metadata, so Velero can write data into the corresponding target PVC. After restore succeeds, DataSync cleans up target Pods marked with trafficless=true.

In offline environments, the trafficless image must also be pullable from the target cluster. It is not the same setting as veleroInstall.imageRegistry; that setting only controls images used to install Velero components.

Relationship With ResourceSync

DataSync moves PVC data. It does not restore the full business resource skeleton. Pilot Light requires both DataSync and ResourceSync:

PathMain contentTarget-side result
ResourceSyncDeployments, StatefulSets, Services, ConfigMaps, Secrets, PVC objects, and other Kubernetes resourcesStandby resources exist, usually with replicas=0
DataSyncPod, PVC, PV metadata and volume file dataTarget PVCs contain the latest synchronized data

A healthy protected state normally requires:

DataSync = Ready
ResourceSync = Ready
DisasterInstance.fsmState = Protected

Relationship With Failover

Failover runs FinalSync, which triggers DataSync and ResourceSync one last time before activation.

The current step order is:

PauseSchedules
-> FinalSync
-> ScaleDownSource
-> ScaleUpTarget

FinalSync must happen before ScaleDownSource because FSB backup needs source Pods to stay running so it can read mounted PVC data. If source workloads are scaled to zero first, the last data sync may not include PVC data.

If the source cluster is already unreachable, users can skip final sync and fail over from the most recent successful DataSync restore point. This increases RPO risk and should be used only when necessary.

Consistency Boundary

DataSync is file-system-level backup and restore orchestration. It does not guarantee database transaction consistency.

ScenarioRiskRecommendation
Databases or queues with active writesBackup time may not match an application-consistent transaction pointUse application freeze hooks, a read-only window, or database-native replication
Large PVCsOne FSB backup/restore may take longer than the RPO scheduleSize Velero Node Agent resources, object storage throughput, and network bandwidth
Applications with many PVCsCross-volume consistency depends on the applicationSync during low traffic or use application-level consistency tooling
Source Pods already stoppedFSB may not read the latest PVC contentsDo not scale down the source before FinalSync during normal failover

Status And Troubleshooting

Key DataSync fields:

FieldMeaning
status.stateReady, InProgress, or Failed
status.reason / status.messageMachine-readable and human-readable failure information
status.lastSyncTimeLast completed sync time
status.lastBackupNameLast related Velero Backup name
status.lastRestoreNameLast related AppRestore name
status.historyBackup name, restore name, counts, duration, and result for recent runs

Useful commands:

kubectl -n disaster-system get datasync
kubectl -n disaster-system get datasync <datasync-name> -o yaml

kubectl -n disaster-system get appbackup \
-l testudo.softcdata.com/app-resource-owner-kind=datasync,\
testudo.softcdata.com/app-resource-owner-name=<datasync-name>

kubectl -n disaster-system get apprestore \
-l testudo.softcdata.com/app-resource-owner-kind=datasync,\
testudo.softcdata.com/app-resource-owner-name=<datasync-name>

Source cluster:

kubectl --context <source-cluster> -n velero get backup \
-l testudo.softcdata.com/app-backup-name=ds-<datasync-name>
kubectl --context <source-cluster> -n velero get podvolumebackup
kubectl --context <source-cluster> -n velero get dataupload

Target cluster:

kubectl --context <target-cluster> -n velero get restore
kubectl --context <target-cluster> -n velero get podvolumerestore
kubectl --context <target-cluster> -n velero get datadownload
kubectl --context <target-cluster> -A get pod -l trafficless=true

Related docs: