Skip to content

Commit c9c463c

Browse files
committed
controller: record exported diff size metric during backup
Set backup_exported_diff_size_bytes on the primary by summing the sizes of all uploaded diff parts, obtained via HeadObject against the object storage. The sum is recomputed and Set() on every reconcile, so the value is idempotent and needs no persistence in MantleBackup.status. The gauge is set from two places: - startUpload, so the value is updated as each diff part finishes uploading during normal operation. - the beginning of reconcileAsPrimary, so the value is restored on the first reconcile after a controller restart. startUpload is not always reached because the replicate path may return early, whereas the beginning of reconcileAsPrimary is always reached. The uploaded parts stay in the object storage until the secondary cluster cleans them up after import, which happens after the backup completes, so HeadObject always succeeds while the backup is in progress. Unlike the secondary, the primary does not initialize objectStorageClient elsewhere, so prepare it lazily before HeadObject. The metric is deleted when the MantleBackup is deleted, rather than when SyncedToRemote becomes True, to avoid removing it before Prometheus scrapes it. DeletePartialMatch is used because the sync_mode label value may already have been removed by the cleanup that runs beforehand. Signed-off-by: Kohya Shiozaki <kouyan120706@gmail.com>
1 parent 6f17390 commit c9c463c

2 files changed

Lines changed: 109 additions & 0 deletions

File tree

internal/controller/mantlebackup_controller.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,18 @@ func (r *MantleBackupReconciler) reconcileAsPrimary(ctx context.Context, backup
529529
return ctrl.Result{}, nil
530530
}
531531

