简单分析Flink算子返回NULL导致的异常

本文分析了Flink中当map算子返回NULL时可能导致的异常情况,探讨了异常产生的原因,主要是由于某些类型如Scala Option、Tuple和Case Class不支持NULL。解决方案包括使用None替代Option的NULL,对于Tuple和Case Class可设置变量为NULL并用flatMap替换map。文章还引用了相关参考链接以便深入理解。

假设我们作业中有这样一段逻辑stream.map(xxx).filter(_ != null).xxx,并且map算子有可能返回NULL,你觉得作业运行会抛NPE吗?明明下游有filter not null,不应该出错才对?但实际情况是运行中有可能抛出异常。

1.异常信息

可能抛出的异常信息大致如下:

// 1. 如果map算子返回值类型为Java Tuple
Caused by: java.lang.NullPointerException
    at org.apache.flink.api.java.typeutils.runtime.TupleSerializer.copy(TupleSerializer.java:111)
    at org.apache.flink.api.java.typeutils.runtime.TupleSerializer.copy(TupleSerializer.java:37)
    at org.apache.flink.streaming.runtime.tasks.OperatorChain$CopyingChainingOutput.pushToOperator(OperatorChain.java:635)
    at org.apache.flink.streaming.runtime.tasks.OperatorChain$CopyingChainingOutput.collect(OperatorChain.java:612)
    at org.apache.flink.streaming.runtime.tasks.OperatorChain$CopyingChainingOutput.collect(OperatorChain.java:592)
    at org.apache.flink.streaming.api.operators.AbstractStreamOperator$CountingOutput.collect(AbstractStreamOperator.java:727)
    at org.apache.flink.streaming.api.operators.AbstractStreamOperator$CountingOutput.collect(AbstractStreamOperator.java:705)
    at org.apache.flink.streaming.api.operators.StreamMap.processElement(StreamMap.java:41)
    ...
    at org.apache.flink.runtime.taskmanager.Task.run(Task.java:530)
    at java.lang.Thread.run(Thread.java:748)
// 2. 如果map算子返回值类型为Scala Case Class或Scala Tuple
Caused by: java.lang.NullPointerException
    at org.apache.flink.api.scala.typeutils.CaseClassSerializer.copy(CaseClassSerializer.scala:92)
    at org.apache.flink.api.scala.typeutils.CaseClassSerializer.copy(CaseClassSerializer.scala:32)
    at org.apache.flink.streaming.runtime.tasks.OperatorChain$CopyingChainingOutput.pushToOperator(OperatorChain.java:635)
    at org.apache.flink.streaming.runtime.tasks.OperatorChain$CopyingChainingOutput.collect(OperatorChain.java:612)
    at org.apache.flink.streaming.runtime.tasks.OperatorChain$CopyingChainingOutput.collect(OperatorChain.java:592)
    at org.apache.flink.streaming.api.operators.AbstractStreamOperator$CountingOutput.collect(AbstractStreamOperator.java:727)
    at org.apache.flink.streaming.api.operators.AbstractStreamOperator$CountingOutput.collect(AbstractStreamOperator.java:705)
    at org.apache.flink.streaming.api.operators.StreamMap.processElement(StreamMap.java:41)
    ...
    at org.apache.flink.runtime.taskmanager.Task.run(Task.java:530)
    at java.lang.Thread.run(Thread.java:748)
// 3. 如果map算子返回值类型为Scala Option
Caused by: scala.MatchError: null
    at org.apache.flink.api.scala.typeutils.OptionSerializer.copy(OptionSerializer.scala:50)
    at org.apache.flink.api.scala.typeutils.OptionSerializer.copy(OptionSerializer.scala:29)
    at org.apache.flink.streaming.runtime.tasks.OperatorChain$CopyingChainingOutput.pushToOperator(OperatorChain.java:635)
    at org.apache.flink.streaming.runtime.tasks.OperatorChain$CopyingChainingOutput.collect(OperatorChain.java:612)
    at org.apache.flink.streaming.runtime.tasks.OperatorChain$CopyingChainingOutput.collect(OperatorChain.java:592)
    at org.apache.flink.streaming.api.operators.AbstractStreamOperator$CountingOutput.collect(AbstractStreamOperator.java:727)
    at org.apache.flink.streaming.api.operators.AbstractStreamOperator$CountingOutput.collect(AbstractStreamOperator.java:705)
    at org.apache.flink.streaming.api.operators.StreamMap.processElement(StreamMap.java:41)
    ...
    at org.apache.flink.runtime.taskmanager.Task.run(Task.java:530)
    at java.lang.Thread.run(Thread.java:748)

