Script de sincronización bidireccional controlada para Proxmox VE (pve-zsync)

rojoblandino

Well-Known Member
Sep 11, 2019
32
4
48
41
Hola comunidad, les comparto una solución que he implementado para mantener sincronizados dos servidores Proxmox (o más) con control dinámico de dirección, ancho de banda y parada/arranque sin detener el proceso principal. Está pensada para entornos donde se necesita un servidor en espejo (réplica) con posibilidad de invertir el sentido de la sincronización en caliente.

¿Qué hace el script?​

  • Lee un archivo de configuración (/etc/script_<nombre>.command) donde se definen:
    • data= : lista de máquinas virtuales a sincronizar, cada una con formato FROM|TO:IP:VMID:NOMBRE.
      • FROM = sincroniza desde esa IP remota hacia el servidor local.
      • TO = sincroniza desde el servidor local hacia esa IP remota.
    • command=Play|Stop : arranca o detiene el bucle de sincronización.
    • limit= : límite de ancho de banda en KB/s (ej. 62500 ≈ 50% de una red 1 Gbps).
    • zpoolname= : nombre del pool ZFS (debe ser el mismo en ambos lados).
    • debug=1|0 : activa logs detallados.
  • Ejecuta pve-zsync sync para cada VM, respetando el sentido indicado.
  • Corre en un bucle infinito controlado por el archivo de comandos, de modo que se puede modificar data o pasar de Play a Stop en caliente.
  • Rotación de logs diaria (/tmp/script_<nombre>.output y .debug).
  • Usa un archivo de bloqueo (/tmp/script_<nombre>.lck) para evitar múltiples instancias.

Ejemplo práctico: ordenadorA (local, IP 192.168.1.240) ←→ ordenadorB (PVE remoto, IP 192.168.1.245)​

En este ejemplo, el script se aloja en ordenadorA (192.168.1.240) y sincronizará desde ordenadorB hacia ordenadorA (copia de seguridad en caliente). Las VM involucradas son las IDs 100, 101 y 102.

1. Crear el script binario en ordenadorA​

Code:
mkdir -p /root/bin
nano /root/bin/sync20260129.sh
Pega el siguiente contenido (ajusta scriptname si quieres otro nombre):
Bash:
#!/bin/bash
source /etc/profile
scriptname="sync20260129"
outputfile="/tmp/script_${scriptname}.output"
debugfile="/tmp/script_${scriptname}.debug"
commandfile="/etc/script_${scriptname}.command"
lckfile="/tmp/script_${scriptname}.lck"

function output_debugfile_rotation(){
        if [[ -f $outputfile ]];then
                if [[ "$(grep "Start Datetime:" ${outputfile} | wc -l)" -eq "1" ]];then
                        fechaoutputfile=$(grep "Start Datetime:" ${outputfile} | tail -1  | awk -F"Start Datetime: " '{print $2}')
                        if [[ "$fechaoutputfile" -ne "$(date +%Y%m%d)"  ]];then
                                mv ${outputfile} ${outputfile}_yesterday
                                > ${outputfile}
                        fi
                fi
        fi
        if [[ -f $debugfile ]];then
                if [[ "$(grep "Start Datetime:" ${debugfile} | wc -l)" -eq "1" ]];then
                        fechadebugfile=$(grep "Start Datetime:" ${debugfile} | tail -1 | awk -F"Start Datetime: " '{print $2}')
                        if [[ "$fechadebugfile" -ne "$(date +%Y%m%d)"  ]];then
                                mv ${debugfile} ${debugfile}_yesterday
                                > ${debugfile}
                        fi
                fi
        fi
}

