Replikation mit rclone und ntfy.ch-Live-Status

Ausgangslage

Für den Abgleich eines großen Datenbestands zwischen zwei Synology-NAS-Systemen (an zwei Standorten) war ursprünglich geplant, Snapshots durch Snapshot Replication direkt auf das entfernte NAS replizieren zu lassen. Leider war jedoch die Übertragungsgeschwindigkeit 🐌-langsam und für ~20TB (Initialsync) an Daten keine Option. Erste Tests mit rclone zeigten dann zum Glück, dass da deutlich mehr möglich wäre:

Methode Übertragungsrate
Synology Snapshot Replication ~8 MB/s
rsync ~8 MB/s
rclone mit mehreren parallelen Transfers über SFTP ~40 MB/s

Der Vorteil von rclone liegt hier vor allem in der Parallelisierung:

1
2
3
4
--transfers 8 \
--checkers 16 \
--multi-thread-streams 4 \
--multi-thread-cutoff 256M

Dadurch lassen sich mehrere Dateien gleichzeitig übertragen und größere Dateien zusätzlich über mehrere Streams verarbeiten.

Ziel

Die fertige Lösung soll:

  • von NAS A nach NAS B replizieren
  • über den DSM-Aufgabenplaner gestartet werden
  • parallele Läufe verhindern
  • das Log während des Syncs live schreiben
  • verschobene Dateien möglichst als Move erkennen
  • alle fünf Minuten einen Fortschrittsstatus erzeugen
  • genau eine ntfy-Live-Statusnachricht aktualisieren
  • diese Live-Nachricht nach Abschluss wieder löschen
  • anschließend eine separate Erfolgs-, Fehler- oder Abbruchmeldung senden

Auf beiden NAS-Systemen laufen zusätzlich lokale Snapshots.

Architektur

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
                         NAS A
                          │ DSM Aufgabenplaner
                  sync-replica.sh
                 ┌────────┴────────┐
                 │                 │
               flock            rclone
                                   │ SFTP
                                 NAS B

rclone stdout/stderr
  status_filter
        ├──── alle 5 Minuten ───► ntfy Live-Status
       tee
      ┌─┴─────────────┐
      ▼               ▼
 dauerhaftes      temporäres
     Log              Log
                Abschlussstatistik
              ntfy Abschlussmeldung

Verzeichnisstruktur auf NAS A

Eine mögliche Struktur sieht so aus:

1
2
3
4
5
6
7
8
/volume1/Tools/
├── rclone
├── sync-replica.sh
├── rclone-sync.log
├── rclone-sync.lock
└── rclone-config/
    ├── rclone.conf
    └── .env

Bedeutung der Dateien:

/volume1/Tools/rclone –> rclone-Binary

/volume1/Tools/sync-replica.sh –> Sync-Script

/volume1/Tools/rclone-sync.log –> Dauerhaftes Live-Log aller Läufe

/volume1/Tools/rclone-sync.lock –> Lock-Datei, damit nicht zwei Syncs gleichzeitig laufen

/volume1/Tools/rclone-config/rclone.conf –> rclone-Konfiguration mit dem SFTP-Remote

/volume1/Tools/rclone-config/.env –> Konfigurationsdatei für ntfy-URL und Token

Berechtigungen

Die sensiblen Dateien sollten nur für root lesbar sein:

1
2
3
4
chmod 700 /volume1/Tools/sync-replica.sh
chmod 700 /volume1/Tools/rclone
chmod 600 /volume1/Tools/rclone-config/rclone.conf
chmod 600 /volume1/Tools/rclone-config/.env

rclone installieren

Die passende rclone -Binary wird auf NAS A abgelegt:

1
/volume1/Tools/rclone

Danach ausführbar machen:

1
chmod +x /volume1/Tools/rclone

Version prüfen:

1
/volume1/Tools/rclone version

SFTP-Remote konfigurieren

Die rclone-Konfiguration liegt unter:

1
/volume1/Tools/rclone-config/rclone.conf

Ein anonymisiertes Beispiel:

1
2
3
4
5
6
7
[backup-nas]
type = sftp
host = 10.0.0.20
user = backupuser
port = 22
key_file = /root/.ssh/id_ed25519
shell_type = unix

