项目中经常会用到对一些数据作周期统计,从而生成报表,通常原始的数据是存放在各种数据库表里的,
需要根据特定的规则进行数据统计后输出,通过写数据库的存储过程是一种方式,但存储过程一般难以跨数据库统计,而且限制于特定的数据库脚本语言,
支持不同类型数据库就需要重写脚本,不易维护。所以开发一套通用的数据统计框架以及统计规则描述模板,可以用更简洁的方式来描述出统计的规则,
适用于对不同数据库数据作周期统计,是有其应用价值的。下面介绍下一种我自己定义和实现的统计模板,实现比较粗糙,比如没有很好的语法检查支持等,
但在实际使用过程中还是收到了很好的效果。
首先给大家看一个统计规则模板的例子:
<dataset name="swrunmem_usage_collect_report"> <source> <sql> <![CDATA[ SELECT dev_id,swrun_id,procname,memusage,cpuusage,procnum,ctime FROM tbl_collect_process_runstatus_tmp WHERE ctime >= $P{begin_time} AND ctime < $P{end_time} order by ctime; ]]> </sql> </source> <field name="dev_id" class="long"><![CDATA[source.$F{dev_id}]]></field> <field name="procname" class="string"><![CDATA[source.$F{procname}]]></field> <field name="ctime" class="timestamp"><![CDATA[source.$F{ctime}]]></field> <field name="memusage" class="string"><![CDATA[expr(1.0*sum(source.$F{memusage}))]]></field> <field name="cpuusage" class="string"><![CDATA[expr(1.0*sum(source.$F{cpuusage}))]]></field> <field name="procnum" class="int"><![CDATA[sum(source.$F{procnum})]]></field> <group>$F{dev_id}</group> <group>$F{procname}</group> <group>$F{ctime}</group> <!-- collect time period --> <param name="begin_time" class="timestamp"/> <param name="end_time" class="timestamp"/> <!-- destination table name --> <param name="dst_table_name" class="string">'tbl_collect_process_runstatus'</param> </dataset>
上面的xml定义了数据的来源,处理,和结果:
首先source的sql字段定义了数据的来源,即通过sql语句查询出本次要统计的数据,
其中begin_time和end_time参数是由周期统计框架传人的本次要统计的数据时间区间,由统计框架来确保数据不会被重复统计;
field标签同时定义了统计结果的字段和字段的计算公式即统计规则,可以通过source.$F{field_name} 的格式直接引用sql语句里的原始数据字段的值,
也可以通过$F{field_name}的格式引用其它结果字段的值,另外支持算术表达式的求值,字符串连接,求子串等运算,或者sum,avg,max,min等分组内的求和,求平均,最大最小值的计算;
group标签定义了对那几个字段进行分组,可以对原始sql语句里的字段分组,也可以对结果字段再作分组,且在分组内对结果字段公式里存在的分组聚合函数如sum,avg等作运算;
dst_table_name参数定义了目的表的名称,这样每次统计结束后,统计框架会将统计结果按模板里的字段建立结果数据库表并将统计的结果入库;
下面来看更多的例子:
<?xml version="1.0" encoding="utf-8"?> <report_def> <dataset name="mem_usage_collect_telnet_report"> <source> <sql> <![CDATA[ SELECT dev_id,task_id,row_index,data_list,collect_time FROM tbl_origin_perf_data WHERE task_id=7 AND collect_time >= $P{begin_time} AND collect_time < $P{end_time} order by collect_time; ]]> </sql> </source> <field name="dev_id" class="long"><![CDATA[source.$F{dev_id}]]></field> <field name="memory_id" class="int"><![CDATA[source.$F{row_index}]]></field> <field name="ctime" class="timestamp"><![CDATA[source.$F{collect_time}]]></field> <!-- MemTotal: 255764 kB MemFree: 118132 kB Buffers: 11472 kB Cached: 87772 kB --> <innerfield name="data" class="long"><![CDATA[split(source.$F{data_list}," ")]]></innerfield> <field name="memory_usage" class="int"> <![CDATA[expr((get($F{data},1)-get($F{data},4)-get($F{data},7)-get($F{data},10))*100/get($F{data},1))]]> </field> <!-- collect time period --> <param name="begin_time" class="timestamp"/> <param name="end_time" class="timestamp"/> <!-- destination table name --> <param name="dst_table_name" class="string">'tbl_collect_memory_usage'</param> </dataset> </report_def>
上面模板中有一个 innerfield字段,这个字段是一个临时字段,其结果并不会写入到结果数据,但会作为中间的结果计算出来,类似于我们写函数时定义了一个临时的变量;
上面的innerfield字段的表达式里使用了一个split方法,这个是模板内置的一个方法,它会将第一个参数即原始字段data_list按第二个参数即空格分隔成一个个的子串;
比如上面的data_list记录的是通过telnet执行命令cat /proc/meminfo取到的linux服务器的内存使用情况,内容如前一行注释所示,分隔出来的子串可以通过get方法来通过下标取得;
因此计算memory_usage即内存使用率的字段时,我们通过将total - free - buffers - cached / total 来计算实际的内存使用情况
<?xml version="1.0" encoding="utf-8"?> <report_def> <dataset name="swrunmem_usage_collect_report_tmp"> <source> <sql> <![CDATA[ SELECT dev_id,collect_time FROM tbl_origin_perf_data WHERE (task_id between 11 and 12) AND collect_time >= $P{begin_time_offset} AND collect_time < $P{end_time} order by collect_time; ]]> </sql> </source> <!--task11: hrSWRunName,hrSWRunStatus,hrSWRunPerfcpu,hrSWRunPerfMem,totalcputime --> <!--task12: User,cpuusage,Memusage,UserNum,cpunum --> <innerfield name="data" class="long"><![CDATA[split((source.$F{data_list}+",1"),",")]]></innerfield> <innerfield name="task_id" class="long"><![CDATA[source.$F{task_id}]]></innerfield> <field name="dev_id" class="long"><![CDATA[source.$F{dev_id}]]></field> <field name="swrun_id" class="int"><![CDATA[source.$F{row_index}]]></field> <field name="procname" class="string"><![CDATA[get($F{data},0)]]></field> <field name="ctime" class="timestamp"><![CDATA[source.$F{collect_time}]]></field> <field name="memusage" class="string"> <case> <pred>source.$F{task_id} == 11</pred> <![CDATA[expr(get($F{data},3)/1024)]]> </case> <otherwise> <![CDATA[get($F{data},2)]]> </otherwise> </field> <field name="cpuusage" class="string"> <case> <pred>source.$F{task_id} == 11</pred> <case> <pred>get($F{data},2) == 0</pred> 0 </case> <otherwise> <![CDATA[expr(delta(get($F{data},2))*100/delta(get($F{data},4)))]]> </otherwise> </case> <otherwise> <![CDATA[expr(get($F{data},1)/get($F{data},4))]]> </otherwise> </field> <field name="procnum" class="int"> <case> <pred>source.$F{task_id} == 11</pred> <![CDATA[get($F{data},5)]]> </case> <otherwise> <![CDATA[get($F{data},3)]]> </otherwise> </field> <group>$F{dev_id}</group> <group>$F{swrun_id}</group> <group>$F{task_id}</group> <!-- discard invalid data --> <filter><![CDATA[($F{ctime} >= $P{begin_time}) || ($F{cpuusage} != 'INF')]]></filter> <!-- collect time period --> <param name="begin_time" class="timestamp"/> <param name="end_time" class="timestamp"/> <!-- select last old data to calc first record --> <param name="begin_time_offset" class="string">minioffset($P{begin_time},"-30")</param> <!-- save each line data when group --> <param name="show_group_detail" class="string">true</param> <!-- destination table name --> <param name="dst_table_name" class="string">'tbl_collect_process_runstatus_tmp'</param> </dataset>
上面的模板中,同时对保存的两种格式的进程性能数据信息进行了统计,两种数据类型的格式如注释所示,分别是由任务11和任务12采集过来的数据,对应到snmp采集和telnet采集两种方式。
两种类型数据的统计方法我们使用case标签来作区分,对任务11采集的进程数据按snmp格式进行解析,对任务12采集到的进程数据按telnet格式进行解析,如果有更多的情况可以继续使用多个case标签作区分,每个case标签通过pred标签来定义它自己的条件,pred里可以是任意的布尔表达式。
最后统计的结果可以通过由filter标签指定的过滤条件进行过滤,这里将丢弃掉时间范围错误或者除法出现溢出的无效行数据
另外模板内定义的参数也可以引用其它参数并进行计算,比如begin_time_offset参数的值是将begin_time的值往前减掉30分钟,使用到了内置时间函数minioffset;
delta支持对同一分组内前后两行数据进行差值运算,例子中,对于进程的cpu使用率的计算,必须将前后两个采集数据相减后才能得到正确的结果。