電通総研 テックブログ

電通総研が運営する技術ブログ

AWS Fargateで実行したWebアプリケーションのマルチスレッド動作について調査した

みなさんこんにちは! 金融ソリューション事業部の市場系ソリューション1部の寺山です。12 月に入ってから一気に寒くなったように感じます。

本記事は電通国際情報サービス Advent Calendar 2022 の 12 月 22 日の記事です。
昨年のアドベントカレンダーでテックブログに参加して以来、普段の業務においても「何か記事にできることはないかな?」と考えるようになり、一周り充実した1年でした!

今回は、AWS Fargate で実行した Web アプリケーション(バックエンド API)のマルチスレッドが想定外の挙動となった点をインフラ視点で調査したので、発生した事象とその対応方法について紹介いたします!

発生した事象

概要

バックエンド API をホストするクラウド構成は下図のとおりです。本件で触れないサービスは割愛しています。

バックエンド API はコンテナ化し、AWS Fargate で ECS タスクとして起動します。API リクエストは Application Load Balancer が受け付け、ターゲットグループに登録した ECS サービスへルーティングする典型的なクラウドアーキテクチャです。
バックエンドAPIJava(11)で実装されています。

事象は、レスポンスの返却に時間を要するリクエストの処理中に発生しました。

ALB のターゲットグループのヘルスチェックは、バックエンド API のヘルスチェック用エンドポイントを指定していました。このエンドポイントがヘルスチェックのタイムアウト時間内に正常レスポンスを返却しないことを閾値の回数以上に繰り返したため、クライアントのリクエスト処理中の Fargate タスクが Unhealthy と判定され、停止されてしまいました。

タスクの停止理由に、Task failed ELB health checks in (target-group arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:targetgroup/m5infr-sandb-D87C6CX3M8FL/a5285befd3edc05d) と出力されています。

調査開始時の予想

私は事象の初回発生時、当該システム/プロジェクトには関与しておらず、後に調査を引き取った形になります。
この事象の話を聞いた当初は CPU リソースの不足が理由だろうからタスクサイズをチューニングすればよい(バックエンド API のタスクサイズは vCPU:1 でした)ものだと考えていました。

実際に検証したところ、私の予想とは異なる結果となりました。まずはその調査結果を紹介します。
事象が発生したアプリケーションとは別にアプリチームが調査用のアプリケーションを用意してくれたので、それを同じ構成で AWS にデプロイし検証しました。

4vCPU を割り当てた Fargate タスクで再現するか検証します。

API リクエストは、コンテナイメージの手動ビルド用途として作成した EC2 インスタンスで行いました。

$ curl http://<ALBのDNS名>:8080/sandbox/greet ; echo everyone!
hello!everyone!

/sandbox/greet はリクエストに対し、即座に hello! と返却する API です。上記のように正常時は curl コマンドでレスポンスを取得できます。