Der Remote-Name lautet in diesem Beispiel:

1
backup-nas:

Verbindung testen:

1
2
3
/volume1/Tools/rclone lsd \
  --config /volume1/Tools/rclone-config/rclone.conf \
  backup-nas:/

Wenn die Verzeichnisse des Ziel-NAS erscheinen, funktioniert die Verbindung.

.env-Datei

Private Werte kommen nicht direkt ins Script, sondern in:

1
/volume1/Tools/rclone-config/.env

Beispiel:

1
2
NTFY_URL="https://ntfy.example.com/synology"
NTFY_TOKEN="tk_REPLACE_ME"

Danach:

1
chmod 600 /volume1/Tools/rclone-config/.env

Das Script lädt diese Datei beim Start.

ntfy.sh-Benachrichtigungen

Seit ntfy.sh v2.16.0 lassen sich Benachrichtigungen nachträglich aktualisieren – wie gemacht für einen regelmäßig aktualisierten Synchronisationsstatus. Statt alle paar Minuten eine neue Meldung zu erzeugen, wird einfach immer dieselbe aktualisiert. Das wollte natürlich unbedingt ausprobiert werden.

rclone kann dafür regelmäßig Statistiken ausgeben:

1
2
--stats 5m \
--stats-log-level NOTICE

Ein typischer Block sieht ungefähr so aus:

1
2
3
4
Transferred:    465.178 GiB / 588.182 GiB, 79%, 23.190 MiB/s, ETA 1h30m31s
Checks:         1168704 / 1168704, 100%, Listed 2432377
Transferred:    22489 / 30600, 73%
Elapsed time:   5h40m0.0s

Um eine ntfy.sh-Nachricht nachträglich bearbeiten zu können, muss man sie zusätzlich mit einer Sequence-ID versenden. Das Script generiert sich eine feste, welche sich aus rclone-sync- sowie dem Hostname zusammensetzt:

1
NTFY_STATUS_SID="rclone-sync-${HOST}"

und sendet anschliessend:

1
-H "X-Sequence-ID: $NTFY_STATUS_SID"

Dadurch wird immer dieselbe Live-Statusnachricht aktualisiert.

Nach Abschluss wird diese Live-Statusnachricht wieder gelöscht und durch eine normale Abschlussmeldung ersetzt.

Verschobene Dateien erkennen

Ein normaler Sync kann eine verschobene Datei als:

1
2
3
alter Pfad gelöscht
+
neuer Pfad hinzugefügt

interpretieren.

Dann würde dieselbe Datei unter Umständen erneut übertragen werden.

rclone kann solche Fälle mit:

1
--track-renames

erkennen.

Da beim verwendeten SFTP-Setup keine brauchbaren kompatiblen Hashes zur Verfügung stehen, wird folgende Strategie genutzt:

1
2
--track-renames \
--track-renames-strategy modtime,leaf

Damit werden insbesondere Dateiname und Änderungszeit zur Erkennung genutzt. Die Dateigröße wird beim Rename-Tracking ebenfalls berücksichtigt.

Das ist für Datenbestände interessant, bei denen Dateien häufig lediglich in andere Ordner verschoben werden.

Live-Logging mit tee

Mit tee landet die Ausgabe gleichzeitig im dauerhaften und im temporären Log:

1
2
3
4
5
6
7
8
9
rclone
status_filter
  tee
  ├── /volume1/Tools/rclone-sync.log
  └── /tmp/rclone-sync.xxxxxx

Das Hauptlog kann damit live angesehen werden:

1
tail -f /volume1/Tools/rclone-sync.log

Wichtig bei Pipes: echter Exit-Code

Bei:

1
rclone ... | tee ...

liefert $? den Exit-Code des letzten Pipeline-Kommandos.

Im Script interessiert aber der Exit-Code von rclone.

Unter Bash wird deshalb verwendet:

1
RC=${PIPESTATUS[0]}

Da im fertigen Script noch status_filter zwischen rclone und tee sitzt, sieht die Pipeline so aus:

1
2
3
[0] rclone
[1] status_filter
[2] tee

Damit bleibt:

1
RC=${PIPESTATUS[0]}

