某文件处理shell脚本的优化经历

本次修改了一个文件处理的shell脚本,功能是从数据库中读获取到一份文件清单,文件清单个格式是这样的:

第一列第二列以后
文件所在目录文件头信息
文件所在目录文件清单
文件所在目录文件清单
文件所在目录文件清单
文件所在目录文件清单
…………
文件所在目录文件头信息
文件所在目录文件清单
文件所在目录文件清单
文件所在目录文件清单
文件所在目录文件清单

整个清单实际上是由若干个清单段组成的,各个段的第一列信息(文件所在目录信息)是不同的,每个段的第一行是一些必要的文件头信息,其后是具体的文件清单。程序的目的是将各个分段拆分成数个清单文件,并按照正确的格式要求调整格式。
从程序上线后发现执行速度远远低于预期值,根据日志发现性能瓶颈位于数据库清单读取完,生成数据文件的地方,具体的代码如下:


ZONE2_NO=
CUR_DIR=
CUR_CHK_FILE_NAME=
CHK_FILE_CONTENT=
THE_DIR_LIST=
OUTPUT_FILE="aaa-#ZONE2_NO#.zip"
dirlist="dirlist.lst"

while read LINE
do
    if [ "$CUR_DIR"x != "$LINE_DIR"x ]
    then
    CUR_DIR=${LINE_DIR}
    THE_DIR_LIST="${THE_DIR_LIST} ${CUR_DIR}"

    #首行的逻辑和其他行不一样

    CUR_CHK_FILE_NAME=`echo ${OUTPUT_FILE}|sed "s/#ZONE2_NO#/${ZONE2_NO}/g"|sed "s/.zip/.CHK/g"`
    CUR_CHK_FILE_NAME="${CUR_DIR}/"`basename ${CUR_CHK_FILE_NAME}`    #取拼接的CHK文件名
    rm -f ${CUR_CHK_FILE_NAME}         
    echo ${LINE} | awk '{printf "%20s%50s%8s%3s%7d%168s\n", $2, $2, $3, $4, $5, " "}' >> ${CUR_CHK_FILE_NAME}
    else
        echo ${LINE} | awk '{printf "%100s%12d%20d%14s%d%109s\n", $2, $3, $4, $5, 1, " "}' >> ${CUR_CHK_FILE_NAME}
    fi
done<$dirlist

从逻辑上来看就是读取源数据文件dirlist,然后一行一行判断其是否是文件头,随后调用awk来进行格式调整,输出。
准备了一个14000行的源数据清单,共计6个段,用time计时得到的结果如下:

real 2m35.54s
user 1m25.91s
sys 1m5.58s

这个效率实在是不可接受,和同事讨论后,最初认为瓶颈是由于反复重定向输出到目标文件导致的,于是选择用一个变量在内存中维护数据,得到如下的修改结果:

ZONE2_NO=
CUR_DIR=
CUR_CHK_FILE_NAME=
CHK_FILE_CONTENT=
THE_DIR_LIST=
OUTPUT_FILE="aaa-#ZONE2_NO#.zip"

dirlist="dirlist.lst"

while read LINE
do
    LINE_DIR=`echo $LINE | awk '{print $1}'`  
    if [ "$CUR_DIR"x != "$LINE_DIR"x ]
    then
    if [ "$CUR_CHK_FILE_NAME"x != ""x ]
        then
            echo ${CHK_FILE_CONTENT} >> ${CUR_CHK_FILE_NAME}
    fi

    CUR_DIR=${LINE_DIR}
    THE_DIR_LIST="${THE_DIR_LIST} ${CUR_DIR}"

    CUR_CHK_FILE_NAME=`echo ${OUTPUT_FILE}|sed "s/#ZONE2_NO#/${ZONE2_NO}/g"|sed "s/.zip/.CHK/g"`
    CUR_CHK_FILE_NAME="${CUR_DIR}/"`basename ${CUR_CHK_FILE_NAME}`    #取拼接的CHK文件名
    rm -f ${CUR_CHK_FILE_NAME}         
    CHK_FILE_CONTENT="${LINE}"
    else
        CHK_FILE_CONTENT="${CHK_FILE_CONTENT}${LINE}"
    fi
done<$dirlist

if [ "$CUR_CHK_FILE_NAME"x != ""x ]
then
    echo ${CHK_FILE_CONTENT} >> ${CUR_CHK_FILE_NAME}
