Proses zombie (defunct process) dan kebocoran Process ID (PID leak) dalam container terjadi ketika subproses terminasi tanpa pembersihan status keluar oleh proses induk melalui sistem panggilan wait() atau waitpid(). Dalam skenario container Linux, jika proses dengan PID 1 tidak dirancang untuk menangani reparenting dan reap child process, sistem kehabisan PID table, menyebabkan crash sistemik pada node host.
1. Anatomi PID 1, Reparenting, dan Zombie Process
Di Linux kernel, setiap proses anak yang induknya mati secara abnormal akan diadopsi oleh PID 1 di dalam namespace proses terkait. PID 1 memikul dua tanggung jawab utama: mengeksekusi sinyal terminasi ke child processes dan memanggil waitid()/waitpid() untuk membersihkan child process yang telah memasuki status TASK_ZOMBIE (status Z).
Aplikasi standar seperti Node.js, Python, atau Java tidak mengimplementasikan signal proxying atau child reaping loop secara internal saat berjalan sebagai PID 1. Ketika container menerima sinyal SIGTERM dari kubelet atau Docker daemon, aplikasi sering kali mengabaikan sinyal tersebut karena kernel memberlakukan proteksi khusus terhadap PID 1. Setelah grace period habis, runtime mengirim SIGKILL secara mendadak, meninggalkan orphaned processes tanpa proses reaping yang valid.
2. Forensik Host: Korelasi /proc dan Cgroups Metadata
Saat terjadi insiden kegagalan deployment berulang (analog dengan fenomena artefak "Green Boots" yang tertinggal di disk/memori), identifikasi proses zombie dilakukan langsung dari level host menggunakan virtual filesystem /proc dan cgroups v2/v1.
Skrip Shell Forensik Inspeksi Zombie
#!/usr/bin/env bash
set -euo pipefail
echo "=== SCANNING FOR ZOMBIE PROCESSES ==="
# Cari proses berstatus Z di /proc
zombies=$(ps -eo pid,ppid,stat,comm | awk '$3 ~ /Z/ { print $1 }')
if [[ -z "$zombies" ]]; then
echo "Tidak ditemukan proses zombie."
exit 0
fi
printf "%-8s %-8s %-12s %-40s\n" "PID" "PPID" "CGROUP_ID" "COMM"
for pid in $zombies; do
if [[ -d "/proc/$pid" ]]; then
ppid=$(awk '{print $4}' "/proc/$pid/stat")
comm=$(awk '{print $2}' "/proc/$pid/stat")
cgroup=$(cat "/proc/$pid/cgroup" | head -n 1 | cut -d: -f3)
# Ambil metadata container ID jika tersedia di path cgroup
container_id=$(echo "$cgroup" | grep -oE '[a-f0-9]{64}' || echo "N/A")
printf "%-8s %-8s %-12s %-40s\n" "$pid" "$ppid" "$container_id" "$comm"
fi
doneUntuk memvalidasi batas saturasi PID pada pod/container, periksa penggunaan PID saat ini terhadap batas maksimal cgroup host:
# Cgroup v2: Memeriksa PID saat ini dan batas maksimal
cat /sys/fs/cgroup/system.slice/docker-*.scope/pids.current
cat /sys/fs/cgroup/system.slice/docker-*.scope/pids.max3. Mitigasi Signal Handling: dumb-init vs tini
Solusi arsitektural untuk memastikan child reaping berjalan otomatis adalah menggunakan init system minimalis seperti tini atau dumb-init sebagai entrypoint container.
- tini: Default pada Docker (flag
--init). Mengimplementasikan subreaper pattern viaprctl(PR_SET_CHILD_SUBREAPER, 1)dan meneruskan sinyal secara presisi ke process group. - dumb-init: Menggunakan pendekatan rewriting signal handler dan secara otomatis meneruskan sinyal ke seluruh process group (
kill(-pid, signum)), efektif untuk menangani child spawn bersarang (nested childs).
Manifest Pod Kubernetes dengan Dumb-Init dan PID Limit
apiVersion: apps/v1
kind: Deployment
metadata:
name: worker-service
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: worker-service
template:
metadata:
labels:
app: worker-service
spec:
shareProcessNamespace: false
containers:
- name: worker
image: python:3.11-slim
command: ["/usr/bin/dumb-init", "--"]
args: ["python", "worker.py"]
resources:
limits:
cpu: "500m"
memory: "512Mi"
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5 && kill -TERM 1"]
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 2
periodSeconds: 54. Health Check dan Automated Rollback
Gunakan konfigurasi rolling update agar deployment gagal langsung dihentikan sebelum menghabiskan resource pool cluster.
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0Jika liveness probe gagal mencapai state sehat dalam durasi failureThreshold * periodSeconds, pod baru akan ditandai unhealthy. Kubelet membatalkan rollout progresif secara otomatis. Untuk memicu rollback otomatis saat terjadi insiden pipa CI/CD, manfaatkan command rollback:
kubectl rollout status deployment/worker-service -n production || kubectl rollout undo deployment/worker-service -n production5. Template Postmortem Root Cause Analysis (RCA)
# POSTMORTEM REPORT: INC-8492 PID Exhaustion & Zombie Leak
## 1. Executive Summary
- Insiden: Kegagalan deployment rilis v2.4.1 memicu thread leak dan zombie processes pada node worker.
- Dampak: 4 node worker degraded (Status: PIDPressure), 18 container gagal start, durasi downtime 28 menit.
- Akar Masalah: Script worker mengeksekusi ffmpeg via child subprocess tanpa memanggil process reap. Aplikasi dijalankan sebagai bare PID 1 di container.
## 2. Timeline Kejadian (UTC)
- 10:14 - Deployment v2.4.1 diterapkan ke cluster staging.
- 10:20 - Kubelet mengirim warning event: `NodeHasPIDPressure`.
- 10:25 - Alert PagerDuty berbunyi: `KubeNodeReadinessFlapping`.
- 10:32 - Forensik `/proc` menemukan 32,768 PID dialokasikan, mayoritas berstatus `Z (defunct)`.
- 10:35 - Rollback manual dieksekusi via `kubectl rollout undo`.
- 10:42 - Node kembali normal setelah docker clean & garbage collection.
## 3. Analisis Penyebab (5 Whys)
1. Node tidak responsif: Host kehabisan alokasi Process ID (kernel pid_max tercapai).
2. PID habis: Terdapat ribuan zombie ffmpeg process di background.
3. Subproses menjadi zombie: Process parent tidak menjalankan waitpid() setelah subproses terminasi.
4. Zombie tidak direap oleh PID 1: Container entrypoint langsung memanggil runtime Python tanpa wrapper init process.
5. Sinyal SIGTERM diabaikan: Default signal handling PID 1 di Linux menolak default handler sinyal dari kubelet.
## 4. Tindakan Perbaikan (Action Items)
- [P0] Terapkan `dumb-init` pada base Dockerfile seluruh production workloads. (Owner: DevOps)
- [P1] Pasang batas PID per-pod via kubernetes manifest `podPidsLimit` di KubeletConfiguration. (Owner: Infra)
- [P2] Konfigurasi healthcheck timeout dan automated rollback logic di deployment pipeline. (Owner: SRE)
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!