~/blog / exit-code-137-oomkilledback

Exit code 137 — konteynerni kim o'ldirdi?

8 daqiqa o'qish

docker compose ps -a da api qatorida Exited (137). Loglar yarim qatorda uzilgan: stack trace yo'q, "shutting down" yo'q, hech narsa yo'q. restart: unless-stopped bo'lsa, manzara yanada chalkash — konteyner o'zi qayta ko'tariladi va bir necha daqiqadan keyin yana yo'qoladi.

Ilova hech narsa yozmaganining sababi oddiy: unga imkon berilmagan. 137 jarayon SIGKILL bilan o'ldirilganini bildiradi, SIGKILL'ni esa na ushlab, na e'tiborsiz qoldirib bo'ladi. Demak savol "nima buzildi" emas, "kim o'ldirdi".

Chiqish kodini o'qish: 128 + signal#

Jarayon o'zi exit(n) bilan tugasa, kod — n. Signal bilan o'ldirilsa, shell va Docker uni 128 + signal raqami ko'rinishida beradi: 137 − 128 = 9, ya'ni SIGKILL. Jadvalni yodlash shart emas, bash'ning o'zi tarjima qiladi:

console
$ kill -l 137
KILL
$ kill -l 143
TERM

Amalda eng ko'p uchraydigan kodlar:

KodMa'nosiOdatiy sabab
0Jarayon o'zi muvaffaqiyatli tugadiBir martalik vazifa ishini bitirdi yoki ilova SIGTERM'ni ushlab, toza yopildi
1Ilova xato bilan chiqdiTutilmagan istisno, konfiguratsiya xatosi — sababi docker logs da
125Docker'ning o'zi konteynerni ishga tushira olmadidocker run ga noto'g'ri flag yoki parametr berilgan
126Buyruq topildi, lekin bajarib bo'lmadiBajarish huquqi yo'q: chmod +x qilinmagan entrypoint.sh
127Buyruq topilmadiImage'da bunday dastur yo'q yoki u PATH da emas
137128 + 9, SIGKILLOOM killer, docker kill, to'xtash muddati tugashi
139128 + 11, SIGSEGVNative kodda xotiraga noto'g'ri murojaat
143128 + 15, SIGTERMTo'xtatish signali jarayonni to'g'ridan-to'g'ri tugatdi, handler yo'q edi

125, 126 va 127 ni odatda docker run ning o'zi qaytaradi — konteyner umuman ishga tushmagan. 126 va 127 da konteyner yaratilgan bo'ladi: docker inspect da u created holatida qoladi, sababi esa State.Error maydonida yoziladi, masalan exec: "nosuchcmd": executable file not found in $PATH. 127 konteyner ichidan ham kelishi mumkin: skriptdagi buyruq topilmasa, shell 127 bilan chiqadi — alpine'da logda sh: nosuchcmd: not found qatori qoladi.

Uchta gumondor#

137 ning uchta odatiy manbai bor va uchalasi ham bir xil kod qoldiradi:

  1. OOM killer. Konteyner xotira chegarasiga yetdi yoki butun serverda xotira tugadi — yadro jarayonni o'ldirdi.
  2. docker kill. Qo'lda yoki skriptdan. docker kill standart holatda aynan SIGKILL yuboradi.
  3. To'xtash muddati tugadi. docker stop, docker compose down yoki deploy paytida konteynerni qayta yaratish avval SIGTERM yuboradi. Jarayon belgilangan vaqtda chiqmasa — SIGKILL.

Kod bir xil, dalillar esa har xil joyda yotadi. Quyidagi misollarda uchala holat yonma-yon yaratilgan: oom-demo 64 MB chegarada tail /dev/zero bilan xotirani to'ldiradi, kill-demo va stop-demo da oddiy sleep ishlaydi, birinchisiga docker kill, ikkinchisiga docker stop qilingan.

1-dalil: State.OOMKilled#

console
$ docker inspect -f '{{.State.ExitCode}} {{.State.OOMKilled}}' oom-demo
137 true
$ docker inspect -f '{{.State.ExitCode}} {{.State.OOMKilled}}' kill-demo
137 false
$ docker inspect -f '{{.State.ExitCode}} {{.State.OOMKilled}}' stop-demo
137 false

OOMKilled true bo'lsa — aybdor xotira, javob topildi. false bo'lsa, qolgan ikki gumondorni ajratish kerak. Ikki nozik joyni bilib qo'ying:

  • Bayroq konteyner qayta ishga tushganda tozalanadi. restart: unless-stopped bo'lsa, siz qaraguningizcha konteyner allaqachon qayta turgan va OOMKilled yana false bo'lishi mumkin. Buni RestartCount o'sib borayotganidan payqaysiz: docker inspect -f '{{.RestartCount}}' <konteyner>.
  • OOM killer butun konteynerni emas, bitta bola jarayonni o'ldirishi ham mumkin — masalan, gunicorn yoki celery worker'ini. Asosiy jarayon tirik qoladi, konteyner Up holatida, chiqish kodi yo'q, lekin OOMKilled baribir true. gunicorn'ning yangi versiyalari bunday holatda logga Worker (pid:...) was sent SIGKILL! Perhaps out of memory? deb yozadi.

2-dalil: yadro logi#

OOM killer — yadro mexanizmi, u har bir qurbonni yadro logiga yozadi. Har bir hodisa uchun shunday juftlik chiqadi (oom-demo niki):

console
$ sudo dmesg | grep -E 'oom-kill:|Killed process'
[246850.337591] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=8762c0287c36a5e44be9e809a5fef78ae208071eb293990745366004a3895de3,mems_allowed=0,oom_memcg=/docker/8762c0287c36a5e44be9e809a5fef78ae208071eb293990745366004a3895de3,task_memcg=/docker/8762c0287c36a5e44be9e809a5fef78ae208071eb293990745366004a3895de3,task=tail,pid=84114,uid=0
[246850.339929] Memory cgroup out of memory: Killed process 84114 (tail) total-vm:66392kB, anon-rss:64512kB, file-rss:712kB, shmem-rss:0kB, UID:0 pgtables:168kB oom_score_adj:0

Qatorlarni shunday o'qing:

  • constraint=CONSTRAINT_MEMCG va Memory cgroup out of memory — konteyner o'z chegarasiga urildi. Butun server xotirasi tugaganda CONSTRAINT_NONE va Out of memory: Killed process bo'ladi. Bu holatda yadro qurbonni butun server bo'yicha tanlaydi, va u aybdor bo'lmasligi mumkin.
  • task=tail,pid=84114 — kim o'ldirildi.
  • oom_memcg=/docker/<id> — qaysi konteyner. Yo'l cgroup drayveriga bog'liq: systemd drayverida u /system.slice/docker-<id>.scope ko'rinishida bo'ladi. ID ni docker ps -a --no-trunc bilan solishtiring.
  • anon-rss:64512kB — o'lim paytida jarayon ushlab turgan xotira: 64 MB chegaraning deyarli hammasi.

systemd'li serverda o'sha yozuvlar journalctl -k da ham bor. Jurnal diskda saqlanadigan bo'lsa, oldingi yuklanishdagi yadro loglari journalctl -k -b -1 bilan ochiladi — server reboot bo'lgandan keyin kerak bo'ladigan narsa.

3-dalil: docker events#

Docker daemon har bir kill, oom va die hodisasini qayd etadi, kill da esa qaysi signal yuborilganini ham yozadi. Konteyneringiz bo'yicha so'rang:

bash
docker events --since 1h --until 0s --filter container=myapp-api-1 \
  --format '{{.Action}} signal={{index .Actor.Attributes "signal"}} exitCode={{index .Actor.Attributes "exitCode"}}'

Uchala demo konteynerda ketma-ketlik shunday chiqdi:

GumondorHodisalar
OOM killeroom, keyin die (exitCode=137) — kill hodisasi yo'q
docker killkill (signal=9), keyin die (exitCode=137)
To'xtash muddatikill (signal=15), kill (signal=9), keyin die (exitCode=137)

Oxirgi holatni daemon logi ham tasdiqlaydi: journalctl -u docker da Container failed to exit within 10s of signal 15 - using the force mazmunidagi qator chiqadi.

Xotira: chegara qo'ying va uni o'lchab tanlang#

Chegarasiz konteyner serverdagi butun xotirani olishi mumkin. Xotira tugaganda yadro odatda eng ko'p xotira ushlab turgan jarayonni tanlaydi — bu xotirani oqizayotgan worker emas, qo'shni konteynerdagi Postgres bo'lishi mumkin. Chegara bilan portlash konteyner ichida qoladi:

yaml
services:
  worker:
    image: python:3.12-alpine
    command: python worker.py
    deploy:
      resources:
        limits:
          memory: 512M
    memswap_limit: 512M

deploy.resources.limits.memory ni Compose v2 Swarm'siz ham qo'llaydi, eski mem_limit kaliti ham ishlaydi. memswap_limit nega kerak: faqat xotira chegarasi berilsa, Docker konteynerga yana shuncha swap ishlatishga ruxsat beradi (serverda swap yoqilgan bo'lsa) — docker inspect da HostConfig.MemorySwap chegaradan ikki barobar katta bo'lib turadi. Swap'ga tushgan servis darhol o'lmaydi, lekin keskin sekinlashishi mumkin, va bu 137 dan ham qiyin topiladigan nosozlik. memswap_limit ni chegaraga teng qo'ysangiz, swap yopiladi.

Qiymatni taxmin qilmang — real yuklama ostida o'lchang. LIMIT ustunida chegara ko'rinsa, u qo'llangan:

console
$ docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}' myapp-worker-1
NAME             MEM USAGE / LIMIT   MEM %
myapp-worker-1   528KiB / 512MiB     0.10%

Bu yerda worker bo'sh turibdi. Eng og'ir vazifa ishlayotgan paytdagi eng yuqori sarfni ko'ring va ustiga zaxira qo'shing. Heap hajmini o'zi tanlaydigan runtime'larga chegarani aniq ayting: JVM'da -XX:MaxRAMPercentage, Node'da --max-old-space-size. Postgres'da esa shared_buffers va work_mem konteyner chegarasiga qarab tanlanishi kerak — ular o'z-o'zidan chegaraga moslashmaydi.

To'xtash: SIGTERM ilovaga yetib borishi kerak#

docker stop va docker compose down konteynerga avval to'xtash signalini yuboradi. Standart holatda bu SIGTERM, image'da STOPSIGNAL bo'lsa — o'sha. Masalan, rasmiy postgres image'ida SIGINT (Postgres uchun tez, lekin toza to'xtash), nginx image'ida SIGQUIT (ochiq ulanishlarni tugatib to'xtash). Keyin Docker kutadi: docker stop da 10 soniya, Compose'da stop_grace_period qancha bo'lsa, shuncha (standart qiymati ham 10 soniya). Jarayon shu vaqtda chiqmasa — SIGKILL va 137.

Ilova SIGTERM'ni ko'pincha PID 1 bo'lgani uchun eshitmaydi. Yadro konteynerdagi PID 1 ga faqat u handler o'rnatgan signallarni yetkazadi, SIGKILL bundan mustasno. Handler'i yo'q ilova PID 1 sifatida SIGTERM'ni umuman sezmaydi. Buni oddiy sleep bilan ko'rish mumkin:

console
$ docker stop plain with-init
plain
with-init
$ docker inspect -f '{{.Name}} {{.State.ExitCode}}' plain with-init
/plain 137
/with-init 143

Ikkalasida ham alpine image'ida sleep 300 ishlayapti, farqi — with-init --init bilan ishga tushirilgan. plain da sleep PID 1, SIGTERM uchun handler'i yo'q: signal yo'qoladi va muddat tugagach Docker SIGKILL yuboradi. with-init da PID 1 — docker-init (bu tini). U SIGTERM'ni bolasiga uzatadi, sleep oddiy jarayon sifatida darhol o'ladi, tini esa 128 + 15 = 143 bilan chiqadi.

Yechim uch qismdan iborat, ko'pincha uchalasi birga kerak.

Ilovaning o'zi SIGTERM'ni ushlasin. gunicorn va uvicorn buni o'zi qiladi. O'z worker siklingiz bo'lsa, handler yozing:

python
import signal

running = True


def stop(signum, frame):
    global running
    running = False  # joriy vazifa tugaydi, yangisi olinmaydi