korrekt.

Vollständiges Script

/volume1/Tools/sync-replica.sh

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
#!/bin/bash

# =============================================================================
# Konfiguration
# =============================================================================

RCLONE=/volume1/Tools/rclone
CONFIG=/volume1/Tools/rclone-config/rclone.conf
ENV_FILE=/volume1/Tools/rclone-config/.env

SRC=/volume1/Daten
DST=backup-nas:/Daten

LOG=/volume1/Tools/rclone-sync.log
LOCK=/volume1/Tools/rclone-sync.lock

NTFY_ON_SUCCESS=1
NTFY_ON_SKIP=0


# =============================================================================
# .env laden
# =============================================================================

if [ -r "$ENV_FILE" ]; then
    # shellcheck disable=SC1090
    . "$ENV_FILE"
fi

: "${NTFY_URL:=}"
: "${NTFY_TOKEN:=}"


# =============================================================================
# Host / Live-Status-ID
# =============================================================================

HOST=$(hostname -s 2>/dev/null || hostname)

NTFY_STATUS_SID="rclone-sync-${HOST}"
LIVE_STATUS_DELETED=0


# =============================================================================
# Normale ntfy-Nachricht
# =============================================================================

notify() {
    [ -n "$NTFY_URL" ] || return 0

    if ! command -v curl >/dev/null 2>&1; then
        echo "$(date): curl nicht gefunden" >> "$LOG"
        return 0
    fi

    local args=(
        -sS
        -f
        -o /dev/null
        --max-time 20
        --retry 3
        --retry-delay 5
        -H "Title: $1"
        -H "Priority: $2"
        -H "Tags: $3"
        --data-binary @-
    )

    [ -n "$NTFY_TOKEN" ] &&
        args+=(-H "Authorization: Bearer $NTFY_TOKEN")

    if ! printf '%s' "$4" |
         curl "${args[@]}" "$NTFY_URL" 2>> "$LOG"; then

        echo "$(date): ntfy-Benachrichtigung fehlgeschlagen" >> "$LOG"
    fi
}


# =============================================================================
# ntfy Live-Status aktualisieren
# =============================================================================

notify_status() {
    [ -n "$NTFY_URL" ] || return 0
    command -v curl >/dev/null 2>&1 || return 0

    local args=(
        -sS
        -f
        -o /dev/null
        --max-time 20
        --retry 3
        --retry-delay 5
        -H "Title: Daten-Sync läuft ($HOST)"
        -H "Priority: 2"
        -H "Tags: hourglass_flowing_sand,floppy_disk"
        -H "X-Sequence-ID: $NTFY_STATUS_SID"
        --data-binary @-
    )

    [ -n "$NTFY_TOKEN" ] &&
        args+=(-H "Authorization: Bearer $NTFY_TOKEN")

    if ! printf '%s' "$1" |
         curl "${args[@]}" "$NTFY_URL" 2>> "$LOG"; then

        echo "$(date): ntfy-Live-Status fehlgeschlagen" >> "$LOG"
        return 1
    fi
}


# =============================================================================
# ntfy Live-Status löschen
# =============================================================================

delete_status() {
    [ "$LIVE_STATUS_DELETED" -eq 0 ] || return 0
    [ -n "$NTFY_URL" ] || return 0
    command -v curl >/dev/null 2>&1 || return 0

    local args=(
        -sS
        -o /dev/null
        --max-time 20
        -X DELETE
    )

    [ -n "$NTFY_TOKEN" ] &&
        args+=(-H "Authorization: Bearer $NTFY_TOKEN")

    if ! curl "${args[@]}" \
         "${NTFY_URL}/${NTFY_STATUS_SID}" \
         2>> "$LOG"; then

        echo "$(date): Live-Status konnte nicht gelöscht werden" >> "$LOG"
        return 1
    fi

    LIVE_STATUS_DELETED=1
}


# =============================================================================
# Überlappung verhindern
# =============================================================================

exec 9>"$LOCK"

if ! flock -n 9; then

    echo "=== $(date): Lauf übersprungen, vorheriger Lauf aktiv ===" >> "$LOG"

    if [ "$NTFY_ON_SKIP" = "1" ]; then
        notify \
            "Sync übersprungen ($HOST)" \
            2 \
            "hourglass" \
            "Der vorherige Sync ist noch aktiv."
    fi

    exit 0