$ (curl http://<ALBのDNS名>:8080/sandbox/sleep ; echo done at `date`) &
[1] 3087
$ curl http://<ALBのDNS名>:8080/sandbox/greet ; echo everyone!
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
</body>
</html>
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
</body>
</html>
everyone!

次は、/sandbox/sleep エンドポイントをバックグラウンドジョブとして実行し、ジョブが終了した日付を表示するようにしています。/sandbox/sleepTimeUnit.sleepを無期限に実行する API です。応答に時間を要する API というのを簡易的に表現しています。
バックグラウンドジョブの後に /sandbox/greetcurl していますが、今度はレスポンスを取得できずに API コンテナが終了したため 502 エラーとなっています。

実際にタスクも停止されており、事象が再現しています。

sleep でアイドル状態になっているので自明ではあるのですが、リソース使用率も低いことがメトリクスより確認できます。

つまり、タスクサイズのチューニングにより解消するという私の予想には反して、本件はタスクサイズに依らず発生する事象であることがわかりました。

原因

JavaのavailableProcessors

バックエンド APIJava のマイクロサービス軽量フレームワークであるHelidonを使用しています。
このフレームワークではデフォルトの動作として、スレッドプール数をRuntime.getRuntime().availableProcessors()の戻り値に設定します。1

ECS/Fargate ではavailableProcessors が vCPU のサイズに依らず 1 となるため、すでにスレッドを使用中の場合に、リクエストを処理するためのスレッドを新たに取得できないことが原因です。

実際に確認してみます。

確認は、ECS Execを利用して Fargate 内で CLI 操作をすることにより行いました。

対象のタスクは先ほどと同じ 4vCPU を割り当てています。

$ aws ecs describe-tasks --cluster m5infrasurvey-poc-ecs-cluster --tasks 6218b4dd29c146a6ac120c5395a6e462
{
    "tasks": [
        {
            # 省略
            "cpu": "4096",
            # 省略
            "memory": "8192",
            # 省略
        }
    ],
    "failures": []
}

ECS Exec よりコンテナのシェルにアクセスし、JShellより availableProcessors を実行します。

$ aws ecs execute-command --cluster m5infrasurvey-poc-ecs-cluster --task 6218b4dd29c146a6ac120c5395a6e462 --container sandbox --interactive --command "sh"

The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.


Starting session with SessionId: ecs-execute-command-0dc2b2f03561dd7b9
#
# jshell
Dec 19, 2022 5:33:04 PM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 11.0.16.1
|  For an introduction type: /help intro

jshell> Runtime.getRuntime().availableProcessors()
$1 ==> 1

jshell> /exit
|  Goodbye

戻り値が 1 であることを確認しました。

比較として、EC2 インスタンスとして起動している Amazon Linux2 に Docker Server をインストールし、同じ Docker イメージを起動して確認してみます。

docker exec コマンドでシェルにアクセスして、上記同様に JShell を実行します。

$ docker exec -it sandbox "sh"
#
# lscpu
Architecture:                    x86_64
CPU op-mode(s):                  32-bit, 64-bit
Byte Order:                      Little Endian
Address sizes:                   46 bits physical, 48 bits virtual
CPU(s):                          8
On-line CPU(s) list:             0-7
Thread(s) per core:              2
Core(s) per socket:              4
Socket(s):                       1
NUMA node(s):                    1
Vendor ID:                       GenuineIntel
CPU family:                      6
Model:                           85
Model name:                      Intel(R) Xeon(R) Platinum 8124M CPU @ 3.00GHz
Stepping:                        4
CPU MHz:                         3416.106
BogoMIPS:                        5999.99
Hypervisor vendor:               KVM
Virtualization type:             full
L1d cache:                       128 KiB
L1i cache:                       128 KiB
L2 cache:                        4 MiB
L3 cache:                        24.8 MiB
NUMA node0 CPU(s):               0-7
Vulnerability Itlb multihit:     KVM: Mitigation: VMX unsupported
Vulnerability L1tf:              Mitigation; PTE Inversion
Vulnerability Mds:               Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown
Vulnerability Meltdown:          Mitigation; PTI
Vulnerability Mmio stale data:   Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown
Vulnerability Retbleed:          Vulnerable
Vulnerability Spec store bypass: Vulnerable
Vulnerability Spectre v1:        Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2:        Mitigation; Retpolines, STIBP disabled, RSB filling
Vulnerability Srbds:             Not affected
Vulnerability Tsx async abort:   Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown
Flags:                           fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe
                                 1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq ssse3 fma
                                 cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowpr
                                 efetch invpcid_single pti fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm mpx avx512f avx512dq rdseed adx
                                 smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves ida arat pku ospke
#
# jshell
Dec 20, 2022 9:07:44 AM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 11.0.16.1
|  For an introduction type: /help intro

jshell> Runtime.getRuntime().availableProcessors()
$1 ==> 8

jshell> /exit
|  Goodbye

インスタンスタイプは c5.2xlarge です。参考として上記では lscpu も実行しています。
コンテナ起動時に CPU オプションは指定していないため、ホストの CPU がそのまま見えています。availableProcessors() は EC2 インスタンスの vCPU 数を返却しており、ECS/Fargate とは挙動が異なっています。

cgroupのcpu.sharesパラメータ

コンテナ仮想化におけるリソース割り当てに利用されるcgroupのcpuサブシステムのsharesパラメータは、コンテナが 利用可能なCPUリソースのスケジューリングに利用されます。Dockerではrunコマンドの--cpu-sharesパラメータで指定します。 割合はcpu.sharesパラメータの値を1024で除算した値で計算されます。

Dockerの場合はこのパラメータにデフォルトで1024が適用されます。

https://docs.docker.jp/config/container/resource_constraints.html#cfs

コンテナへの配分を定めるもので、デフォルト値は1024 です。

つまり、ホスト上で実行するコンテナが1台で他にリソース制限を付与するパラメータが指定されていない場合、割合は1024/1024=1となりホストの CPUリソースを全て利用可能となります。

一方、ECSではデフォルト値としてcpu.shares2が適用されます。

https://aws.amazon.com/jp/blogs/news/how-amazon-ecs-manages-cpu-and-memory-resources/

注: コンテナに CPU ユニットを指定しない場合、ECS は内部的に cgroup に対して 2 (Linux カーネルが許容する最小値) という値の Linux CPU 配分 (cpu.shares) を設定します。

Fargateタスクに対し、Java-Xlog:os+container=traceオプションを使用してJVMのリソース認識状況を出力したものが以下になります。

$ aws ecs execute-command --cluster m5infrasurvey-poc-ecs-cluster --task 83d8e044611d4d22bbeaf65d164af34f --container sandbox --interactive --command "/bin/sh"

The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.


Starting session with SessionId: ecs-execute-command-0dc479f32fcd9891d
#
# java -Xlog:os+container=trace -version
[0.000s][trace][os,container] OSContainer::init: Initializing Container Support
[0.001s][debug][os,container] Detected cgroups hybrid or legacy hierarchy, using cgroups v1 controllers
[0.001s][trace][os,container] Path to /memory.use_hierarchy is /sys/fs/cgroup/memory/memory.use_hierarchy
[0.001s][trace][os,container] Use Hierarchy is: 1
[0.001s][trace][os,container] Path to /memory.limit_in_bytes is /sys/fs/cgroup/memory/memory.limit_in_bytes
[0.001s][trace][os,container] Memory Limit is: 9223372036854771712
[0.001s][trace][os,container] Non-Hierarchical Memory Limit is: Unlimited
[0.001s][trace][os,container] Path to /memory.stat is /sys/fs/cgroup/memory/memory.stat
[0.001s][trace][os,container] Hierarchical Memory Limit is: 8589934592
[0.001s][info ][os,container] Memory Limit is: 8589934592
[0.001s][trace][os,container] Path to /cpu.cfs_quota_us is /sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us
[0.001s][trace][os,container] CPU Quota is: -1
[0.001s][trace][os,container] Path to /cpu.cfs_period_us is /sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us
[0.001s][trace][os,container] CPU Period is: 100000
[0.001s][trace][os,container] Path to /cpu.shares is /sys/fs/cgroup/cpu,cpuacct/cpu.shares
[0.001s][trace][os,container] CPU Shares is: 2
[0.001s][trace][os,container] CPU Share count based on shares: 1
[0.001s][trace][os,container] OSContainer::active_processor_count: 1
[0.001s][trace][os,container] CgroupSubsystem::active_processor_count (cached): 1
[0.001s][trace][os,container] CgroupSubsystem::active_processor_count (cached): 1
# 省略

実際に CPU Share is: 2 と出力されていることが確認できました。2/1024≒0.002のためコンテナに割り当てられる CPUリソース数が極端に少なく、JVMは利用可能な CPU 数の最小値である1を適用する挙動をしたと判明しました。ECS/FargateとDockerにて挙動の異なる点にも説明がつきます。

対応

コンテナ定義のcpuパラメータの指定

前のセクションで説明した通り、cpu.sharesがデフォルトでは2であることが、タスクサイズに依らず1スレッドしか利用できない事象の深層的な原因であるとわかりました。

ECSにおいてcpu.sharesを指定するには、コンテナ定義のcpuパラメータを指定すれば良いです。タスク定義におけるタスクサイズのcpuとは別のパラメータであることに注意してください。

私の検証では環境構築に AWS CDK を利用したので、対応例としてソースコードの一部を紹介します。

利用しているバージョンは package.json の一部の紹介で代替いたします。

{
  //省略
  "devDependencies": {
    "@types/jest": "^29.2.3",
    "@types/node": "^18.11.9",
    "@typescript-eslint/eslint-plugin": "^5.20.0",
    "@typescript-eslint/parser": "^5.20.0",
    "aws-cdk": "^2.51.0",
    "eslint": "^8.13.0",
    "eslint-config-prettier": "^8.5.0",
    "jest": "^29.3.1",
    "prettier": "^2.6.2",
    "ts-jest": "^29.0.3",
    "ts-node": "^10.9.1",
    "typescript": "^4.9.3"
  },
  "dependencies": {
    "aws-cdk-lib": "^2.51.0",
    "constructs": "^10.0.0",
    "source-map-support": "^0.5.16"
  }
}
const sandboxTaskdef = new TaskDefinition(this, 'sandboxTaskdef', {
  compatibility: Compatibility.FARGATE,
  networkMode: NetworkMode.AWS_VPC,
  family: `${SYSTEM_NAME}-${props.m5Props.m5Env}-ecs-taskdef-sandbox`,
  executionRole: sandboxTaskRole,
  taskRole: sandboxTaskRole,
  cpu: '4096',
  memoryMiB: '8192',
});
sandboxTaskdef.addContainer('sandbox', {
  image: ContainerImage.fromEcrRepository(
    Repository.fromRepositoryArn(
      this,
      'sandbox',
      'arn:aws:ecr:ap-northeast-1:XXXXXXXXXXXX:repository/m5infrasurvey-poc-ecr-sandbox'
    )
  ),
  containerName: 'sandbox',
  cpu: 1024, // cpu.sharesに設定される
  /*省略*/
});

宣言したTaskDefinitionクラスのaddContaierメソッドからコンテナ定義のcpuパラメータを指定しています。

上記のコードをデプロイして起動されたFargateタスクのcpu.sharesを実際に確認してみます。
まずは起動されたタスクの情報です。

$ aws ecs describe-tasks --cluster m5infrasurvey-poc-ecs-cluster --tasks 46e1d6fd883c47419093ad0e84a41ad1
{
    "tasks": [
        {
            # 省略
            "containers": [
                {
                    "containerArn": "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:container/m5infrasurvey-poc-ecs-cluster/46e1d6fd883c4741
9093ad0e84a41ad1/342343c2-0d87-4cdf-b60a-4496307cfc33",
                    "taskArn": "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task/m5infrasurvey-poc-ecs-cluster/46e1d6fd883c47419093ad0e84
a41ad1",
                    "name": "sandbox",
                    # 省略
                    "managedAgents": [
                        {
                            "lastStartedAt": "2022-12-21T17:30:48.947000+09:00",
                            "name": "ExecuteCommandAgent",
                            "lastStatus": "RUNNING"
                        }
                    ],
                    "cpu": "1024"
                }
            ],
            "cpu": "4096",
            # 省略
        }
    ],
    "failures": []
}

タスクサイズとは別にコンテナ定義として"cpu": "1024"が反映されていることを確認しました。

続いて、JVMが認識するリソース情報と availableProcessors の戻り値を確認します。

$ aws ecs execute-command --cluster m5infrasurvey-poc-ecs-cluster --task 46e1d6fd883c47419093ad0e84a41ad1 --container sandbox --interactive --command "/bin/sh"

The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.


Starting session with SessionId: ecs-execute-command-0d914bdaa48aec635
#
# java -Xlog:os+container=trace -version
[0.000s][trace][os,container] OSContainer::init: Initializing Container Support
[0.001s][debug][os,container] Detected cgroups hybrid or legacy hierarchy, using cgroups v1 controllers
[0.001s][trace][os,container] Path to /memory.use_hierarchy is /sys/fs/cgroup/memory/memory.use_hierarchy
[0.001s][trace][os,container] Use Hierarchy is: 1
[0.001s][trace][os,container] Path to /memory.limit_in_bytes is /sys/fs/cgroup/memory/memory.limit_in_bytes
[0.001s][trace][os,container] Memory Limit is: 9223372036854771712
[0.001s][trace][os,container] Non-Hierarchical Memory Limit is: Unlimited
[0.001s][trace][os,container] Path to /memory.stat is /sys/fs/cgroup/memory/memory.stat
[0.001s][trace][os,container] Hierarchical Memory Limit is: 8589934592
[0.001s][info ][os,container] Memory Limit is: 8589934592
[0.001s][trace][os,container] Path to /cpu.cfs_quota_us is /sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us
[0.001s][trace][os,container] CPU Quota is: -1
[0.001s][trace][os,container] Path to /cpu.cfs_period_us is /sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us
[0.001s][trace][os,container] CPU Period is: 100000
[0.001s][trace][os,container] Path to /cpu.shares is /sys/fs/cgroup/cpu,cpuacct/cpu.shares
[0.001s][trace][os,container] CPU Shares is: 1024
[0.001s][trace][os,container] OSContainer::active_processor_count: 4
[0.001s][trace][os,container] CgroupSubsystem::active_processor_count (cached): 4
[0.001s][trace][os,container] CgroupSubsystem::active_processor_count (cached): 4
[0.037s][trace][os,container] Path to /cpu.cfs_quota_us is /sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us
[0.038s][trace][os,container] CPU Quota is: -1
[0.038s][trace][os,container] Path to /cpu.cfs_period_us is /sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us
[0.038s][trace][os,container] CPU Period is: 100000
[0.038s][trace][os,container] Path to /cpu.shares is /sys/fs/cgroup/cpu,cpuacct/cpu.shares
[0.038s][trace][os,container] CPU Shares is: 1024
[0.038s][trace][os,container] OSContainer::active_processor_count: 4
# 省略
#
# jshell
Dec 21, 2022 5:55:00 PM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 11.0.16.1
|  For an introduction type: /help intro

jshell> Runtime.getRuntime().availableProcessors()
$1 ==> 4

以下が確認できました。

  • CPU Shares is: 1024 と出力され、cpu.shares が意図通りに設定されていること
  • active_processor_count: 4 と出力され、タスクのvCPUと同値が認識されていること
  • availableProcessorsの戻り値が4と出力され、タスクのvCPUと同値であること

最後に、本件の事象が解消しているかを事象の再現方法と同じように確認します。

$ curl http://m5infrasurvey-poc-alb-sandbox-352400359.ap-northeast-1.elb.amazonaws.com:8080/sandbox/greet ; echo everyone!
hello!everyone!
$
$ (curl http://m5infrasurvey-poc-alb-sandbox-352400359.ap-northeast-1.elb.amazonaws.com:8080/sandbox/sleep ; echo done at `date`) &
[1] 4454
$ (curl http://m5infrasurvey-poc-alb-sandbox-352400359.ap-northeast-1.elb.amazonaws.com:8080/sandbox/sleep ; echo done at `date`) &
[2] 4457
$ (curl http://m5infrasurvey-poc-alb-sandbox-352400359.ap-northeast-1.elb.amazonaws.com:8080/sandbox/sleep ; echo done at `date`) &
[3] 4460
$
$ curl http://m5infrasurvey-poc-alb-sandbox-352400359.ap-northeast-1.elb.amazonaws.com:8080/sandbox/greet ; echo everyone!
hello!everyone!

スレッドプール数は 4 になっているので、事象の再現に使用した /sandbox/sleepAPI を 3 リクエスト受け付け後も、/sandbox/greetAPI からレスポンスを取得できています。想定通りの結果を得ることができました。

XX:ActiveProcessorCountオプションの利用

前のセクションの対応で、cpu.sharesの設定方法がわかりました。
しかしながら、18.0.2+, 17.0.5+, 11.0.17+からOpenJDKにてcpu.sharesを用いて利用可能なCPU数の制御を廃止する(2)ため、Javaのバージョン次第で前述した方法では事象が再現します。

そこで、Java の拡張ランタイムオプションである -XX:ActiveProcessorCount=N を利用します。このパラメータを利用すると、JVM が認識する CPU の数を上書きできます。
前述した対応ではコンテナのリソース量を環境に合わせて動的に割り当て可能ですが、こちらの場合は環境ごとにパラメータを変更する必要があります。

-XX:ActiveProcessorCount=Nを利用するケースにおいても、対応例としてAWS CDKのソースコードの一部を紹介します。

const cpuNum = 4;
const sandboxTaskdef = new TaskDefinition(this, 'sandboxTaskdef', {
  compatibility: Compatibility.FARGATE,
  networkMode: NetworkMode.AWS_VPC,
  family: `${SYSTEM_NAME}-${props.m5Props.m5Env}-ecs-taskdef-sandbox`,
  executionRole: sandboxTaskRole,
  taskRole: sandboxTaskRole,
  cpu: String(cpuNum * 1024),
  memoryMiB: '8192',
});
sandboxTaskdef.addContainer('sandbox', {
  image: ContainerImage.fromEcrRepository(
    Repository.fromRepositoryArn(
      this,
      'sandbox',
      'arn:aws:ecr:ap-northeast-1:XXXXXXXXXXXX:repository/m5infrasurvey-poc-ecr-sandbox'
    )
  ),
  containerName: 'sandbox',
  environment: {
    TZ: 'Asia/Tokyo',
    JAVA_TOOL_OPTIONS: `-XX:ActiveProcessorCount=${cpuNum}`, // 環境変数で-XX:ActiveProcessorCountを設定
  },
});

タスクに割り当てる vCPU の数をあらかじめ宣言しておき(cpuNum)、タスクサイズの cpu パラメータと -XX:ActiveProcessorCount= の値に使用します。
本番環境と開発環境間でタスクサイズは異なることが多いので、-XX:ActiveProcessorCount=JAVA_TOOL_OPTIONS環境変数を利用して外部から注入する形で付与するのがベターと考えました。
このようにクラウドのサービスやリソースを宣言的に構築できる点や、値やオブジェクト等を再利用可能なところもIaCのメリットですね!

前のセクションと同じように反映されたか確認します。

$ aws ecs execute-command --cluster m5infrasurvey-poc-ecs-cluster --task e4696c0103574ad0a07a8eabc1360087 --container sandbox --interactive --command "/bin/sh"

The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.


Starting session with SessionId: ecs-execute-command-0c5d73c5e08474f67
#
# jshell
Picked up JAVA_TOOL_OPTIONS: -XX:ActiveProcessorCount=4
Dec 20, 2022 6:58:07 PM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 11.0.16.1
|  For an introduction type: /help intro

jshell> Runtime.getRuntime().availableProcessors()
$1 ==> 4

JAVA_TOOL_OPTIONS 環境変数が適用されていること、および、availableProcessors の戻り値も指定した値になっていることが確認できました。
実際にAPIにリクエストを行う動作確認は前述と同じ結果になったため記載は割愛します。

非同期処理を検討する

前述までの対応は、タスクの vCPU を 2 以上に設定することが前提となっています。
常時多数のリクエストを受けるつけるようなワークロードであればよいですが、それ以外の場合はリソースに余剰が発生しコスト最適化の設計原則に反しています。

理想は、必要最小限のタスクサイズとタスク数で ECS サービスを起動し、負荷に応じてスケールすることです。

今回の発端となったような応答まで時間を要するような処理は非同期で実行することで、スレッドを長時間占有することを回避するという案です。
AWS においては SQS と Lambda を利用してメッセージキューイングによる非同期処理をサーバレスアーキテクチャで実現する方法が知られています。

上記の図の例では、バックエンドAPIenqueueに成功した時点でレスポンスを返却するため、本件の事象を回避するという意図になります。
非同期化した場合は API クライアント側にポーリングやリフレッシュの仕組みが必要になるため、API 仕様として許容可能かの調整も必要となる点には注意が必要です。

おわりに

今回は ECS/Fargate で発生した事象について紹介いたしました。
今後は他のクラウドプロバイダのコンテナ関連サービスにおける事象の発生有無についても調査したいと考えています。 最後までご覧いただきありがとうございました。皆さま良いお年を!


私たちは一緒に働いてくれる仲間を募集しています!

金融機関のようなお客様においてもクラウドファーストなアプローチやマイクロサービスのようなモダンな技術の採用が進みつつあります。ご自身のインフラ/クラウドスキルをエンタープライズ領域でも活用したい・挑戦したいという方がいらっしゃいましたら、ぜひご応募ください!

金融システム インフラ・アーキテクト 金融システム インフラエンジニア

執筆:寺山 輝 (@terayama.akira)、レビュー:@mizuno.kazuhiroShodoで執筆されました