fi

即将原先的AWK输出换成直接拼接(格式调整由数据库端完成)。然而使用同一组数据,运行后所得的结果更差:
real 11m39.27s
user 6m2.13s
sys 5m32.63s

即使去掉写入文件的消耗后处理的时间仍然高达6~7分钟,尅见使用A=$A”data”这样都简单的变量累加的方式不但没有提高效率,反而加重了系统的负担。

由于C++程序写多了,在编写shell脚本的过程中产生了每执行一句语句的代价都是很小的印象。但对于shell脚本而言,其实每个命令,甚至用管道隔开的两个命令都应该理解成启动一个独立的进程进行处理,代价是很高昂的。
在这个实例中,while后的read,后面的echo,awk都等于是启动一个进程,有多少行源数据就要启动多少个进程,1W多的数据行,就会导致系统在执行脚本的过程中不得不运行数万个进程,给CPU带来了很大的负担。

实际上由于每段文本的数据头都有一个“业务日期”数据项,且和前后的数据项用空格隔开,我们可以很方便地从dirlist中提取到所有的数据头

BIZ_DATE="20150713 "
grep "${BIZ_DATE} " ${dirlist} > ${file_head}

然后以文件头为循环项,再次使用grep对“文件所在目录”进行过滤,得到分隔号的每个文件段。

grep ${LINE_DIR} ${dirlist} >${tmp_file}

最后调用awk对格式进行调整

    awk '
    {if (NR==1)
    {
        {printf "%20s%50s%8s%3s%7d%168s\n", $2, $2, $3, $4, $5, " "}
    }
    else
    {
        {printf "%100s%12d%20d%14s%d%109s\n", $2, $3, $4, $5, 1, " "}
    }}' ${tmp_file} > ${CUR_CHK_FILE_NAME}

所得的最终程序如下:

dirlist="dirlist.lst"
file_head="file_head.lst"
BIZ_DATE="20150713 "
grep "${BIZ_DATE} " ${dirlist} > ${file_head}
tmp_file="tmp_file.lst"

ZONE2_NO=
CUR_DIR=
CUR_CHK_FILE_NAME=
CHK_FILE_CONTENT=
THE_DIR_LIST=
OUTPUT_FILE="aaa-#ZONE2_NO#.zip"

while read LINE
do
    LINE_DIR=`echo $LINE | awk '{print $1}'`
    CUR_DIR=${LINE_DIR}
    THE_DIR_LIST="${THE_DIR_LIST} ${CUR_DIR}"
    ZONE2_NO=`echo ${CUR_DIR}|awk -F'/' '{print $8}'`
    CUR_CHK_FILE_NAME=`echo ${OUTPUT_FILE}|sed "s/#ZONE2_NO#/${ZONE2_NO}/g"|sed "s/.zip/.CHK/g"`
    CUR_CHK_FILE_NAME="${CUR_DIR}/"`basename ${CUR_CHK_FILE_NAME}`    #取拼接的CHK文件名
    rm -f ${CUR_CHK_FILE_NAME}         
    grep ${LINE_DIR} ${dirlist} >${tmp_file}
    awk '
    {if (NR==1)
    {
        {printf "%20s%50s%8s%3s%7d%168s\n", $2, $2, $3, $4, $5, " "}
    }
    else
    {
        {printf "%100s%12d%20d%14s%d%109s\n", $2, $3, $4, $5, 1, " "}
    }}' ${tmp_file} > ${CUR_CHK_FILE_NAME}
done<$file_head

使用同一组数据,运行后所得的结果是这样的:
real 0m0.63s
user 0m0.18s
sys 0m0.12s
即使在其后面对百万级的数据规模,运行速度仍然维持在数秒内,运行效率得到了很大的提升。

经验:
1. 编写shell脚本和普通程序语言不同,除去shell内置命令外,其余在shell中出现的每个都是命令都是独立的程序,都会调起一个进程。因此shell脚本语句总数要尽可能精简。熟练掌握awk和sed工具会有较大帮助。
2. 进行性能优化的过程中不能想当然,一定要参照实际运行结果来进行改造。

剩下的疑问:
1. shell的=好像是一个语句(我们这里用的是ksh),诸如CHK_FILE_CONTENT=” CHKFILECONTENT {LINE}”的操作代价并不低。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值