Como se menciona en la respuesta de Greg , mysqldump db_name | mysql new_db_name
es la forma gratuita, segura y fácil de transferir datos entre bases de datos. Sin embargo, también es muy lento .
Si está buscando hacer una copia de seguridad de los datos, no puede permitirse el lujo de perder datos (en esta u otras bases de datos), o está usando tablas diferentes innodb
, entonces debe usarlas mysqldump
.
Si está buscando algo para el desarrollo, haga una copia de seguridad de todas sus bases de datos en otro lugar y se sienta cómodo purgando y reinstalando mysql
(posiblemente manualmente) cuando todo sale mal, entonces podría tener la solución para usted.
No pude encontrar una buena alternativa, así que construí un script para hacerlo yo mismo. Gasté mucho tiempo haciendo que esto funcione la primera vez y, sinceramente, me aterra un poco hacer cambios ahora. Las bases de datos Innodb no estaban destinadas a copiarse y pegarse de esta manera. Pequeños cambios hacen que esto falle de maneras magníficas. No he tenido ningún problema desde que finalicé el código, pero eso no significa que no lo harás.
Sistemas probados (pero aún pueden fallar):
- Ubuntu 16.04, mysql predeterminado, innodb, archivos separados por tabla
- Ubuntu 18.04, mysql predeterminado, innodb, archivos separados por tabla
Que hace
- Obtiene
sudo
privilegios y verifica que tenga suficiente espacio de almacenamiento para clonar la base de datos
- Obtiene privilegios de root mysql
- Crea una nueva base de datos con el nombre de la rama git actual
- Estructura de clones a nueva base de datos
- Cambia al modo de recuperación para innodb
- Elimina los datos predeterminados en la nueva base de datos.
- Detiene mysql
- Clona datos en una nueva base de datos
- Inicia mysql
- Vincula datos importados en una nueva base de datos
- Cambia del modo de recuperación para innodb
- Reinicia mysql
- Da acceso de usuario mysql a la base de datos
- Limpia archivos temporales
Cómo se compara con mysqldump
En una base de datos de 3 gb, usar mysqldump
y mysql
tomaría 40-50 minutos en mi máquina. Usando este método, el mismo proceso solo tomaría ~ 8 minutos.
Como lo usamos
Tenemos nuestros cambios de SQL guardados junto con nuestro código y el proceso de actualización está automatizado tanto en producción como en desarrollo, con cada conjunto de cambios haciendo una copia de seguridad de la base de datos para restaurar si hay errores. Un problema con el que nos encontramos fue cuando estábamos trabajando en un proyecto a largo plazo con cambios en la base de datos, y tuvimos que cambiar ramas en el medio para corregir un error o tres.
En el pasado, utilizamos una única base de datos para todas las sucursales, y tendríamos que reconstruir la base de datos cada vez que cambiamos a una sucursal que no era compatible con los nuevos cambios en la base de datos. Y cuando volviéramos, tendríamos que ejecutar las actualizaciones nuevamente.
Intentamos mysqldump
duplicar la base de datos para diferentes sucursales, pero el tiempo de espera fue demasiado largo (40-50 minutos) y, mientras tanto, no pudimos hacer nada más.
Esta solución acortó el tiempo de clonación de la base de datos a 1/5 del tiempo (piense en el café y el baño en lugar de un almuerzo largo).
Tareas comunes y su tiempo
Cambiar entre sucursales con cambios incompatibles en la base de datos lleva más de 50 minutos en una sola base de datos, pero no hay tiempo después del tiempo de configuración inicial con mysqldump
este código. Este código resulta ser ~ 5 veces más rápido que mysqldump
.
Aquí hay algunas tareas comunes y aproximadamente cuánto tiempo tomarían con cada método:
Cree una rama de características con cambios en la base de datos y fusione de inmediato:
- Base de datos única: ~ 5 minutos
- Clonar con
mysqldump
: 50-60 minutos
- Clonar con este código: ~ 18 minutos
Cree una rama de características con cambios en la base de datos, cambie a master
una corrección de errores, realice una edición en la rama de características y fusione:
- Base de datos única: ~ 60 minutos
- Clonar con
mysqldump
: 50-60 minutos
- Clonar con este código: ~ 18 minutos
Cree una rama de características con cambios en la base de datos, cambie a master
una corrección de errores 5 veces mientras realiza ediciones en la rama de características intermedias y combine:
- Base de datos única: ~ 4 horas, 40 minutos
- Clonar con
mysqldump
: 50-60 minutos
- Clonar con este código: ~ 18 minutos
El código
No use esto a menos que haya leído y entendido todo lo anterior.
#!/bin/bash
set -e
# This script taken from: https://stackoverflow.com/a/57528198/526741
function now {
date "+%H:%M:%S";
}
# Leading space sets messages off from step progress.
echosuccess () {
printf "\e[0;32m %s: %s\e[0m\n" "$(now)" "$1"
sleep .1
}
echowarn () {
printf "\e[0;33m %s: %s\e[0m\n" "$(now)" "$1"
sleep .1
}
echoerror () {
printf "\e[0;31m %s: %s\e[0m\n" "$(now)" "$1"
sleep .1
}
echonotice () {
printf "\e[0;94m %s: %s\e[0m\n" "$(now)" "$1"
sleep .1
}
echoinstructions () {
printf "\e[0;104m %s: %s\e[0m\n" "$(now)" "$1"
sleep .1
}
echostep () {
printf "\e[0;90mStep %s of 13:\e[0m\n" "$1"
sleep .1
}
MYSQL_CNF_PATH='/etc/mysql/mysql.conf.d/recovery.cnf'
OLD_DB='YOUR_DATABASE_NAME'
USER='YOUR_MYSQL_USER'
# You can change NEW_DB to whatever you like
# Right now, it will append the current git branch name to the existing database name
BRANCH=`git rev-parse --abbrev-ref HEAD`
NEW_DB="${OLD_DB}__$BRANCH"
THIS_DIR=./site/upgrades
DB_CREATED=false
tmp_file () {
printf "$THIS_DIR/$NEW_DB.%s" "$1"
}
sql_on_new_db () {
mysql $NEW_DB --unbuffered --skip-column-names -u root -p$PASS 2>> $(tmp_file 'errors.log')
}
general_cleanup () {
echoinstructions 'Leave this running while things are cleaned up...'
if [ -f $(tmp_file 'errors.log') ]; then
echowarn 'Additional warnings and errors:'
cat $(tmp_file 'errors.log')
fi
for f in $THIS_DIR/$NEW_DB.*; do
echonotice 'Deleting temporary files created for transfer...'
rm -f $THIS_DIR/$NEW_DB.*
break
done
echonotice 'Done!'
echoinstructions "You can close this now :)"
}
error_cleanup () {
exitcode=$?
# Just in case script was exited while in a prompt
echo
if [ "$exitcode" == "0" ]; then
echoerror "Script exited prematurely, but exit code was '0'."
fi
echoerror "The following command on line ${BASH_LINENO[0]} exited with code $exitcode:"
echo " $BASH_COMMAND"
if [ "$DB_CREATED" = true ]; then
echo
echonotice "Dropping database \`$NEW_DB\` if created..."
echo "DROP DATABASE \`$NEW_DB\`;" | sql_on_new_db || echoerror "Could not drop database \`$NEW_DB\` (see warnings)"
fi
general_cleanup
exit $exitcode
}
trap error_cleanup EXIT
mysql_path () {
printf "/var/lib/mysql/"
}
old_db_path () {
printf "%s%s/" "$(mysql_path)" "$OLD_DB"
}
new_db_path () {
printf "%s%s/" "$(mysql_path)" "$NEW_DB"
}
get_tables () {
(sudo find /var/lib/mysql/$OLD_DB -name "*.frm" -printf "%f\n") | cut -d'.' -f1 | sort
}
STEP=0
authenticate () {
printf "\e[0;104m"
sudo ls &> /dev/null
printf "\e[0m"
echonotice 'Authenticated.'
}
echostep $((++STEP))
authenticate
TABLE_COUNT=`get_tables | wc -l`
SPACE_AVAIL=`df -k --output=avail $(mysql_path) | tail -n1`
SPACE_NEEDED=(`sudo du -s $(old_db_path)`)
SPACE_ERR=`echo "$SPACE_AVAIL-$SPACE_NEEDED" | bc`
SPACE_WARN=`echo "$SPACE_AVAIL-$SPACE_NEEDED*3" | bc`
if [ $SPACE_ERR -lt 0 ]; then
echoerror 'There is not enough space to branch the database.'
echoerror 'Please free up some space and run this command again.'
SPACE_AVAIL_FORMATTED=`printf "%'d" $SPACE_AVAIL`
SPACE_NEEDED_FORMATTED=`printf "%'${#SPACE_AVAIL_FORMATTED}d" $SPACE_NEEDED`
echonotice "$SPACE_NEEDED_FORMATTED bytes needed to create database branch"
echonotice "$SPACE_AVAIL_FORMATTED bytes currently free"
exit 1
elif [ $SPACE_WARN -lt 0 ]; then
echowarn 'This action will use more than 1/3 of your available space.'
SPACE_AVAIL_FORMATTED=`printf "%'d" $SPACE_AVAIL`
SPACE_NEEDED_FORMATTED=`printf "%'${#SPACE_AVAIL_FORMATTED}d" $SPACE_NEEDED`
echonotice "$SPACE_NEEDED_FORMATTED bytes needed to create database branch"
echonotice "$SPACE_AVAIL_FORMATTED bytes currently free"
printf "\e[0;104m"
read -p " $(now): Do you still want to branch the database? [y/n] " -n 1 -r CONFIRM
printf "\e[0m"
echo
if [[ ! $CONFIRM =~ ^[Yy]$ ]]; then
echonotice 'Database was NOT branched'
exit 1
fi
fi
PASS='badpass'
connect_to_db () {
printf "\e[0;104m %s: MySQL root password: \e[0m" "$(now)"
read -s PASS
PASS=${PASS:-badpass}
echo
echonotice "Connecting to MySQL..."
}
create_db () {
echonotice 'Creating empty database...'
echo "CREATE DATABASE \`$NEW_DB\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci" | mysql -u root -p$PASS 2>> $(tmp_file 'errors.log')
DB_CREATED=true
}
build_tables () {
echonotice 'Retrieving and building database structure...'
mysqldump $OLD_DB --skip-comments -d -u root -p$PASS 2>> $(tmp_file 'errors.log') | pv --width 80 --name " $(now)" > $(tmp_file 'dump.sql')
pv --width 80 --name " $(now)" $(tmp_file 'dump.sql') | sql_on_new_db
}
set_debug_1 () {
echonotice 'Switching into recovery mode for innodb...'
printf '[mysqld]\ninnodb_file_per_table = 1\ninnodb_force_recovery = 1\n' | sudo tee $MYSQL_CNF_PATH > /dev/null
}
set_debug_0 () {
echonotice 'Switching out of recovery mode for innodb...'
sudo rm -f $MYSQL_CNF_PATH
}
discard_tablespace () {
echonotice 'Unlinking default data...'
(
echo "USE \`$NEW_DB\`;"
echo "SET foreign_key_checks = 0;"
get_tables | while read -r line;
do echo "ALTER TABLE \`$line\` DISCARD TABLESPACE; SELECT 'Table \`$line\` imported.';";
done
echo "SET foreign_key_checks = 1;"
) > $(tmp_file 'discard_tablespace.sql')
cat $(tmp_file 'discard_tablespace.sql') | sql_on_new_db | pv --width 80 --line-mode --size $TABLE_COUNT --name " $(now)" > /dev/null
}
import_tablespace () {
echonotice 'Linking imported data...'
(
echo "USE \`$NEW_DB\`;"
echo "SET foreign_key_checks = 0;"
get_tables | while read -r line;
do echo "ALTER TABLE \`$line\` IMPORT TABLESPACE; SELECT 'Table \`$line\` imported.';";
done
echo "SET foreign_key_checks = 1;"
) > $(tmp_file 'import_tablespace.sql')
cat $(tmp_file 'import_tablespace.sql') | sql_on_new_db | pv --width 80 --line-mode --size $TABLE_COUNT --name " $(now)" > /dev/null
}
stop_mysql () {
echonotice 'Stopping MySQL...'
sudo /etc/init.d/mysql stop >> $(tmp_file 'log')
}
start_mysql () {
echonotice 'Starting MySQL...'
sudo /etc/init.d/mysql start >> $(tmp_file 'log')
}
restart_mysql () {
echonotice 'Restarting MySQL...'
sudo /etc/init.d/mysql restart >> $(tmp_file 'log')
}
copy_data () {
echonotice 'Copying data...'
sudo rm -f $(new_db_path)*.ibd
sudo rsync -ah --info=progress2 $(old_db_path) --include '*.ibd' --exclude '*' $(new_db_path)
}
give_access () {
echonotice "Giving MySQL user \`$USER\` access to database \`$NEW_DB\`"
echo "GRANT ALL PRIVILEGES ON \`$NEW_DB\`.* to $USER@localhost" | sql_on_new_db
}
echostep $((++STEP))
connect_to_db
EXISTING_TABLE=`echo "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$NEW_DB'" | mysql --skip-column-names -u root -p$PASS 2>> $(tmp_file 'errors.log')`
if [ "$EXISTING_TABLE" == "$NEW_DB" ]
then
echoerror "Database \`$NEW_DB\` already exists"
exit 1
fi
echoinstructions "The hamsters are working. Check back in 5-10 minutes."
sleep 5
echostep $((++STEP))
create_db
echostep $((++STEP))
build_tables
echostep $((++STEP))
set_debug_1
echostep $((++STEP))
discard_tablespace
echostep $((++STEP))
stop_mysql
echostep $((++STEP))
copy_data
echostep $((++STEP))
start_mysql
echostep $((++STEP))
import_tablespace
echostep $((++STEP))
set_debug_0
echostep $((++STEP))
restart_mysql
echostep $((++STEP))
give_access
echo
echosuccess "Database \`$NEW_DB\` is ready to use."
echo
trap general_cleanup EXIT
Si todo va bien, debería ver algo como: