大流量场景下统计问题整理
前言
电视剧《恰同学少年》中,给我印象最深的就是杨昌济先生晨读之前大声朗读的两句话:
“杨昌济,时光易逝,汝当惜之;先贤至理,汝当常忆。”
人生苦短,汝当惜之。向圣贤学习,时常警惕、自审,整理今日所获如下。
背景
流量分析过程中,有一个定时统计任务,对网络请求数据中的源IP、目的IP、访问总量进行统计,得到每个源IP,在一天中的各个小时访问的目的IP、及访问这些目的IP的总量。统计结果作为为一条记录,存入Sequoiadb数据库中。统计结果的实体定义如下:
public class SourceIpVisitSum {
private String _id;
//统计时间
private String date;
//统计类型
private String type;
//源IP访问数据topN
private List<SourceVisitInfo> topN;
}
public class SourceVisitInfo implements Comparable<SourceVisitInfo>{
/**
* 源IP
*/
private String srcIp;
/**
* 访问总流量数
*/
private Long trafficTotal;
/**
* 该源IP访问的目的IP集合:不存储xml,实时生成xml
*/
private List<String> destIps;
/**
* 该源IP访问过的目的IP的统计结果
*/
private List<BSONObject> visitServers;
/**
* 该该源IP户当天的访问趋势
*/
private List<TrafficTrendData> trendData;
/**
* 日汇总的数据:每天的这个属性,汇集就是周、月的数据了
*/
private TrafficTrendData dailyTotal;
}
public class TrafficTrendData {
//统计时间:小时或者天
private String time;
//该时间段内的流量总数
private Long trafficTotal;
}
抽象这三个实体,统计一天的流量数据中各个源IP的访问趋势情况,以JSON格式存储入库。业务需求实时展示访问情况,所以开启了一个每隔5分钟统计一次的Quartz定时任务,页面定时获取最新的统计结果进行展示。
大流量场景下统计异常
开发过程中进行过测试,随机插入700万条的记录,源IP,目的IP总量不过40个,做一次实时统计操作耗时一分钟,没有出现过定时任务积压的情况。
但是,在真实流量数据场景下,500万条数据统计过程,耗时40分钟。统计结果显示有一万多个源IP,一千多个目的IP,访问关系排列总数较多,统计得到的SourceIpVisitSum对象的JSON数据大小达到了34M。而Sequoiadb单条插入的BSON大小限制为16M。
最直接的结果就是,这个每5分钟一次的统计任务,真正耗时一个多小时,而且统计得到结果准备入库的时候,插入异常,log4j记录下大量的异常日志,其中主要的就是这个JSON参数信息。今天看昨天的log4j的日志文件,从18:02开始第一次定时任务,到今天凌晨,日志文件达到了348M。
而今天凌晨开始的产生的日志文件更是夸张,日志大小达到了2.7G。
而今天的日志文件,EditPlus根本打不开,用了一个大文件工具查看,全是这个统计结果的JSON信息。再加上其他的异常信息,累计就到了这个级别了。还真吓倒我了,从来没有见到过这么大的日志量。
定时任务并行问题
由于开始用的是Quartz的并行定时任务,每隔5分钟一次,但是一次耗时40分钟的任务,导致Quartzs的工作线程很快就被这一个类型的定时任务占满了,其他正常任务就无法执行了。
解决办法,将这个定时任务设置成不能并行的,必须等上一次任务执行完成后再进行下一次任务。其实Quartz提供了StatefulJob接口,它被废弃了,被注解@DisallowConcurrentExecution替代。只要这样一行代码就可以让它们顺次执行了。
但是,我用了一个笨方法,用Java的线程池的scheduleAtFixedRate方法,多写了至少30行代码,这点,我不能释怀,脑子再灵活一点搜一下Quartz的非并行任务就好了。
统计伴随的高CPU消耗
再说说这个统计过程吧,由于Sequoiadb的聚集操作相当耗时,所以采用的是逐条遍历原始流量数据,然后用Java的Map和List进行统计处理方式。
现实场景下,源和目的IP量大且组合总量众多,统计操作里面的Map和List将需要频繁扩容,而我初始化的Map的容量是128,而真实的源IP个数为一万多个,那么这么Map的扩容会相当频繁;其次,就是每个源IP的目的IP情况,初始化为64,而真实的目的IP也有一千多个,那么也会频繁扩容。
这样就出现了一种现象:每次该任务执行时,过一会就开始出现好几个高CPU消耗的线程,而且线程ID相互挨着:
打出堆栈日志,除了第一个是这个统计任务线程,其他高CPU消耗的线程都是JVM的GC线程。
降低GC消耗,采取的方式是:修改JVM参数,因为原来的tomcat的启动参数配置的xmx为512M,太小了,目标服务器的内存是32G,所以这可能是导致GC频繁的原因。稍微调大到2G后,这种现象消失。
JAVA_OPTS=”-server -Xms512m -Xmx2048m -XX:PermSize=512M -XX:MaxPermSize=512m”
启示录
那个2.7G的日志文件,VPN传送了一上午只传了76%就终止了。用打文件打开工具,逐行滚动,查看它的内容,结果发现,全是统计JSON,终于知道这个大日志是怎么回事了。
这样一个大文件,直接用vi打开,黑屏上全是闪烁的01形象,看得人眼晕。还好,我够有耐心,混迹于1048576的世界里,我依旧只有一个感受:编程路漫漫,我还在路上。
备注:偶然发现1048576,这个数字串是如此的有趣,1024*1024=1024K。
推荐阅读