AWS ParallelCluster スポットインスタンス中断時ローカルストレージのデータを S3 に退避する方法

AWS ParallelCluster スポットインスタンス中断時ローカルストレージのデータを S3 に退避する方法

2024.09.15

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

クラウド HPC において、スポットインスタンスの利用は非常にコスト効率の高い選択肢です。しかし、長時間のジョブ実行中にスポットインスタンスの中断が発生すると、計算中のデータを失うリスクがあります。本記事では、AWS ParallelCluster でスポットインスタンスを使用する際に、インスタンスストア(ローカルストレージ)のデータを S3 に退避する方法を紹介します。

本ブログで紹介するアーキテクチャは以下です。

RecureSpotData(2)

導入背景

AWS ParallelCluster では、コスト効率を高めるためにコンピュートノードでスポットインスタンスが活用されています。私のユースケースでは、インスタンスストア(ローカルストレージ)を中間ファイルの保存先として利用しています。インスタンスストアは高速な I/O 性能と一時的なストレージ領域を提供しますが、スポットインスタンスが中断された場合にデータを失うリスクがあります。

特に長期間実行されるジョブの場合、スポット中断によるデータロストは時間と、コスト面で手痛い出戻りとなります。そこで、スポット中断の通知を受けてから、インスタンスストアのデータを S3 に退避させる仕組みを構築することでこの課題を解決します。

短期間で終わるジョブの場合は再度ジョブを投げることとし、ここではその問題は扱いません。

アーキテクチャ概要

本構成では、インスタンスストア付きの EC2 インスタンス(インスタンスタイプ名に'd'が付くもの)のデフォルトマウントポイント /scratch 配下のデータを退避対象とします。

  1. EventBridge がスポットインスタンスの中断通知を検知
  2. Lambda が起動し、Systems Manager Run Command を実行
  3. 対象 EC2 インスタンスの /scratch 領域のデータを S3 へコピー

RecureSpotData(3)

青枠の箇所は CDK のサンプルコードを提供します。検証に使用したクラスターのコンフィグルも本ブログに掲載してあります。

https://github.com/bigmuramura/rescue-spot-data

注意事項

対応必須

コンピュートノードの IAM ロールに、退避用 S3 バケットへのアクセス権限の追加が必須です。ParallelCluster の設定で対応してください。

RecureSpotData(4)

その他留意点

  • スポット中断の通知から実際の中断までの時間は 2 分しかありません。
  • 大容量データの場合は全てを退避できない可能性があります
  • データ転送料と、データ転送速度の観点から、S3 の VPC エンドポイント(ゲートウェイ型)の利用を推奨します

実装の詳細

データ退避用の S3 バケット作成

S3 バケット名は任意の名前を設定できます。このサンプルではテスト目的でスタック削除時に S3 バケットも削除する設定になっていますが、実運用時はデータ保持のため RETAIN を推奨します。

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.RemovalPolicy.html

const backupBucket = new s3.Bucket(this, 'RescueSpotDataBucket', {
  bucketName: 'rescue-spot-data-bucket',
  removalPolicy: cdk.RemovalPolicy.DESTROY, # 実運用時は RETAIN 推奨
  autoDeleteObjects: true,
  encryption: s3.BucketEncryption.S3_MANAGED,
});

EventBridge でスポット中断検知

以下のブログで紹介されている内容です。

https://dev.classmethod.jp/articles/monitoring-spot-instance-by-eventbridge-and-sns/

データ退避コマンド実行 Lambda

データソースを /scratch としています。コンピュートノードの EBS にデータを保存している場合は、適宜パスを修正してください。

  • 中断対象のインスタンス ID は EventBridge で検知した情報から引いてきます
  • 退避先の S3 バケットは Lambda 環境変数に登録してあります
import boto3
import os
import json

ssm = boto3.client('ssm')
s3 = boto3.client('s3')

def handler(event, context):
    print("Received event: " + json.dumps(event, indent=2))

    instance_id = event['detail']['instance-id']
    bucket_name = os.environ['BUCKET_NAME']

    # SSM Run Command を使用してデータをバックアップ
    response = ssm.send_command(
        InstanceIds=[instance_id],
        DocumentName='AWS-RunShellScript',
        Parameters={
            'commands': [
                f'aws s3 sync /scratch s3://{bucket_name}/{instance_id}/'
            ]
        }
    )

    command_id = response['Command']['CommandId']
    print(f"Backup command sent. Command ID: {command_id}")

    return {
        'statusCode': 200,
        'body': json.dumps('Spot instance interruption handled successfully!')
    }

コンピューノードの IAM ロールに権限追加(対応必須)

コンピュートノードから退避用の S3 バケットへアクセスできる権限が必要です。以下の方法を参考に、ParallelCluster の設定を変更または追加してください。

