Spark Strutured Streaming自动将时间戳转换为本地时间

时间:2018-02-13 12:37:32

标签: java scala apache-spark apache-spark-sql spark-structured-streaming

我有UTC和ISO8601的时间戳,但使用结构化流,它会自动转换为本地时间。有没有办法阻止这种转换?我想在UTC中使用它。

我从Kafka读取json数据,然后使用from_json Spark函数解析它们。

输入:

{"Timestamp":"2015-01-01T00:00:06.222Z"}

流速:

SparkSession
  .builder()
  .master("local[*]")
  .appName("my-app")
  .getOrCreate()
  .readStream()
  .format("kafka")
  ... //some magic
  .writeStream()
  .format("console")
  .start()
  .awaitTermination();

架构:

StructType schema = DataTypes.createStructType(new StructField[] {
        DataTypes.createStructField("Timestamp", DataTypes.TimestampType, true),});

输出:

+--------------------+
|           Timestamp|
+--------------------+
|2015-01-01 01:00:...|
|2015-01-01 01:00:...|
+--------------------+

如您所见,小时数自行增加。

PS:我试图尝试from_utc_timestamp Spark功能,但没有运气。

4 个答案:

答案 0 :(得分:21)

对我来说,它可以使用:

spark.conf.set("spark.sql.session.timeZone", "UTC")

它告诉spark SQL使用UTC作为时间戳的默认时区。我在spark SQL中使用它,例如:

select *, cast('2017-01-01 10:10:10' as timestamp) from someTable

我知道它在2.0.1中不起作用。但适用于Spark 2.2。我也在SQLTransformer中使用过它。

我不确定流媒体。

答案 1 :(得分:12)

注意

这个答案在Spark<中主要 非常有用2.2。对于较新的Spark版本,请the answer

查看astro-asz

但是我们应该注意到,截至今天(Spark 2.4.0),spark.sql.session.timeZone未设置user.timezonejava.util.TimeZone.getDefault)。因此,单独设置``spark.sql.session.timeZone`会导致SQL和非SQL组件使用不同时区设置的相当尴尬的情况。

因此,即使设置了user.timezone,我仍建议明确设置spark.sql.session.timeZone

TL; DR 不幸的是,这就是Spark现在处理时间戳的方式,除了直接在纪元时间上运行之外,实际上没有内置替代方案,而不使用日期/时间工具。

您可以对Spark开发人员列表进行深入讨论:SQL TIMESTAMP semantics vs. SPARK-18350

到目前为止,我发现最干净的解决方法是为驱动程序和执行程序设置-Duser.timezoneUTC。例如,提交:

bin/spark-shell --conf "spark.driver.extraJavaOptions=-Duser.timezone=UTC" \
                --conf "spark.executor.extraJavaOptions=-Duser.timezone=UTC"

或通过调整配置文件(spark-defaults.conf):

spark.driver.extraJavaOptions      -Duser.timezone=UTC
spark.executor.extraJavaOptions    -Duser.timezone=UTC

答案 2 :(得分:0)

尽管提供了两个非常好的答案,但我发现它们都有些沉重,无法解决问题。我不想要任何需要修改整个应用程序中的时区解析行为的方法,或者想要一种可以更改我的JVM的默认时区的方法。痛苦过后,我确实找到了解决方案,我将在下面分享...

将时间[/ date]字符串解析为时间戳以进行日期操作,然后正确地将结果呈现回

首先,让我们解决以下问题:如何使Spark SQL正确地将date [/ time]字符串(给定格式)解析为时间戳,然后正确地将该时间戳渲染回以使其显示相同的日期[/ time] ]作为原始字符串输入。通用方法是:

- convert a date[/time] string to time stamp [via to_timestamp]
    [ to_timestamp  seems to assume the date[/time] string represents a time relative to UTC (GMT time zone) ]
- relativize that timestamp to the timezone we are in via from_utc_timestamp 

下面的测试代码实现了这种方法。 “我们所在的时区”作为第一个参数传递给timeTricks方法。该代码将输入字符串“ 1970-01-01”转换为localizedTimeStamp(通过from_utc_timestamp),并验证该时间戳记的“ valueOf”是否与“ 1970-01-01 00:00:00”相同。

object TimeTravails {
  def main(args: Array[String]): Unit = {

    import org.apache.spark.sql.SparkSession
    import org.apache.spark.sql.functions._

    val spark: SparkSession = SparkSession.builder()
      .master("local[3]")
      .appName("SparkByExample")
      .getOrCreate()

    spark.sparkContext.setLogLevel("ERROR")

    import spark.implicits._
    import java.sql.Timestamp

    def timeTricks(timezone: String): Unit =  {
      val df2 = List("1970-01-01").toDF("timestr"). // can use to_timestamp even without time parts !
        withColumn("timestamp", to_timestamp('timestr, "yyyy-MM-dd")).
        withColumn("localizedTimestamp", from_utc_timestamp('timestamp, timezone)).
        withColumn("weekday", date_format($"localizedTimestamp", "EEEE"))
      val row = df2.first()
      println("with timezone: " + timezone)
      df2.show()
      val (timestamp, weekday) = (row.getAs[Timestamp]("localizedTimestamp"), row.getAs[String]("weekday"))

      timezone match {
        case "UTC" =>
          assert(timestamp ==  Timestamp.valueOf("1970-01-01 00:00:00")  && weekday == "Thursday")
        case "PST" | "GMT-8" | "America/Los_Angeles"  =>
          assert(timestamp ==  Timestamp.valueOf("1969-12-31 16:00:00")  && weekday == "Wednesday")
        case  "Asia/Tokyo" =>
          assert(timestamp ==  Timestamp.valueOf("1970-01-01 09:00:00")  && weekday == "Thursday")
      }
    }

    timeTricks("UTC")
    timeTricks("PST")
    timeTricks("GMT-8")
    timeTricks("Asia/Tokyo")
    timeTricks("America/Los_Angeles")
  }
}

结构化流传输问题的解释为将输入日期[/ time]字符串解释为UTC(而非本地时间)

下面的代码说明了如何应用上述技巧(稍作修改),以纠正时间戳因本地时间与GMT之间的偏移而发生偏移的问题。

object Struct {
  import org.apache.spark.sql.SparkSession
  import org.apache.spark.sql.functions._

  def main(args: Array[String]): Unit = {

    val timezone = "PST"

    val spark: SparkSession = SparkSession.builder()
      .master("local[3]")
      .appName("SparkByExample")
      .getOrCreate()

    spark.sparkContext.setLogLevel("ERROR")

    val df = spark.readStream
      .format("socket")
      .option("host", "localhost")
      .option("port", "9999")
      .load()

    import spark.implicits._


    val splitDf = df.select(split(df("value"), " ").as("arr")).
      select($"arr" (0).as("tsString"), $"arr" (1).as("count")).
      withColumn("timestamp", to_timestamp($"tsString", "yyyy-MM-dd"))
    val grouped = splitDf.groupBy(window($"timestamp", "1 day", "1 day").as("date_window")).count()

    val tunedForDisplay =
      grouped.
        withColumn("windowStart", to_utc_timestamp($"date_window.start", timezone)).
        withColumn("windowEnd", to_utc_timestamp($"date_window.end", timezone))

    tunedForDisplay.writeStream
      .format("console")
      .outputMode("update")
      .option("truncate", false)
      .start()
      .awaitTermination()
  }
}

代码需要通过套接字输入...我使用像这样启动的程序“ nc”(网猫):

nc -l 9999

然后我启动Spark程序,并为net cat提供一行输入:

1970-01-01 4

我得到的输出说明了偏移偏移的问题:

-------------------------------------------
Batch: 1
-------------------------------------------
+------------------------------------------+-----+-------------------+-------------------+
|date_window                               |count|windowStart        |windowEnd          |
+------------------------------------------+-----+-------------------+-------------------+
|[1969-12-31 16:00:00, 1970-01-01 16:00:00]|1    |1970-01-01 00:00:00|1970-01-02 00:00:00|
+------------------------------------------+-----+-------------------+-------------------+

请注意,date_window的开始和结束距离输入有8个小时的偏移(因为我位于GMT-7 / 8时区PST)。但是,我使用to_utc_timestamp纠正了这一转变,以获取包含输入的一天窗口的正确开始和结束日期时间:1970-01-01 00:00:00,1970-01-02 00:00:00。 / p>

请注意,在显示的第一段代码中,我们使用了from_utc_timestamp,而在结构化流解决方案中,我们使用了to_utc_timestamp。我还没有弄清楚在给定情况下要使用这两个中的哪一个。 (如果您知道,请提示我!)。

答案 3 :(得分:0)

另一个对我有用的解决方案是将jvm默认时区设置为目标时区(在您的情况下为UTC)。

TimeZone.setDefault(TimeZone.getTimeZone("UTC"));

我在将spark数据帧写入数据库之前添加了以上代码。