532+
// Restore the exported-diff-size metric so that the gauge holds the correct value
533+
// even right after the controller restarts. startUpload also sets it, but the
534+
// replicate path below may return early before reaching startUpload, so we set it
535+
// here, which is always reached, to guarantee restoration on the first reconcile.
536+
largestCompletedUploadPartNum, err := r.getLargestCompletedUploadPartNum(ctx, backup)
537+
if err != nil {
538+
return ctrl.Result{}, fmt.Errorf("failed to get largest completed upload part number: %w", err)
539+
}
540+
if err := r.setExportedDiffSizeMetric(ctx, backup, largestCompletedUploadPartNum); err != nil {
541+
return ctrl.Result{}, fmt.Errorf("failed to set exported diff size metric: %w", err)
542+
}
543+
532544
if !backup.IsVerifiedTrue() && !backup.IsVerifiedFalse() {
533545
if err := r.verify(ctx, backup); err != nil {
534546
return ctrl.Result{}, err
@@ -1020,6 +1032,15 @@ func (r *MantleBackupReconciler) finalizeStandalone(
10201032
return ctrl.Result{}, err
10211033
}
10221034

1035+
// Delete the metric by partial match on the stable labels, because the sync_mode
1036+
// label value (set from the mantle.cybozu.io/sync-mode annotation) may already
1037+
// have been removed by the cleanup above.
1038+
metrics.BackupExportedDiffSizeBytes.DeletePartialMatch(prometheus.Labels{
1039+
"persistentvolumeclaim": backup.Spec.PVC,
1040+
"resource_namespace": backup.GetNamespace(),
1041+
"mantlebackup": backup.GetName(),
1042+
})
1043+
10231044
controllerutil.RemoveFinalizer(backup, MantleBackupFinalizerName)
10241045
if err := r.Update(ctx, backup); err != nil {
10251046
logger.Error(err, "failed to remove finalizer", "finalizer", MantleBackupFinalizerName)
@@ -1539,6 +1560,10 @@ func (r *MantleBackupReconciler) startUpload(ctx context.Context, targetBackup *
15391560
return fmt.Errorf("failed to handle completed upload jobs: %w", err)
15401561
}
15411562

1563+
if err := r.setExportedDiffSizeMetric(ctx, targetBackup, largestCompletedUploadPartNum); err != nil {
1564+
return fmt.Errorf("failed to set exported diff size metric: %w", err)
1565+
}
1566+
15421567
if err := r.createOrUpdateUploadJobs(
15431568
ctx,
15441569
targetBackup,
@@ -1551,6 +1576,83 @@ func (r *MantleBackupReconciler) startUpload(ctx context.Context, targetBackup *
15511576
return nil
15521577
}
15531578

1579+
// setExportedDiffSizeMetric sets the backup_exported_diff_size_bytes gauge to the
1580+
// total size of all diff parts uploaded so far. The sizes are obtained by
1581+
// HeadObject-ing each part in the object storage every time, so the gauge converges
1582+
// to the correct value no matter how many times reconciliation runs (idempotent) and
1583+
// can be restored even after the controller restarts without persisting anything to
1584+
// MantleBackup.status. The uploaded parts remain in the object storage until the
1585+
// backup completes and the secondary cluster cleans them up, so HeadObject always
1586+
// succeeds while the backup is in progress.
1587+
func (r *MantleBackupReconciler) setExportedDiffSizeMetric(
1588+
ctx context.Context,
1589+
backup *mantlev1.MantleBackup,
1590+
largestCompletedUploadPartNum int,
1591+
) error {
1592+
if largestCompletedUploadPartNum < 0 {
1593+
return nil
1594+
}
1595+
1596+
// The primary controller does not initialize objectStorageClient elsewhere
1597+
// (only the secondary path does), so initialize it here before HeadObject.
1598+
if err := r.prepareObjectStorageClient(ctx); err != nil {
1599+
return fmt.Errorf("failed to prepare object storage client: %w", err)
1600+
}
1601+
1602+
totalSize := int64(0)
1603+
for partNum := 0; partNum <= largestCompletedUploadPartNum; partNum++ {
1604+
key := MakeObjectNameOfExportedData(backup.GetName(), string(backup.GetUID()), partNum)
1605+
size, err := r.objectStorageClient.GetSize(ctx, key)
1606+
if err != nil {
1607+
return fmt.Errorf("failed to get size of exported data part %d: %w", partNum, err)
1608+
}
1609+
totalSize += size
1610+
}
1611+
1612+
metrics.BackupExportedDiffSizeBytes.WithLabelValues(
1613+
backup.Spec.PVC,
1614+
backup.GetNamespace(),
1615+
backup.GetName(),
1616+
backup.GetAnnotations()[annotSyncMode],
1617+
).Set(float64(totalSize))
1618+
1619+
return nil
1620+
}
1621+
1622+
// getLargestCompletedUploadPartNum returns the largest part number among the
1623+
// completed upload Jobs of the backup, or -1 if there is none. Unlike
1624+
// handleCompletedUploadJobs, it does not delete any Job, so it is safe to call from
1625+
// the beginning of reconciliation to restore the exported-diff-size metric.
1626+
func (r *MantleBackupReconciler) getLargestCompletedUploadPartNum(
1627+
ctx context.Context,
1628+
backup *mantlev1.MantleBackup,
1629+
) (int, error) {
1630+
var jobList batchv1.JobList
1631+
if err := r.List(ctx, &jobList, &client.ListOptions{
1632+
Namespace: r.managedCephClusterID,
1633+
LabelSelector: labels.SelectorFromSet(map[string]string{
1634+
"app.kubernetes.io/name": labelAppNameValue,
1635+
"app.kubernetes.io/component": labelComponentUploadJob,
1636+
}),
1637+
}); err != nil {
1638+
return -1, fmt.Errorf("failed to list upload Jobs: %w", err)
1639+
}
1640+
1641+
largestPartNum := -1
1642+
for _, job := range jobList.Items {
1643+
if !IsJobConditionTrue(job.Status.Conditions, batchv1.JobComplete) {
1644+
continue
1645+
}
1646+
partNum, ok := ExtractPartNumFromUploadJobName(job.GetName(), backup)
1647+
if !ok {
1648+
continue
1649+
}
1650+
largestPartNum = max(largestPartNum, partNum)
1651+
}
1652+
1653+
return largestPartNum, nil
1654+
}
1655+
15541656
func (r *MantleBackupReconciler) handleCompletedUploadJobs(ctx context.Context, backup *mantlev1.MantleBackup) (int, error) {
15551657
hook := func(partNum int) error {
15561658
pvc := corev1.PersistentVolumeClaim{}

internal/controller/mantlebackup_controller_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,6 +1556,13 @@ var _ = Describe("export and upload", func() {
15561556
)
15571557
mbr.ceph = testutil.NewFakeRBD()
15581558

1559+
// The exported-diff-size metric HeadObjects each uploaded part via the
1560+
// object storage client. Stub it so the upload flow can run in tests.
1561+
mockObjectStorage := objectstorage.NewMockBucket(mockCtrl)
1562+
mockObjectStorage.EXPECT().GetSize(gomock.Any(), gomock.Any()).
1563+
Return(int64(0), nil).AnyTimes()
1564+
mbr.objectStorageClient = mockObjectStorage
1565+
15591566
ns = resMgr.CreateNamespace()
15601567
})
15611568

0 commit comments

Comments
 (0)