- 论坛徽章:
- 0
|
本帖最后由 ailms 于 2013-07-22 10:32 编辑
原文链接 :http://blog.ailms.me/2013/07/20/ ... wk-for-xml-job.html
完整脚本 :http://static2.ailms.me/scripts/tuan-proc.tar.gz
受人之托写一个脚本,需求如下
1、定时从多个团购网站下载 XML (全量,有些超巨大)
2、要并发下载+实时处理。也就是不能一个下载完处理后,再下载第2个,再处理 (话说这个要求是楼主自己提的 ..)
3、对下载的 XML 按照每文件1000个商品的数量进行分割。XML 的格式如下 :<goods id=n></goods> 为一个商品
<?xml version=”1.0″ encoding=”UTF-8″?>
<data>
<apiversion><![CDATA[3.0]]></apiversion>
<site_name><![CDATA[满座网]]></site_name>
<goodsdata>
<goods id=”1″>
<xxxx>
<yyyy>
</goods>
<goods id=”2″>
<xxxx>
<yyyy>
</good>
….
针对第一点 :
如何快速下载。wget 是传统的单线程,所以楼主搜索了一下,有一个 aria2 ,看了一下 man ,非常满意。实际测试速度也很不错。
另外这个东西功能过于强大,强大到可以支持 session 和 daemon 模式,具体楼主还没研究,目前只是简单使用,其他等待以后吧。
另外有一个要注意的,因为不是无时无刻这些 XML 都在更新,所以并不需要每次都全量下载,所以楼主用了 HEAD 方法记录这些
XML 文件的 ETag、Mtime、Size 并存成文件,下次下载前检查一下这3项信息,并与磁盘上实际的文件大小比较,如果相同则不下载。
- declare -a aria2cOpts=()
- aria2cOpts+=("--log $tempDir/$i.log --log-level=info")
- aria2cOpts+=("--stop=$(($singleURLTimeout * 60)) --max-concurrent-downloads=10")
- aria2cOpts+=("--auto-file-renaming=false --continue")
- #aria2cOpts+=("--http-accept-gzip=true --enable-http-keep-alive=true")
- aria2cOpts+=("-d $tempDir/ -o ${i}.xml --quiet")
- aria2c ${aria2cOpts[*]} $url 2>$logDir/${i}/${i}.err
复制代码 针对第二点 :
后台多任务这个简单,但多少才是合适呢? 这个楼主做了测试,先是用 wget 下载一个 500MB 的 XML 文件,记录下耗时。
由于这个时候没有指定 CPU cores ,所以默认是bind 在任意一个 Cores 都可以。接下来再用 taskset 启动相同的 wget 命令,
但这次不同的是,只bind 到一个 core 。经过多次测试,发现时间节省了 50% ! 所以楼主用下面的代码实现了一个功能,就是
每一轮取出跟 cpu core 数量相同的下载任务,然后分配到不同的 cpu core 上,再并发多任务下载。
- cpuCoreNum=$(cat /proc/cpuinfo | grep -c '^processor[[:space:]]')
- batchDownloadTask=$(egrep -v '^#|^[[:space:]]* $1 | xargs -n $cpuCoreNum)
- count=0
- while read line ; do
- (
- <spawn arai2c job>
- ) &
- backgroundJobPid=$!
- sleep 0.5
- taskset -c -p $count $backgroundJobPid &>/dev/null
- info_log "$i" "bind pid [$backgroundJobPid] to cpu #$(taskset -c -p $backgroundJobPid 2>/dev/null | awk -F '[ :]+' '{print $NF}')"
- ((count++))
- done <<< "$batchDownloadTask"
复制代码 针对第三点 :
【方法1】一开始是想用 grep + sed 的。就是找到所有 <goods id=n></goods> 行的行号,并把每个 pair 凑成一行。然后
每1000个pair做成一个文件,再取出文件的第一个和最后一个数字,这2个数字就是代表1000个商品的范围,然
后动态生成 sed 命令,每次取出指定的范围。但按照这个想法实现后,发现性能太差了,因为假设有10w 个商品,
那么需要切割为10次, 意味着需要访问10次 XML 文件,性能自然不行了,实际测试也证明了这点:将近10分钟 ,
所以不能接受
【方法2】因为第一种方法的瓶颈是在于 sed 多次访问XML 文件,所以这次的目的就是如何减少对 XML 文件访问的次数。前
一种方法之所以需要多次访问,是因为没有找到标记“这里是第1000个商品”的类似信息,如果我们能提供这种信息
,那么就可以使用操作系统自带的 csplit 命令来一次性分割了。所以这次放弃 grep + sed 的组合,先用 awk 读取文
件,每读取1000 个 </goods> 行就打印出一个标记行“—cut-here—“ 。在所有标记行打印完毕后,用 csplit 命令分
割,分割之前需要 grep 检查一共有多少个标记行。
![]()
【方法3】第2种方法比起第1种是快了不少,但扔因为 cpslit 的使用而显得不够轻巧,因为 awk 已经可以识别出每1000 个
</goods> 行的所在,所以直接把遇到第n*1000个 <\/goods> 行的内容放入字符串,当遇到第 n*1000个</goods>
行时就直接 print 到文件,这样可以最大程度的减少 IO 次数。但很不幸,这个方法失败了,因为字符串太大了,导致
mawk 执行非常久,而且 cpu 的消耗达到了几乎 100%
【方法4】既然方法3的字符串存储行不通,那么就直接输出到文件。每次打开一个文件,并将当前行输出到该文件,当遇到
第n*1000个</goods> 行时,就关闭文件,同时设置一个新的文件名,继续上面的循环。这个方法经过测试是最
快的,+500MB的文件,在30s内可以完成切割,而且几乎不占 cpu 和内存。
————————————————————— 中途休息 —————————————————————————-
楼主用的是 Ubuntu 12.04 ,默认安装的是 mawk 。但对方是 centOS 5.8 ,安装的是 gawk ,所以当楼主把脚本给到对方
测试时,发现很慢,通过检查发现是 awk 的版本不同。所以让对方安装了 mawk ,则速度一下子就提高了非常多。楼主当
即进行了自测,发现结果非常惊人,mawk 是30s 左右,而 gawk 竟然要1分27秒,相差了1.5倍左右。
mawk 是目前已知的性能最高的 awk的实现。怪不得 Ubuntu 12.04 默认安装的是 mawk
下面是用于切割 XML 文件的 shell 脚本完整代码,整个脚本由于比较大,不好贴出,请点击这里下载
- #!/bin/bash
- mawk -v _result_dir=$2 -v _flag_line="$3" -v _goods_per_file=$4 '
- BEGIN { count=0 ; batch=0 ; }
- {
- output=_result_dir"/cut."sprintf("%03d",batch)
- output_cmd="cat >>"output
- if ( $0 !~ _flag_line ) {
- print $0 | output_cmd
- } else {
- print $0 | output_cmd
- count++
- if ( count % _goods_per_file == 0 ) {
- fflush(output_cmd)
- close(output_cmd)
- batch++
- }
- }
- }' $1
- cd $2
- lastCutFile=$(ls cut.??? | tail -n 1)
- for i in cut.000 $lastCutFile ; do
- sed -r -n '/<goods id=/,/<\/goods>/p' $i > $i.new
- mv $i.new $i
- done
复制代码 脚本整体输出如下(一共 2.3GB 的 XML ,5个) 。由于楼主的 VPS 是在米国,所以下载速度不如国内,如果是在国内执行,时间会比下面的小
![]() |
|