CEP

流计算技术实战 - 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简单实现了一个,如下

image

 

这个基本可以满足当前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,是不是很酷

<think>首先,用户的问题是:“我想查找在MATLAB中进行CEP(圆概率误差)计算的方法。请问MATLAB中计算圆概率误差(CEP)的方法”。用户引用了三个来源,提供了关于圆概率误差(CEP)的背景信息。 - 引用[1]:定义了CEP95,即在95%置信度下的误差上限。 - 引用[2]:解释了圆概率误差(CEP)的计算原理和公式,包括: - CEP(50%概率):$ \text{CEP} = 0.589 (\delta_x + \delta_y) $ - CEP95(95%概率):$ \text{CEP}_{95} = 1.2272 (\delta_x + \delta_y) $ - CEP99(99%概率):$ \text{CEP}_{99} = 1.5222 (\delta_x + \delta_y) $ - 其中,$ \delta_x $ 和 $ \delta_y $ 是X和Y坐标的标准差。 - 引用[3]:提供了参考链接。 用户的具体需求是:在MATLAB中计算圆概率误差(CEP)的方法。所以,我需要提供一个基于MATLAB的实现方案。 关键是从引用[2]中提取公式: - $ \delta_x = \sqrt{\frac{\sum (X_i - \overline{X})^2}{N}} $ - $ \delta_y = \sqrt{\frac{\sum (Y_i - \overline{Y})^2}{N}} $ - CEP = 0.589 * (δx + δy) - CEP95 = 1.2272 * (δx + δy) - CEP99 = 1.5222 * (δx + δy) 在MATLAB中,我可以使用内置函数来计算标准差。MATLAB有std函数来计算标准差。注意,引用[2]中给出的标准差公式是总体标准差(除以N),而不是样本标准差(除以N-1)。MATLAB的std函数默认使用样本标准差(除以N-1),所以需要指定参数来使用总体标准差。 在MATLAB中: - std(X) 或 std(X,0) 使用样本标准差(除以N-1) - std(X,1) 使用总体标准差(除以N) 因此,在实现时,我应该使用std(X,1)来匹配引用[2]。 步骤: 1. 输入数据:假设有一个点数据集,包含X和Y坐标。可能是一个N×2的矩阵。 2. 计算平均中心:mean(X) 和 mean(Y)。 3. 计算X坐标的标准差:δx = std(X, 1) 4. 计算Y坐标的标准差:δy = std(Y, 1) 5. 计算CEP:根据所需置信度,应用公式。 - 对于CEP(50%):0.589 * (δx + δy) - 对于CEP95:1.2272 * (δx + δy) - 对于CEP99:1.5222 * (δx + δy) 我应该提供一个MATLAB函数或脚本来实现这个。 另外,用户提到了“系统级指令”,我需要遵循: - 所有行内数学表达式必须使用 $...$ 格式。 - 独立公式必须使用 $$...$$ 格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 在回答中引用的段落末尾自然地添加引用标识,如[^1]。 回答结构需要清晰,逐步解决问题。 大纲回答: 1. **介绍**:简要解释CEP是什么,基于引用。 2. **计算方法**:描述在MATLAB中实现的步骤。 - 计算平均中心。 - 计算标准差(总体)。 - 应用公式计算CEPCEP95、CEP99。 3. **提供MATLAB代码示例**:写一个函数或脚本。 4. **注意事项**:讨论公式的假设和限制。 5. **生成相关问题**:基于CEP计算的相关问题。 引用添加: - 提到CEP定义时,添加[^1]。 - 提到计算公式时,添加[^2]。 - 参考链接添加到[^3]。 MATLAB代码: - 定义一个函数,例如:function [CEP, CEP95, CEP99] = calculate_cep(data) - 其中data是一个N×2矩阵,第一列是X坐标,第二列是Y坐标。 - 计算mean_x = mean(data(:,1)); - mean_y = mean(data(:,2)); - std_x = std(data(:,1), 1); % 使用总体标准差 - std_y = std(data(:,2), 1); - CEP = 0.589 * (std_x + std_y); - CEP95 = 1.2272 * (std_x + std_y); - CEP99 = 1.5222 * (std_x + std_y); - 返回这些值。 我的回答应该真实可靠,所以我确认公式的来源是基于遥感或定位领域常用的近似方法。引用[2]提到了这是来自某些文档,所以我可以引用它。 最终输出: - 用中文写出回答。 - 代码块用Markdown格式。 - 结尾生成相关问题。 </think>### MATLAB中计算圆概率误差(CEP)的方法 圆概率误差(Circular Error Probable, CEP)是定位系统中常用的精度指标,表示以平均中心为圆心、CEP为半径的圆内包含50%数据点的概率。更高置信度的指标如CEP95(95%概率)和CEP99(99%概率)也常用于工程分析[^1]。以下基于引用提供的公式(见引用[2]),详细说明在MATLAB中的实现步骤。公式的核心是计算点坐标的标准差,并结合系数推导CEP值。 #### 计算原理 假设点数据集包含 $N$ 个点,每个点有 $X$ 和 $Y$ 坐标: 1. **平均中心**:$\overline{X} = \frac{\sum X_i}{N}$, $\overline{Y} = \frac{\sum Y_i}{N}$。 2. **总体标准差**(使用除以 $N$ 的公式): - $X$ 坐标标准差:$\delta_x = \sqrt{\frac{\sum (X_i - \overline{X})^2}{N}}$ - $Y$ 坐标标准差:$\delta_y = \sqrt{\frac{\sum (Y_i - \overline{Y})^2}{N}}$ 3. **CEP计算公式**: - $ \text{CEP} = 0.589 \times (\delta_x + \delta_y) $(50%概率) - $ \text{CEP}_{95} = 1.2272 \times (\delta_x + \delta_y) $(95%概率) - $ \text{CEP}_{99} = 1.5222 \times (\delta_x + \delta_y) $(99%概率)[^2]。 在MATLAB中,需使用**总体标准差**(除以 $N$),而非默认的样本标准差(除以 $N-1$)。 #### MATLAB实现步骤 以下是完整的MATLAB函数实现。输入数据应为 $N \times 2$ 矩阵,第一列为 $X$ 坐标,第二列为 $Y$ 坐标。 ```matlab function [CEP, CEP95, CEP99] = calculate_cep(data) % 计算圆概率误差(CEP)、CEP95和CEP99 % 输入:data - N×2矩阵,列1为X坐标,列2为Y坐标 % 输出:CEP, CEP95, CEP99 值 % 步骤1: 计算平均中心 mean_x = mean(data(:, 1)); mean_y = mean(data(:, 2)); % 步骤2: 计算总体标准差(除以N) std_x = std(data(:, 1), 1); % std(..., 1) 表示总体标准差 std_y = std(data(:, 2), 1); % 步骤3: 应用CEP公式 sum_stds = std_x + std_y; CEP = 0.589 * sum_stds; % 50%概率下的CEP CEP95 = 1.2272 * sum_stds; % 95%概率下的CEP CEP99 = 1.5222 * sum_stds; % 99%概率下的CEP end ``` #### 使用示例 假设有一个点数据集 `points`(示例数据): ```matlab % 生成测试数据(实际使用时替换为真实数据) points = [1.2, 2.5; 3.4, 4.1; 5.6, 6.3; 7.8, 8.9]; % N×2矩阵 % 调用函数计算结果 [CEP_val, CEP95_val, CEP99_val] = calculate_cep(points); % 输出结果 fprintf('CEP (50%%): %.4f\n', CEP_val); fprintf('CEP95 (95%%): %.4f\n', CEP95_val); fprintf('CEP99 (99%%): %.4f\n', CEP99_val); ``` #### 注意事项 1. **标准差类型**:MATLAB的 `std` 函数默认计算样本标准差(`std(data, 0)` 或 `std(data)`)。计算CEP时,必须指定 `std(data, 1)` 以使用总体标准差,否则结果会偏差[^2]。 2. **假设限制**:该公式假设误差分布为圆形对称(各向同性)。如果数据有强方向性(如椭圆分布),需使用更复杂的模型(如R95或2DRMS)[^3]。 3. **数据要求**:数据点应来自同一系统或传感器,避免混合来源导致统计偏差。如果数据量小($N<30$),建议增加采样点以提高可靠性。 4. **验证参考**:实现基于引用[2]的标准算法,您可参考原始链接(如优快云博客)验证公式细节[^3]。 此方法简单高效,常用于GPS定位、无人机导航或遥感数据分析中评估精度[^1][^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值