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:
$ docker run -d --name keras alpine:3.20 sh -c 'while true; do sleep 60; done'Output yang Dihasilkan:
9e78592496716252ecc01886ec05b20da05563990911f75eae74d1e5db12975e$ time docker stop kerasOutput yang Dihasilkan:
keras
real 0m10.412s
user 0m0.024s
sys 0m0.011s$ docker ps -aOutput yang Dihasilkan:
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 kerasMari kita telaah kematian keras kepala itu detik demi detik:
timemembuka tirainya — barisreal 0m10.412sadalah waktu nyata yang kita tunggu; untuk mematikan skrip kecil sepuluh detik adalah masa yang absurd, dan angka ini adalah gejala, bukan kebetulan.- Detik ke-0:
SIGTERMdiketuk dan tak dijawab — daemon mengirim sinyal ke PID 1, sish; karenashtidak punya penangan terdaftar untuk sinyal itu, kernel mengabaikannya demi aturan pelindung PID 1 — loopwhile truetidak terganggu sedetik pun. - Detik ke-10: masa tenggang habis,
SIGKILLturun —docker stopkehilangan kesabaran tepat pada batas bawaan sepuluh detik yang kita kenal, dan proses mati seketika bersama seluruh isi container-nya. Exited (137)mencatat cara kematiannya —128 + 9, konvensiSIGKILLyang 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:
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:
process.on("SIGTERM", ...)— Node mendaftarkan penangan untukSIGTERM; sejak baris ini berlaku, PID 1 container myapp memiliki telinga yang terdaftar di kernel, sehingga ketukandocker stopbenar-benar sampai ke aplikasi.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.process.exit(0)di dalam callback — proses baru keluar setelah koneksi terakhir benar-benar tertutup, dan keluar dengan kode0;docker stoppada myapp kini selesai jauh di bawah satu detik dengan statusExited (0), bukan sepuluh detik denganExited (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:
$ docker run -d --name web --init nginx:alpineOutput yang Dihasilkan:
f7c2a91b3e55913c4a5f1c6c0a4d8e2b3f4a5b6c7d8e9f0a1b2c3d4e5f60718a$ docker exec web psOutput yang Dihasilkan:
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 processMari kita telaah susunan keluarga di dalam web:
- PID 1 bukan lagi nginx —
/sbin/docker-initmenduduki 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. - 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.
- 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. - docker-init menerima dan meneruskan sinyal — ketika
docker stopmengetuk, ketukan diterima oleh PID 1 yang penuh kewarasan, lalu diteruskan kepada nginx master — proses yang sejak awal pandai menangani sinyal berhenti. - 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 1 | Penyebabnya | Solusi yang tepat |
|---|---|---|
docker stop selalu makan masa tenggang penuh | aplikasi tidak memasang penangan sinyal | daftarkan handler di aplikasi, atau pakai --init |
| koneksi diputus di tengah permintaan | proses berakhir tanpa menguras koneksi | handler dengan server.close atau padanannya |
| proses zombie menumpuk di dalam container | PID 1 tidak memanggil wait untuk anak yatim | --init bertindak sebagai reaper |
| sinyal tidak pernah sampai ke aplikasi | aplikasi dibungkus sh, misalnya lewat shell form | pakai 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:
$ docker stop webOutput yang Dihasilkan:
web$ docker rm keras webOutput yang Dihasilkan:
keras
webSepanjang 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 #
- docker container run
— flag
--init,--stop-timeout, dan perilaku sinyal saat start. - docker container stop
— urutan
SIGTERMlaluSIGKILLbeserta masa tenggang bawaannya. - Dockerfile Reference
— bentuk exec versus shell form serta instruksi
STOPSIGNAL.