lapar tewas oleh SIGKILL tanpa sempat berpamitan — dan chapter sebelumnya menutup dengan satu pertanyaan yang mengganjal: mengapa container tidak bisa dibunuh dengan cara yang lebih sopan? Jalur sopan itu memang ada — docker stop selalu mengetuk pintu lebih dulu dengan SIGTERM, memberi aplikasi kesempatan menutup koneksi, mem flush penulisan, dan mundur dengan tertib. Tetapi ketukan hanya berguna bila ada yang mendengarkan, dan di dalam container pendengar itu tidak selalu ada: tidak ada systemd yang selalu siaga seperti di sistem Linux biasa — proses nomor satu di sana sini adalah aplikasi kita sendiri, dan kernel memperlakukan proses nomor satu dengan aturan istimewa yang membuat aplikasi yang ceroboh menjadi tuli. Chapter ini membedah aturan istimewa PID 1 itu beserta dua tugas yang melekat padanya, membuktikan masalahnya dengan container yang menolak mati selama sepuluh detik, lalu menutupnya dengan dua jalan keluar: memperbaiki aplikasi kita, atau menghadirkan inisialisator pembantu lewat satu flag kecil bernama --init.

Mengapa PID 1 Istimewa #

Di sistem Linux biasa, proses pertama — PID 1 — adalah systemd atau salah satu kerabatnya, dan proses itu mengemban dua tugas seumur hidup. Tugas pertama adalah mendengarkan sinyal: init yang baik mendaftarkan penangan untuk sinyal-sinyal penting agar sistem bisa dimatikan dan dimuat ulang secara tertib. Tugas kedua adalah menjadi rumah akhir bagi anak yatim: saat proses induk mati lebih dulu, anak-anaknya diadopsi oleh PID 1; dan saat proses apa pun berakhir, entri prosesnya di kernel harus “dipungut” oleh induknya lewat panggilan wait — bila tidak, entri itu tertinggal sebagai zombie, baris proses mati yang terus memakan tempat di tabel proses. Di dalam container, kedua tugas itu jatuh ke tangan aplikasi kita — dan inilah sumber seluruh masalah chapter ini.

Kernel menambahkan satu aturan pelindung yang membuat keadaan makin menarik: sinyal yang tidak memiliki penangan terdaftar pada PID 1 diabaikan begitu saja. Aturan ini masuk akal di sistem biasa — init tidak boleh mati karena kecelakaan — tetapi di container, aturan yang sama membuat aplikasi yang lupa memasang penangan sinyal menjadi benar-benar tuli terhadap ketukan docker stop. Aplikasi tidak mati karena sinyal, jadi tidak; ia hanya tidak pernah mendengar bahwa ada yang mengetuk. Masa tenggang sepuluh detik habis tanpa jawaban, dan SIGKILL turun sebagai satu-satunya bahasa yang selalu didengar — persis kisah web dan uji-tunggu yang kita saksikan pada Chapter Menghentikan dan Menjalankan Container, kini dengan sebab yang kita pahami sepenuhnya.

Demo: docker stop yang makan sepuluh detik #

Mari kita bangun korban yang paling jujur menampilkan masalah ini: skrip shell. sh tidak pernah mendaftarkan penangan SIGTERM untuk dirinya, sehingga loop tak berujung berikut adalah telinga yang paling kedap:

bash
$ docker run -d --name keras alpine:3.20 sh -c 'while true; do sleep 60; done'

Output yang Dihasilkan:

text
9e78592496716252ecc01886ec05b20da05563990911f75eae74d1e5db12975e
bash
$ time docker stop keras

Output yang Dihasilkan:

text
keras

real    0m10.412s
user    0m0.024s
sys     0m0.011s
bash
$ docker ps -a

Output yang Dihasilkan:

text
CONTAINER ID   IMAGE         COMMAND                     CREATED         STATUS                        PORTS   NAMES
9e7859249671   alpine:3.20   "sh -c while true; do sl…"   2 minutes ago   Exited (137) 33 seconds ago          keras

Mari kita telaah kematian keras kepala itu detik demi detik:

  1. time membuka tirainya — baris real 0m10.412s adalah waktu nyata yang kita tunggu; untuk mematikan skrip kecil sepuluh detik adalah masa yang absurd, dan angka ini adalah gejala, bukan kebetulan.
  2. Detik ke-0: SIGTERM diketuk dan tak dijawab — daemon mengirim sinyal ke PID 1, si sh; karena sh tidak punya penangan terdaftar untuk sinyal itu, kernel mengabaikannya demi aturan pelindung PID 1 — loop while true tidak terganggu sedetik pun.
  3. Detik ke-10: masa tenggang habis, SIGKILL turun — docker stop kehilangan kesabaran tepat pada batas bawaan sepuluh detik yang kita kenal, dan proses mati seketika bersama seluruh isi container-nya.
  4. Exited (137) mencatat cara kematiannya — 128 + 9, konvensi SIGKILL yang kita baca berkali-kali di Chapter *Memahami Exit Code`; sepuluh detik yang seharusnya menjadi masa singkat habis hanya untuk menunggu ketukan yang tidak pernah dijawab.

Masa tenggang dan –stop-timeout #

Angka sepuluh detik itu bukan hukum alam. Flag --stop-timeout pada docker run menetapkan masa tenggang milik container itu sendiri — nilainya tersimpan di konfigurasi dan dipakai setiap kali docker stop mematikannya — sementara flag -t pada docker stop menimpa nilainya untuk satu kali pemanggilan saja, sebagaimana kita praktikkan pada Chapter Menghentikan dan Menjalankan Container. Memperpanjang masa tenggang menolong aplikasi yang benar-benar butuh lama untuk beres-beres; tetapi bagi aplikasi yang tuli, memperpanjang masa tenggang hanya memperpanjang penderitaan — jam pasir yang lebih besar tidak menolong pendengar yang tidak ada. Satu perangkat terakhir yang berhubungan dengan sinyal sudah juga kita kenal: instruksi STOPSIGNAL pada Dockerfile mengubah jenis ketukan pertama yang dikirim docker stop — materi Chapter Instruksi Lanjutan — dan semuanya, sinyal apa pun jenisnya, tetap menuntut pendengar di sisi PID 1.

Solusi Pertama: Perbaiki Aplikasinya #

Jalan keluar paling lurus adalah membenarkan telinga itu sendiri: aplikasi kita mendaftarkan penangan sinyal, lalu melakukan penutupan yang tertib ketika ketukan tiba. Beberapa runtime mendaftarkan penangan secara bawaan, tetapi penangan bawaan biasanya berhenti pada “mati seketika” — proses berakhir begitu sinyal tiba tanpa menguras koneksi yang sedang berjalan. Pola yang benar untuk aplikasi web adalah tiga langkah: dengar sinyalnya, tolak koneksi baru, tunggu koneksi lama selesai, baru keluar. myapp kita berbasis Node.js, dan pada server.js pola itu terwujud dalam beberapa baris:

js
const server = app.listen(8000, () => {
  console.log("myapp mendengarkan di port 8000");
});

process.on("SIGTERM", () => {
  console.log("SIGTERM diterima, menutup koneksi...");
  server.close(() => process.exit(0));
});

Mari kita telaah tiga baris intinya:

  1. process.on("SIGTERM", ...) — Node mendaftarkan penangan untuk SIGTERM; sejak baris ini berlaku, PID 1 container myapp memiliki telinga yang terdaftar di kernel, sehingga ketukan docker stop benar-benar sampai ke aplikasi.
  2. server.close(...) — server berhenti menerima koneksi baru, lalu menunggu koneksi yang sedang berjalan menyelesaikan permintaan terakhirnya; inilah pengurasan yang membedakan penutupan tertib dari putus mendadak.
  3. process.exit(0) di dalam callback — proses baru keluar setelah koneksi terakhir benar-benar tertutup, dan keluar dengan kode 0; docker stop pada myapp kini selesai jauh di bawah satu detik dengan status Exited (0), bukan sepuluh detik dengan Exited (137).

Pola yang sama berlaku di mana-mana dengan kosakatnya masing-masing: Go cukup memanfaatkan signal.Notify dan http.Server.Shutdown, Python memakai signal.signal dan menutup server di dalamnya, dan Rust memantau saluran sinyal sambil menutup listener secara bertahap. Yang selalu sama adalah tiga langkahnya: daftarkan penangan, kuras dengan sopan, keluar dengan sadar.

Solusi Kedua: Hadirkan Init lewat –init #

Tetapi sering kali kita tidak bisa menyentuh kodenya — aplikasi pihak ketiga, image warisan, atau sekadar skrip pembungkus yang tidak bisa diubah. Untuk keadaan itu Docker menyediakan satu flag: --init menyisipkan proses kecil bernama /sbin/docker-init — turunan dari proyek tini — sebagai PID 1, lalu menjalankan perintah kita sebagai anaknya. Kita uji pada web varian alpine yang sudah lama menetap di mesin kita:

bash
$ docker run -d --name web --init nginx:alpine

Output yang Dihasilkan:

text
f7c2a91b3e55913c4a5f1c6c0a4d8e2b3f4a5b6c7d8e9f0a1b2c3d4e5f60718a
bash
$ docker exec web ps

Output yang Dihasilkan:

text
PID   USER     TIME  COMMAND
    1 root      0:00 /sbin/docker-init -- /docker-entrypoint.sh nginx -g daemon off;
    6 root      0:00 nginx: master process nginx -g daemon off;
    7 nginx     0:00 nginx: worker process
    8 nginx     0:00 nginx: worker process

Mari kita telaah susunan keluarga di dalam web:

  1. PID 1 bukan lagi nginx — /sbin/docker-init menduduki kursi nomor satu, dengan perintah asli kita tercantum setelah tanda --; kursi istimewa kernel kini diisi oleh proses yang memang dirancang untuk dua tugas PID 1.
  2. nginx master turun satu generasi — master kini anak dengan PID 6, diikuti dua worker dengan PID 7 dan 8; hierarki ini tidak mengubah apa pun bagi pengunjung web, hanya bagi kernel.
  3. worker berjalan sebagai user nginx — UID 101, persis pembagian identitas yang kita bahas pada Chapter User dan Permissions; keistimewaan root hanya dipakai oleh docker-init dan master.
  4. docker-init menerima dan meneruskan sinyal — ketika docker stop mengetuk, ketukan diterima oleh PID 1 yang penuh kewarasan, lalu diteruskan kepada nginx master — proses yang sejak awal pandai menangani sinyal berhenti.
  5. docker-init juga menjadi reaper — setiap anak yatim di dalam container diadopsi dan dipungut lewat wait, sehingga zombie tidak pernah menumpuk berapa pun banyaknya proses pendek yang lahir dan mati.

Kita buktikan hasil akhirnya dengan jalan pematian yang sama yang dulu menyiksa keras — dan kali ini tanpa time pun terasa, karena perintah berikut kembali hampir seketika: docker stop web pada susunan ini selesai dalam hitungan milidetik, karena ketukan SIGTERM diterima, diteruskan, dan dijawab dengan penutupan yang rapi oleh master nginx.

Memilih Solusi yang Tepat #

Kedua solusi tidak saling meniadakan, dan pilihan di antaranya mengikuti satu pertanyaan: apakah kodenya bisa kita sentuh? Bila ya — aplikasi milik kita sendiri — perbaiki aplikasinya; itu solusi paling benar karena tertib saat mati menjadi bagian dari aplikasi itu sendiri, ke mana pun ia dijalankan, dengan atau tanpa Docker. Bila tidak — aplikasi pihak ketiga yang didistribusikan sebagai image, skrip pembungkus yang kita tidak kuasai, atau kebutuhan cepat akan reaper — --init adalah tameng yang selalu siap. Menggabungkan keduanya pun sah dan bahkan lazim: aplikasi yang tertib tetap mendapat reaper tambahan dari docker-init.

Masalah PID 1PenyebabnyaSolusi yang tepat
docker stop selalu makan masa tenggang penuhaplikasi tidak memasang penangan sinyaldaftarkan handler di aplikasi, atau pakai --init
koneksi diputus di tengah permintaanproses berakhir tanpa menguras koneksihandler dengan server.close atau padanannya
proses zombie menumpuk di dalam containerPID 1 tidak memanggil wait untuk anak yatim--init bertindak sebagai reaper
sinyal tidak pernah sampai ke aplikasiaplikasi dibungkus sh, misalnya lewat shell formpakai exec form, atau akhiri skrip dengan exec

Dua bekas latihan kita bereskan dengan urutan sopan yang menjadi kebiasaan course ini — keras sudah tergeletak sejak demo pertama, dan web berhenti seketika seperti barusan kita buktikan:

bash
$ docker stop web

Output yang Dihasilkan:

text
web
bash
$ docker rm keras web

Output yang Dihasilkan:

text
keras
web

Sepanjang section ini kita telah memasang satu per satu knob konfigurasi — perintah, environment, user, port, direktori kerja, kebijakan bangkit, batas resource, kini tata kelola sinyal — dan di setiap chapter kita berpapasan dengan potongan-potongan kejadian: container yang dibuat, berjalan, dibekukan, berhenti, dibangkitkan, hingga dihapus. Potongan-potongan itu sebenarnya menyusun satu peta besar yang belum pernah kita lihat utuh: seluruh state yang bisa dihuni sebuah container, perintah apa yang memindahkannya dari satu state ke state lain, dan bagaimana membaca posisinya dari luar. Chapter penutup section ini, Lifecycle Container, menggambar peta itu lengkap — dari Created sampai terhapus tanpa bekas.

Referensi #