🛝

コンテナのCPU共有を観察してみる

に公開

はじめに

AWSのECS等のコンテナ実行基盤では、1つのタスクにアプリコンテナとサイドカー(プロキシ、メトリクスエージェント、ログルーターなど)を共存させることもあると思います。
タスク全体にCPU/メモリの上限を設定して、その中でコンテナ同士が共有のCPU/メモリを使用します。

サイドカーを足すとアプリが遅くなることは直感的に分かるものの、どのような変化になるのでしょうか?
今回は下記の観点で実験をしました。

  • サイドカーの負荷を上げるとどの程度アプリが遅くなるか
  • GoとNextで限界点はどれくらい違うか
  • メモリを増やせばCPUの占有等は変化があるか

実験

環境

項目 値
マシン Apple M2(8コア) macOS 15.4
Docker Docker Desktop 27.5.1(Linux VM上のcgroup v2)
Go 1.22-alpine
Next 14.2.5(Node 20-alpine)
負荷生成 hey 0.1.5(10並列、30秒間)

ECSでは、1タスク内の全コンテナが同一のcgroupでCPUを共有します。
一方、docker composeはコンテナごとに個別のcgroupを作ります。
この違いを解消するため、本実験では1つのDockerコンテナ内にアプリプロセスとサイドカープロセスを同居させ、docker run --cpus でCPU上限を制御する方式を採用しました。

実行構成

参考の実行コマンドです。

# 例: Next + 0.25 vCPU + サイドカー3個(各5%負荷)
docker run -d \
  --name ecs-task-exp \
  --cpus=0.25 \
  --memory=2g \
  -p 8080:8080 \
  -e APP_TYPE=next \
  -e SIDECAR_COUNT=3 \
  -e SIDECAR_LOAD=5 \
  ecs-task-runner

--cpus と --memory がcgroupのハードリミットになります。コンテナ内部ではentrypoint.shがアプリ1プロセス、サイドカーN個が起動し、全プロセスが同一cgroupの制限を共有します。

┌─── docker run --cpus=0.25 --memory=2g ────────────┐
│  cgroup v2 (cpu.max = 25000 100000)                │
│                                                     │
│  ┌──────────┐  ┌─────────┐┌─────────┐┌──────────┐ │
│  │ Go/Next  │  │ proxy   ││ agent   ││log_router│ │
│  │  (app)   │  │ 5% CPU  ││ 5% CPU  ││ 5% CPU   │ │
│  └──────────┘  └─────────┘└─────────┘└──────────┘ │
│       ↑                                             │
│    :8080                                            │
└─────────────────────────────────────────────────────┘
         ↑
   hey -z 30s -c 10 http://localhost:8080/work

Dockerイメージはマルチステージビルドで、Go binary・Next standalone・Pythonサイドカーを1つのイメージに収めています。

Dockerfile(クリックで展開)
# Stage 1: Go
FROM golang:1.22-alpine AS go-builder
WORKDIR /build
COPY app-go/go.mod app-go/main.go ./
RUN go build -o server main.go

# Stage 2: Next
FROM node:20-alpine AS next-builder
WORKDIR /build
COPY app-next/package.json app-next/package-lock.json* ./
RUN npm install
COPY app-next/ .
RUN npm run build

