海量数据处理语言sawzall入门简介

最近在公司换了一个新的部门,接触到了一些新的技术。而且有些技术是公开或者是部分公开的,觉得有点可能性把这些东西都拿出来讲一讲。其实写博客有两个目的,一个是把知识分享出去给更多人。另外一方面就是防止自己的健忘。我就是一个比较健忘的人,经常1分钟之前想做的事情,一分钟之后都忘记了。1年之前很精通的知识,可能1年之后也忘记的一干二净。小时候老师总是说教给你们的东西,你们不要一年之后就还给我了。其实也是人之常情,可能人的脑子里的cache是有限的。 能够在cache里的东西都是当下急需要用的,其他长期不用的就慢慢淘汰了。 所以写给自己的博客也是有点必要的。

开场白拉拉杂杂讲了这么多,无非是给自己写博客找一点动力和理由。人做事总是需要一点理由的。讲到这里我又想讲一个故事,也跟分享有关。看的人你也可以把这个故事当笑话来听。也可能不是真的,我是道听途说。讲的是一个很大的公司的程序员, 他一毕业就进了这个公司,在公司里浸淫十几年,把公司的很多私有的技术都摸了个透。所谓私有技术,就是这个公司自己发明创造出来,也只给自己公司用的技术。这个公司就有很多这种私有技术,比如自己的云计算平台,自己的编译器等等。这些技术很牛的, 不过从来没有公开过,公司外面的人也不知道牛不牛。这个程序员呢,在公司干了十几年,然后就出来了。可是他一出来就发现傻了眼。 虽然他在原来公司里是很牛叉的技术专家,可是他懂的都只是私有技术,外面的人不知道,当然也不会用。而外面人用的技术,他也不怎么懂。所以这个故事讲的是一个技术专家离开他所在的技术领域可能就不那么专家的故事。但你也可以理解为技术分享是很有必要的,不然私有技术再牛逼别人也不知道你牛逼。往行业大背景方面的来说, 私有技术不分享,怎么能推动行业进步。所以分享,是一件很利人利己的事情。

动力找完了,下面进入正题,讲一讲sawzall是什么,能干什么以及怎么干的问题。

按照八股文的风格,我们先来破题。

sawzall我个人理解,应该拆开来读, sawz all这两个单词。 sawz这个单词再拆开saw z, saw是锯子、锯开的意思,可以当动词也可以当名词,而这个字母"z"我本着不负责任的去麻烦的原则就不去考察它的含义,我姑且把它当成一个象声词,比如锯子的“子"。all就不用我来解释了。sawz all连起来就是锯开一切的含义。并且,有一个公司生产的一把很出名的电锯就叫sawzall, 这把电锯貌似很出名, 在youtube可以搜出来它的视频。这段视频里有一个男人拿着一把sawzall电锯开了各种东西,反正就是这个锯子很牛逼的意思。

sawzall这门语言是一门专门用来进行的数据分析的编程语言。它是google公司发明并且使用的,用来进行海量的日志记录处理的。在2003年google公司对外公布了这门语言sawzall, 在2010年开源了sawzall语言的运行工具szl。注意编程语言和运行工具的关系。一个就好比c语言,而另一个就好比gcc等编译器。 你知道了语言的语法和功能,但你没有编译器去编译也没辙。

sawzall这门语言和电锯的联系,我分析可能在于: 写日志记录分析的人可能会有体会,就是进行单条日志分析的第一步就是把日志中每个字段都划分开。这就有点像电锯把这些数据都锯开。是不是有点形象。

不过,google在公布szl的时候其实也不是完全的公布。因为sawzall这门语言其实是和google当时的技术环境有关的。在sawzall之前, google内如果要进行海量的日志处理,需要写基于mapreduce框架的程序。而后来发现,其实大部分的代码是重复的,于是就把记录处理的操作抽象成专门的脚本处理,再用一个专门的mapreduce程序读取这个脚本进行计算。而这个脚本语言就是后来的sawzall是公开的,而进行mapreduce程序,google却没有公布细节。我猜这和它公司内其他的技术的耦合度太高而无法分割有关。

