使用Perl Storable 及Hash结构来分析大量数据
2015-04-21 18:07:18 阿炯

接业务部门要求,提取weibo库表中的上一月发表记录在5条(含)以上的用户登录名及最后的别名。但这两个字段在原始表中均为varchar类型且无索引,下面使用了两种方法来处理:利用mysql本身来计算,其二是使用perl及Storable和Hash数据结构的方法计算。

大致环境:
硬件:dell r720 mem:64gb,cpu:40core,hd:600gb sas 15k with raid5
数据库:mysql5.1 myisam merge by month

线上的生产服务器,从库,平均负载:1.2。

方法一:利用mysql的分组计算功能

为了最大限度减少对原始生产表的影响,将所要的字段复制到监时表中。我们从对应的表中取得属于今年3月的数据第一次的id吧,这样效率最高。

select weibo_content_id,origin_author_id,origin_author_name,capture_time from weibo_content_m03 where capture_time>='2015-03-01 00:00:00' order by weibo_content_id asc limit 100;

CREATE TABLE test.wei03 (
  `origin_author_id` varchar(32) DEFAULT NULL,
  `origin_author_name` varchar(128) DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=gbk;

就两个字段,加不加索引应该没有多大区别了吧。

insert into test.wei03 select origin_author_id,origin_author_name from weibo_content_m03 where weibo_content_id>=20130816970420965;
mysql> insert into test.wei03 select origin_author_id,origin_author_name from weibo_content_m03 where weibo_content_id>=20130816970420965;
Query OK, 78151834 rows affected (5 min 32.50 sec)
Records: 78151834  Duplicates: 0  Warnings: 0

文件大小为:
-rw-rw---- 1 mysql mysql 8.5K Apr 21 15:48 wei03.frm
-rw-rw---- 1 mysql mysql 2.2G Apr 21 15:53 wei03.MYD
-rw-rw---- 1 mysql mysql 1.0K Apr 21 15:53 wei03.MYI

查询指定条件的数据
select origin_author_id,origin_author_name,count(1) from test.wei03 group by origin_author_id having count(1)>=5;

还是将结果存入文件中吧
select origin_author_id,origin_author_name,count(1) into outfile '/tmp/content.csv' FIELDS TERMINATED BY ',' ENCLOSED BY  '"' ESCAPED BY '\\' LINES TERMINATED BY '\r\n' from test.wei03 group by origin_author_id having count(1)>=5;

会导致从上的复制有比较大的延迟,触发系统复制高延迟报警,系统负载从平均1.2上升到2.7。

Query OK, 3416271 rows affected (14 min 36.19 sec)

在/tmp目录下生成了109M的content.csv,共计3416271行。

方法二:perl的Storable Hash计算

大致过程:从数据库中按固定数量读取记录,分析后存入hash数组中,完成后存为storable类型文件。后在分析该文件,得出结果。

use v5.12;
use DBI;
use Storable;
use DBD::mysql;
use Time::Piece;
use Data::Dumper;
use Cwd qw(abs_path realpath);
use File::Basename qw(dirname);

binmode(STDOUT, ":encoding(utf8)");
my ($stfile,$lastd,$currd,$rs)=('wm03.st');

#get storable variable
if(-e $stfile){
 $lastd=retrieve($stfile);
}

# Init&Conn mysql db
my ($db,$dbhost,$dbuser,$dbpaswd)=('iis_application_data','localhost','root','freeoa');

#mysql obj attr
my %mydbattr=(RaiseError=>1, AutoCommit=>0,mysql_enable_utf8=>1,RaiseError=>1);
my $st=20130816970420965;
#每一次处理100万的数据量
my $offset=1000000;

#create database handle
my $dbh=DBI->connect_cached("DBI:mysql:database=$db;host=$dbhost;mysql_socket=/tmp/mysql.sock",$dbuser,$dbpaswd,\%mydbattr) or die $DBI::errstr;

my $cnt=$dbh->selectrow_hashref(qq{select max(weibo_content_id) as count from weibo_content_m03 where weibo_content_id>=$st});
say "Done to:$cnt->{count}";

while($st<$cnt->{count}){
 my $sth=$dbh->prepare("select weibo_content_id,origin_author_id,origin_author_name from weibo_content_m03 where weibo_content_id>=$st limit $offset;",{Slice => {}});
 $st+=$offset;
 $sth->execute;
 my $tst=time();
 while (my $rec=$sth->fetchrow_hashref){
  my $v=$rec->{origin_author_id};
  my $vn=$rec->{origin_author_name};
  $rs->{$v}->{cnt}++;
  $rs->{$v}->{name}=$vn;
 }
 my $est=time();
 say 'i have do to:'.$st;
 say 'this time it used(s):'.($est-$tst);
 say '-' x 30;
}

store($rs,$stfile);

END {
 $dbh->disconnect;
}

平均每次用时8s,共计用时21分钟,用top指令观察到其所使用的物理内存为10.8GB,负载平均在2.1,对mysql服务器几乎没有影响。
运行结束后,会在目录下生成一个Storable类型文件,大小:926M

现在就是程序来分析那些大小等于5次的数据了。
use v5.12;
use Storable;
use Time::Piece;
use Data::Dumper;

binmode(STDOUT, ":encoding(utf8)");
my ($stfile,$lastd,$currd,$rs)=('wm03.st');

#get storable variable
if(-e $stfile){
 $lastd=retrieve($stfile);
}

foreach my $uid(keys %$lastd){
 say qq["$uid","$lastd->{$uid}->{name}","$lastd->{$uid}->{cnt}"] if($lastd->{$uid}->{cnt}>=5);
}

time perl parsest.pl>pcontent.csv

脚本会将上页的结果文件载入内存,通过top指令可以看到占用全部内存的13%,用时4分钟计算出结果,其本身负载很小,共计3416284行。

两者不知道为什么会有十几条差异呢,应该是分组的字段登录名前或后有空格的关系吧,不过对于三白万的数据也算不了什么。当然,也可以不需要存入Storable文件来保存中间结果,直接分析内存的大hash数组就可以直接得到结果了。