集算结构化,SQL查日志

通常,日志文件都是文本格式,其中的内容是非结构化的文本串。这就使得我们查询日志信息时,一般只能使用文本编辑软件的搜索功能,输入关键字后,靠眼力去侦查每处匹配结果。在日志量不大,或者只是偶尔查一下时,这么操作倒也无妨。不过,再简单的事情也怕多次重复。如果需要频繁查询,量变就可能引起质变。如果每次还都要靠人工搜索,那么就算有再好的视力,也会有头晕目眩的时候。因此,想要轻松查询日志,就必须找到一款合适的工具,有了合适的工具,就可以一边喝着咖啡,一边轻弹条件回车就行了。

工具里面,首先想到的,就是利用各种计算机开发语言,外加关系数据库。但这类工具开发过程繁琐,还需要准备好多工作环境,包括配置语言开发环境,安装数据库服务,安装数据库查询应用等。

对于这么“重”的方案,我们果断撇开。因为今天就要介绍一个轻巧方便的工具——集算器,利用集算器,可以将文本日志变成结构化数据,然后就可以使用我们熟悉的SQL式查询了。

这里,我们利用到了集算语言(Structured Process Language,简称SPL)的两大优点:

  • 将日志内容结构化为数据表结构,SPL远比常用开发语言简单、易用、直观。
  • SPL支持直接对结构化的文件进行SQL查询,不再需要安装配置第三方数据库软件。

下面就是具体的实施过程。

1、日志结构分析

不同的日志文件,其内容格式五花八门,每一个看上去都杂乱无章。但对于某个特定的具体的日志来说,它一定会有它自己的结构。拿到日志文件后,首先要做的就是分析日志内容,提炼数据结构,总结出可以结构化的字段。

作为示例,我们用腾讯视频软件下的一个启动日志来做案例。如果你也用过腾讯视频,就可以利用下面的代码来体验和学习,分析一下自己的使用行为了。这个日志文件,位于当前用户的AppData路径下,并且以QQLive.exe[Main]开头。在我的机器上,这个文件就是:

C:\Users\[Joancy]\AppData\Roaming\Tencent\QQLive\Log\QQLive.exe[Main][2018-8-3 21-5-35-557][12164].log

上述路径中[Joancy]是我的Windows登录用户名,在你的机器中,将会是你的用户名。QQLive.exe[Main]开头的日志文件有很多,随便取一个就可以。

扫描二维码关注公众号,回复: 3047242 查看本文章

下面就是这个日志文件中的两行:

[18-07-19 14:35:06][9416]-[31ms][QQLiveMainModule.dll][CQQLiveModule::ParseCommandLine] cmd=”C:\Program Files (x86)\Tencent\QQLive\QQLive.exe” -system_startup

[18-07-19 14:35:08][9416]-[2266ms][HttpModule.dll][CDownloadMgr::AddTask]keyid = 1,url = http://182.254.116.117/d?dn=vv.video.qq.com.&ttl=1

可以看到,这个日志的内容比较规整,一行一条记录。每行中一对中括号中的内容为一节,对应一个字段。只是最后的两节有点特殊,其中倒数第二节可以省略,而最后一节没用中括号括起来。这样,我们就可以整理出日志表的数据结构如下,并且把第一行内容作为对应的示例:

  字段名 类型 分节内容
1 记录时间 DateTime [18-07-19 14:35:06]
2 线程编号 Integer [9416]
3 加载时刻 Integer [31ms]
4 加载模块 String [QQLiveMainModule.dll]
5 加载函数 String [CQQLiveModule::ParseCommandLine]
6 日志内容 String cmd=”C:\Program Files (x86)\Tencent\QQLive\QQLive.exe” -system_startup

表(1)

解析各个字段时,需要注意:

1)记录时间:由于年份只有两位,所以在转成日期时间类型时,需要指定相匹配的日期格式,否则18就会被当成公元18年,而不是2018年了。具体的操作方法是打开集算器菜单中的选项,在弹出的窗口中点击环境页面,设置属性‘日期时间格式’为‘yy-MM-dd HH:mm:ss’。

2)加载时刻:描述的是程序自从启动后,当前模块从启动后历经的毫秒数。由于字串后面有单位‘ms’,因此需要在转换为整数前先去掉这个单位。

3)加载函数:这个字段不一定有,所以需要在分析结构时进行判断,没有该字段时,需要插入空值。

2、结构化实施