这就是sawzall语言今天的状况。它的语言部分是公开的,你可以看到它的语法是完全处理单条记录的,很明晰。然后google又公开了一个单机版的运行环境szl, 你可以自己单机运行自己写的脚本,进行一些单机的数据处理。但是, sawzall语言的并行计算处理程序,google并没有公布,这就很坑爹了。因为sawzall的最牛逼之处,就在于用很简单的脚本再利用mapreduce框架进行并行计算达到很高的处理性能又同时达到简单的编码难度。而现在google不公布并行计算的那一部分,就相当于生生折了sawzall的一翼。所以在业界,这门语言显得既陌生又冷门。只怪生父google太不给力啊。

如何安装szl

前文讲过, szl其实是sawzall语言的解释器。 要运行sawzall脚本必须要安装szl解释器。

安装szl其实也非常简单,只要从google的szl下载页面上下载一个包然后./configure && make && make install就可以了。

下载地址是:http://code.google.com/p/szl/downloads/detail?name=szl-1.0.tar.gz

不过在安装szl的时候需要依赖几个东西:

1 protoc 这个是protobuf的编译程序。大名鼎鼎的protobuf就不再介绍了,如果没有安装的可以去搜索引擎搜索protobuffer然后下载, 在debian或者ubuntu环境下可以通过apt工具安装:

aptitude install protobuf-compiler

aptitude install libprotobuf-dev


2 pcre, 这个也是大名鼎鼎的正则表达式库,在debian或者ubuntu环境下可以通过apt工具安装:

aptitude install libpcre3-dev 

编译时候的发现几个问题

a /usr/include/google/protobuf下面没有compile文件夹, 我直接从protobuf文件下面拷贝ile一个过去

b 找不到-lprotoc, 我发现/usr/lib下面有 libprotoc.so.6,于是我软链了一个libprotoc.so

c 编译szl时报错

libtool: compile:  g++ -DHAVE_CONFIG_H -I. -I.. -pthread -g -O2 -g -Wall -Wwrite-strings -Woverloaded-virtual -Wno-sign-compare -DNDEBUG -pthread -O2 -DSZL_BIG_ENDIAN=1 -DSZL_LITTLE_ENDIAN=2 -DSZL_BYTE_ORDER=2 -g -O1 -pthread -g -O2 -g -Wall -Wwrite-strings -Woverloaded-virtual -Wno-sign-compare -DNDEBUG -MT analyzer.lo -MD -MP -MF .deps/analyzer.Tpo -c engine/analyzer.cc  -fPIC -DPIC -o .libs/analyzer.o
In file included from ./engine/globals.h:20,
                 from engine/analyzer.cc:20:
./public/porting.h:51: error: conflicting declaration ‘typedef long unsigned int uintptr_t’
/usr/include/stdint.h:129: error: ‘uintptr_t’ has a previous declaration as ‘typedef unsigned int uintptr_t’
奇怪,居然重复定义了uintptr_t。 我直接在代码里去掉了这个定义。

start up第一个例子

一个文件里每一行都是这样的格式 ”日期 ip“,字段之间以空格分隔, 需要计算每个ip的访问次数。

sawzall脚本

#ip_statistic.szl
ip_count_table: table sum[ip:string] of int;
line_array := sawzall(input, "[^ ]*");

date:= line_array[0];
ip := line_array[1];

emit ip_count_table[ip] <- 1;


输入文件

2012-1-1 1.1.1.1
2012-1-2 1.1.1.1
2012-1-3 1.1.1.1
2012-1-4 1.1.1.1
2012-1-5 1.1.1.1
2012-1-6 1.1.1.1
2012-1-1 2.2.2.2
2012-1-3 2.2.2.2
2012-1-4 2.2.2.2
2012-1-5 2.2.2.2


运行szl

在命令行中输入命令

szl --program=ip_statistic.szl --ignore_undefs=true --table_output=ip_count_table ip_log.txt
sawzall的脚本代码一般都是以.szl结尾, 程序的注释是和一般脚本相同,用#开头的行都是注释。

程序很简单,一共5行,我一行一行解释。

第一行