fi


# =============================================================================
# Logrotation bei 50 MB
# =============================================================================

if [ -f "$LOG" ] &&
   [ "$(stat -c%s "$LOG")" -gt 52428800 ]; then

    mv "$LOG" "${LOG}.old"
fi


# =============================================================================
# Temporäres Log
# =============================================================================

TMPLOG=$(mktemp /tmp/rclone-sync.XXXXXX) ||
    TMPLOG="/tmp/rclone-sync.$$"


# =============================================================================
# Cleanup
# =============================================================================

cleanup() {
    delete_status
    rm -f "$TMPLOG"
}

trap cleanup EXIT


# =============================================================================
# Start
# =============================================================================

START_TS=$(date +%s)

echo "=== Start $(date) ===" >> "$LOG"

START_MSG="Quelle:      $SRC
Ziel:        $DST
Status:      Sync gestartet
Start:       $(date '+%d.%m.%Y %H:%M:%S')
Fortschritt: wird ermittelt ..."

notify_status "$START_MSG"


# =============================================================================
# Statistikfilter
# =============================================================================

status_filter() {

    local line
    local clean

    local bytes=""
    local checks=""
    local files=""
    local deleted=""
    local errors=""
    local elapsed=""

    local in_stats=0

    while IFS= read -r line; do

        # Originalzeile unverändert an tee weiterreichen
        printf '%s\n' "$line"

        # Datum/Uhrzeit/Log-Level entfernen
        clean=$(printf '%s\n' "$line" |
            sed -E \
            's/^[0-9]{4}\/[0-9]{2}\/[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} [A-Z]+:[[:space:]]*//')

        # Beginn Statistikblock
        if printf '%s\n' "$clean" |
           grep -Eq '^Transferred:[[:space:]]+[0-9.]+ ?[KMGTPE]?i?B'; then

            bytes=$(printf '%s\n' "$clean" |
                sed -E 's/^Transferred:[[:space:]]+//')

            checks=""
            files=""
            deleted=""
            errors=""
            elapsed=""

            in_stats=1
            continue
        fi

        [ "$in_stats" -eq 1 ] || continue

        if printf '%s\n' "$clean" | grep -Eq '^Checks:'; then

            checks=$(printf '%s\n' "$clean" |
                sed -E 's/^Checks:[[:space:]]+//')

            continue
        fi

        if printf '%s\n' "$clean" | grep -Eq '^Deleted:'; then

            deleted=$(printf '%s\n' "$clean" |
                sed -E 's/^Deleted:[[:space:]]+//')

            continue
        fi

        if printf '%s\n' "$clean" |
           grep -Eq '^Transferred:[[:space:]]+[0-9]+ / [0-9]+'; then

            files=$(printf '%s\n' "$clean" |
                sed -E 's/^Transferred:[[:space:]]+//')

            continue
        fi

        if printf '%s\n' "$clean" | grep -Eq '^Errors:'; then

            errors=$(printf '%s\n' "$clean" |
                sed -E 's/^Errors:[[:space:]]+//')

            continue
        fi

        # Ende des Statistikblocks
        if printf '%s\n' "$clean" | grep -Eq '^Elapsed time:'; then

            elapsed=$(printf '%s\n' "$clean" |
                sed -E 's/^Elapsed time:[[:space:]]+//')

            [ -n "$bytes" ]   || bytes="keine Angabe"
            [ -n "$files" ]   || files="keine Angabe"
            [ -n "$checks" ]  || checks="keine Angabe"
            [ -n "$deleted" ] || deleted="0 (files), 0 (dirs)"
            [ -n "$errors" ]  || errors="0"

            STATUS_MSG="Quelle:     $SRC
Ziel:       $DST
Daten:      $bytes
Dateien:    $files
Prüfungen:  $checks
Gelöscht:   $deleted
Fehler:     $errors
Laufzeit:   $elapsed
Stand:      $(date '+%d.%m.%Y %H:%M:%S')"

            notify_status "$STATUS_MSG"

            in_stats=0
        fi
    done
}


