事件生成JOB调优笔记(spark streaming)

来源:互联网 发布:java解析json数组 编辑:程序博客网 时间:2024/06/05 08:33

说明:

1.Spark Streaming Application资源配置executor-number:3,executor-memory:2g,driver-memory:2g(因为是我们自研平台上运行的APP的Application,所以资源不能申请太多)

2.时间窗口:5min


业务场景:

有三种原始日志,其中一种是告警日志取名为evt(用event_id和device_ip标记为唯一的一条,会不断发送相同的告警,但end_time、severity会变化),另外有两种流量日志取名为evt_srcip_traffic和evt_traffic_total分别表示该告警中关联的源IP的流量和告警流量。


事件生成要做的事情很简单:
1.两种流量日志需要归并(比如evt_traffic_total相同event_id和device_ip的同一天的需要合并为一条,流量之类的需要取总值和峰值)
2.需要生成事件和事件详情,可以理解为:
evt表
event_id device_ip stime etime 
1        1.1.1.1 2016-08-04 00:00:00 2016-08-04 01:00:00
1        1.1.1.1 2016-08-04 00:00:00 2016-08-04 02:00:00
1        1.1.1.1 2016-08-04 00:00:00 2016-08-04 03:00:00


evt_traffic_total
event_id device_ip bytes packets
1        1.1.1.1   1       1
1        1.1.1.1   2       2


evt_srcip_traffic
event_id device_ip sip         bytes packets
1        1.1.1.1   1.1.1.2     1       1
1        1.1.1.1   1.1.1.3     2       2


最后生成的事件是:
event_id device_ip stime               etime                 tbytes tpackets mbytes mpackets
1      1.1.1.11   2016-08-04 00:00:00  2016-08-04 03:00:00   3       3       2       2
生成的事件详情是:
event_id device_ip  sip          stime                etime                 tbytes tpackets mbytes mpackets
1        1.1.1.1    1.1.1.2      2016-08-04 00:00:00  2016-08-04 03:00:00   3       3       2       2
1        1.1.1.1    1.1.1.3      2016-08-04 00:00:00  2016-08-04 03:00:00   3       3       2       2


两种日流量归并:
total_merge
event_id device_ip stat_time tbytes tpackets mbytes mpackets
1        1.1.1.1  2016-08-04 3     3        2      2


sip_merge
event_id device_ip sip          stat_time tbytes tpackets mbytes mpackets
1        1.1.1.1   1.1.1.2      2016-08-04 3     3        2      2
1        1.1.1.1   1.1.1.3      2016-08-04 3     3        2      2


前提:
数据全都是从kafka读取的,16个partition,并行读的好处是快,当然可以理解为接到的顺序是无序的了


业务特征:
1.收到的数据日流量表数据可以直接单表生成,而事件表和事件详情表的数据则需要两种日志进行补齐字段
2.每一批数据都会触发四张表的插入(全新数据)、更新(已有数据的结束时间、流量等字段更新)


难点:
1.如何知道哪些数据需要更新,哪些数据需要插入
2.插入的数据用copy很快,但更新的数据update就算是batch update也非常慢(表上有4个左右索引,为了前台的查询)
tips:pg的更新有点小变态,它的MVCC(多版本控制),所以每update一次(即使是非索引字段)它也会新建一个新行,并把老行标记为删除状态;然后如果新行和老行在一个数据块里,则索引无需更新(HOT更新);如果新行和老行不在一个数据块里,则索引需要更新,造成速度比较慢


已验证存在缺憾的方法:
1.每收到一批数据,首先用它拼接where语句,然后去pg库查询已有的数据,然后取出所有已有的数据和内存中的数据一起在内存里更新,关联不上的(新增的)则在内存里加入,最后delete需要更新的数据,将“插入+更新”的数据copy到表里
结论:a.sql是有长度限制的,数据量大的情况下sql长度超过限制,那么只能改为500条一批的查询,大大影响查询效率 b.所有操作都在driver上进行,分分钟out of memory c.索引多delete也慢,所以用delete+copy取代update+copy一点也没有看到优势


