EMR DAG terminates before all steps are completed

Viewed 1355

I am using the EMR CreateJobFlow, AddSteps,StepSensor and TerminateJobFlow operators in my DAG to start up an EMR cluster, add steps (2 spark apps, and dist-cp), and terminate when all steps are completed or 1 fails. I am able to do this when I have a 2 step DAG (1st is Spark app, 2nd is dist-cp), however, when I have 2 spark apps, the cluster successfully runs the 1st step, and terminates without moving on to the 2nd and 3rd steps.

Through some digging, I can see that Airflow "pokes" steps to see if they are still running. In this instance, it seems to think that it's "Successful" only when 1 step is finished.

My spark apps are fairly simple. The first creates and writes a dataframe to local HDFS. The 2nd reads the data from HDFS and joins to another data set and writes back to HDFS. The third step is s3-dist-cp to copy the data from HDFS to s3. All 3 steps can be successfully run within Spark-Shell interactively, or as Spark-Submit jobs. I have also cloned the EMR cluster myself (without airflow) and saw all steps succeed without any errors, so EMR and Spark are not the issue here.

The DAG is below


    from datetime import timedelta

    import airflow
    from airflow import DAG
    from airflow.contrib.operators.emr_create_job_flow_operator \
        import EmrCreateJobFlowOperator
    from airflow.contrib.operators.emr_add_steps_operator \
        import EmrAddStepsOperator
    from airflow.contrib.sensors.emr_step_sensor import EmrStepSensor
    from airflow.contrib.operators.emr_terminate_job_flow_operator \
        import EmrTerminateJobFlowOperator

    DEFAULT_ARGS = {
        'owner': 'airflow',
        'depends_on_past': False,
        'start_date': airflow.utils.dates.days_ago(2)   
    }

    SPARK_TEST_STEPS = [
        {
            'Name': 'monthly_agg',
            'ActionOnFailure': 'CONTINUE',
            'HadoopJarStep': {
                'Jar': 'command-runner.jar',
                'Args': ['spark-submit',
                          '--deploy-mode',
                           'cluster',
                          '--class' ,
                          'AggApp',
                          's3://jar1.jar' ]
                            }
        },   
        {
            'Name': 'monthly_agg2',
            'ActionOnFailure': 'CONTINUE',
            'HadoopJarStep': {
                'Jar': 'command-runner.jar',
                'Args': ['spark-submit',
                          '--deploy-mode',
                           'cluster',
                          '--class' ,
                          'SimpleApp',
                          's3:/jar2.jar' ]
                            }
        },  

        {
            'Name': 'copy-data',
            'ActionOnFailure': 'CONTINUE',
            'HadoopJarStep': {
                'Jar': 'command-runner.jar',
                'Args': ['s3-dist-cp',
                          '--src',
                          '/tempo',
                          '--dest',
                           's3://mydata/'
                        ]
                            }
        }
    ]

    JOB_FLOW_OVERRIDES = {
    'Instances': {'Ec2SubnetId': 'subnet-mysubnetid', 
        'InstanceGroups': [
            {
                'Name': 'Master nodes',
                'Market': 'ON_DEMAND',
                'InstanceRole': 'MASTER',
                'InstanceType': 'r4.2xlarge',
                'InstanceCount': 1
            },
            {
                'Name': 'Slave nodes',
                'Market': 'ON_DEMAND',
                'InstanceRole': 'CORE',
                'InstanceType': 'r4.2xlarge',
                'InstanceCount': 8,
                'EbsConfiguration': {'EbsBlockDeviceConfigs':[{'VolumeSpecification':{'SizeInGB':128,'VolumeType':'gp2'},'VolumesPerInstance':1}],'EbsOptimized':True}
            }
        ]},
        'Name':'airflow-monthly_agg_custom',
        'Configurations': [
        {
        'Classification':'spark-defaults','Properties':
            {'spark.sql.crossJoin.enabled':'true',
            'spark.serializer':'org.apache.spark.serializer.KryoSerializer',
            'spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version':'2',
            "maximizeResourceAllocation":"true"},
        'Configurations':[]
        },
        {
        'Classification':'spark-hive-site','Properties':
        {'hive.metastore.client.factory.class':'com.amazonaws.glue.catalog.metastore.AWSGlueDataCatalogHiveClientFactory'}, 
        'Configurations':[]
        }
        ]}

    dag = DAG(
        'monthly_agg_custom',
        default_args=DEFAULT_ARGS,
        dagrun_timeout=timedelta(hours=4),
        schedule_interval='@once'
    )

    cluster_creator = EmrCreateJobFlowOperator(
        task_id='create_job_flow',
        job_flow_overrides=JOB_FLOW_OVERRIDES,
        aws_conn_id='aws_default',
        emr_conn_id='emr_default',
        dag=dag
    )

    step_adder = EmrAddStepsOperator(
        task_id='add_steps',
        job_flow_id="{{ task_instance.xcom_pull('create_job_flow', key='return_value') }}",
        aws_conn_id='aws_default',
        steps=SPARK_TEST_STEPS,
        dag=dag
    )

    step_checker = EmrStepSensor(
        task_id='watch_step',
        job_flow_id="{{ task_instance.xcom_pull('create_job_flow', key='return_value') }}",
        step_id="{{ task_instance.xcom_pull('add_steps', key='return_value')[0] }}",
        aws_conn_id='aws_default',
        dag=dag
    )

    cluster_remover = EmrTerminateJobFlowOperator(
        task_id='remove_cluster',
        job_flow_id="{{ task_instance.xcom_pull('create_job_flow', key='return_value') }}",
        aws_conn_id='aws_default',
        dag=dag
    )

    cluster_creator.set_downstream(step_adder)
    step_adder.set_downstream(step_checker)
    step_checker.set_downstream(cluster_remover)
1 Answers

The problem is that you are feeding EmrStepSensor all steps in as one stepadder, so as soon as one finishes, it terminates the cluster.

The solution is to separate all steps and give the last step'id to the EmrStepSensor. Alternatively, you can only the last step with its separate step adder (step_adder_actual_step) from others and provided it to EmrStepSensor

step_adder_pre_step = EmrAddStepsOperator(
    task_id='pre_step',
    job_flow_id="{{ task_instance.xcom_pull('create_job_flow2', key='return_value') }}",
    aws_conn_id='aws_default',
    steps=pre_step,
    dag=dag
)

step_adder_actual_step = EmrAddStepsOperator(
    task_id='actual_step',
    job_flow_id="{{ task_instance.xcom_pull('create_job_flow2', key='return_value') }}",
    aws_conn_id='aws_default',
    steps=actual_step,
    dag=dag
)

step_checker = EmrStepSensor(
    task_id='watch_step',
    job_flow_id="{{ task_instance.xcom_pull('create_job_flow2', key='return_value') }}",
    step_id="{{ task_instance.xcom_pull('actual_step', key='return_value')[0] }}",
    aws_conn_id='aws_default',
    dag=dag
)

cluster_remover = EmrTerminateJobFlowOperator(
    task_id='remove_cluster',
    job_flow_id="{{ task_instance.xcom_pull('create_job_flow2', key='return_value') }}",
    aws_conn_id='aws_default',
    dag=dag
)

cluster_creator >> step_adder_pre_step >> step_adder_actual_step >> step_checker >> cluster_remover 
Related