# =============================================================================
# rclone
# =============================================================================

"$RCLONE" sync \
    --config "$CONFIG" \
    "$SRC" "$DST" \
    --track-renames \
    --track-renames-strategy modtime,leaf \
    --transfers 8 \
    --checkers 16 \
    --multi-thread-streams 4 \
    --multi-thread-cutoff 256M \
    --exclude "@eaDir/" \
    --exclude "#recycle/" \
    --exclude "#snapshot/" \
    --exclude "@tmp/" \
    --exclude ".DS_Store" \
    --exclude "Thumbs.db" \
    --stats 5m \
    --stats-log-level NOTICE \
    --log-level INFO \
    2>&1 | status_filter | tee -a "$LOG" "$TMPLOG"

RC=${PIPESTATUS[0]}


# =============================================================================
# Laufzeit
# =============================================================================

END_TS=$(date +%s)
DUR=$((END_TS - START_TS))

DAUER=$(printf '%02d:%02d:%02d' \
    $((DUR / 3600)) \
    $((DUR % 3600 / 60)) \
    $((DUR % 60)))


# =============================================================================
# Exit-Code
# =============================================================================

case $RC in
    0)   STATUS="OK" ;;
    1)   STATUS="FEHLER: nicht näher bestimmt" ;;
    2)   STATUS="FEHLER: Syntax- oder Nutzungsfehler" ;;
    3)   STATUS="FEHLER: Verzeichnis nicht gefunden" ;;
    4)   STATUS="FEHLER: Datei nicht gefunden" ;;
    5)   STATUS="FEHLER: nach Wiederholungsversuchen fehlgeschlagen" ;;
    6)   STATUS="FEHLER: nicht wiederholbar" ;;
    7)   STATUS="FEHLER: Abbruch wegen fatalem Problem" ;;
    8)   STATUS="FEHLER: Transferlimit erreicht" ;;
    130) STATUS="ABGEBROCHEN: SIGINT" ;;
    143) STATUS="ABGEBROCHEN: SIGTERM" ;;
    *)   STATUS="FEHLER: unbekannter Code $RC" ;;
esac


# =============================================================================
# Abschlussstatistik
# =============================================================================

S_BYTES=$(
    grep -E 'Transferred:[[:space:]]+[0-9.]+ ?[KMGTPE]?i?B' "$TMPLOG" |
    tail -n 1 |
    sed -E 's/^.*Transferred:[[:space:]]+//'
)

S_FILES=$(
    grep -E 'Transferred:[[:space:]]+[0-9]+ / [0-9]+' "$TMPLOG" |
    tail -n 1 |
    sed -E 's/^.*Transferred:[[:space:]]+//'
)

S_CHECKS=$(
    grep -E 'Checks:[[:space:]]+' "$TMPLOG" |
    tail -n 1 |
    sed -E 's/^.*Checks:[[:space:]]+//'
)

S_DELETED=$(
    grep -E 'Deleted:[[:space:]]+' "$TMPLOG" |
    tail -n 1 |
    sed -E 's/^.*Deleted:[[:space:]]+//'
)

S_ERRORS=$(
    grep -E 'Errors:[[:space:]]+' "$TMPLOG" |
    tail -n 1 |
    sed -E 's/^.*Errors:[[:space:]]+//'
)

S_ELAPSED=$(
    grep -E 'Elapsed time:[[:space:]]+' "$TMPLOG" |
    tail -n 1 |
    sed -E 's/^.*Elapsed time:[[:space:]]+//'
)

[ -n "$S_BYTES" ]   || S_BYTES="keine Angabe"
[ -n "$S_FILES" ]   || S_FILES="0 / 0"
[ -n "$S_CHECKS" ]  || S_CHECKS="0 / 0"
[ -n "$S_DELETED" ] || S_DELETED="0 (files), 0 (dirs)"
[ -n "$S_ERRORS" ]  || S_ERRORS="0"

echo "=== Ende $(date) | Exit-Code $RC | $STATUS ===" >> "$LOG"


# =============================================================================
# Abschlussmeldung
# =============================================================================