signal.signal(signal.SIGTERM, stop)

while running:
    process_next_job()  # sizning ish funksiyangiz

process_next_job navbatni timeout bilan o'qishi kerak: aks holda sikl yangi vazifa kelguncha kutib qoladi va running ni tekshira olmaydi.

Shell o'ramida exec. entrypoint.sh dagi oxirgi buyruq exec bilan chaqirilmasa, PID 1 shell bo'lib qoladi va ilova SIGTERM'ni ko'rmaydi:

bash
#!/bin/sh
set -e
alembic upgrade head
exec gunicorn app:app --bind 0.0.0.0:8000

exec shell jarayonini gunicorn bilan almashtiradi — signalni endi to'g'ridan-to'g'ri ilova oladi. PID 1 da kim turganini docker compose exec api cat /proc/1/comm ko'rsatadi.

init: true. Compose'da bu kalit PID 1 ga docker-init ni qo'yadi. U signallarni bolaga uzatadi va tugagan bola jarayonlarni yig'adi — ilova o'zi jarayon ochsa, zombi'lar to'planib qolmaydi.

Signal yetib borsa ham, yopilishga vaqt kerak. Muddatni ilovaning haqiqiy ehtiyojiga moslang. Masalan, gunicorn'ning --graceful-timeout standart qiymati 30 soniya: gunicorn joriy so'rovlarni 30 soniyagacha kutishga tayyor, Docker esa 10 soniyada SIGKILL yuboradi.

yaml
services:
  api:
    image: python:3.12-alpine
    command: gunicorn app:app --bind 0.0.0.0:8000 --graceful-timeout 30
    init: true
    stop_grace_period: 40s

Qo'llanganini tekshiring:

console
$ docker inspect -f '{{.Config.StopTimeout}} {{.HostConfig.Init}}' myapp-api-1
40 true

To'xtatilgandan keyingi kod ham ko'p narsa aytadi:

  • 0 — ilova SIGTERM'ni ushladi va o'zi toza chiqdi. Maqsad shu.
  • 143 — SIGTERM jarayonni handler'siz o'ldirdi. Tez, lekin ilovaning yopilish kodi ishlamadi.
  • 137 — muddat ichida chiqmadi va SIGKILL oldi. Yarim yozilgan fayllar va uzilgan so'rovlar shu yerdan keladi.

Qisqa tekshiruv tartibi#

  1. docker inspect -f '{{.State.ExitCode}} {{.State.OOMKilled}} {{.RestartCount}}' <konteyner> — kod, OOM bayrog'i va konteyner necha marta qayta ishga tushgani.
  2. sudo dmesg | grep -E 'oom-kill:|Killed process' yoki journalctl -k — yadro kimni va nima uchun o'ldirgani.
  3. docker events — oom, kill va die ketma-ketligi.
  4. journalctl -u docker | grep "using the force" — to'xtash muddati tugaganmi.
  5. docker compose exec api cat /proc/1/comm — PID 1 da kim turgani.

Amaliyot#

Xotira chegarasi, to'xtash muddati va init — quyidagi lab'larda aynan shu sozlamalar yetishmaydi:

labosonXotira chegarasi hammagaChegarasiz konteyner yiqilganda o'zi emas, qo'shnisi o'ladi.mashqni ochish labo'rtaBaza serverni yeb qo'ydiKatta so'rov paytida Postgres 6GB oldi va `api` OOM bo'ldi. Chegara yo'q edi.mashqni ochish labo'rtaDeploy paytida so'rovlar uzilib qolyaptiDocker SIGTERM yuboradi, 10 soniyadan keyin SIGKILL. Uzun so'rovlar yakunlanmaydi.mashqni ochish labo'rtaWorker zombi jarayonlar bilan to'lib ketdi`ps` da yuzlab `<defunct>`. Ilova PID 1 — u zombilarni yig'ishni bilmaydi.mashqni ochish

docker inspect, docker stats va --memory bilan ishlashni mashq qilish uchun:

buyruq mashqi2-bosqich · 16 vazifaDocker asoslariKonteyner ko'tarilmayapti, log kerak, ichiga kirish kerak. Kundalik docker.bosqichni boshlash