繁体   English   中英

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

[英]flink parsing JSON in map: InvalidProgramException: Task not serializable

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

这是一些代码示例,我无法理解其为何如此行事。

情况一:

在这种情况下,我正在按照jackson-module-scala的官方示例代码告诉我的那样做

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

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

  3. 调用readValue以便将JSON解析为Map

我得到的错误是: 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调用方法可以正常工作? 是否因为ObjectMapper本身可序列化(如此处ObjectMapper.java#L114所述)

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

情况三:

我创建了一个新的case class Jsen ,并将其用作相应的解析类,注册了Scala模块。 而且它也运行良好。

但是,如果您输入的JSON经常变化,则这种方法不太灵活。 管理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]))
    ...

它也工作正常。

我的主要问题是: 究竟是什么导致Task无法registerModule(DefaultScalaModule)下进行序列化的问题?

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

事实是,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必须可序列化。 杰克逊(Jackson)库的设计师意识到了这种需求,并且由于在映射器中根本没有不可分割的东西,因此他们使ObjectMapper和默认的Module s可序列化。 不幸的是,Scala Jackson Module的设计师错过了这一点,并通过使ScalaTypeModifier和所有子类不可序列化,使其DefaultScalaModule变得不可序列化。 这就是为什么您的第二个代码工作而第一个代码不工作的原因:“原始” ObjectMapper可序列化,而带有预注册DefaultScalaModule ObjectMapper无法序列化。

有一些可能的解决方法。 可能最简单的方法是包装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的实例不可序列化,但创建DefaultScalaModule实例的功能却是可序列化的。


更新:@transient呢?

如果我添加lazy 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)
}

lazy val上的@transient影响两个后备字段。 现在,您可以看到为什么lazy val技巧起作用了:

  1. 它在本地有效,因为它会延迟mapperValue字段的初始化,直到首次访问mapper方法为止,因此在执行序列化检查时,该字段为null

  2. 因为MapperWrapper是完全可序列化的,并且MapperWrapper lazy val初始化逻辑放入同一类的方法中,所以它可以远程工作(请参阅def mapper )。

但是请注意,AFAIK的lazy val编译方式是当前Scala编译器的实现细节,而不是Scala规范的一部分。 如果稍后将类似于.Net Lazy的类添加到Java标准库,则Scala编译器可能会开始生成不同的代码。 这很重要,因为它为@transient提供了一种@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这不是问题,因为该字段根本不会被序列化。

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

object也有一个窍门,因为Scala编译器会为object s生成自定义反序列化逻辑(覆盖readResolve )以返回相同的单例对象。 这意味着包含lazy val值的object并未真正反序列化,而是使用了object本身的值。 这意味着在远程场景中, object内部的@transient lazy valclass内部的更适合未来使用。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM