GOGC=off設定がAmazon ECS Fargate においてamazon-ssm-agentに影響を与える問題の調査と解決

こちらは [Applibot Advent Calendar 2024](https://qiita.com/advent-calendar/2024/applibot) 4日目の記事になります。

本日は、Applibot SREチームの西村が務めさせていただきます。

# 1. はじめに
コンテナ化されたGoアプリケーションの運用において、予期せぬメモリリークは深刻な問題となります。
私たちは最近、Amazon ECS Fargateで稼働するアプリケーションにて特異なメモリ使用量の増加に直面しました。

ECS ExecuteCommand機能を有効にした環境で、コンテナのメモリ使用量が継続的に増加し、最終的にはタスクがOOMにより再起動するという事象が発生しました。
問題は一見、アプリケーションのメモリリークに見えましたが、調査を進めることでFargate環境特有の問題であることが判明しました。
アプリケーション自体のメモリ使用量は制限内に収まっているにもかかわらず、コンテナ全体のメモリは増加し続けていたのです。

この記事では、Go言語の環境変数がFargate内のamazon-ssm-agentに及ぼす影響と、その調査・解決プロセスについて共有します。
同様の問題に直面するチームの参考になれば幸いです。


## 1.1 要約
### 問題の概要
- ECS ExecuteCommand有効環境でメモリ使用量が継続的に増加
- アプリケーションのメモリ使用量は2000MiB以下で制御されている
- Fargateタスクは3072MBを確保しているが、上限に達し再起動が発生する

#### コンテナ メモリ使用量
![ECS_Cluster_Memory使用率__1_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38286/a5c53976-896f-8d54-b03e-24ecee70701a.png)


### 原因
- アプリケーションはGo言語で実装し、GC設定を最適化する為`GOGC=off`、`GOMEMLIMIT=2000MiB`を環境変数に設定
    - [過去の事例](https://ascii.jp/elem/000/004/218/4218294/2/)もあり、GCを最適化することでCPU負荷を軽減させる狙い
        - > Go自体の設定変更もパフォーマンス改善の効果をもたらした。GC（garbage collector）の頻度を減らすためにGOGCをオフにして、適切なGOMEMLIMITを設定することでCPU負荷は9%低下。
- [amazon-ssm-agent](https://github.com/aws/amazon-ssm-agent)もGo言語で実装されている為、環境変数の影響を受ける
  - Fargateで`enableExecuteCommand": true`とした場合、**同一コンテナ内**にssm-agentが起動する
    - ```bash
        # Fargate内のプロセス(PID 1がアプリケーション)
        $ ps aux
        PID USER TIME COMMAND
        1   root 1:04 /usr/src/hoge-server/api
        10  root 0:00 /managed-agents/execute-command/amazon-ssm-agent
        27  root 0:00 /managed-agents/execute-command/ssm-agent-worker
        ```
- 結果、amazon-ssm-agentにおいても`GOGC=off`となり、`GOMEMLIMIT`までGCが発生しないという状態となった


# 2. 原因

## 2.1 技術的背景

1. Amazon SSM Agentの特性
    - Go言語で実装されている
    - ECS ExecuteCommand有効時に同一コンテナで起動
    - ECSタスクの環境変数を継承
2. Go言語の環境変数の影響
    - `GOGC=off`：ガベージコレクションを完全に無効化
    - `GOMEMLIMIT`：メモリ使用量の上限を設定
    - コンテナ内の全Goプロセスで共通の設定となる

## 2.2 メモリ使用パターン

アプリケーション：
```
- 環境変数 : GOGC=off,GOMEMLIMIT=2000MiB
- 定期的なGCにより適切なメモリ解放
- heap_inuseメトリクスは安定して推移
```

Amazon SSM Agent：
```
- 同じく環境変数 : GOGC=off,GOMEMLIMIT=2000MiB が参照される
- GOMEMLIMIT=2000MiBまでGCは発生しない
- プロセス起動時間に比例して増加
```

となる為、アプリケーション 2GB + Amazon SSM Agent 2GB = 4GBのメモリ確保が必要となってしまった。


# 3. 調査プロセス

## 3.1 現象の確認

### 開発環境においてのメモリ使用状況比較
ssm agentのメモリ使用量に開きがあることを確認
```bash
devA環境：
api : 1886 MB
ssm : 244 MB

devB環境：
api : 391 MB
ssm : 31 MB

devC環境：
app : 1985 MB
ssm : 324 MB
```

### コンテナ内プロセスの確認
```bash
/usr/src/hoge-server # ps aux
PID USER TIME COMMAND
1   root 1:04 /usr/src/hoge-server/api
10  root 0:00 /managed-agents/execute-command/amazon-ssm-agent
27  root 0:00 /managed-agents/execute-command/ssm-agent-worker
```

### プロセスのRSSメモリ監視
```bash
# アプリケーションのメモリ使用量
$ cat /proc/1/status |grep VmRSS
VmRSS: 1886032 kB

# SSM Agentのメモリ使用量
$ cat /proc/10/status |grep VmRSS
VmRSS: 244452 kB
```


## 3.2 検証
GOGC,GOMEMLIMITを設定変更し経過観察
1. GOGC=off,GOMEMLIMIT=100MiB
2. GOGC=100（デフォルト）,GOMEMLIMIT=500MiB
3. GOGC=100（デフォルト）,GOMEMLIMIT削除

## 3.3 調査結果
![ECS_Fargate_api_Memory使用量.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38286/a2806e65-5444-6ce2-845a-696f842abae7.png)

1. GC無効
```
GOGC=off
GOMEMLIMIT=100MiB
結果：
メモリ使用量の継続的な増加を確認
GOMEMLIMIT=100MiBなので、一定以上の増加は抑えられている
```

2. メモリ制限あり
```
GOGC=100（デフォルト）
GOMEMLIMIT=500MiB
結果：時間経過によるメモリ使用量の増加なし
```

2. デフォルト設定
```
GOGC=100（デフォルト）
GOMEMLIMIT：なし
結果：時間経過によるメモリ使用量の増加なし
```

## 3.4 AWSサポートによる確認

AWSサポートへ問い合わせを行い、amazon-ssm-agentもGo製のアプリケーションである為、環境変数設定の影響はある旨を確認いただきました。

この問題は、Fargate環境におけるコンテナ内環境変数の共有とGo言語特有の設定が組み合わさったことで発生した複合的な課題でした。

# 4. 解決策

ExecuteCommandの適切な管理
```yaml
# 通常時は無効化
enableExecuteCommand: false

# デバッグ時のみ有効化(再デプロイの必要あり)
aws ecs update-service \
  --cluster ${cluster} \
  --service ${service} \
  --enable-execute-command
```

そもそもFargateのコンテナにログインしなくてはならない機会は、ほとんどなかったので無効としました。（この件の調査の為に一番使った・・・）
本質的にはDockerfile内でプロセス移動の際にGOGC設定を行うと良いかと思います。

# 5. まとめ

## 5.1 問題の再確認
- ECS Fargate & ExecuteCommand有効環境で発生したメモリリーク問題
- アプリケーションのGC最適化設定（`GOGC=off`）が、予期せずamazon-ssm-agentに影響
- コンテナ内で共有される環境変数が、想定外の動作を引き起こす事例

## 5.2 得られた知見
1. Fargate環境での注意点
   - 環境変数はコンテナ内の全プロセスで共有される
   - AWS管理のコンポーネントも同じ環境変数の影響を受ける
   - 特にGo言語の場合、ランタイム設定の影響範囲を慎重に考慮する必要がある

2. 運用面での教訓
   - ECS ExecuteCommandは必要時のみ有効化
   - デバッグツールの影響も考慮したリソース設計が重要
   - メモリ使用量の継続的なモニタリングの重要性


本事例は、Fargate環境における環境変数の影響範囲と、AWSマネージドサービスとの相互作用について、改めて確認する良い機会となりました。
同様の問題に直面した際の参考となれば幸いです。