如果采用数据库来存储结构化后的日志数据,首先需要安装和配置数据库,然后安装合适的数据库查询应用程序,这个过程可能会比较麻烦。如果利用现成的生产数据库,那么首先可能更新不方便,其次更重要的,会影响生产系统的性能。

而利用集算器,由于SPL支持直接对结构化的文件进行SQL查询,那就简单了,只需将结构化后的内容保存到文件就行了。

下面开始编程。在集算器设计器中,新建文件,设定它的网格参数为‘fileName’,用于指定需要转换的日志文件,然后把文件保存为convert.dfx。具体的实现脚本如下:

  A B C D
1 =create(记录时间,线程编号,加载时刻,加载模块,加载函数,日志内容)
2 =file@s(fileName:”UTF-8″) =A2.read@n()    
3 for B2 if len(trim(A3))==0 next  
4   =[]    
5   for 4 =substr@l(A3,”]”) >A3=substr(A3,”]”)
6     =substr(C5,”[“)
7     if B5==3 >C6=substr@l(C6,”ms”)
8     =parse(C6) =B4=B4|C8
9   if left(A3,1)==”[“ =substr@l(A3,”]”) >A3=substr(A3,”]”)
10     =substr(C9,”[“) =parse(C10)
11     =B4=B4|D10
12   else =B4.insert(5,null)
13   =B4=B4|A3    
14   >A1.record(B4)  
15 =A2.name() =filename@d(A15) =filename@n(A15)  
16 =outFile=B15+”\\”+C15+”.txt”    
17 >file(outFile).export@t(A1)    
18 >output(“转换为 “+outFile+” 成功。”)    

表(2)

这段脚本的功能就是接收日志文件,将文本内容按行读取为一个序表。然后逐行拆分信息成为一条记录。最后将转换后的序表保存到同目录下另一个文件。脚本中用到最多的一个函数是substr(src,target),作用是在src中找target,找到后返回srctarget后面(右边)的字串,找不到时返回空。如果使用选项@l时,则返回前面(左边)的字串。注意:文中表格的合并格,仅仅是为了阅读方便。集算器网格没有合并格,将代码复制到集算器网格中时,只能对应到合并格的首格。

脚本详解:

1)A1格的create函数类似数据库的Create Table命令,作用是创建一个空表,字段名就是create函数的参数,但不需要事先指定数据类型。

2)A2格使用file函数,用选项@s加载位于寻址路径下的fileName文件,通过参数”UTF-8”指明该日志文件是UTF-8字符集的,且包含了中文(如果中文出现乱码,那就是字符集不对)。

3)B2将文件内容读入到序列,选项@n表示读取的内容是文本。

4)A3为循环,依次遍历B2的成员。

5)B3和C3用于跳过空行。

6)B4初始化一个空序列,用于容纳解析后的每个字段值。

7)B5先循环4次,分离前4个字段内容。这是因为每一行日志仅有前4节由‘[]’分开,而第5节如果有‘[]’,则其中内容解析为‘加载函数’,如果没有就将后面的所有信息解析为‘日志内容’。同时,之所以不是一次就按中括号全部拆分,是因为日志内容里面仍然可能包含‘[]’,所以随后的两个字段需要单独解析。

8)代码块C5到D8,使用substr函数分离出字符串值,再用parse函数将字符串值串解析到正确的字段类型,然后将解析出来的结果追加到B5定义的序列中。其中代码D7处理特殊的第3个字段内容,去掉多余的‘ms’。

9)代码块B9到C11判断当前的串如果仍是‘[]’分隔的内容,则解析出字段5‘加载函数’。

10)代码块B12到C12是在没有‘加载函数’内容时,插入一个null空值。

11)B13把解析的最后一节内容,全部追加到序列B4,对应最后一个字段‘日志内容’。

12)B14把当前解析出来的所有字段值,追加到A1定义的数据表中。

13)A15到C15获取当前文件的绝对路径,分别拆分出路径和文件名。

14)A16重新构建一个相同目录下,扩展名为txt的输出文件名。

15)A17将结构化后的内容导出到输出文件。这里用到了文件的导出函数export,该函数用于将序表的内容导出到指定文件,选项t表示导出的仍然是文本文件,而且文本的第一行是字段名。这里特别说明一下,选项t可以改为b,意思是导出为二进制的集文件,使用集文件时,存储和读取的效率都比文本文件更高。这个例子中我们先使用文本文件,方便直接查看导出后的内容。

16)A18向控制台打印一个转换完成的信息。