是一个定义:

ip_count_table: table sum[ip:string] of int;
这种定义的方法和其他的语言不同。 比如C语言是写 int i; 先是类型,然后是变量名。 而 sawzall的定义变量的顺序与C语言相反, ip_count_table是变量名, ":"之后的是变量类型:
table sum[ip:string] of int
table是关键字,表明ip_count_table的类型是一个table。 table是sawzall语言一个很重要的概念, 它是统计数据的输出。一般的table可以有多个key、一个value和一个weight组成。


在这里我们定义的ip_count_table的key是一个string。(ip:string也是名称:类型的定义方式)ip_count_table的value是一个int。

第二行


line_array := sawzall(input, "[^ ]*");
这一行其实定义了一个叫做line_array的数组, sawzall这是一个内建函数,后面会介绍,其实就是把第一个参数input分割成一个string数组,有点类似python中的str.split的功能。不过sawzall这个内建函数更强大, 它的第二个参数是一个正则表达式。


 "[^ ]*"
这个正则表达式的含义不清楚的同学请翻书,其实就是多个非空格字符的含义。


那么sawzall这个内建函数的功能就是把input,按照多个非空格字符为一个字符串,分割成多个字符串,并且返回字符串数组。

看懂的人会问,input这个变量没定义过啊。 确实,这是一个szl内部变量,表示一条输入记录。注意,我并没有说是一行文件。不过在我们这个例子中,一条输入记录相当于一行文件。

第三四行


date:= line_array[0];
ip := line_array[1];
这两行没什么可讲, 就是定义了两个变量。和前面两行一样,定义的方式也是名称:类型。 line_array上一行定义过,是把一条记录分割成一个字符串数组。 date就是字符串数组第一个元素。 ip是第二个元素。


第五行


emit ip_count_table[ip] <- 1;

emit是sawzall的一个关键字。这行的含义是在结果table ip_count_table中,对应的ip的记录数量++。

看懂这个例子,对于一般的sawzall的处理流程就大概了解了。 大概就3步:

1 定义结果table

2 将记录分割成字段

3 处理字段,将处理的值塞入各种table。


运行结果

ip_count_table[1.1.1.1] = 6
ip_count_table[2.2.2.2] = 4

sawzall语法

sawzall的基本语法可以用一句话概括,就是类似于C语言的语法。用花括号作为一个语法block。 条件判断用if else。

可能有一点不同在于变量定义,前文已经讲过,C语言的变量定义是先类型然后变量名, sawzall是相反的。

sawzall类型

基本类型

sawzall有这些基本类型bool,int,uint,float,bytes,string,time, fingerprint。

bool
最简单true or false
int
64位有符号整数
uint
64位无符号整数
float
64位,浮点数
bytes
二进制的串
time

时间类型, 精确到毫秒。以T开头的字符串,

比如 T"Mon Feb 9 14:55:44.5 PST 2004"

fingerprint
哈希值,用于内建的哈希函数。整数加上'p'结尾,比如0P

复合类型

sawzall有几种复合类型

1 tuple, tuple相当于C语言的结构体。

可以把一个table的value定义成tuple类型,对于同一个key可以输出几个结果。

adcount: table sum[creative: string][{ hour: time, tz: string }] of
         { count: int, revenue: float };
 { count: int, revenue: float }
这就是一个tuple

sawzall内建函数

内建函数,可以当成库函数来使用。sawzall的库函数有几十个。 不过常用的也就那么几个。

saw

saw严格来讲,在语言定义中将它定义成一个operator,而不是intrincs。 可能因为saw是一个变参数的函数吧。

saw的用法就是把一个字符串,分割成多个子串。分割的方法是根据指定的多个正则表达式。

比如:

a := saw("abcdef", "abc", "e", "f");

返回的结果就是一个字符串数组。 a=['abc', 'e', 'f']

sawn

sawn和saw的用法类似,但不用指定多个正则表达式。 假如saw所指定的多个正则表达式类似,则可以使用sawn:

比如:

line_array := sawn(2, string(input), "[^ ]*");

sawn把input锯成两段,每一段都是 [^ ]*

