AWS GlueジョブのCloudWatch Logsへ出力されるログを整理した

    AWS GlueジョブのCloudWatch Logsへ出力されるログを整理した

     

    目次

      はじめに

      クラウドインテグレーション部の渡邊です。
      今回は、AWS Glueジョブのログ種別について説明します。
      S3へ出力されるSparkUI(デフォルトの出力先s3://aws-glue-assets-XXXXXXXXXXXX-ap-northeast-1/sparkHistoryLogs/)のログや、
      AWS Glue Crawler(デフォルトの出力先/aws-glue/crawlers)に関する記述はありませんので、ご了承ください。

      ログ種別

      以下のログを、Amazon CloudWatch Logsへ出力できます。

        ログ名 CloudWatch Logs 説明
      Glue Job AWS Glue ジョブのリアルタイム連続ログ /aws-glue/jobs/logs-v2 Apache Spark ジョブのログ
        標準出力ログ /aws-glue/jobs/output Glue Spark ジョブ実行時における標準出力ログ
        エラーログ /aws-glue/jobs/error Glue Spark ジョブ実行時におけるエラーログ
      AWS Glue Studio Glue interactive session のエラーログ /aws-glue/sessions/error インタラクティブセッション使用時のエラーログ※1

      ※1 ログの命名およびセッション中に出力される点から記述してますが、該当のログの記述を公式ドキュメントから見つけられませんでした

      後に共有する出力結果からもわかりますが、エラー系のログストリームには、エラー以外のログも出力されます。


      Glue Jobのログ

      AWS Glue ジョブ実行インサイトの例を参考に、CloudWatch Logsを出力します。

      使用する、ジョブのスクリプトは以下の通りです。

      import sys
      from awsglue.transforms import *
      from awsglue.utils import getResolvedOptions
      from pyspark.context import SparkContext
      from awsglue.context import GlueContext
      from awsglue.job import Job
      from pyspark.sql.types import *
      from pyspark.sql.functions import udf,col

      args = getResolvedOptions(sys.argv, ['JOB_NAME'])

      sc = SparkContext()
      glueContext = GlueContext(sc)
      spark = glueContext.spark_session
      job = Job(glueContext)
      job.init(args['JOB_NAME'], args)

      data_set_1 = [1, 2, 3, 4]
      data_set_2 = [5, 6, 7, 8]

      scoresDf = spark.createDataFrame(data_set_1, IntegerType())

      print(spark)
      print("サンプルログです")
      def data_multiplier_func(factor, data_vector):
      import tensorflow as tf
      with tf.compat.v1.Session() as sess:
      x1 = tf.constant(factor)
      x2 = tf.constant(data_vector)
      result = tf.multiply(x1, x2)
      return sess.run(result).tolist()

      data_multiplier_udf = udf(lambda x:data_multiplier_func(x, data_set_2), ArrayType(IntegerType(),False))
      factoredDf = scoresDf.withColumn("final_value", data_multiplier_udf(col("value")))
      print(factoredDf.collect())

      必要なモジュール(tensorflow)をインポートし忘れて、データを解析して機械学習モデルを構築してしまっている想定です。
      途中で実行される以下処理が、標準出力として記録される想定です。

      print(spark)
      print("サンプルログです")


      ログの確認

      スクリプトをもとにジョブを実行すると、実行に失敗するはずです。
      失敗したジョブの詳細を確認します。

      202401_gluejob_01

      するとCloudWatch Logsにリンクが埋め込まれているので、それぞれクリックすると、ログを確認できます。
      以降で、順番にログを確認します。

      • All logs(/aws-glue/jobs/logs-v2)
      • Output logs(/aws-glue/jobs/output)
      • Error logs(/aws-glue/jobs/error)

      AWS Glue ジョブのリアルタイム連続ログ(/aws-glue/jobs/logs-v2)

      エグゼキュター(Executor)およびドライバー(Driver)単位で、ログストリームが出力されています。
      エグゼキュターとは、タスク実行を担当するプロセスです。

      ドライバーログ、エグゼキュターログ、および Apache Spark ジョブの進行状況バーを含む、Amazon CloudWatch のリアルタイムの Apache Spark ジョブログを表示できます。

      引用元:AWS Glue ジョブの連続ログ記録

      202401_gluejob_02

      ログストリームの末尾の番号が、Executor IDと一致しています。
      202401_gluejob_03-1

      • エグゼキュターログの例
      23/11/14 22:05:26 INFO Executor: Starting executor ID 8 on host XXX.XXX.XXX.XXX

      AWS Glue ETL パフォーマンス・チューニング① 基礎知識編 p. 48を見ると、エグゼキュターログとドライバーログのストリームが出力される旨の記載がありました。
      エラーログに絞って、ログを確認します。
      いずれのログでも、同様のエラーが確認できました。

      • ドライバーログの例
      23/11/14 22:05:33 ERROR ProcessLauncher: Error from Python:Traceback (most recent call last):
      File "/tmp/watanabe-sample.py", line 35, in <module>
      print(factoredDf.collect())
      File "/opt/amazon/spark/python/lib/pyspark.zip/pyspark/sql/dataframe.py", line 818, in collect
      sock_info = self._jdf.collectToPython()
      File "/opt/amazon/spark/python/lib/py4j-0.10.9.5-src.zip/py4j/java_gateway.py", line 1321, in __call__
      return_value = get_return_value(
      File "/opt/amazon/spark/python/lib/pyspark.zip/pyspark/sql/utils.py", line 196, in deco
      raise converted from None
      pyspark.sql.utils.PythonException:
      An exception was thrown from the Python worker. Please see the stack trace below.
      Traceback (most recent call last):
      File "/tmp/watanabe-sample.py", line 33, in <lambda>
      File "/tmp/watanabe-sample.py", line 26, in data_multiplier_func
      ModuleNotFoundError: No module named 'tensorflow'

      こちらでも同様に、エラーが確認できるログストリームと、そうでないログストリームがありました。
      Executor IDが2,3,8のものがエラーログとしてヒットしています。

      202401_gluejob_04

      エグゼキュターログ

      23/11/14 22:05:32 ERROR Executor: Exception in task 26.0 in stage 0.0 (TID 26)
      org.apache.spark.api.python.PythonException: Traceback (most recent call last):
      File "/tmp/watanabe-sample.py", line 33, in <lambda>
      File "/tmp/watanabe-sample.py", line 26, in data_multiplier_func
      ModuleNotFoundError: No module named 'tensorflow'

      at org.apache.spark.api.python.BasePythonRunner$ReaderIterator.handlePythonException(PythonRunner.scala:559) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.sql.execution.python.PythonUDFRunner$$anon$2.read(PythonUDFRunner.scala:86) ~[spark-sql_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.sql.execution.python.PythonUDFRunner$$anon$2.read(PythonUDFRunner.scala:68) ~[spark-sql_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.api.python.BasePythonRunner$ReaderIterator.hasNext(PythonRunner.scala:512) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.InterruptibleIterator.hasNext(InterruptibleIterator.scala:37) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at scala.collection.Iterator$$anon$11.hasNext(Iterator.scala:491) ~[scala-library-2.12.15.jar:?]
      at scala.collection.Iterator$$anon$10.hasNext(Iterator.scala:460) ~[scala-library-2.12.15.jar:?]
      at scala.collection.Iterator$$anon$10.hasNext(Iterator.scala:460) ~[scala-library-2.12.15.jar:?]
      at org.apache.spark.sql.catalyst.expressions.GeneratedClass$GeneratedIteratorForCodegenStage2.processNext(Unknown Source) ~[?:?]
      at org.apache.spark.sql.execution.BufferedRowIterator.hasNext(BufferedRowIterator.java:35) ~[spark-sql_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.sql.catalyst.expressions.GeneratedClass$GeneratedIteratorForCodegenStage2.hasNext(Unknown Source) ~[?:?]
      at org.apache.spark.sql.execution.WholeStageCodegenExec$$anon$1.hasNext(WholeStageCodegenExec.scala:968) ~[spark-sql_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.sql.execution.SparkPlan.$anonfun$getByteArrayRdd$1(SparkPlan.scala:383) ~[spark-sql_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.rdd.RDD.$anonfun$mapPartitionsInternal$2(RDD.scala:890) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.rdd.RDD.$anonfun$mapPartitionsInternal$2$adapted(RDD.scala:890) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.rdd.MapPartitionsRDD.compute(MapPartitionsRDD.scala:52) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.rdd.RDD.computeOrReadCheckpoint(RDD.scala:365) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.rdd.RDD.iterator(RDD.scala:329) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.scheduler.ResultTask.runTask(ResultTask.scala:90) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.scheduler.Task.run(Task.scala:138) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.executor.Executor$TaskRunner.$anonfun$run$3(Executor.scala:548) ~[spark-core_2.12-3.3.0-amzn-1.jar:?]
      at org.apache.spark.util.Utils$.tryWithSafeFinally(Utils.scala:1516) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.executor.Executor$TaskRunner.run(Executor.scala:551) ~[spark-core_2.12-3.3.0-amzn-1.jar:?]
      at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) ~[?:1.8.0_382]
      at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) ~[?:1.8.0_382]
      at java.lang.Thread.run(Thread.java:750) ~[?:1.8.0_382]

      標準出力ログ(/aws-glue/jobs/output)

      同様に、CloudWatch Logsから確認すると、ログの出力が確認できました。

      202401_gluejob_05

      202401_gluejob_06

      <pyspark.sql.session.SparkSession object at 0x7f8078345840>
      サンプルログです

      エラーログ(/aws-glue/jobs/error)

      他のログと同様に確認すると、以下2パターンでログストリームが出力されています。

      • タスク単位でのエラーログ
      • ドライバーにおけるエラーログ

      202401_gluejob_07

      ログを検索すると、ログの内容から、g-から始まる番号はタスクIDを表していることがわかります。202401_gluejob_08

      任意のログストリームでERRORを探すと見つからない場合もありました。
      エラーの発生したタスクと、発生していないタスクがあるためだと思われます。

      202401_gluejob_09

      さきほど、「連続ログ記録の有効化によるドライバーログ」の章にて、エラーを確認したExecutor IDは、2、3および8でした。
      タスクは特定のエグゼキュターに割り当てられているため、これらのIDのエグゼキュターに割り当てられているタスクのログから、エラーが確認できるはずです。
      以下をそれぞれ実施することで、エラーが発生したタスクと、そのタスクに割り当てられたExecutor IDがわかります。

      • エラーログを含むログストリームを確認する

      • 各ログストリーム内でタスクが割り当てられたエグゼキュターを特定する

      それぞれのタスクのログを確認します。

      • Executor IDが2のものに割り当てられたタスクのログ202401_gluejob_10202401_gluejob_11

      • Executor IDが3のものに割り当てられたタスクのログ202401_gluejob_12202401_gluejob_13

      • Executor IDが8のものに割り当てられたタスクのログ202401_gluejob_14202401_gluejob_21

      よって、タスクのエラーログからも、エラーが発生したExecutor IDは、2、3および8であることが確認できました。
      以下でエラーが出力されているタスクの一例を示します(他の、エラーが出力されたタスクIDを含むログでも同様に、同じようなログが出力されていました)。

      2023-11-14 22:05:32,465 ERROR [Executor task launch worker for task 8.2 in stage 0.0 (TID 37)] executor.Executor (Logging.scala:logError(98)): Exception in task 8.2 in stage 0.0 (TID 37)
      org.apache.spark.api.python.PythonException: Traceback (most recent call last):
      File "/tmp/watanabe-sample.py", line 33, in <lambda>
      File "/tmp/watanabe-sample.py", line 26, in data_multiplier_func
      ModuleNotFoundError: No module named 'tensorflow'

      at org.apache.spark.api.python.BasePythonRunner$ReaderIterator.handlePythonException(PythonRunner.scala:559) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.sql.execution.python.PythonUDFRunner$$anon$2.read(PythonUDFRunner.scala:86) ~[spark-sql_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.sql.execution.python.PythonUDFRunner$$anon$2.read(PythonUDFRunner.scala:68) ~[spark-sql_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.api.python.BasePythonRunner$ReaderIterator.hasNext(PythonRunner.scala:512) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.InterruptibleIterator.hasNext(InterruptibleIterator.scala:37) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at scala.collection.Iterator$$anon$11.hasNext(Iterator.scala:491) ~[scala-library-2.12.15.jar:?]
      at scala.collection.Iterator$$anon$10.hasNext(Iterator.scala:460) ~[scala-library-2.12.15.jar:?]
      at scala.collection.Iterator$$anon$10.hasNext(Iterator.scala:460) ~[scala-library-2.12.15.jar:?]
      at org.apache.spark.sql.catalyst.expressions.GeneratedClass$GeneratedIteratorForCodegenStage2.processNext(Unknown Source) ~[?:?]
      at org.apache.spark.sql.execution.BufferedRowIterator.hasNext(BufferedRowIterator.java:35) ~[spark-sql_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.sql.catalyst.expressions.GeneratedClass$GeneratedIteratorForCodegenStage2.hasNext(Unknown Source) ~[?:?]
      at org.apache.spark.sql.execution.WholeStageCodegenExec$$anon$1.hasNext(WholeStageCodegenExec.scala:968) ~[spark-sql_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.sql.execution.SparkPlan.$anonfun$getByteArrayRdd$1(SparkPlan.scala:383) ~[spark-sql_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.rdd.RDD.$anonfun$mapPartitionsInternal$2(RDD.scala:890) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.rdd.RDD.$anonfun$mapPartitionsInternal$2$adapted(RDD.scala:890) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.rdd.MapPartitionsRDD.compute(MapPartitionsRDD.scala:52) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.rdd.RDD.computeOrReadCheckpoint(RDD.scala:365) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.rdd.RDD.iterator(RDD.scala:329) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.scheduler.ResultTask.runTask(ResultTask.scala:90) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.scheduler.Task.run(Task.scala:138) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.executor.Executor$TaskRunner.$anonfun$run$3(Executor.scala:548) ~[spark-core_2.12-3.3.0-amzn-1.jar:?]
      at org.apache.spark.util.Utils$.tryWithSafeFinally(Utils.scala:1516) ~[spark-core_2.12-3.3.0-amzn-1.jar:3.3.0-amzn-1]
      at org.apache.spark.executor.Executor$TaskRunner.run(Executor.scala:551) ~[spark-core_2.12-3.3.0-amzn-1.jar:?]
      at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) ~[?:1.8.0_382]
      at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) ~[?:1.8.0_382]
      at java.lang.Thread.run(Thread.java:750) ~[?:1.8.0_382]

      また、唯一タスクIDが含まれていないドライバーのログストリームで検索しても、いくつかのエラーを確認することができました。
      202401_gluejob_15

      想定のエラーも見つかりましたので、本検証においてタスク単位およびドライバー単位のログストリームでエラーを確認できることがわかりました。

      2023-11-14 22:05:33,348 ERROR [main] glue.ProcessLauncher (Logging.scala:logError(77)): Error from Python:Traceback (most recent call last):
      File "/tmp/watanabe-sample.py", line 35, in <module>
      print(factoredDf.collect())
      File "/opt/amazon/spark/python/lib/pyspark.zip/pyspark/sql/dataframe.py", line 818, in collect
      sock_info = self._jdf.collectToPython()
      File "/opt/amazon/spark/python/lib/py4j-0.10.9.5-src.zip/py4j/java_gateway.py", line 1321, in __call__
      return_value = get_return_value(
      File "/opt/amazon/spark/python/lib/pyspark.zip/pyspark/sql/utils.py", line 196, in deco
      raise converted from None
      pyspark.sql.utils.PythonException:
      An exception was thrown from the Python worker. Please see the stack trace below.
      Traceback (most recent call last):
      File "/tmp/watanabe-sample.py", line 33, in <lambda>
      File "/tmp/watanabe-sample.py", line 26, in data_multiplier_func
      ModuleNotFoundError: No module named 'tensorflow'

      Glue interactive session のエラーログ

      VisualETLでData previewを使用すると/aws-glue/sessions/errorにログが出力されます。
      スクリプトの修正を試みると、以下の通り、ビジュアルモードからスクリプトモードになります。

      Unlocking the job script will convert your job from visual mode to script-only mode. This action cannot be undone. To keep a copy of the visual-mode job, clone the job on the Jobs page of Glue Studio.

      スクリプトモードだと、ビジュアルジョブエディタの機能である、Data previewが使用できなかったです。

      よって、本章では、VisualETLで任意の処理を実行して、ログ出力しようと思います(エラーログの出力などは考慮しない)。
      以下を設定してData previewを実行しました。

      • Data preview用のIAMロールの設定
      • 空のバケットを対象にData sourceを定義
      • データフォーマットはJSONを指定

      以上の設定を完了すると、実行がスタートします。
      バケットは空なので、表示するデータがない旨のメッセージが出力されます。

      202401_gluejob_16

      /aws-glue/sessions/errorを確認します。
      他のログのように、タスクがエグゼキュターに割り当てられています。
      202401_gluejob_17202401_gluejob_18

      それぞれのログストリームを確認すると、他のログと同様、出力内容に差異がありました。
      ストリームの命名から、エラーログ(/aws-glue/jobs/error)と同様に、タスク単位およびドライバー単位でログが出力されていると思われます。
      202401_gluejob_19

      タスクIDが末尾についているログストリーム


      202401_gluejob_20

      タスクIDが末尾にないログストリーム

      まとめ

      • AWS GlueジョブのCloudWatch Logsへ出力されるログは以下の通り
        • /aws-glue/jobs/logs-v2
          • 連続ログ記録の有効化によるドライバーログ
          • エグゼキュター単位、ドライバー単位でログストリームが出力される
        • /aws-glue/jobs/output
          • Glue Spark ジョブ実行時における標準出力ログ
        • /aws-glue/jobs/error
          • Glue Spark ジョブ実行時におけるエラーログ
          • タスク単位、ドライバー単位でログストリームが出力される
          • エラー以外のログも出力される
        • /aws-glue/sessions/error
          • Glue Spark ジョブ実行時におけるエラーログ
          • タスク単位、ドライバー単位でログストリームが出力される
          • エラー以外のログも出力される
      • 各タスクはエグゼキュターに割り当てられる

      【参考】

      AWS Glue ジョブの連続ログ記録

      AWS Glue Immersion day

      AWS Glue インタラクティブセッションの概要

      AWS Glue ETL パフォーマンス・チューニング① 基礎知識編

      ログ記録の動作。