flink解析地图中的JSON:InvalidProgramException:任务不可序列化

时间:2018-01-22 10:41:23

标签: scala serialization jackson apache-flink flink-streaming

我正在研究Flink项目,并希望将源JSON字符串数据解析为Json Object。我正在使用jackson-module-scala进行JSON解析。但是,我在Flink API(例如map)中使用JSON解析器时遇到了一些问题。

以下是代码的一些示例,我无法理解其背后的原因,为什么它会像这样。

情况1:

在这种情况下,我正在做jackson-module-scala's official exmaple code told me to do

  1. 创建新的ObjectMapper
  2. 注册DefaultScalaModule
      

    DefaultScalaModule是一个Scala对象,包含对所有当前支持的Scala数据类型的支持。

  3. 调用readValue以将JSON解析为Map
  4. 我收到的错误是:org.apache.flink.api.common.InvalidProgramException: Task not serializable

    object JsonProcessing {
      def main(args: Array[String]) {
    
        // set up the execution environment
        val env = StreamExecutionEnvironment.getExecutionEnvironment
    
        // get input data
        val text = env.readTextFile("xxx")
    
        val mapper = new ObjectMapper
        mapper.registerModule(DefaultScalaModule)
        val counts = text.map(mapper.readValue(_, classOf[Map[String, String]]))
    
        // execute and print result
        counts.print()
    
        env.execute("JsonProcessing")
      }
    
    }
    

    情况2:

    然后我做了一些Google,并提出了以下解决方案,其中registerModule被移入map函数。

    val mapper = new ObjectMapper
    val counts = text.map(l => {
      mapper.registerModule(DefaultScalaModule)
      mapper.readValue(l, classOf[Map[String, String]])
    })
    

    然而,我无法理解的是:为什么这会起作用,使用外部定义的对象mapper的调用方法?是因为{{1} }本身是Serializable,如此处所述ObjectMapper.java#L114

    现在,JSON解析工作正常,但每次都要调用ObjectMapper,我认为这可能会导致一些性能问题( 是吗? )。我还尝试了另外一种解决方案。

    情况3:

    我创建了一个新的mapper.registerModule(DefaultScalaModule),并将其用作相应的解析类,注册Scala模块。它也工作正常。

    但是,如果您的输入JSON经常变化,则这不是那么灵活。管理班级case class Jsen是不可维护的。

    Jsen

    此外,我还尝试使用case class Jsen( @JsonProperty("a") a: String, @JsonProperty("c") c: String, @JsonProperty("e") e: String ) object JsonProcessing { def main(args: Array[String]) { ... val mapper = new ObjectMapper val counts = text.map(mapper.readValue(_, classOf[Jsen])) ... } 而未调用JsonNode,如下所示:

    registerModule

    它的工作正常。

    我的主要问题是:实际导致 ... val mapper = new ObjectMapper val counts = text.map(mapper.readValue(_, classOf[JsonNode])) ... 引发任务不可序列化问题的原因是什么?

    如何确定您的代码是否可能在编码过程中导致此不可序列化的问题?

1 个答案:

答案 0 :(得分:3)

问题在于Apache Flink旨在分发。这意味着它需要能够远程运行您的代码。因此,这意味着您的所有处理功能都应该是可序列化的。在当前实现中,即使您不在任何分布式模式下运行此流程,也可以在构建流式流程时尽早确保。这是一种权衡,显而易见的好处是可以向您提供反馈,直至违反此合同的行(通过异常堆栈跟踪)。

所以当你写

val counts = text.map(mapper.readValue(_, classOf[Map[String, String]]))

你实际写的是

val counts = text.map(new Function1[String, Map[String, String]] {
    val capturedMapper = mapper

    override def apply(param: String) = capturedMapper.readValue(param, classOf[Map[String, String]])
})

这里重要的是你从外部上下文中捕获mapper并将其存储为必须可序列化的Function1对象的一部分。这意味着mapper必须是可序列化的。杰克逊图书馆的设计师认识到了这种需求,因为在映射器中没有任何根本不可分辨的东西,他们使ObjectMapper和默认的Module可序列化。不幸的是,Scala Jackson Module的设计师错过了这一点,并通过使ScalaTypeModifier和所有子类不可序列化而使DefaultScalaModule深度非串行化。这就是为什么你的第二个代码有效,而第一个代码没有:" raw" ObjectMapper可序列化,而预先注册ObjectMapper的{​​{1}}则不可。

有一些可能的解决方法。可能最简单的就是包裹DefaultScalaModule

ObjectMapper

然后将其用作

object MapperWrapper extends java.io.Serializable {
  // this lazy is the important trick here
  // @transient adds some safety in current Scala (see also Update section)
  @transient lazy val mapper = {
    val mapper = new ObjectMapper
    mapper.registerModule(DefaultScalaModule)
    mapper
  }

  def readValue[T](content: String, valueType: Class[T]): T = mapper.readValue(content, valueType)
} 

val counts = text.map(MapperWrapper.readValue(_, classOf[Map[String, String]])) 技巧有效,因为虽然lazy的实例不可序列化,但创建DefaultScalaModule实例的函数是。

更新:@transient怎么样?

  

如果我添加DefaultScalaModulelazy val相比,这里有什么不同?

这实际上是一个棘手的问题。编译@transient lazy val的内容实际上是这样的:

lazy val

object MapperWrapper extends java.io.Serializable { // @transient is set or not set for both fields depending on its presence at "lazy val" [@transient] private var mapperValue: ObjectMapper = null [@transient] @volatile private var mapperInitialized = false def mapper: ObjectMapper = { if (!mapperInitialized) { this.synchronized { val mapper = new ObjectMapper mapper.registerModule(DefaultScalaModule) mapperValue = mapper mapperInitialized = true } } mapperValue } def readValue[T](content: String, valueType: Class[T]): T = mapper.readValue(content, valueType) } 上的@transient影响两个支持字段。所以现在你可以看到为什么lazy val技巧起作用了:

  1. 在本地工作,因为它会延迟lazy val字段的初始化,直到第一次访问mapperValue方法,因此在执行序列化检查时字段是安全的mapper

  2. 远程工作,因为null是完全可序列化的,并且应该将MapperWrapper如何初始化的逻辑放入同一类的方法中(参见lazy val)。

  3. 但是请注意,AFAIK编译def mapper的这种行为是当前Scala编译器的实现细节,而不是Scala规范的一部分。如果稍后将类似于.Net Lazy的类添加到Java标准库中,则Scala编译器可能会开始生成不同的代码。这很重要,因为它为lazy val提供了一种权衡。现在添加@transient的好处是它可以确保像这样的代码也能正常工作:

    @transient

    如果没有val someJson:String = "..." val something:Something = MapperWrapper.readValue(someJson:String, ...) val counts = text.map(MapperWrapper.readValue(_, classOf[Map[String, String]])) ,上面的代码将失败,因为我们强制初始化@transient支持字段,现在它包含一个不可序列化的值。使用lazy这不是问题,因为该字段根本不会被序列化。

    @transient的一个潜在缺点是,如果Scala更改了生成@transient的代码的方式,并且该字段标记为lazy val,则实际上可能未对其进行反序列化远程工作场景。

    还有@transient的技巧,因为对于object,Scala编译器生成自定义反序列化逻辑(覆盖readResolve)以返回相同的单例对象。这意味着包含object的对象并未真正反序列化,并且使用了lazy val本身的值。这意味着object内的@transient lazy val比远程方案中的object更能面向未来。