流计算技术实战 - CEP
CEP,Complex event processing
Wiki定义
“Complex event processing, or CEP, is event processing that combines data from multiple sources[2] to infer events or patterns that suggest more complicated circumstances. The goal of complex event processing is to identify meaningful events (such as opportunities or threats)[3] and respond to them as quickly as possible.”
通过上面的Wiki定义,可以看出CEP的特点主要是,
复杂性:多个流join,窗口聚合,事件序列或patterns检测
低延迟:秒或毫秒级别,比如做信用卡盗刷检测,或攻击检测
高吞吐:每秒上万条消息
CEP和数据库
CEP的概念出现比较早,用于解决传统数据库所无法解决的实时需求
传统数据库,数据是静态的,查询是动态的,但做不到实时和连续的输出查询结果
而CEP反其道而行之,查询是静态的,数据是动态的,这样就可以满足实现和连续查询的需求,但是无法满足ad hoc查询需求
所以CEP和传统数据库相结合,可以用于解决金融,商业,网络监控等领域的问题
比如比较知名的Esper,功能非常强大,并提供EPL这样类sql语言,让用户感觉到类似使用数据库的体验
流计算下的CEP
流式计算概念可以认为是从Storm或Yahoo S4那个时候开始被大家广泛接受的
流式计算概念的出现,主要针对当时主流的像Hadoop这样的MapReduce系统在实时性上的缺陷;时势造英雄,加上像Twitter这样普及的实时应用,让大家认识到数据实时性的重要性,从此实时大数据的时代渐渐来临
CEP和流式计算是在不同的时代背景下产生的,而由于他们所要解决问题域的重合,注定了在技术上会产生融合;
在Storm的年代,Storm主要是封装和提供一种类似MapReduce的编程模型,所以当时流式计算业务主要还是ETL和简单聚合;
为了满足CEP需求,可以将Esper引擎跑在Storm上,但是Esper虽然功能很强大,但是实在太重而且比较低效
后续出现轻量级的CEP引擎,如Siddhi,
但我们最终也没有规模使用,最主要的原因是,它没有考虑event time和数据乱序的问题,比较难于用于实际的线上场景
在Dataflow论文出来前,确实没有任何计算平台,在平台层面对event time和数据乱序提出系统的方案,Flink实现了Dataflow中的窗口模型,在平台层面解决了event time和数据乱序的问题
并且Flink提供了专门的CEP的lib,FlinkCEP - Complex event processing for Flink
当然这个CEP lib是会考虑并解决event time和数据乱序问题的
下面我们先来看看Flink CEP是怎么使用的
Flink CEP
Example
我们先产生一个输入流,这个输入Event流由Event对象和event time组成
那么要使用EventTime,除了指定TimeCharacteristic外,在Flink中还要assignTimestampsAndWatermarks,其中分别定义了Eventtime和WaterMark,
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
// (Event, timestamp)
DataStream<Event> input = env.fromElements(
Tuple2.of(new Event(1, "start", 1.0), 5L),
Tuple2.of(new Event(2, "middle", 2.0), 1L),
Tuple2.of(new Event(3, "end", 3.0), 3L),
Tuple2.of(new Event(4, "end", 4.0), 10L), //触发2,3,1
Tuple2.of(new Event(5, "middle", 5.0), 7L),
// last element for high final watermark
Tuple2.of(new Event(5, "middle", 5.0), 100L) //触发5,4
).assignTimestampsAndWatermarks(new AssignerWithPunctuatedWatermarks<Tuple2<Event, Long>>() {
@Override
public long extractTimestamp(Tuple2<Event, Long> element, long previousTimestamp) {
return element.f1; //定义Eventtime
}
@Override
public Watermark checkAndGetNextWatermark(Tuple2<Event, Long> lastElement, long extractedTimestamp) {
return new Watermark(lastElement.f1 - 5); //定义watermark
}
}).map(new MapFunction<Tuple2<Event, Long>, Event>() {
@Override
public Event map(Tuple2<Event, Long> value) throws Exception {
return value.f0;
}
});
接着我们定义需要匹配的pattern,需求就是找出包含”start“, ”middle“, ”end“的一组事件
具体语法参考Flink文档,这里不详述
Pattern<Event, ?> pattern = Pattern.<Event>begin("start").where(new SimpleCondition<Event>() {
@Override
public boolean filter(Event value) throws Exception {
return value.getName().equals("start");
}
}).followedByAny("middle").where(new SimpleCondition<Event>() {
@Override
public boolean filter(Event value) throws Exception {
return value.getName().equals("middle");
}
}).followedByAny("end").where(new SimpleCondition<Event>() {
@Override
public boolean filter(Event value) throws Exception {
return value.getName().equals("end");
}
});
最终在输入流上执行CEP,
这里实现PatternSelectFunction来处理匹配到的pattern,处理逻辑是打印出匹配到的3个Event对象的id
DataStream<String> result = CEP.pattern(input, pattern).select(
new PatternSelectFunction<Event, String>() {
@Override
public String select(Map<String, List<Event>> pattern) {
StringBuilder builder = new StringBuilder();
System.out.println(pattern);
builder.append(pattern.get("start").get(0).getId()).append(",")
.append(pattern.get("middle").get(0).getId()).append(",")
.append(pattern.get("end").get(0).getId());
return builder.toString();
}
}
);
result.print();
大家想想,这里匹配到的是哪些Event?
从上面Event的顺序看应该是 1,2,3
但结果是 1,5,4,因为这里考虑的是Eventtime的顺序,这个特性在生产环境中很关键,因为我们无法保证采集数据达到的顺序。
Implementation
对于EventTime部分的实现,可以看下AbstractKeyedCEPPatternOperator中的实现,
public void processElement(StreamRecord<IN> element) throws Exception {
if (isProcessingTime) {
// there can be no out of order elements in processing time
NFA<IN> nfa = getNFA();
processEvent(nfa, element.getValue(), getProcessingTimeService().getCurrentProcessingTime());
updateNFA(nfa);
} else { //EventTime
long timestamp = element.getTimestamp();
IN value = element.getValue();
if (timestamp >= lastWatermark) { //只处理非late record
// we have an event with a valid timestamp, so
// we buffer it until we receive the proper watermark.
saveRegisterWatermarkTimer();
List<IN> elementsForTimestamp = elementQueueState.get(timestamp);
elementsForTimestamp.add(element.getValue());
elementQueueState.put(timestamp, elementsForTimestamp); //放到队列中
}
}
}
如果是EventTime,不会直接processEvent并更新NFA,而是先放到一个队列elementQueueState里面。
等后面收到watermark触发onEventTime时,
会把队列里面的数据按时间排序,从小到大,并把大于watermark的拿出来挨个处理,这样就实现了按EventTime有序,解决了乱序问题。
Improvement
应用中实际使用Flink CEP时,发现有些不方便的地方:
首先,patterns需要用java代码写,需要编译,很冗长很麻烦,没法动态配置;需要可配置,或提供一种DSL
再者,对于一个流同时只能设置一个pattern,比如对于不同的用户实例想配置不同的pattern,就没法支持;需要支持按key设置pattern
DSL
对于第一个问题,我刚开始考虑开发一套DSL,这样成本比较高,而且社区也在考虑支持SQL
所以我就先基于JSON简单实现了一个,如下
这个基本可以满足当前Flink CEP的常用语法,扩展也比简单
通过一个JSONArray来表示一个pattern sequence,每个pattern中可以定义多个并,或条件
每个条件由三部分组成,比如,["sql", "contains", "delete"], "sql"是字段名,”contains“是Op,”delete“是value, 意思就是找出sql字段中包含delete的log
现在就不需要用java来写pattern了,直接传入配置就ok,如下,
JSONArray jsonArray = JSON.parseArray("pattern配置");
CepBuilder<Log> cepBuilder = new CepBuilder<Log>();
Pattern<Log, ?> pattern = cepBuilder.patternSequenceBuilder(jsonArray);
这里我实现一个CepBuilder可以把JSON配置直接转换成Pattern对象
按Key配置多patterns
为了满足为不同的用户配置不同的pattern的需求,我修改了下Flink CEP提供的接口,
原先Flink CEP,是这样定义CEP的,
PatternStream = CEP.pattern(input, pattern)
可以看到对一个input只能定义一个pattern,
所以我定义GroupPatternStream,可以传入一组patterns
public class GroupPatternStream<K, T> {
// underlying data stream
private final DataStream<T> inputStream;
private final Map<K, Pattern<T, ?>> patterns;
GroupPatternStream(final DataStream<T> inputStream, final Map<K, Pattern<T, ?>> patterns) {
this.inputStream = inputStream;
this.patterns = patterns;
}
然后在createPatternStream逻辑中,把每个pattern compile成相应的NFAFactory,最终将nfaFactoryMap作为参数创建KeyedCEPGroupPatternOperator
public SingleOutputStreamOperator<Map<String, List<T>>> createPatternStream(DataStream<T> inputStream, Map<K, Pattern<T, ?>> patterns) {
final TypeSerializer<T> inputSerializer = inputStream.getType().createSerializer(inputStream.getExecutionConfig());
Map<K, NFACompiler.NFAFactory<T>> nfaFactoryMap = new HashMap<>();
if(patterns != null){
for(K key: patterns.keySet()){
Pattern<T, ?> pattern = patterns.get(key);
nfaFactoryMap.put(key, NFACompiler.compileFactory(pattern, inputSerializer, false));
}
}
if (inputStream instanceof KeyedStream) {
patternStream = keyedStream.transform(
"KeyedCEPPatternOperator",
(TypeInformation<Map<String, List<T>>>) (TypeInformation<?>) TypeExtractor.getForClass(Map.class),
new KeyedCEPGroupPatternOperator<>(
inputSerializer,
isProcessingTime,
keySerializer,
nfaFactory,
nfaFactoryMap,
true));
} else {
//not-support non-keyed stream
patternStream = null;
}
return patternStream;
}
KeyedCEPGroupPatternOperator,也是我新建的,和原来的KeyedCEPPatternOperator比多了个参数nfaFactoryMap,并且重写了getNFA函数
public class KeyedCEPGroupPatternOperator<IN, KEY> extends KeyedCEPPatternOperator {
Map<KEY, NFACompiler.NFAFactory<IN>> nfaFactoryMap;
public KeyedCEPGroupPatternOperator( TypeSerializer<IN> inputSerializer,
boolean isProcessingTime,
TypeSerializer<KEY> keySerializer,
NFACompiler.NFAFactory<IN> nfaFactory,
Map<KEY, NFACompiler.NFAFactory<IN>> nfaFactoryMap,
boolean migratingFromOldKeyedOperator){
super(inputSerializer, isProcessingTime, keySerializer, nfaFactory,
migratingFromOldKeyedOperator);
this.nfaFactoryMap = nfaFactoryMap;
}
@Override
public NFA<IN> getNFA() throws IOException {
NFA<IN> nfa = (NFA<IN>) nfaOperatorState.value();
if(nfa == null) {
Object key = getCurrentKey();
NFACompiler.NFAFactory<IN> factory = nfaFactoryMap.get(key);
if(factory != null){
nfa = factory.createNFA();
}
//if the key didn't define pattern, add EmptyNFA
if(nfa == null){
nfa = new EmptyNFA<>();
}
}
return nfa;
}
}
核心逻辑就在getNFA, 主要就是通过修改这个逻辑来满足需求
在KeyedCEPPatternOperator中,他每次都会生成同样的NFA
public NFA<IN> getNFA() throws IOException {
NFA<IN> nfa = nfaOperatorState.value();
return nfa != null ? nfa : nfaFactory.createNFA();
}
而在我的逻辑里面,
会先取出当前上下文的key,
并根据不同的key,创建不同的NFA,这样就可以实现对不同的key使用不同的pattern进行匹配。这些NFA状态机是作为key的state存在stateBackend里面的,所以每次有相应的key的record流过时,都可以从stateBackend中取到。
然后我们就可以这样用,
先准备测试数据,
Log log = new Log();
log.putItem("id", "1");
log.putItem("sql", "start counting!");
logs.add(log);
log = new Log();
log.putItem("id", "2");
log.putItem("sql", "start counting!");
logs.add(log);
log = new Log();
log.putItem("id", "1");
log.putItem("sql", "end counting");
logs.add(log);
log = new Log();
log.putItem("id", "2");
log.putItem("sql", "select from 1");
logs.add(log);
log = new Log();
log.putItem("id", "2");
log.putItem("sql", "end counting");
logs.add(log);
DataStream<Log> input = env.fromCollection(logs).keyBy(new KeySelector<Log, String>() {
public String getKey(Log log){
return (String)log.getItem("id");
}
});
构造pattern,
JSONArray jsonArray = JSON.parseArray(
"[{"id":"start","conditions":[[["sql","contains","start"]]]},{"id":"middle","conditions":[[["sql","contains","end"]]]}]");
JSONArray jsonArray2 = JSON.parseArray(
"[{"id":"start","conditions":[[["sql","contains","start"]]]},{"id":"middle","conditions":[[["sql","contains","select"]]]},{"id":"end","conditions":[[["sql","contains","end"]]]}]");
CepBuilder<Log> cepBuilder = new CepBuilder<Log>();
Pattern<Log, ?> pattern = cepBuilder.patternSequenceBuilder(jsonArray);
Pattern<Log, ?> pattern2 = cepBuilder.patternSequenceBuilder(jsonArray2);
Map<String, Pattern<Log, ?>> patternMap = new HashedMap();
patternMap.put("1", pattern);
patternMap.put("2", pattern2);
对于id=”1“的log,找出包含”start“,”end“的pattern
对于id=”2“的log,找出包含”start“,”select“,”end“的pattern
运行CEP,
GroupPatternStream<String, Log> groupPatternStream = new GroupPatternStream<>(input, patternMap);
DataStream<String> result =groupPatternStream.select(
new PatternSelectFunction<Log, String>() {
return pattern.toString();
}
});
result.print();
得到运行结果,
2> {middle=[{id=2, sql=select from 1}], start=[{id=2, sql=start counting!}], end=[{id=2, sql=end counting}]}
4> {middle=[{id=1, sql=end counting}], start=[{id=1, sql=start counting!}]}
可以看到对于不同的key,匹配到了不同的pattern,是不是很酷
流计算技术实战 - CEP的更多相关文章
- 从flink-example分析flink组件(3)WordCount 流式实战及源码分析
前面介绍了批量处理的WorkCount是如何执行的 <从flink-example分析flink组件(1)WordCount batch实战及源码分析> <从flink-exampl ...
- 从小白到架构师(4): Feed 流系统实战
「从小白到架构师」系列努力以浅显易懂.图文并茂的方式向各位读者朋友介绍 WEB 服务端从单体架构到今天的大型分布式系统.微服务架构的演进历程.读了三篇万字长文之后各位想必已经累了(主要是我写累了), ...
- 大数据开发实战:Spark Streaming流计算开发
1.背景介绍 Storm以及离线数据平台的MapReduce和Hive构成了Hadoop生态对实时和离线数据处理的一套完整处理解决方案.除了此套解决方案之外,还有一种非常流行的而且完整的离线和 实时数 ...
- 实战限流(guava的RateLimiter)
关于限流 常用的限流算法有漏桶算法和令牌桶算法,guava的RateLimiter使用的是令牌桶算法,也就是以固定的频率向桶中放入令牌,例如一秒钟10枚令牌,实际业务在每次响应请求之前都从桶中获取令牌 ...
- 第一章-Flink介绍-《Fink原理、实战与性能优化》读书笔记
Flink介绍-<Fink原理.实战与性能优化>读书笔记 1.1 Apache Flink是什么? 在当代数据量激增的时代,各种业务场景都有大量的业务数据产生,对于这些不断产生的数据应该如 ...
- Storm流计算之项目篇(Storm+Kafka+HBase+Highcharts+JQuery,含3个完整实际项目)
1.1.课程的背景 Storm是什么? 为什么学习Storm? Storm是Twitter开源的分布式实时大数据处理框架,被业界称为实时版Hadoop. 随着越来越多的场景对Hadoop的MapRed ...
- flink入门实战总结
随着大数据技术在各行各业的广泛应用,要求能对海量数据进行实时处理的需求越来越多,同时数据处理的业务逻辑也越来越复杂,传统的批处理方式和早期的流式处理框架也越来越难以在延迟性.吞吐量.容错能力以及使用便 ...
- 《Java8 Stream编码实战》正式推出
当我第一次在项目代码中看到Stream流的时候,心里不由得骂了一句"傻X"炫什么技.当我开始尝试在代码中使用Stream时,不由得感叹真香. 记得以前有朋友聊天说,他在代码中用了 ...
- Flink项目实战(一)---核心概念及基本使用
前言.flink介绍: Apache Flink 是一个分布式处理引擎,用于在无界和有界数据流上进行有状态的计算.通过对时间精确控制以及状态化控制,Flink能够运行在任何处理无界流的应用中,同时对有 ...
随机推荐
- JAVA(三)JAVA常用类库/JAVA IO
成鹏致远 | lcw.cnblog.com |2014-02-01 JAVA常用类库 1.StringBuffer StringBuffer是使用缓冲区的,本身也是操作字符串的,但是与String类不 ...
- vs2015打开慢的解决方法
1.首先是这里,这里默认是用的软件加速,把"基于客户端性能自动调整视觉体验"去掉勾选.然后把下面的第一个选项去掉,第二选项勾选.我在想,它的"自动"基于 ...
- c++对c的加强
1.register关键字的加强 register修饰符暗示编译程序相应的变量将被频繁地使用,如果可能的话,应将其保存在CPU的寄存器中,以加快其存储速度,这只是一种请求,编译器可以拒绝这种申请. ( ...
- python 中的map,dict,lambda,reduce,filter
1.map(function,sequence) 对sequence 中的item依次执行function(item), 见执行结果组成一个List返回 例如: #!/usr/bin/python # ...
- Django 源码小剖: Django 中的 WSGI
Django 其内部已经自带了一个方便本地测试的小服务器, 所以在刚开始学习 Django 的时候并不需搭建 apache 或者 nginx 服务器. Django 自带的服务器基于 python w ...
- Java知多少(101)图像缓冲技术
当图像信息量较大,采用以上直接显示的方法,可能前面一部分显示后,显示后面一部分时,由于后面一部分还未从文件读出,使显示呈斑驳现象.为了提高显示效果,许多应用程序都采用图像缓冲技术,即先把图像完整装入内 ...
- MySQL视图小例子
场景: 某查询接口 查询sql语句已确定,用该sql语句去查 表 t_strategy_stock 中的数据,但是 表t_strategy_stock 的字段名称和 sql 语句中写死的名称不同. 需 ...
- flume 1.8 安装部署
环境 centos:7.2 JDK:1.8 Flume:1.8 一.Flume 安装 1) 下载 wget http://mirrors.tuna.tsinghua.edu.cn/apa ...
- python使用上下文对代码片段进行计时,非装饰器
之前发过了一组常用的装饰器,包括了一个where_is_it_called的装饰器,可以计时和对入参和返回结果,被何处调用进行记录,十分强大. 这是用上下文,上下文的好处是,不需要抽成函数才能计时. ...
- [Cubieboard] 镜像资源汇总
Linaro Server 14.04 (SDCard) 下载:cb2-lubuntu-server-tsd-tfcard-v2.0.img.gz 内核:GNU/Linux 3.4.79 armv7l ...