保存脚本,并且将日志文件更名为QQLive.log也放置到相同目录,同时在集算器的选项窗口中,找到环境页面下的寻址路径,将上述convert.dfx文件的目录(我这里使用的是D:\demo)加入到寻址路径。然后打开DOS命令执行窗口,进入到安装路径下的bin目录(我这里的安装路径是D:\temp\esProc)。输入esprocx命令执行convert.dfx效果如下:

图(1)

从图(1)中的打印信息可以看出,日志文件被结构化后,已经保存到d:\demo\QQLive.txt文件中。用文本编辑器打开QQLive.txt,可以看到第一行是字段名称,后面的字段内容都已经规整,并用制表符分开。如下图:

图(2)

3、使用SQL查询日志

使用集算器查询文件序表的内容非常简单,就跟查询数据库一模一样。具体的做法也是建立连接对象,然后调用query方法,所不同的是,文件数据库的连接名不用填。

下面这个例子新建了一个query.dfx文件,定义一个参数‘sql’用于传入需要执行的SQL命令。脚本如下:

  A B
1 =connect()  
2 =A1.query( sql )  
3 return A2  

表(3)

脚本详解:

1)A1建立一个参数为空的连接对象,用于对本地文件的查询。

2)A2与执行数据库的查询一样,使用该连接对象的query方法,执行sql语句。

3)A3返回查询结果。

同样,将该文件放置在寻址路径下,看下执行效果:

图(3)

上述esprocx命令用到了选项-r,用于执行完dfx后,打印出返回的结果。从图(3)中可以看出,转换的日志记录共有1108条记录。需要注意的是,作为命令行参数时,由于sql语句中也包含空格,所以需要加上双引号。

通常查询日志时,往往会关注日志内容中包含某个关键字的信息,下面是对应的查询:

图(4)

从图(4)可以看出,这个查询是查找日志内容中包含‘http’的日志信息。同时,这里还用到了SQL中的top 10关键字,以防记录太多,把查询条件滚屏滚没了,不方便截图。实际情况中,如果查询到的结果很多,DOS屏幕一屏显示不下,只能看到最后的十来行,这就比较尴尬了。此时只要利用下DOS的小技巧,将查询到的内容输出到另一个文件,就解决了:

图(5)

图(5)的查询就是将日志中包含‘http’的日志都查询出来,并且保存到了d:\demo\http.txt文件中。

为了验证当前脚本的鲁棒性,我再从腾讯视频下找到一个比较大的日志文件,大约有99兆,将它复制到D:\demo下,并命名为QQLive99.log。再次执行转换操作:

图(6)

结果转换失败。看图(6)中的堆栈信息,原来是Java heap space异常。99兆大的日志文件,按说也不算大,为啥就内存溢出了呢?先查看下当前命令程序的内存分配状况,该内存堆大小的参数在bin\config.txt中指定,我当前的最大堆参数设定为-Xmx200m,才200兆,所以内存溢出是可能的。再回头分析下表(2)中B2的脚本‘=A2.read@n()’,该语句的意思为将文件A2的内容全部读为内存序表。还有B14中的‘>A1.record(B4)’,该语句的意思为解析后的记录全部追加在A1格子中。也就是分配的最大堆内存至少要大于两倍的源文件大小,才可能顺利算完。这还只是粗算,因为还有其他代码占用内存,以及文本文件转为内存对象后,内存占用也会增大。当然,解决办法之一就是提高最大堆参数。但这种改法治标不治本,一是物理内存总是有限的,不可能无限增大。二是堆大小增大后,过得了当前99M的文件,却未必过得了1G的文件。所以还是得从脚本上下功夫,看看有没有游标之类的办法,使用游标每次读一块数据,处理完后立即输出,就一劳永逸了。

4、使用游标规避内存溢出

SPL有文件游标,只要使用文件对象的cursor方法就可以返回游标。不过,游标对象是数据库的概念,从游标取出的数据都是序表。但日志文件本身就是一行行的串,读取它就是为了分析后才能得出序表。这就像一个近视眼想去配眼镜,却又看不清柜台远处眼镜的价格。只能问售货员“眼镜怎么卖啊?”,“你自己看”——柜员懒懒地回答。“我能看清还买你的眼镜做什么?!”于是,柜员的慵懒造成了死循环……

要打破这个循环,首先得由柜员告诉顾客眼镜的价格。同理,我们在构造游标时可以使用s选项,先不解析文本,直接将整行当一个字段值返回就是,此时使用缺省的字段名:‘_1’。