https://dev.classmethod.jp/articles/how-to-attach-multiple-iam-policies-to-aws-parallelcluster/

設定する IAM ポリシーの例を以下に示します。S3 バケット名は、作成したバケットの実際の名前に置き換えてください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "s3:ListAllMyBuckets",
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket",
                "s3:GetBucketLocation"
            ],
            "Resource": "arn:aws:s3:::rescue-spot-data-bucket"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:PutObjectAcl",
                "s3:GetObject",
                "s3:GetObjectAcl",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::rescue-spot-data-bucket/*"
        }
    ]
}

動作検証

インスタンスストア持ちのスポットインスタンスが起動可能なクラスター環境と、本記事で紹介したスポット中断検知の仕組みを CDK からデプロイした環境で動作確認します。

  1. 検証環境でのスポットインスタンス起動
  2. ダミーデータ作成
  3. AWS FIS からスポット中断リクエスト
  4. S3 へのデータ退避の確認

RecureSpotData(1)

検証環境

項目 値
CDK 2.158.0 (build 4b8714d)
AWS ParallelCluster 3.10.1
OS Ubuntu 22.04
Compute Node c7gd.large
Simultaneous Multi-Threading 無効

以下のクラスターコンフィグから作成したクラスター環境を使用しました。

クラスターコンフィグ折りたたみ
Region: ap-northeast-1
Image:
  Os: ubuntu2204
Tags:
  - Key: Name
    Value: rescue-spot-cluster

# ----------------------------------------------------------------
# Head Node Settings
# ----------------------------------------------------------------
HeadNode:
  InstanceType: t4g.micro
  Networking:
    ElasticIp: false
    SubnetId: subnet-035be95eeaa091603
  LocalStorage:
    RootVolume:
      Size: 40
      Encrypted: true
      VolumeType: gp3
      Iops: 3000
      Throughput: 125
  Iam:
    AdditionalIamPolicies:
      - Policy: arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
      - Policy: arn:aws:iam::123456789012:policy/transfer-s3express

# ----------------------------------------------------------------
# Compute Node Settings
# ----------------------------------------------------------------
Scheduling:
  Scheduler: slurm
  SlurmSettings:
    ScaledownIdletime: 5
  SlurmQueues:
  # ------ Compute 1 ------
    - Name: p1
      ComputeResources:
        - Name: test
          Instances:
            - InstanceType: t4g.micro
          MinCount: 0
          MaxCount: 30
          DisableSimultaneousMultithreading: true
      ComputeSettings:
        LocalStorage:
          RootVolume:
            Size: 40
            Encrypted: true
            VolumeType: gp3
            Iops: 3000
            Throughput: 125
      CapacityType: SPOT
      AllocationStrategy: price-capacity-optimized
      Networking:
        SubnetIds:
          - subnet-035be95eeaa091603
          - subnet-0ba7369f9caba6f93
        PlacementGroup:
          Enabled: false
      Iam:
        AdditionalIamPolicies:
          - Policy: arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
          - Policy: arn:aws:iam::123456789012:policy/transfer-s3express
    # ------ Compute 2 ------
    - Name: p2
      ComputeResources:
        - Name: c7glarge
          Instances:
            - InstanceType: c7g.large
          MinCount: 0
          MaxCount: 30
          DisableSimultaneousMultithreading: true
      ComputeSettings:
        LocalStorage:
          RootVolume:
            Size: 40
            Encrypted: true
            VolumeType: gp3
            Iops: 3000
            Throughput: 125
      CapacityType: SPOT
      AllocationStrategy: price-capacity-optimized
      Networking:
        SubnetIds:
          - subnet-035be95eeaa091603
          - subnet-0ba7369f9caba6f93
        PlacementGroup:
          Enabled: false
      Iam:
        AdditionalIamPolicies:
          - Policy: arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
          - Policy: arn:aws:iam::123456789012:policy/transfer-s3express
    # ------ Compute 2 ------
    - Name: p3
      ComputeResources:
        - Name: c7gdlarge
          Instances:
            - InstanceType: c7gd.large
          MinCount: 0
          MaxCount: 30
          DisableSimultaneousMultithreading: true
      ComputeSettings:
        LocalStorage:
          RootVolume:
            Size: 40
            Encrypted: true
            VolumeType: gp3
            Iops: 3000
            Throughput: 125
      CapacityType: SPOT
      AllocationStrategy: price-capacity-optimized
      Networking:
        SubnetIds:
          - subnet-035be95eeaa091603
          - subnet-0ba7369f9caba6f93
        PlacementGroup:
          Enabled: false
      Iam:
        AdditionalIamPolicies:
          - Policy: arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
          - Policy: arn:aws:iam::123456789012:policy/transfer-s3express

# ----------------------------------------------------------------
# Shared Storage Settings
# ----------------------------------------------------------------
# SharedStorage:
#   - MountDir: /mnt/efs-elastic
#     Name: efs1
#     StorageType: Efs
#     EfsSettings:
#       FileSystemId: fs-0846dc947572a66a1
#   - MountDir: /mnt/efs-bursting
#     Name: efs2
#     StorageType: Efs
#     EfsSettings:
#       FileSystemId: fs-046b02d3ba107c2b2

# ----------------------------------------------------------------
#  Other Settings
# ----------------------------------------------------------------
Monitoring:
  Logs:
    CloudWatch:
      Enabled: true
      RetentionInDays: 180
      DeletionPolicy: "Delete"
  Dashboards:
    CloudWatch:
      Enabled: false

コンピュートノードを起動

テストジョブをサブミットして起動したコンピュートノードへ、AWS Systems Manager のセッションマネージャーを使用して接続しました。

インスタンスストアが/scratchにマウントされていることを確認します。

ubuntu@p3-dy-c7gdlarge-1:~$ df -h | grep ephemeral
/dev/mapper/vg.01-lv_ephemeral          108G   24K  103G   1% /scratch

ダミーデータとして、100K のファイルを 1000 個作成しました。

$ cd /scratch
$ dd if=/dev/zero of=test.file bs=100K count=1;for i in {1..1000};do cp test.file test_${i}.file;done

スポットインスタンスの中断

AWS FIS を利用してコンピュートノードにスポット中断イベントを注入します。詳細は以下のブログを確認してください。

https://dev.classmethod.jp/articles/aws-fis-spot-instance-interruptions-with-aws-parallelcluster/

中断イベント注入後、コンピュートノードのメタデータから 05:14:58 に終了されることが確認できました。

$ TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"` && curl -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/spot/instance-action
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    56  100    56    0     0  62992      0 --:--:-- --:--:-- --:--:-- 56000
*   Trying 169.254.169.254:80...
* Connected to 169.254.169.254 (169.254.169.254) port 80 (#0)
> GET /latest/meta-data/spot/instance-action HTTP/1.1
> Host: 169.254.169.254
> User-Agent: curl/7.81.0
> Accept: */*
> X-aws-ec2-metadata-token: AQAEANPE2BWOy3bN6eoXPwNMbJvXdHEjMTTAH_UGeVNZIN6B_urT4g==
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< X-Aws-Ec2-Metadata-Token-Ttl-Seconds: 21600
< Content-Type: text/plain
< Accept-Ranges: none
< Last-Modified: Fri, 13 Sep 2024 05:12:58 GMT
< Content-Length: 52
< Date: Fri, 13 Sep 2024 05:13:43 GMT
< Server: EC2ws
< Connection: close
<
* Closing connection 0
{"action":"terminate","time":"2024-09-13T05:14:58Z"}