# Stage 3: Runtime
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends curl \
    && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
    && apt-get install -y --no-install-recommends nodejs \
    && rm -rf /var/lib/apt/lists/*

COPY --from=go-builder /build/server /app-go/server
COPY --from=next-builder /build/.next/standalone /app-next/
COPY --from=next-builder /build/.next/static /app-next/.next/static
COPY sidecar/sidecar.py /sidecar/sidecar.py
COPY task-runner/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

EXPOSE 8080
CMD ["/entrypoint.sh"]

変数

項目 設定値
タスクCPU 0.25 / 0.50 / 1.00 / 2.00 vCPU
アプリ種別 Go API (約0.6ms/req) / Next SSR (約2.0ms/req)
サイドカー数 0〜3個
サイドカー負荷 0〜35%/個(基本5%刻み、閾値付近は1%刻み)
メモリ上限 基本2GB(追加実験時のみ256MB〜4GB)

合計228条件(160 + 精密測定34 + メモリ変数化34)を実験しました。

計測対象とサイドカーの構成

メインアプリには、GoとNextで全く同じ「素数計算(2〜10,000のカウント)」を実装しました。
サイドカーはPythonスクリプトで、100msサイクルのbusy/sleepループにより指定したCPU使用率を再現します(例: 5%負荷 = 5ms busy + 95ms sleep)。

結果

サイドカー負荷とレイテンシの関係

サイドカー負荷を1%刻みで変化させた測定の結果です。

Go 0.25 vCPU + 3サイドカー:

合計負荷 P50(ms) P99(ms) RPS
0% 2.4 104.6 369
15% 3.7 109.1 245
18% 3.6 194.9 235
21% 3.6 194.7 238
24% 3.4 191.7 245
27% 3.5 189.8 248
30% 4.1 192.3 234
33% 3.4 194.1 240
36% 3.8 193.1 236
39% 3.8 195.1 241
42% 3.7 197.8 234
45% 3.4 196.6 226
75% 3.6 195.1 232
105% 3.3 195.8 235

Next 0.25 vCPU + 3サイドカー:

合計負荷 P50(ms) P99(ms) RPS
0% 7.3 288.4 233
3% 8.6 300.2 190
6% 89.6 397.5 131
9% 92.6 397.2 119
12% 97.8 482.0 93
15% 103.6 499.9 67
45% 199.1 693.7 50
75% 191.6 596.0 59
105% 193.6 598.7 55

測定
4構成の測定結果。左軸がレイテンシ、右軸がRPS。

まとめると下記になります。

構成 変化点(合計負荷) P99の変化
Go 0.25vCPU + 3サイドカー 15%→18% 109ms → 195ms
Next 0.25vCPU + 3サイドカー 3%→6% 300ms → 398ms
Next 0.50vCPU + 3サイドカー 18%→21% 100ms → 106ms
Next 0.25vCPU + 1サイドカー 6%→7% 299ms → 309ms

GoとNextの比較

0.25 vCPU + 3サイドカーで、同一条件のGo/Nextを並べた結果です。

合計負荷 Go P99(ms) Go RPS Next P99(ms) Next RPS
0% 104.6 369 288.4 233
15% 109.1 245 499.9 67
45% 196.6 226 693.7 50
75% 195.1 232 596.0 59
105% 195.8 235 598.7 55

構成別RPS残存率の推移
ベースラインRPSに対する残存率。Go 0.25vCPU(青)、Next 0.25vCPU(橙)、Next 0.50vCPU(緑)。

2.0 vCPUではGo/Nextともに合計105%負荷でもP99は62ms/14ms程度で安定していました。

メモリ上限の影響

CPU負荷を固定し、メモリ上限だけを256MB〜4GBに変えた結果です。
0.25〜0.50 vCPUではGo/Nextともにメモリによる差は出ませんでした。1.00 vCPUでの結果は以下の通りです。

メモリ上限 Go RPS Next RPS
512MB 1,403 1,960
1GB 1,427 2,256
2GB 1,393 2,308
4GB 1,436 3,344

(1.00vCPU, サイドカー3個, 各15%負荷)

Goはメモリに関わらず横ばいです。Nextは512MB→4GBでRPSが約1.7倍になっています。

Go vs Next メモリとRPSの関係
1.00 vCPU + 3サイドカー構成でのメモリ別RPS。

全条件データ

Go API 全条件RPS(クリックで展開)
vCPU 構成 負荷0% 5〜15% 25〜45% 50〜75% 105%
0.25 app_only 377 — — — —
0.25 +proxy 372 318 321 — —
0.25 +agent 379 274 266 266 —
0.25 +agent+log 369 245 226 232 235
0.50 app_only 888 — — — —
0.50 +proxy 889 759 746 — —
0.50 +agent 899 740 617 631 —
0.50 +agent+log 897 654 531 528 554
1.00 app_only 2601 — — — —
1.00 +proxy 2527 2218 2100 — —
1.00 +agent 2536 2276 1652 1829 —
1.00 +agent+log 2556 2044 1409 1463 1497
2.00 app_only 7649 — — — —
2.00 +proxy 7531 7128 6341 — —
2.00 +agent 7639 7106 6166 5075 —
2.00 +agent+log 7589 6835 5396 4371 4558
Next SSR 全条件RPS(クリックで展開)
vCPU 構成 負荷0% 5〜15% 25〜45% 50〜75% 105%
0.25 app_only 242 — — — —
0.25 +proxy 219 155 131 — —
0.25 +agent 222 93 75 70 —
0.25 +agent+log 233 67 50 59 55
0.50 app_only 1634 — — — —
0.50 +proxy 1500 1113 849 — —
0.50 +agent 1581 995 487 461 —
0.50 +agent+log 1690 696 273 338 346
1.00 app_only 4537 — — — —
1.00 +proxy 4567 4193 3233 — —
1.00 +agent 4334 4117 3074 2380 —
1.00 +agent+log 4672 3854 2196 1818 1966
2.00 app_only 5615 — — — —
2.00 +proxy 5442 5643 5346 — —
2.00 +agent 5665 5403 5533 5428 —
2.00 +agent+log 5663 5552 5257 5215 4572
P99レイテンシ全条件テーブル(クリックで展開)

Go API P99(ms):

vCPU\合計負荷 0% 15% 45% 75% 105%
0.25 104.6 109.1 196.6 195.1 195.8
0.50 93.6 97.9 100.9 99.9 99.5
1.00 77.6 80.5 85.1 87.1 86.2
2.00 42.5 47.1 56.2 62.7 62.5

Next SSR P99(ms):

vCPU\合計負荷 0% 15% 45% 75% 105%
0.25 288.4 499.9 693.7 596.0 598.7
0.50 77.7 100.1 199.8 194.0 192.8
1.00 18.1 34.1 66.7 79.1 78.6
2.00 4.0 4.0 4.6 4.9 14.5

(いずれもサイドカー3個構成)

まとめ

今回の実験では以下のことが確認できました。

  • 0.25 vCPUではサイドカーの合計負荷がわずか数%〜十数%で性能が変化する。アプリのCPU消費が大きいほど余裕がなくなる
  • 0.50 vCPU以上を確保すれば、サイドカー合計15%程度では影響が小さい
  • 2.00 vCPUでは合計105%負荷でもほぼ影響なし
  • メモリはCPUが足りていないときには効かない。CPUに余裕があるNextでは、メモリを増やすとRPSが改善した

Discussion