MSG="Quelle:     $SRC
Ziel:       $DST
Daten:      $S_BYTES
Dateien:    $S_FILES
Prüfungen:  $S_CHECKS
Gelöscht:   $S_DELETED
Fehler:     $S_ERRORS
Dauer:      $DAUER${S_ELAPSED:+ (rclone: $S_ELAPSED)}
Exit-Code:  $RC ($STATUS)"

delete_status

if [ "$RC" -eq 0 ]; then

    if [ "$NTFY_ON_SUCCESS" = "1" ]; then

        notify \
            "Daten-Sync OK ($HOST)" \
            3 \
            "white_check_mark,floppy_disk" \
            "$MSG"

    fi

else

    ERRLINES=$(
        grep -E '(ERROR|CRITICAL)' "$TMPLOG" |
        tail -n 5
    )

    if [ -n "$ERRLINES" ]; then

        MSG="$MSG

Letzte Meldungen:
$ERRLINES"

    fi

    if [ "$RC" -eq 130 ] || [ "$RC" -eq 143 ]; then

        notify \
            "Daten-Sync ABGEBROCHEN ($HOST)" \
            4 \
            "stop_sign,warning" \
            "$MSG"

    else

        notify \
            "Daten-Sync FEHLGESCHLAGEN ($HOST)" \
            5 \
            "rotating_light,warning" \
            "$MSG"

    fi
fi

exit "$RC"

Script installieren

Script speichern:

1
/volume1/Tools/sync-replica.sh

Ausführbar machen:

1
chmod 700 /volume1/Tools/sync-replica.sh

Manuell testen:

1
/volume1/Tools/sync-replica.sh

Log parallel ansehen:

1
tail -f /volume1/Tools/rclone-sync.log

DSM-Aufgabenplaner einrichten

In DSM:

1
2
3
4
5
Systemsteuerung
→ Aufgabenplaner
→ Erstellen
→ Geplanter Task
→ Benutzerdefiniertes Skript

Als Benutzer wird ein Account benötigt, der Zugriff auf:

  • Quelldaten
  • rclone-Binary
  • rclone-Konfiguration
  • SSH-Key
  • Log-Verzeichnis

hat.

Als Befehl reicht:

1
bash /volume1/Tools/sync-replica.sh

Danach den gewünschten Zeitplan festlegen.

Snapshots auf beiden NAS-Systemen

Auf NAS A und NAS B werden unabhängig vom rclone-Sync lokale Snapshots erzeugt. Damit ergibt sich praktisch folgende Struktur:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
                 NAS A
          ┌─────────────────┐
          │ aktuelle Daten  │
          │ + Snapshots     │
          └────────┬────────┘
                   │ rclone / SFTP
          ┌─────────────────┐
          │ aktuelle Kopie  │
          │ + Snapshots     │
          └─────────────────┘
                 NAS B

Damit bleibt auf beiden Seiten eine lokale Snapshot-Historie erhalten, während rclone die eigentliche Replikation übernimmt. Im konkreten Setup ergibt sich damit funktional der gewünschte Nutzen der ursprünglichen Snapshot-Replication-Idee, aber mit deutlich höherer beobachteter Übertragungsrate.

Fazit

Ausgangspunkt war eine einfache Anforderung:

Daten von Synology NAS A nach Synology NAS B replizieren

Snapshot Replication brachte alles dafür mit, war mir mit ~6 MB/s aber deutlich zu langsam. Auch rsync war ähnlich 🐌-lahm. Mit rclone und parallelen Transfers waren dagegen Werte von ~40 MB/s möglich.

Das fertige Setup ergänzt rclone um die Funktionen, die für einen dauerhaften DSM-Job praktisch sind:

  • flock gegen parallele Läufe
  • Live-Logging mit tee
  • Logrotation
  • SFTP als Transport
  • Rename-Erkennung mit modtime,leaf
  • Statistik alle fünf Minuten via ntfy.sh
  • separate Abschlussmeldung
  • Fehlerauswertung
  • Behandlung von SIGINT und SIGTERM

Damit übernimmt rclone die schnelle Replikation, während die Snapshot-Historie weiterhin lokal auf beiden Synology-Systemen geführt wird.

Formerly known as struband.net
Build 27.08.2026