S3 バケット確認

中断イベントを EventBridge で検知し Lambda が起動して、コンピュートノードに作成したダミーデータが S3 バケットへコピーされていました。

rescue-spot-data-bucket_-_S3_バケット___S3___ap-northeast-1

動作確認は終了です。

まとめ

本記事で紹介した方法により、スポットインスタンスの中断時でもローカルデータの退避が可能になります。AWS ParallelCluster を使用した HPC 環境において、長時間実行するジョブでスポットインスタンスとローカルストレージを併用している場合は、本構成の導入をご検討ください。

肝心なのは中間ファイルから処理を再実行できるかなので、AWS ParallelCluster の環境下で再実行できるのか、できないのか、できる場合は再実行方法も事前に検証しておくと良いでしょう。

おわりに

EventBridge から直接 SSM の Run Command を呼び出すことは可能ですが、中断通知が発行されたインスタンス ID を動的に指定できないという制限がありました。そのため、Lambda を挟む構成を採用しました。

この構成には以下の懸念事項があります。

  • Lambda のコールドスタート
    • 多くの場合は Lambda はコールドスタートとなり、貴重な 2 分間の中断通知時間のうち、わずかではありますが Lambda の起動待ちに時間を費やします。
  • 大容量ファイルの退避
    • 数 GB の大容量ファイルが大量に生成されるケースでは、2 分間で退避が完了しない可能性はあります
    • Lambda のメモリサイズを上げることで処理速度が上がる可能性はありますので、中間ファイル生成のワークロードに応じた調整する余地があります。
  • スポット中断の通知すべてに反応してしまう
    • ParallelCluster のコンピュートノードの場合のみ Lambda を実行したかったのですが、判定に使える情報が見当たりませんでした。

大量のファイルへのアクセス速度を向上させる S3 Express の採用を検討しましたが、現時点で CDK の L2 Construct が対応していないことが分かりました。
今後の展望としては、CDK の S3 Express 対応されたら改めて設計を見直したいと思います。

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3express-readme.html

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事