Motorola
2002 年 12 月 IBM Informix 数据移动实用程序提供了一个简便的方法将数据库从一台机器移动到另一台上。大型数据库,尤其是那些有大量相关存储过程和触发器的数据库,会引起特别的难题。本文提供了一个您可以修改的样本脚本,以使该作业更方便更迅速。
简介
dbexport 和 dbimport 是 IBM Informix® 数据移动实用程序,它们将数据库从一台机器移至另一台。整个过程十分简单明了。dbexport 创建一个特殊目录,在该目录中它创建一个记录当前数据库结构的模式文件,然后将数据卸装到每个数据库对象的 ASCII 文件中。可以将那些模式和数据文件保存在磁盘或磁带上,然后通过使用 UNIX® tar 或其它实用程序方便地将它们转移到另一台机器。同时,dbexport 在用户的当前目录中创建一个消息文件,以记录 dbexport 操作期间发生的任何错误和警告。消息文件的名称是 dbexport.out 。
dbimport 实用程序使用由 dbexport 创建的模式和数据文件,并在另一台机器上创建完全相同的数据库。与 dbexport 一样,dbimport 也在用户的当前目录中创建消息文件 dbimport.out ,以记录 dbimport 操作期间的错误和警告。
这些实用程序用于 逻辑备份。逻辑备份是指备份数据库结构或模式及逻辑数据库对象(如表、索引、视图、触发器和存储过程)。逻辑备份与用诸如 ontape、on-archive 或 on-bar 之类的实用程序产生的物理备份完全不同,后者备份整个 Informix 实例和原始磁盘空间。
逻辑备份的一大好处是它的灵活性。dbexport 创建的模式文件是一个 ASCII 文件,在导入数据库前可以随时对其进行编辑。您可以修改几个方面:
- 表的数据块大小。可以扩大第一个和第二个表数据块大小,以使表有较少的但更大的数据块。这可以提高总体性能。
- 表或索引的物理位置。根据您对操作系统的磁盘 I/O 的研究,您可能想将一些表和索引移至另一个位置来减少 I/O 负载。
- 索引和约束的名称。您可能想用更有意义的名称自动替换生成的索引和约束名称。
- 数据库对象的结构。通过编辑模式文件,可以将另一种分段策略应用于表和索引,以便在导入数据库时,表和索引在不同的磁盘中分段以达到更好的性能。
此外,可以更改导出数据库的名称,这样就可以将同一数据库的多个副本保存在同一 Informix 实例中。对于那些总是喜欢为进行比较而保存原始数据库的开发人员和测试人员来说,这真的很有用。
逻辑备份的另一个好处是其更细的颗粒度。在恢复时,您无需关闭整个 Informix 实例;而只要在数据库级别上就可以恢复它。这样,可以避免妨碍其他正在操作其它数据库的人。
由于这些好处,我们在进行开发、单元测试、黑盒或白盒测试以及集成测试时常常使用 dbexport 和 dbimport 实用程序。
我们碰到的问题
大多数数据库都十分小,大小只有几百兆字节。但少数数据库相当大 — 不仅因为数据的大小而巨大,而且在触发器和存储过程的数量上也很大。其中的一个数据库超过二吉字节,包含四百多个存储过程和约一千个触发器。
因为 IBM Informix 存储过程语言(SPL)是一个方便的中间层 API,所以许多开发人员和程序员都喜欢用它来实现业务规则,并将其用作前端应用程序和数据库服务器之间的链接。我们认为存储过程和触发器的数量会随发行版的更迭而增长。在导出和导入小型数据库(几百 MB 的数据库)时,我们没有任何问题,但在导入大型数据库(有大量数据及众多触发器和存储过程的数据库)时,我们在长事务和长编译时间上遇到问题。
长事务
IBM Informix 将导入数据库的整个过程当作单个事务。当数据很大,而且有许多触发器和存储过程时,很容易陷入“长事务”状态。Informix 将长事务定义为这样一个事务:它在一个逻辑日志文件中启动,并在数据库服务器需要重用同一个逻辑日志文件时不被提交。当长事务达到长事务高水位标记时,它开始回滚;当长事务独占地访问高水位标记时,在回滚长事务时 Informix 服务器就被阻塞了。换句话说,将挂起该服务器上的其它数据库操作(如 SELECT、INSERT、DELETE 或 UPDATE),直到长事务完成回滚为止。避免这一情况的常用方法是:关闭数据库日志记录,或增加逻辑日志以允许较长的事务通过。
长编译时间
当导入大型数据库时,大多数的时间都花在编译存储过程上,尤其是当存储过程又长又复杂时。这一时间特别长,而且在编译期间会发生内存核心转储。这常会使我们删除了导入了一半的数据库,而得从头重新启动整个导入过程。这是一个相当耗时的过程。
解决问题
为解决这些问题,我们编写了一个 UNIX shell 脚本以使数据库导入过程更容易。该脚本使得导入大型数据库的过程更加容易且平稳,而且最重要的是,它极大地提高了速度;它使整个导入过程至少快了四到五倍。在有这个脚本之前,我们需要花四五个小时导入大型数据库;有了它,我们花费的时间减少到一个小时,而且避免了前面所描述的错误。
系统的描述
要理解脚本的逻辑,需要对系统配置有些了解。我们有两个系统类型:主系统和辅助系统。在主系统上,有四个数据库:big_db、small_db1、small_db2 和 small_db3。在这四个数据库中,big_db 是最大的;它大约有 2 GB 的数据,并有五百多个存储过程和上千个触发器。这个数据库会在导入和导出时给我们造成最大的问题。其它数据库都非常小,大小只有几兆字节,而且几乎没有存储过程或触发器。
在辅助系统上,只有三个小型数据库:small_db1、small_db2 和 small_db3。
脚本的逻辑
脚本由许多函数组成,每个函数都致力于使数据库的导出和导入操作平稳。 图 1显示了这些函数间的关系:
图 1. 导出和导入操作
注:P 表示主系统,S 表示辅助系统。
正如您在流程图中所见的,该脚本的核心由两个函数组成:export_db 和 import_db。这里的基本逻辑是分而治之。在 export_db 中,我们利用 dbexport/dbimport 实用程序,使我们能够编辑已导出的模式。在导出 big_db 后,我们将其已导出模式分成如下三部分:
- 表 — 仅包含所有表结构和数据
- 过程 — 包含全部存储过程
- 其余 — 包含所有视图、索引、约束和触发器
import_db 函数使用不同的方法处理每个部分:
- 对于表,使用 dbimport 实用程序来创建数据库,然后只导入具有数据的表。这个部分执行非常迅速,因为没有表的索引和约束。
- 第二部分(存储过程)常常是瓶颈,因为 dbimport 实用程序会逐行编译存储过程。由于 dbimport 已创建了数据库,所以现在可以将 UNIX pipe 实用程序用于这一部分。这会极大地提高速度,因为 UNIX pipe 实用程序可以执行批处理来编译存储过程。
- 将同样的方法用于第三部分来创建所有视图、索引、约束和触发器。
稍后,我将更详细地研究它。
详细理解脚本
让我们仔细研究 清单 1中的脚本,并了解如何实现“分而治之”的逻辑。记住,代码是“按现状”提供给您的,没有任何形式的保证。
清单 1. stop_start 和 rename_db 函数
#!/bin/ksh
. /usr/informix/.profile
DBACCESS=${INFORMIXDIR}/bin/dbaccess
DBEXPORT=${INFORMIXDIR}/bin/dbexport
DBIMPORT=${INFORMIXDIR}/bin/dbimport
EXPORT_PATH="/usr/gsm/db"
STAT_DB=stat_db
WEN_DB=wen_db
CONFIG_BIN=/SYS_SETUP/SYSCONFIG
LOGFILE="/usr/gsm/logs/cutover/db_exim.$$"
#######################
# stop_start_informix #
#######################
stop_start_informix() {
echo ""
echo "Stopping/Starting Informix."
echo "This may take up to 200 seconds. Please Wait..."
/usr/bin/su - informix -c "onmode -ky" >/dev/null 2>&1
#Wait for 50 seconds then start the informix engine backup
sleep 50
/usr/bin/su - informix -c "oninit; exit 0" >/dev/null 2>&1
rc=1
while [ $rc -ne 5 ]
do
echo "Informix oninit return code is $rc"
echo "Waiting for 50 seconds..."
sleep 50
echo "Checking if the Informix Engine is Online..."
/usr/bin/su - informix -c "onstat -" >/dev/null 2>&1
rc=$?
#
# onstat return codes:
# 0 = Initialization
# 1 = Quiescent
# 2 = Fast Recovery
# 3 = Archive Backup
# 4 = Shutting Down
# 5 = Online
# 6 = System Aborting
#
done
echo "The Informix Engine is Online."
echo ""
}
#############
# rename_db #
#############
rename_db() {
rc=1
while [ $rc -ne 0 ]
do
echo "Trying to rename the database $1 to $2"
echo "rename database $1 to $2" | $DBACCESS
rc=$?
if [ $rc -ne 0 ]
then
echo "WARNING: Failed to rename the database $1 to $2."
echo "Retrying. Please wait..."
#
# bounce the Informix engine
#
stop_start_informix
fi
done
#
# bounce the Informix engine
#
stop_start_informix
}
|
清单 1中的两个函数的目的是:确保在导出数据库前先“杀死”所有数据库连接。在导出数据库时,需要对数据库的独占访问;否则会得到一条来自 Informix 的错误,并且无法导出。这里的逻辑是强制 Informix 除去所有数据库连接,然后在 Informix 引擎恢复时立即重命名数据库,以便其它应用程序不能访问我们要导出的数据库。然后,就可以在没有任何干扰的情况下导出数据库了。
清单 2显示了该脚本中最重要的函数,split_schema 函数。
清单 2. split_schema 函数
###########################
# split schema #
###########################
split_schema() {
#define file seperator
file_seperator0="^CREATE PROCEDURE"
file_seperator1="^{ TABLE"
file_seperator2="^create view"
TABLE_SQL=table.sql
PROC_SQL=proc.sql
REST_SQL=rest.sql
#copy old schema file
echo "Copying $EXPORTED_SQL ..."
cp ${EXPORTED_SQL} ${EXPORTRED_SQL}.orig
if [ "$?" -ne 0 ]
then
echo "error with creating files.please check directory permission"
exit 1
fi
echo "Parsing ${EXPORTED_SQL}..."
#parse into three parts
position0=$(( $(cat ${EXPORTED_SQL} | grep -n -i "$file_seperator0" | head -1 |cut -f1 -d:) - 1 ))
position1=$(( $(cat ${EXPORTED_SQL} | grep -n -i "$file_seperator1" | head -1 |cut -f1 -d:) - 1 ))
position2=$(( $(cat ${EXPORTED_SQL} | grep -n -i "$file_seperator2" | head -1 |cut -f1 -d:) - 1 ))
if [ $position0 -lt 0 ]
then
echo "First seperator $file_seperator0 is not found in file. "
exit 1
elif [ $position1 -lt 0 ]
then
echo "Second seperator $file_seperator1 is not found in file."
exit 1
elif [ $position2 -lt 0 ]
then
echo "Second seperator $file_seperator2 is not found in file."
exit 1
fi
echo "$position0 $position1 $position2"
diff_12=`expr $position2 - $position1`
head -${position0} ${EXPORTED_SQL}> ${EXPORTED_DIR}/${TABLE_SQL}
head -${position1} ${EXPORTED_SQL} | tail +${position0} > ${EXPORTED_DIR}/${PROC_SQL}
tail +${position1} ${EXPORTED_SQL} | head -${diff_12} >>${EXPORTED_DIR}/${TABLE_SQL}
tail +${position2} ${EXPORTED_SQL} > ${EXPORTED_DIR}/${REST_SQL}
mv ${EXPORTED_DIR}/${TABLE_SQL} ${EXPORTED_SQL}
}
|
这是脚本的核心, 脚本的逻辑中详细说明了这一逻辑。该函数在用 清单 3中所示的 export_db 函数导出了 big_db 之后,立即调用。
清单 3. export_db 函数
#############
# export_db #
#############
export_db() {
date
DB=$1
lockedDB=$2
EXPORTED_SQL="$EXPORT_PATH/$lockedDB.exp/$lockedDB.sql"
EXPORTED_DIR="$EXPORT_PATH/$lockedDB.exp"
if [ -d "$EXPORT_PATH/$lockedDB.exp" ]; then
echo "Moving $EXPORT_PATH/$lockedDB.exp to another directory."
mv $EXPORT_PATH/$lockedDB.exp $EXPORT_PATH/$lockedDB.exp.old
fi
if [ "$DB" = "small_db2" ]
then
su - informix -c "echo 'grant dba to root' | $DBACCESS $DB; exit 0"
fi
rename_db $DB $lockedDB
echo "Exporting the $lockedDB database. Please Wait..."
$DBEXPORT -q $lockedDB -o $EXPORT_PATH -ss
ok=`grep "dbexport comleted" dbexport.out`
if [ "$ok" = "dbexport completed" ]
then
echo "dbexport completed with no error."
else
echo "Check dbexport.out file; your export has problems!"
tail dbexport.out
exit 1;
fi
rename_db $lockedDB $DB
if [ "$DB" = "big_db" ]
then
split_schema
fi
}
|
正如前面我们提到的,export_db 是脚本的主要函数之一,所以让我们再多研究研究。
首先,该函数调用 rename_db 函数。目的是获取对要导出的数据库的独占访问,这样就可以在不受其它程序干扰的情况下导出它。然后,该函数导出数据库。注:在导出数据库时它使用安静方式。由于安静方式在屏幕上不显示导出期间的错误和警告,所以在导出数据库后检查一下 dbexport.out 极其重要;否则可能会遗漏错误和警告。导出后,函数立即再次调用 rename_db 函数来重命名刚导出的数据库,以便其它程序能够访问它。然后,该函数检查数据库名称参数或自变量,以判断它是否是 big_db。如果是 big_db,它将导出模式分成三部分。对于小型数据库,我们不必分割它们的导出模式。
清单 4. 打开或关闭日志记录的函数
###################
# turn on logging #
###################
turnonlogging() {
infxcon="/usr/informix/etc/onconfig"
cp ${infxcon} ${infxcon}.ori
cat $infxcon.ori |sed 's/^TAPEDEV.*$/TAPEDEV \/dev\/null/' > $infxcon
/usr/bin/su informix -c "ontape -s -U $1;exit 0"
cp ${infxcon}.ori ${infxcon}
}
|
清单 4中函数的目的是:在导入大型数据库时关闭日志记录,这样 dbimport 实用程序使用的逻辑日志数量最少,从而防止导入操作陷入“长事务”状态并回滚。然后,在导入之后打开日志记录。
清单 5显示如何调用该函数。
清单 5. import_db
#############
# import_db #
#############
import_db() {
date
DB=$1
lockedDB=$2
DBSPACE=$3
EXPORTED_SQL="$EXPORT_PATH/$lockedDB.exp/$lockedDB.sql"
EXPORTED_DIR="$EXPORT_PATH/$lockedDB.exp"
echo "Importing the $lockedDB database. Please Wait..."
if [ "$DB" = "big_db" ]
then
$DBIMPORT -q $lockedDB -d $DBSPACE -i $EXPORT_PATH >$TEMP3 2>&1
/usr/bin/su - informix -c "cat ${EXPORTED_DIR}/proc.sql | $DBACCESS $DB; exit 0"
/usr/bin/su - informix -c "cat ${EXPORTED_DIR}/rest.sql | $DBACCESS $DB; exit 0"
turnonlogging $DB
else
$DBIMPORT -q $lockedDB -d $DBSPACE -l -i $EXPORT_PATH >$TEMP3 2>&1
fi
ok=`grep "dbimport completed" dbimport.out`
if [ "$ok" = "dbimport completed" ]
then
echo "dbimport completed with no errors."
else
echo "Check dbimport.out; problems in dbimport."
tail dbimport.out
exit 1;
fi
rename_db $lockedDB $DB
}
|
import_db( 清单 5)是脚本的另一个主要函数。它首先检查传入的数据库名称参数或自变量。如果是 big_db,它就使用 dbimport 实用程序处理导出模式的表部分,然后使用 UNIX pipe 实用程序编译存储过程、索引和触发器。目的是:加速整个过程,并且(正如我前面提到的)通过以这种方法进行,用于导入 big_db 的时间至少减少了 50-60%。对于 big_db,我们还要在导入前关闭日志记录,并在导入后打开日志记录,以避免“长事务”。我们只对 big_db 这样做;对于其它数据库,我们相信有很多逻辑日志,而且根本不会陷入“长事务”。
脚本中的其余部分是创建一个用户友好的且易于使用的菜单,我在本文中不作描述,但您可以自己在可下载的脚本中查看它。
结束语
正如您已看到的,脚本的核心是这样一个逻辑:将导出模式分成三部分并用不同的 Informix dbimport 和 UNIX cat 实用程序来处理每个部分。对我们来说,这个方法比只使用 Informix dbimport 实用程序导入大型数据库更有效;它大大加速了整个数据库导入过程,而且更好地使用了 Informix 和系统资源(如逻辑日志和内存)。由于脚本由许多函数组成,所以它非常灵活,可以方便地修改它以满足特定需求。该脚本会在如何更有效地导入大型数据库方面为您提供了一些实用的、有帮助的意见和建议。 |