Exit code 137 — konteynerni kim o'ldirdi?
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:
$ kill -l 137
KILL
$ kill -l 143
TERMAmalda eng ko'p uchraydigan kodlar:
| Kod | Ma'nosi | Odatiy sabab |
|---|---|---|
0 | Jarayon o'zi muvaffaqiyatli tugadi | Bir martalik vazifa ishini bitirdi yoki ilova SIGTERM'ni ushlab, toza yopildi |
1 | Ilova xato bilan chiqdi | Tutilmagan istisno, konfiguratsiya xatosi — sababi docker logs da |
125 | Docker'ning o'zi konteynerni ishga tushira olmadi | docker run ga noto'g'ri flag yoki parametr berilgan |
126 | Buyruq topildi, lekin bajarib bo'lmadi | Bajarish huquqi yo'q: chmod +x qilinmagan entrypoint.sh |
127 | Buyruq topilmadi | Image'da bunday dastur yo'q yoki u PATH da emas |
137 | 128 + 9, SIGKILL | OOM killer, docker kill, to'xtash muddati tugashi |
139 | 128 + 11, SIGSEGV | Native kodda xotiraga noto'g'ri murojaat |
143 | 128 + 15, SIGTERM | To'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:
- OOM killer. Konteyner xotira chegarasiga yetdi yoki butun serverda xotira tugadi — yadro jarayonni o'ldirdi.
docker kill. Qo'lda yoki skriptdan.docker killstandart holatda aynan SIGKILL yuboradi.- To'xtash muddati tugadi.
docker stop,docker compose downyoki 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#
$ 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 falseOOMKilled 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-stoppedbo'lsa, siz qaraguningizcha konteyner allaqachon qayta turgan vaOOMKilledyanafalsebo'lishi mumkin. BuniRestartCounto'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
Upholatida, chiqish kodi yo'q, lekinOOMKilledbaribirtrue. gunicorn'ning yangi versiyalari bunday holatda loggaWorker (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):
$ 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:0Qatorlarni shunday o'qing:
constraint=CONSTRAINT_MEMCGva Memory cgroup out of memory — konteyner o'z chegarasiga urildi. Butun server xotirasi tugagandaCONSTRAINT_NONEva 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>.scopeko'rinishida bo'ladi. ID nidocker ps -a --no-truncbilan 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:
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:
| Gumondor | Hodisalar |
|---|---|
| OOM killer | oom, keyin die (exitCode=137) — kill hodisasi yo'q |
docker kill | kill (signal=9), keyin die (exitCode=137) |
| To'xtash muddati | kill (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:
services:
worker:
image: python:3.12-alpine
command: python worker.py
deploy:
resources:
limits:
memory: 512M
memswap_limit: 512Mdeploy.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:
$ 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:
$ docker stop plain with-init
plain
with-init
$ docker inspect -f '{{.Name}} {{.State.ExitCode}}' plain with-init
/plain 137
/with-init 143Ikkalasida 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:
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 funksiyangizprocess_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:
#!/bin/sh
set -e
alembic upgrade head
exec gunicorn app:app --bind 0.0.0.0:8000exec 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.
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: 40sQo'llanganini tekshiring:
$ docker inspect -f '{{.Config.StopTimeout}} {{.HostConfig.Init}}' myapp-api-1
40 trueTo'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#
docker inspect -f '{{.State.ExitCode}} {{.State.OOMKilled}} {{.RestartCount}}' <konteyner>— kod, OOM bayrog'i va konteyner necha marta qayta ishga tushgani.sudo dmesg | grep -E 'oom-kill:|Killed process'yokijournalctl -k— yadro kimni va nima uchun o'ldirgani.docker events—oom,killvadieketma-ketligi.journalctl -u docker | grep "using the force"— to'xtash muddati tugaganmi.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:
docker inspect, docker stats va --memory bilan ishlashni mashq qilish uchun: