web-dev-qa-db-ja.com

Spark構造化ストリーミングはタイムスタンプを現地時間に自動的に変換します

UTCとISO8601のタイムスタンプがありますが、構造化ストリーミングを使用すると、自動的に現地時間に変換されます。この変換を停止する方法はありますか? UTCで持ちたいです。

私はjsonデータをKafkaから読み取り、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関数を試してみましたが、運はありません。

17
Martin Brišiak

私にとっては、それが使用するのに役立ちました:

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

タイムスタンプのデフォルトのタイムゾーンとしてUTCを使用するようにspark SQLに指示します。たとえば、spark SQLで使用しました。

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

2.0.1では機能しないことは知っています。しかし、Spark 2.2で動作します。SQLTransformerでも使用し、動作しました。

しかし、ストリーミングについてはわかりません。

28
astro_asz

この回答は有用です主にSpark <2.2。新しいSparkバージョンは answer by 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セマンティクスvs. SPARK-1835

これまでに見つけた最もクリーンな回避策は、-Duser.timezoneからUTCへ。ドライバーとエグゼキューターの両方。たとえば、送信の場合:

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
15
user6910411

2つの非常に良い答えが提供されましたが、私はそれらの両方が問題を解決するための少し重いハンマーであることがわかりました。アプリ全体でタイムゾーンの解析動作を変更する必要のあるものや、JVMのデフォルトのタイムゾーンを変更する方法は必要ありませんでした。私は多くの苦痛の後に解決策を見つけました。

time [/ date]文字列を日付操作のタイムスタンプに解析してから、結果を正しくレンダリングして戻す

まず、Spark SQLを取得して日付[/時間]文字列を正しく解析し(形式を指定)、タイムスタンプを取得し、そのタイムスタンプを適切にレンダリングして表示する方法の問題に対処しましょう元の文字列入力と同じ日付[/時刻]一般的なアプローチは次のとおりです。

- 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 

以下のテストコードは、このアプローチを実装しています。 「timezone we are」は、timeTricksメソッドの最初の引数として渡されます。このコードは、入力文字列「1970-01-01」をlocaledTimeStampに変換し(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")
  }
}

構造化されたストリーミングの問題に対する解決策、受信日付[/時間]文字列を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'(net cat)を使用します。

nc -l 9999

次に、Sparkプログラムを開始し、net catに1行の入力を提供します。

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を含む1日のウィンドウの適切な開始日時と終了日時を取得します。

提示されたコードの最初のブロックではfrom_utc_timestampを使用しましたが、構造化ストリーミングソリューションではto_utc_timestampを使用しました。特定の状況でこれら2つのうちどちらを使用するかはまだわかりません。 (ご存じの場合は、私を手がかりにしてください!)。

0
Chris Bedford