sawzall

sawzall的用法前面已经讲过了。它的使用方法是不需要指定多个正则表达式,也不需要指定表达式的个数。

比如:

line_array := sawzall(input, "[^ ]*");

sawzall的输出table

sawzall输出一组有统计意义的数据集合。 在做数据挖掘的时候我们通常需要做的就是对原始数据进行一些求和、求次数、求平均、求样本、求最大值、求最小值的等等操作。而sawzall本身内建的一些table类型就可以简化我们的编程的工作。例如有一组搜索引擎爬虫的数据中,对每个站点记录着爬取的平均时间等信息,而我们需要挖掘出下载出错最多的网站。如果用通用型的语言来做这样的数据选取工作,恐怕是比较麻烦的,不可避免的需要要涉及到累加和一些比较操作。而如果用sawzall这样的数据挖掘专用语言,就方便得多。

top_error_site: table top(10) of site: string weight count: int;

在sawzall里处理这样的问题,只需要先定义一个top table作为输出。top_error_site是名称。 table是关键字,说明是输出表。top(10)是输出的类型,表示需要top 10的站点。 of site: string说明需要的结果站点的top 10。string是站点的类型。 weight count: int表示进行排序的key值。我们需要对出错的次数进行排序。


emit top_error_site <- site weight 1;
在运行时,使用上面一行语句就搞定。其余的操作不需要操心。所有进行累加和排序的逻辑全部都在这一句中做完。对比一般通用性的编程语言,这样简便了很多,同时减少了出错的机会。


sawzall定义有多种不同的table适用于不同的使用场景。例如top, maxium, minimum等等。

collection

collection是最简单的一种table。 实际上它就是一个普通的映射关系表。例如:


error_site: table collection[site:string] of count: int;
上面的定义的就是一个 site -> count的映射。有点类似于stl的std::map<std::string, int>这样的定义。在使用了emit error_site["www.qq.com"] <- 1之后, 就在内存中创建了一个  "www.qq.com" -> 1的映射。当下一次更改error_site["www.qq.com"]的值为2 之后,前一次的值就丢弃掉了。 


不过collection有一个特点(几乎szl语言的每一种table都有这个特点)是, collection可以定义多个key。

例如需要统计出,每天,每个小时,每个站点的出错次数, 就可以用多个key来指定。


error_site: table collection[date: string][hour: string][site: string] of count: int;

sample

sample也是非常常用的table。 顾名思义,sample的作用就是在数据集合中对与满足条件的数据进行采样。例如对于每个出错的站点采样10个url。

则可以这样定义:


site_url_sample: sample(10)[site: string] of url:string;



再配合emit语句则可以对每个site输出10个url作为采样。对于不足10个出错url的site,输出所有的Url。对于超过10个出错url的site则抽出前10条。

sum

顾名思义,sum是用来求和的。例如求每个站点的错误数量之和:

site_error_count: sum[site: string] of int;

top

top是用来求最大的N个值。上文已经给了一个例子了。

maximum和minimum

maximum和minimum这两个是用来求最大和最小的N个独立的值。

maximum和top的区别有点类似。区别在于maxmium是对于独立的单条记录最大值,而top是对于同类型的记录求和的最大值。

例如原始数据是 "IP" "时间" 访问次数, 有两个统计表:

max_ip: table maximum(1) of ip: string weight count: int;

top_ip: table top(1) of ip: string weight count: int;
那么max_ip所求的就是某时间上访问次数最多的ip, 而top_ip所计算的是所有时间内,访问次数最多的ip。

set

set的功能类似于sample,如果超过了数量限制,则会直接丢弃掉数据。

例如 ip_set: table set(10)[site: string] of string: url;

这个set是为了对每个站点取10个url, 但是如果这个站点有多余10个url,则直接丢弃掉数据。

sawzall的性能

首先需要澄清的是, 我只能测量sawzall单机版本的性能。 集群版本的数据处理能力其实取决于机器的数量和map reduce计算框架的性能。 单机的处理能力只能在一定程度上反应sawzall的处理性能, 但却又不是决定因素。 因为在利用map reduce计算框架的时候, 至关重要的是计算能力的线性扩展能力, 而不是单机处理能力。

数据样本和运行环境

数据的样本来自于一份服务器的请求处理速度的日志记录文件,有这么几个字段:

时间 A类型请求数 B类型请求数 错误请求数 IP

每个字段之间用\t进行分隔。

例如:

2013-07-11 13:13:01.602726      0       269115  0       10.173.x.x

样本的数据量是400万条记录。

测试的环境是一台linux的计算机, 8个Intel(R) Xeon(R) CPU [email protected]和足够用的内存。

top

第一个测量的项目是top table的计算性能。 计算的是所有机器的B类型请求也就是第三个字段的统计量总和并排序。

sawzall代码如下:

top_machines : table  top(100) of ip:string weight request_sum: uint;

g_line_array : array of string = sawzall(string(input), "[^\t]*");

if (len(g_line_array) >= 5 )
{
    ip := g_line_array[4];
    count := uint(g_line_array[2]);
    emit top_machines <- ip weight count;
}
运行的命令:

time szl top.szl costa_stat.out --table_output=top_machines
costa_stat.out是输入的测试样本。

完成分析所需要的时间为:
real    0m27.308s
user    0m27.078s
sys     0m0.128s

便于对比,我用python同样实现了这个功能。

代码如下:

#codint: utf-8
import sys
ip_count_map = {}
f = open(sys.argv[1])
while True:
    line = f.readline()
    if not line:
        break
    line = line.strip('\n')
    g_line_array = line.split('\t')
    if len(g_line_array) >= 5:
        ip = g_line_array[4]
        count = int(g_line_array[2])
        if not ip_count_map.has_key(ip):
            ip_count_map[ip] = 0
        ip_count_map[ip] += count
ip_count_list = ip_count_map.items()
ip_count_list.sort(key = lambda x: x[1])
ip_count_list.reverse()
for ip, count in ip_count_list:
    print "top_machines [] %s => %d" % (ip, count)
运行的时间为:

real    0m14.145s
user    0m14.065s
sys     0m0.052s


可以看出同为脚本语言, sawzall比python的运行效率还是相差了不少,几乎相差在一倍。不过在代码量上sawzall还是精简了不少。

maximum

第二个测量的是maximum, 查找B类型请求的单条记录最高的100条记录。

sawzall的代码如下:

top_machines : table  maximum(100) of ip:string weight request_sum: uint;

g_line_array : array of string = sawzall(string(input), "[^\t]*");

if (len(g_line_array) >= 5 )
{
    ip := g_line_array[4];
    count := uint(g_line_array[2]);
    emit top_machines <- ip weight count;
}
可以看到,这段代码和上一段代码相比, 只是把top换成了maximum, 其他东西都没有变。

运行的时间为:

real    0m25.944s
user    0m25.778s
sys     0m0.072s

对比依然是python

#codint: utf-8
import sys
from heapq import *
f = open(sys.argv[1])
max_heap = []
max_heap_count = 100
while True:
    line = f.readline()
    if not line:
        break
    line = line.strip('\n')
    g_line_array = line.split('\t')
    if len(g_line_array) >= 5:
        ip = g_line_array[4]
        count = int(g_line_array[2])
        if len(max_heap) >= max_heap_count:
            min_count , min_ip = max_heap[0]
            if count > min_count:
                heapreplace( max_heap, (count, ip))
        else:
            heappush(max_heap, (count, ip))
l = []
while len(max_heap) > 0:
    count, ip = heappop(max_heap)
    l.append((count,ip))
l.reverse()
for count, ip in l:
    print "top_machines [] %s => %d" % (ip, count)
python使用了heap数据结构, 在运行过程中保存最大的100条记录。

运行时间为:
real    0m13.993s
user    0m13.877s
sys     0m0.088s

可以看出python的速度依然比sawzall快1倍左右。

性能结论

sawzall在单机处理数据的能力上并不出色, 甚至不如通用型语言python。 不过在代码的精简度上来看,sawzall还是会胜过通用型语言。

你可能感兴趣的:(海量数据处理语言sawzall入门简介)