コンテナの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に対する残存率。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倍になっています。

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