ECS Task OOMKilled: przekroczenie limitu pamięci kontenera
Naprawa ECS Task OOMKilled: identyfikacja przyczyny przekroczenia limitu pamięci, dobór rozmiaru task definition, konfiguracja rezerwacji pamięci i limity twarde/miękkie.
Jeśli problem dotyczy połączeń z bazą danych po restarcie kontenera, zobacz nasz runbook o limicie połączeń RDS. Dla ogólnych problemów z zatrzymywaniem task, zobacz ECS Fargate Task zatrzymany niespodziewanie.
Objaw
Task pada i jest restartowany przez ECS service scheduler:
# Sprawdź przyczynę zatrzymania task
aws ecs describe-tasks \
--cluster my-cluster \
--tasks $(aws ecs list-tasks --cluster my-cluster --service-name my-service \
--desired-status STOPPED --query 'taskArns[0]' --output text) \
--query 'tasks[0].{Status:lastStatus,Reason:stoppedReason,Code:containers[0].exitCode}'
# Typowy output:
# {
# "Status": "STOPPED",
# "Reason": "OutOfMemoryError: Container killed due to memory usage",
# "Code": 137
# }
# Sprawdź metryki pamięci (ostatnia godzina)
aws cloudwatch get-metric-statistics \
--namespace AWS/ECS \
--metric-name MemoryUtilization \
--dimensions Name=ClusterName,Value=my-cluster Name=ServiceName,Value=my-service \
--start-time $(date -u -v-1H +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 60 --statistics Maximum \
--query 'sort_by(Datapoints, &Timestamp)[-10:].[Timestamp,Maximum]'
Kluczowe wskaźniki:
- Exit code 137 = SIGKILL (OOM killer)
- Exit code 1 z logami „java.lang.OutOfMemoryError” = JVM OOM (inny problem)
- MemoryUtilization >95% przed restartem
Przyczyna
1. Twardy limit pamięci za niski w task definition
Task definition ustawia memory (twardy limit) na wartość, którą aplikacja regularnie przekracza pod obciążeniem. Kernel (OOM killer) zabija kontener.
2. Wyciek pamięci (memory leak) w aplikacji
Aplikacja stopniowo alokuje pamięć bez zwalniania (cache bez polityki usuwania, wyciek goroutine, nieograniczona kolejka). Zużycie rośnie liniowo aż do limitu.
3. JVM heap nie ograniczony poprawnie (Java <10)
Kontener Java z limitem 512 MB, ale JVM w wersji starszej niż Java 10 (lub Java 8 przed aktualizacją 8u191) domyślnie alokuje 1/4 RAM hosta, nie kontenera. JVM nie widzi ograniczeń cgroup i ustawia heap na podstawie pamięci fizycznej maszyny.
Uwaga: Java 10+ i Java 8u191+ domyślnie respektują limity cgroup (-XX:+UseContainerSupport włączone domyślnie). Jeśli używasz nowoczesnej JVM, ten problem nie powinien wystąpić, chyba że jawnie wyłączyłeś UseContainerSupport.
4. Kontenery sidecar zużywają współdzieloną pamięć
Task z kontenerem aplikacji + sidecar (envoy, datadog-agent, log router). Sumaryczne zużycie przekracza limit pamięci na poziomie task.
5. Nagły wzrost ruchu powoduje skok alokacji
Aplikacja radzi sobie ze średnim ruchem, ale nagły wzrost ruchu (peak traffic) powoduje jednorazowy skok (np. wiele jednoczesnych odpowiedzi buforowanych w pamięci).
Naprawa
A) Zidentyfikuj rzeczywiste zużycie pamięci
# Container Insights (jeśli włączone)
aws logs filter-log-events \
--log-group-name "/aws/ecs/containerinsights/my-cluster/performance" \
--filter-pattern '{ $.TaskId = "TASK_ID" }' \
--start-time $(date -d '2 hours ago' +%s000)
# Alternatywnie - ECS Exec do running container
aws ecs execute-command \
--cluster my-cluster \
--task RUNNING_TASK_ARN \
--container my-app \
--interactive \
--command "/bin/sh"
# Wewnątrz kontenera:
cat /sys/fs/cgroup/memory/memory.usage_in_bytes
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
# Stosunek usage/limit > 0.85 = blisko OOM
B) Zwiększ memory limit w task definition
{
"family": "my-app",
"cpu": "512",
"memory": "1024",
"containerDefinitions": [
{
"name": "my-app",
"image": "my-app:latest",
"memoryReservation": 768,
"memory": 1024,
"essential": true
}
]
}
Zasady:
memory(twardy limit) = maksimum, po przekroczeniu kontener jest zabijanymemoryReservation(miękki limit) = wartość rezerwowana przy planowaniu rozmieszczenia. Kontener może użyć więcej, jeśli host ma wolną pamięć (tylko EC2 launch type)- Na Fargate:
memoryna poziomie task to twardy limit dla całego task. SumamemoryReservationwszystkich kontenerów nie może go przekroczyć.
# Zarejestruj nową wersję task definition
aws ecs register-task-definition --cli-input-json file://task-def.json
# Zaktualizuj service
aws ecs update-service \
--cluster my-cluster \
--service my-service \
--task-definition my-app:NEW_REVISION \
--force-new-deployment
C) Dobór rozmiaru (right-sizing): określ prawidłowy limit
# Zbierz dane o zużyciu z ostatnich 7 dni (p99)
aws cloudwatch get-metric-statistics \
--namespace AWS/ECS \
--metric-name MemoryUtilization \
--dimensions Name=ClusterName,Value=my-cluster Name=ServiceName,Value=my-service \
--start-time $(date -u -v-7d +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 3600 --statistics p99 \
--query 'max(Datapoints[].p99)'
Reguła: hard limit = p99 zużycia * 1.3 (30% headroom). Jeśli p99 to 700 MB, ustaw limit na 910 MB (zaokrąglij do najbliższej wartości Fargate: 1024 MB).
Dozwolone wartości na Fargate:
| CPU | Dozwolone memory (MB) |
|---|---|
| 256 | 512, 1024, 2048 |
| 512 | 1024, 2048, 3072, 4096 |
| 1024 | 2048, 3072, 4096, 5120, 6144, 7168, 8192 |
| 2048 | 4096 - 16384 (w krokach 1024) |
| 4096 | 8192 - 30720 (w krokach 1024) |
D) Napraw JVM memory (Java)
# Dockerfile - ustaw JVM heap relative to container limit
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=50.0"
# LUB jawnie:
# ENV JAVA_OPTS="-Xmx768m -Xms512m"
# Pamiętaj: JVM zużywa pamięć poza heap (metaspace, threads, native)
# Reguła: container memory = Xmx * 1.5
E) Ogranicz sidecar memory
{
"containerDefinitions": [
{
"name": "my-app",
"memory": 768,
"memoryReservation": 512,
"essential": true
},
{
"name": "datadog-agent",
"memory": 256,
"memoryReservation": 128,
"essential": false
},
{
"name": "envoy",
"memory": 128,
"memoryReservation": 64,
"essential": false
}
],
"memory": "1024"
}
Suma kontenerów (768+256+128 = 1152) może przekraczać limit task (1024) - to dopuszczalne na EC2, bo to miękkie limity. Na Fargate suma memory kontenerów musi być <= memory task.
Weryfikacja
# Sprawdź czy nowy deployment działa stabilnie
aws ecs describe-services \
--cluster my-cluster --services my-service \
--query 'services[0].{Running:runningCount,Desired:desiredCount,Deployments:deployments[0].status}'
# Monitoruj MemoryUtilization przez 24h
# Powinno być <80% task memory po stabilizacji
# Sprawdź historię zatrzymań (nie powinno być nowych OOM)
aws ecs list-tasks --cluster my-cluster --service-name my-service --desired-status STOPPED \
--query 'taskArns' --output text | wc -w
Jeśli service utrzymuje runningCount == desiredCount przez 24h bez restartów z kodem 137, problem rozwiązany. Ustaw CloudWatch Alarm na MemoryUtilization >80% aby reagować zanim dojdzie do kolejnego OOM.
Kontenery padają z OOM?
Umów bezpłatną 30-minutową rozmowę. Pomożemy z right-sizing i stabilizacją usług ECS/Fargate.