2.问题代码

可以看出,上述异常都是StreamMap.processElement方法抛出的,这个对应了我们代码中的map操作,具体异常信息和map的返回值类型有关;

以返回值为Java/Scala Tuple为例示意一下问题代码:

// java版本  
text.map(e -> {
  if(xxxx) {
    return Tuple2.of("hello", 1);
  } else {
    // 在某些条件下返回NULL
    return null;
  }
}).returns(new TypeHint<Tuple2<String, Integer>>(){})
.filter(Objects::nonNull)
.print()

// scala版本
text.map(e => {
  if(xxxx) {
    ("hello", 1)
  } else {
    // 在某些条件下返回NULL
    null
  }
})
.filter(_ != null)
.print()

2. 原因分析

 

(这里为了演示故意设置了disableOperatorChaining,一般情况这两个算子会串起来),如果“Map”想要传一个NULL值给下游的“Filter”,那它必须传一个具体的值给下游来表明是NULL(如果什么都不传的话下游根本不知道有数据);那么应该传什么值来表示NULL呢?不同的数据类型实现方法不同,但其本质思路是一样的,就是通过一个标志位来表示是不是NULL,类似 <nullFlag><value>

以String为例,Flink中StringSerializer会先写一个int标志位来表示String的长度;如果string == null,则标志位为0;否则的话标志位为string.length() + 1;这样的话NULL就是 <0>,空字符串就是 <1>,其他字符串是 <string.length + 1><stringContent>(当然,这里的标志位还担任着记录字符串长度的职责);

// 核心代码逻辑
public static final void writeString(CharSequence cs, DataOutput out) throws IOException {
  if (cs != null) {
   // 如果string不为null,则标志位是string的长度加1;
   // the length we write is offset by one, because a length of zero indicates a null value
   int lenToWrite = cs.length()+1;
   // 可以看出最大能够序列化长度为Integer.MAX_VALUE - 1的字符串;
  if (lenToWrite < 0) {
    throw new IllegalArgumentException("CharSequence is too long.");
  }
  ...
  } else {
    // 如果string为null,则标志位写0;
    out.write(0);
  }
}

对于大部分数据类型,使用标志位的好处是可以支持传递NULL值;缺点也很明显,就是浪费了带宽,多了标志位信息的传递。

那为什么还会出现上面的异常呢?那是因为并不是所有类型都支持NULL的,目前我所知的不支持NULL的类型包括Scala Option、Java/Scala Tuple和Scala Case Class;至于为什么不支持NULL,根据我搜到的解释,原因如下:

  • Scala Option不支持NULL,是因为Option就是设计来避免NULL的;如果Option类型返回NULL,本身就是个BUG(但奇怪的是Java Optional类型就可以返回NULL,不过Java Optional是通过KryoSerializer序列化的;Scala Option是通过CaseClassSerializer序列化的);
Stephan Ewen: Using null for an Option value is by itself a bug (after all, Option is explicitly designed to avoid null)
  • Java/Scala Tuple和Scala Case Class不支持NULL的原因不确定,不过它们是支持把变量设置为NULL的,具体方式见下一章节;

3. 解决方法

  • 对于Option类型,用None替代NULL;
  • 对于Tuple或Scala Case Class,可以设置变量为NULL;比如new Tuple2<String, Integer>(null, null)
  • 用flatMap替换map:
text.flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() {
  @Override
  public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception {
    if(xxx) {// 如果生成的数据不为NULL;      
      out.collect(Tuple2.of("hello", 1));
    }
  }
}).print()

4. 参考

 

往期精选▼

Spark性能调优之在实际项目中广播大变量

Spark Shuffle调优之调节map端内存缓冲与reduce端内存占比

Spark Shuffle调优之合并map端输出文件

Flink调优法则

5个Hadoop优化技巧

4个角度轻松理解 Flink中的Watermark

Flink中Checkpoint和Savepoint 的 3 个不同点

Flink实现固定时长或消息条数的触发器

Flink方案设计中的4大误区

使用 Broadcast State 的 4 个注意事项

3种Flink State Backend | 你该用哪个?

一文搞定 Flink 异步 I/O

Flink State 使用的4点建议

Flink在开发中的7点建议

 

转载是一种动力 分享是一种美德, 欢迎关注 大数据与数据仓库公众号, 回复 spark 领取资料

在 Apache Flink 中,当出现 `Failed to forward element to next operator` 错误时,通常表示某个操作符在尝试将数据元素传递给下游操作符时遇到了问题。该问题可能由多种原因引起,以下是常见的异常原因及对应的解决方案。 ### 1. **序列化/反序列化失败** Flink 需要将数据在操作符之间传输,因此所有数据必须是可序列化的。如果数据类型无法被正确序列化或反序列化,会导致元素无法转发。 - **原因**:使用了不支持的自定义数据类型,或者数据中包含不可序列化的字段(如 `InputStream`、`Socket` 等)。 - **解决方案**: - 确保所有自定义数据类实现 `Serializable` 接口。 - 使用 Flink 提供的序列化框架(如 `TypeInformation`)验证数据类型。 - 考虑使用 `Kryo` 或 `Avro` 等通用序列化机制处理复杂对象。 ### 2. **网络缓冲区不足** Flink 使用网络缓冲区(Netty buffers)在任务之间传输数据。如果缓冲区不足,可能导致无法转发元素。 - **原因**:高吞吐量场景下,网络缓冲区池耗尽。 - **解决方案**: - 增加 `taskmanager.network.memory.fraction` 配置值,以分配更多内存用于网络缓冲区。 - 调整 `taskmanager.network.memory.min` 和 `taskmanager.network.memory.max` 以确保足够的缓冲区可用[^1]。 ### 3. **背压(Backpressure)** 当下游操作符处理速度跟不上上游发送速度时,可能引发背压,导致元素无法及时转发。 - **原因**:下游算子处理性能不足,或者状态操作(如窗口计算)延迟。 - **解决方案**: - 检查 Flink Web UI 中的背压状态,识别瓶颈算子。 - 增加并行度,提升下游处理能力。 - 优化算子逻辑,减少耗时操作,如减少外部调用或复杂计算。 ### 4. **状态后端问题** 在使用状态操作(如 `KeyedProcessFunction`、`WindowOperator`)时,状态访问或序列化问题可能导致转发失败。 - **原因**:状态后端配置不当,或状态数据过大导致序列化失败。 - **解决方案**: - 检查状态后端类型(如 `RocksDBStateBackend` 或 `FsStateBackend`)是否适合当前场景。 - 确保状态数据可被正确序列化。 - 对于 RocksDB,考虑调整内存限制或启用增量检查点。 ### 5. **异步 I/O 超时或异常** 在使用 `AsyncFunction` 进行异步调用时,如果异步操作失败或超时,可能导致元素无法继续转发。 - **原因**:异步调用未正确处理异常,或异步操作耗时过长。 - **解决方案**: - 在 `AsyncFunction` 中正确捕获和处理异常,并确保回调完成。 - 增加异步请求超时时间(`asyncInvoke` 中的超时设置)。 - 控制并发请求数量,避免资源耗尽。 ### 6. **Operator Chain 中断** Flink 默认会将多个操作符链接在一起,如果其中一个操作符抛出异常,可能导致整个链中断,从而无法转发元素。 - **原因**:某个操作符抛出异常导致整个链路终止。 - **解决方案**: - 使用 `disableChaining()` 或 `startNewChain()` 分离关键操作符。 - 在关键路径中添加异常捕获逻辑,防止异常传播。 ### 示例代码:添加异常捕获逻辑 ```java public class SafeMapFunction implements MapFunction<String, String> { @Override public String map(String value) { try { // 业务逻辑 return process(value); } catch (Exception e) { // 记录日志并返回默认值或跳过 System.err.println("Error processing element: " + value); return null; // 或抛出特定异常 } } private String process(String value) { // 实际处理逻辑 if (value == null) { throw new IllegalArgumentException("Value is null"); } return value.toUpperCase(); } } ``` ### 7. **检查点或保存点问题** 在进行检查点操作时,某些状态操作可能因为序列化或状态不一致而失败,进而影响元素转发。 - **原因**:检查点过程中状态无法正确快照。 - **解决方案**: - 确保所有状态变量可被正确序列化。 - 使用 `CheckpointedFunction` 接口管理非托管状态。 - 启用对齐检查点(Aligned Checkpoints)或非对齐检查点(Unaligned Checkpoints)以适应不同场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值