function write_debugfile(){
        output_debugfile_rotation
        if [[ "$(grep -v "^#" $commandfile | grep "debug=")" == "debug=1" ]];then
                echo "$(date +"%Y%m%d %H:%M:%S") - $@" >> $debugfile
        fi
}

function write_outputfile(){
        output_debugfile_rotation
        echo "$(date +"%Y%m%d %H:%M:%S") - $@" >> $outputfile
}

function zfsSyncMigration(){
        p1_source=$1
        p2_destination=$2
        p3_name=$3
        write_debugfile "VM ${p3_name}"
        date1=$(date +"%Y%m%d%H%M%S")
        if [[ "$(grep -v "^#" $commandfile | grep "limit=" | wc -l)" == "1" ]];then
                limit=$(grep -v "^#" $commandfile | grep "limit=" | awk -F"=" '{print $2}')
                write_debugfile "Limit=$limit"
                /usr/sbin/pve-zsync sync --source ${p1_source} --dest ${p2_destination} --name ${p3_name} --verbose --limit ${limit} >> ${debugfile} 2>&1
        else
                write_debugfile "ERROR on file ${commandfile}, command limit missed!"
        fi
        date2=$(date +"%Y%m%d%H%M%S")
        write_debugfile "Start: ${date1} - END: ${date2} - LAPSE: $((date2-date1))"
}


if [[ ! -f $lckfile ]];then
        write_debugfile "==================================================================================================="
        write_debugfile "Archivo $lckfile no existe"
        touch $lckfile

        if [[ ! -f $commandfile ]];then
                echo "#data=FROM:IP:ID:NAME;FROM:IP:ID:NAME
data=FROM:192.168.1.245:100:ServidorApp;FROM:192.168.1.245:101:ServidorTest;FROM:192.168.1.245:102:ServidorBD
#command=Stop o command=Play
command=Stop
debug=1
# 25% = 31250 | 50% = 62500 | 75% = 93750 | 90% = 112500 | 95% = 118750
limit=62500
zpoolname=zpool1" > $commandfile
        fi

        write_debugfile "[[ Command=$(grep -v "^#" $commandfile | grep "command=") ; Debug=$(grep -v "^#" $commandfile | grep "debug=") ; Limit=$(grep -v "^#" $commandfile | grep "limit=" | awk -F"=" '{print $2}') ]]"

        write_debugfile "Se ha creado el archivo $lckfile"
        write_debugfile "Start Datetime: $(date +"%Y%m%d")"
        write_outputfile "Start Datetime: $(date +"%Y%m%d")"


        zpoolname=$(grep -v "^#" $commandfile | grep "zpoolname=" | awk -F"=" '{print $2}')
        while true;do
                data=$(grep -v "^#" $commandfile | grep "data=" | awk -F"=" '{print $2}')
                write_debugfile "** Data: ${data} **"
                for item in $(echo ${data} | sed "s/;/ /g");do
                        write_debugfile "** Line: ${item} **"
                        if [[ "$(grep -v "^#" ${commandfile} | grep "command=")" == "command=Play" ]];then
                                IFS=":" read -r fromto ip vm name <<< ${item}
                                write_debugfile "** Syncing VM ${vm} **"
                                write_outputfile "** Start of Syncing VM ${vm} **"
                                if [[ "${fromto}" == "FROM" ]];then
                                    zfsSyncMigration ${ip}:${vm} ${zpoolname} ${name}
                                elif [[ "${fromto}" == "TO" ]];then
                                    zfsSyncMigration ${vm} ${ip}:${zpoolname} ${name}
                                fi
                                wait
                                write_outputfile "** End of Syncing VM ${vm} **"
                                write_debugfile "==================================================================================================="
                        elif [[ "$(grep -v "^#" $commandfile | grep "command=")" == "command=Stop" ]];then
                                write_debugfile "Comando STOP"
                                write_outputfile "Comando STOP"
                                break
                        fi
                done
                if [[ "$(grep -v "^#" $commandfile | grep "command=")" == "command=Stop" ]];then
                        write_debugfile "Comando STOP"
                        write_outputfile "Comando STOP"
                        break
                fi
        done
        rm $lckfile
        # Probar: trap 'rm -f $lckfile; exit' INT TERM EXIT
        write_debugfile "Se ha borrado el archivo $lckfile"
        write_debugfile "==================================================================================================="
#else
#       write_debugfile "No es posible ejecutar por la existencia del archivo $lckfile"
fi
Dale permisos de ejecución:
chmod +x /root/bin/sync20260129.sh

2. Crear el archivo de configuración (se generará automáticamente la primera vez, pero es mejor crearlo manualmente)​

vim /etc/script_sync20260129.command
Contenido para este ejemplo (sincronización desde ordenadorB hacia ordenadorA):
Bash:
# Dirección: FROM = desde el IP indicado hacia el local
#           TO   = desde el local hacia el IP indicado
data=FROM:192.168.1.245:100:ServidorApp;FROM:192.168.1.245:101:ServidorTest;FROM:192.168.1.245:102:ServidorBD
# command=Play o Stop
command=Stop
debug=1
# 25% = 31250 | 50% = 62500 | 75% = 93750 | 90% = 112500 | 95% = 118750
limit=62500
zpoolname=zpool1

Explicación de data:
FROM:192.168.1.245:100:ServidorApp significa que el script, ejecutándose en ordenadorA (192.168.1.240), tomará la VM con ID 100 desde el servidor remoto 192.168.1.245 y la sincronizará al pool local zpool1, con el nombre de snapshot ServidorApp.
Si quisieras sincronizar hacia ordenadorB, cambiarías a TO:192.168.1.245:100:ServidorApp (entonces la VM local ID 100 se enviaría al remoto).

3. Configurar crontab para lanzar el script cada minuto​

crontab -e
Añade la línea:
Bash:
* * * * * /root/bin/sync20260129.sh >/dev/null 2>&1
De esta forma, si el script se detiene (por ejemplo tras cambiar command=Stop), el próximo minuto volverá a arrancar respetando la nueva configuración.

4. Poner en marcha la sincronización​

Edita /etc/script_sync20260129.command y cambia command=Stop por command=Play.
En menos de un minuto el script comenzará a sincronizar las VM una tras otra en un bucle continuo. Para detenerlo, vuelve a poner command=Stop.

ADVERTENCIA IMPORTANTE​

Antes de modificar el sentido (FROM ↔ TO) o añadir/eliminar VM en la línea data=, debes:
  1. Primero asegurarse que el nombre del zpool donde se van a transferir o mantener sincronizadas las VM posee el mismo nombre (Pueden probar con nombres distintos, pero creo que da error)
  2. Detener la VM.
  3. Esperar a que termine la iteración actual (puedes vigilar el log /tmp/script_sync20260129.output).
  4. Realizar los cambios en data=.
  5. Vigila el log.
  6. Iniciar la VM en el servidor donde en el sentido de la sincronizacion si el sentido es FROM LOCAL TO REMOTE, entonces la VM debe estar ejecutada en el servidor LOCAL, pero si es FROM REMOTE TO LOCAL, entonces la VM debe iniciarse en el servidor REMOTO
Esto evita conflictos y posibles inconsistencias en la replicación.
Sincronizar una VM detendia hacia una VM encendida en el destino, puede corromper los datos. La réplica debe hacerse siempre desde una VM apagada o desde un snapshot consistente.

Observaciones sobre el rendimiento​

  • La primera sincronización puede durar horas o días según el tamaño de las VM, ya que transfiere todos los datos.
  • Las siguientes ejecuciones solo envían los cambios (snapshots incrementales), por lo que el tiempo se reduce drásticamente (menos de 15 minutos en entornos típicos).
  • El parámetro limit permite ajustar el ancho de banda para no saturar el enlace. Valores orientativos para red de 1 Gbps:
    • 25% → 31250 KB/s
    • 50% → 62500 KB/s
    • 75% → 93750 KB/s
    • 90% → 112500 KB/s

Requisitos​

  • Ambos servidores Proxmox con ZFS y el mismo nombre de pool (por ejemplo zpool1).
  • Comunicación SSH sin contraseña entre los nodos (root o usuario con privilegios), puedes crear una llave sin password etre ambos servidores.
  • El comando pve-zsync instalado (no viene por defecto en Proxmox VE).
  • El script debe ejecutarse como root.
El script se entrega como plantilla; pueden adaptar libremente las variables (scriptname, rutas, etc.). Espero que les sea tan útil como lo ha sido para mí en entornos de producción.

NOTA:​

Les recomiendo hacer un laboratorio primero, aunque es sencillo de entender, les sugiero entiendan lo que hace antes de ponerlo en producción, en nuestro caso, cuando el cliente desea hacer el cambio por motivos de mantenimiento, el cambio de dirección es sencillo, se les pide primero detener las VM y se espera a que termine de sincronizarse, esto se detectua cuando la sincronización ya es casi de 12kb por ejemplo en cambios y los tiempos ya son casi de 0 segundos y ya no ha variacion, al estar apagadas las vm en ambos lados, se hace el cambio de sentido en el data, y se monitoreo el log, una vez detectado el cambio de sincronización, entonces se da inicio en el nuevo destino.

Saludos cordiales.
 
Please try to translate your post into English and post that as a reply...
 
  • Like
Reactions: Stubennerd