另外,游标和文件操作在使用上有一些区别,使用文件的read函数得到的是一个序列,其成员就是每行文本。而游标返回的是一个序表,序表的成员是记录,需要再取记录中的字段值才能得到每行文本。所以,使用游标处理时要多处理一层,稍微麻烦一些。弄清了这个区别之后,后面的拆分就都一样了。

此处我仍然贴出全部脚本,方便读者直接将脚本复制到集算器执行。为了和前面的convert.dfx有所区分,使用游标转换的文件命名为convertCursor.dfx。脚本如下:

  A B C D E
1 =create(记录时间,线程编号,加载时刻,加载模块,加载函数,日志内容) =now()
2 =file@s(fileName:”UTF-8″) =A2.cursor@s()      
3 =A2.name() =filename@d(A3) =filename@n(A3)    
4 =outFile=B3+”\\”+C3+”.btx” =file(outFile) >movefile(B4) =0  
5 for B2,100000 for A5 =B5=B5._1    
6     if len(trim(B5))==0 next  
7     =[]    
8     for 4 =substr@l(B5,”]”) >B5=substr(B5,”]”)
9       =substr(D8,”[“)  
10       if C8==3 >D9=substr@l(D9,”ms”)
11       =parse(D9) =C7=C7|D11
12     if left(B5,1)==”[“ =substr@l(B5,”]”) >B5=substr(B5,”]”)
13       =substr(D12,”[“) =parse(D13)
14       =C7=C7|E13
15     else =C7.insert(5,null)
16     =C7=C7|B5    
17     >A1.record(C7)  
18   >B4.export@ab(A1)    
19   >D4=D4+A1.len() >output(“转换完成:”/D4)
20   >A1.reset()      
21 =now() =interval@s(E1,A21)    
22 >output(“转换为 “+outFile+” 成功。耗时 “/B21+” 秒。”)  

表(4)

表(4)中跟表(2)的拆分脚本以及读取文件等相同代码就不再解释,下面只解释不同的部分:

1)E1中使用now()函数记录脚本计时开始。

2)B2使用cursor@s将文件的每一行作为一条记录,使用缺省字段名‘_1’,返回游标。

3)A3到C4代码块为拆分文件名,生成输出文件。由于使用游标每次只处理一部分数据,处理完的数据直接追加到输出文件。所以在最开始时要将已经存在的输出文件删除,否则每执行一回当前脚本,就会累加一次重复数据。其中用到了movefile(fn,path),该函数的作用为移动文件fnpath下,当path省略时表示删除当前文件。

4)D4为定义一个计数器,用于累计当前完成了多少记录的转换,用于后续信息输出。

5)A5为循环游标取数,后面的参数100000为一次读取的记录数。这个数值不能太小,否则读取文件的次数太频繁,效率低。但也不能太大,太大时,如果内存比较小,也可能内存溢出。所以这个值需要在可分配内存下,满足不溢出时,尽可能取大一些。具体多少需要根据分配给esprocx的堆大小测试得到。

6)B5为循环遍历A5中的序表。

7)C5为B5循环时,每个成员为一条记录,此处将B5中记录的‘_1’字段值取出来。

8)B18将分析后的值导出到输出文件,注意使用了选项a,将每次的数据追加到当前文件。

9)B19和C19为计数并输出计数信息,以便转换时可以看到转换进度。注意:SPL中需要将字符串跟数值拼接到一起时,不能使用‘+’,而要使用‘/’符号!

10)B20使用了reset函数将当前的内容清空,好为下一次分析容纳数据。

11)A21到A22为计时,并输出计时信息。

再来看下使用游标转换QQLive99.log的效果:

图(7)

从图(7)中信息可以看出,每次从游标取十万条记录来分析时不再内存溢出,且转换耗时14秒。为了比较下不同块大小时的效率,我将每次取数改为100,再看下效果:

图(8)

从图(8)可以看出,效率明显降低,耗时增加了一倍还要多。所以,只要内存不溢出时,每次游标取数的记录数,尽可能大一些为好。在convertCursor.dfx脚本中,输出的文件格式是二进制的集文件,下面看下集文件跟文本文件的区别,修改输出文件为文本文件后,执行转换效果为:

图(9)

可以看到相对于图(7)使用集文件,转换为文本文件耗时多了2秒,转换效率变差了。同时文件大小也不同,集文件格式更紧凑,参考下图:

图(10)

图(10)中,集文件QQLive99.btx不到80M,比转换后的文本文件QQLive99.txt小了10%多。可见集文件无论在处理速度上,还是存储性能上,都要优于文本文件,所以一般情形下,尽量使用集文件存储结构化后的序表。

5、使用多路游标并行计算

第4节讲到使用游标来处理数据时,为了提高计算效率,我们可以增大每次从游标取数的行数。但是这个取数的块大小跟物理内存大小是成正比的,当达到了内存极限时,这个数据就没法再提高了。那么是不是就没法再提高代码效率了呢?

当然不是,对于程序计算来说,计算的效率无非就是两方面:一是尽可能将数据加载到内存再计算,充分利用内存的高效IO,减少对硬盘的多次低效访问;二是现在的CPU几乎都是多核的,可以高效并发处理数据,如果采用并发,充分利用多核性能,也能提高计算效率。那么SPL是如何利用多线程并发呢?

SPL开发多线程并发程序很简单,比如第4节中用的游标,只要加上选项m(Multi,多路)就可以返回多路游标。至于这个“多路”具体是多少,可以在“选项”中的“常规”面板下,由最大并行数来指定。这个数据也不是越大越好,毕竟CPU的核数一定,太多线程时,反而因为不断调度而浪费时间。一般情形下,这个最大并行数最好不要超过CPU的物理核数,性能最佳。我的电脑是4核的,所以我采用最大并行数为4。

下面贴出实现代码,使用多路游标转换的文件命名为convertMCursor.dfx。脚本如下:

  A B C D E F
1 =file@s(fileName:”UTF-8″) =A1.cursor@ms() =now()      
2 =A1.name() =filename@d(A2) =filename@n(A2)      
3 =outFile=B2+”\\”+C2+”.btx” =file@a(outFile) >movefile(B3)      
4 fork B1 =create(记录时间,线程编号,加载时刻,加载模块,加载函数,日志内容)
5   for B1,100000 for B5 =C5=C5._1    
6       if len(trim(C5))==0 next  
7       =[]    
8       for 4 =substr@l(C5,”]”) >C5=substr(C5,”]”)
9         =substr(E8,”[“)
10         if D8==3 >E9=substr@l(E9,”ms”)
11         =parse(E9) =D7=D7|E11
12       if left(C5,1)==”[“ =substr@l(C5,”]”) >C5=substr(C5,”]”)
13         =substr(E12,”[“) =parse(E13)
14         =D7=D7|F13  
15       else =D7.insert(5,null)
16       =D7=D7|C5  
17       >B4.record(D7)  
18   >B3.export@ab(B4) >output(“游标转换:”/B4.len()) return B4.len()  
19 =now() =interval@s(C1,A19) >output(“总共转换:”/A4.sum())      
20 >output(“转换为 “+outFile+” 成功。耗时 “/B19+” 秒。”)

表(5)

脚本详解:

1)B1使用的游标加了选项m,返回多路游标。本机采用4路游标。

2)由于4路游标是并行执行的,但是写文件操作却没法并行。此时需要在B3格中的打开文件时,使用选项a,意思是打开的文件在并发写时,线程间自动排队,而不报告文件锁异常。

3)A4中,多路游标的使用,得用fork语句,由于多个游标是并发执行的,所以需要将创建表结构的create语句放入A5,让每个游标有自己的局部序表。

4)B18到D18为导出游标数据,打印当前游标的转换数目后,将转换数目返回。需要注意的是由于游标是并行执行的,所以转换后的结果不一定对应源文件的次序。

5)A19到A20代码块,打出转换的总记录数以及耗时信息。

看下多路游标执行的效果:

图(11)

从图(11)打出的信息可以看出,共采用了4路游标并发执行转换,总共耗时8秒,比起没并发前的16秒,速度提高了1倍,可见并发的效率还是很高的。还有就是当前的并发结果都要写出到同一个文件,由于写出文件仍然是串行的,没法充分利用并发,要不速度还能更快些。

此外,针对集文件的查询,也是可以并行的,且语法跟Oracle的语法保持一致。比如执行“select /*+parallel(4)*/ * from QQLive99.btx”时,查询实际是产生了4个游标在并发查询。但要注意的是,只有使用z选项导出生成的集文件,才能并行。因为并行时,每个线程必须拥有自己的一段文件,使用z选项才会写出可分段的集文件。

猜你喜欢

转载自blog.csdn.net/raqsoft/article/details/82216057