2.先用sqlcontext把pg的表load为dataframe,再用内存的dataframe,两个dataframe都注册为表,用内存的表左关联pg的dataframe的表,如果右表没有字段为null,则表明是需要更新的记录,反之则为需要插入的记录;更新记录作为dataframe1,插入的记录作为datafram2,两个dataframe在mappartitions里做批量的更新和copy
结论:1.sqlcontext直接load全表很慢,它并不能够智能的根据后一步左关联只取一部分数据 2.batch update很慢
优点:所有操作都在executor上进行了,且不会把数据全部屯在内存,不会再出现out of memory了


最优解法:
1.将内存中的dataframe保存为pg中的表,用tmp_开头
示例:
String tbRandomName = Constant.DBNAME + "."+ "tmp_" + java.util.UUID.randomUUID().toString().replace("-", "_");
Properties props = new Properties();
props.put("user", username);
props.put("password", password);
evtTrafficTotalDayMerge.write().jdbc(String.format("jdbc:postgresql://%s:%s/%s", ip, port, database), tbRandomName, props);


2.利用sqlcontext jdbc读取左关联的表,得到需要更新的dataframe和需要插入的dataframe 
示例:
Map<String,String> options1 = new HashMap<String,String>();
options1.put("url",String.format("jdbc:postgresql://%s:%s/%s?user=%s&password=%s",ip,port,database,username,password));
options1.put("dbtable","(select xxx from tmp_table left join evt on xxxx.... where evt.xx is not null) as temp");
DataFrame relateUpdateDf = sqlContext.read().format("jdbc").options(options1).load();


3.需要更新的dataframe mapparittions,将它存为临时表;然后使用update xxx set xxx from 临时表 where xxx进行更新


4.需要插入的dataframe mappartitions,将它写成文件,然后copy到表中


tips:
1.四张表都属于频繁更新表,故fillfactor要设小一些,方便HOT更新
2.update xxx set xxx from 临时表 where xxx进行更新,此方法是最快的更新方式,比batch update还要快
3.“千万不要相信先删除索引,再插入数据,再重建索引”这样的话,大数据量表上重建索引的速度醉人
4.每一张表做完update+copy后,运行vacuum analyze释放空间以及更新执行规划----当然,请另起一个线程做这个事情:)


速度那么就妥妥的稳过4000+/s了:


另外,以前dataframe都是转成javaRDD再做mapPartitions的,其实dataframe可以直接做mappartitons,写法如下:

1.AbstractFunction没有序列化,所以需要自己写个Function继承AbstractFunction并且implements Serializable

例如:

public abstract class MyFunction1<T1,T2> extends scala.runtime.AbstractFunction1<scala.collection.Iterator<Row>, scala.collection.Iterator<Row>> implements Serializable{
}

2.dataframe mappartition

scala.reflect.ClassTag<org.apache.spark.sql.Row> curClassTag = scala.reflect.ClassTag$.MODULE$.apply(org.apache.spark.sql.Row.class);
        relateUpdateDf.mapPartitions(new MyFunction1<scala.collection.Iterator<Row>, scala.collection.Iterator<Row>>() {
            public scala.collection.Iterator<Row> apply(scala.collection.Iterator<Row> v1) {

............


更加详尽的测试结果:

构造的数据为:
以0.5的概率发送旧数据---触发两张日归并表、事件表、事件详情表更新
以0.5的概率发送新数据---每5分钟触发10万不重复事件id与30万不重复源IP流量,触发两张日归并表、事件表、事件详情表写入
原始日志量为120万/5分钟

一个小时的监控截图:


从图中可以看出,除了19:20:00左右,yarn出现卡顿现象时,有两个task分别高达4.1和8.6分钟,速度降为2400~4000/s时。

其余的时间均为4000+/s,比较平稳。


1 0
原创粉丝点击