Proses benchmark backend dan evaluasi model otomatis sering kali mengalami hang tanpa melempar exit code error. Gejala umumnya spesifik: job berjalan normal pada awal eksekusi, lalu berhenti total pada iterasi tertentu—misalnya pada batch N=22—hingga batas global timeout CI tercapai. Kondisi ini menguras kuota menit komputasi, menahan runner pool, dan memblokir antrean merge request lain.
Penyebab utama benchmark macet di CI umumnya berkisar pada kebocoran file descriptor (FD), deadlock pada thread pool komputasi, atau socket connection pool yang kehabisan slot tanpa konfigurasi timeout granular. Panduan ini membahas instrumentasi worker, penerapan watchdog heartbeat, serta safe rollback runner.
Observabilitas Worker: CPU, File Descriptor, dan Timeout Leak
Saat proses benchmark berhenti merespons, pipeline default CI hanya menampilkan log kosong. Diperlukan pencatatan telemetri level proses untuk mengetahui apakah worker berada dalam status busy-wait (CPU 100%) atau deadlock/I-O wait (CPU 0%).
1. Diagnostik Resource Secara Periodik
Jalankan background daemon sederhana di dalam step CI untuk memantau alokasi resource proses benchmark:
#!/usr/bin/env bash
# monitor.sh: Monitor resource benchmark
PID=$1
while kill -0 "$PID" 2>/dev/null; do
FD_COUNT=$(ls -1 /proc/"$PID"/fd 2>/dev/null | wc -l)
CPU_MEM=$(ps -p "$PID" -o %cpu,%mem,stat --no-headers)
echo "[MONITOR] $(date +%T) | PID: $PID | FDs: $FD_COUNT | CPU/MEM/STAT: $CPU_MEM"
sleep 5
doneJika metrik FD_COUNT naik terus menerus mendekati batas sistem (ulimit -n) hingga iterasi macet di N=22, masalahnya adalah resource leak pada socket connection pool atau unclosed file stream. Jika STAT berubah menjadi D (uninterruptible sleep) atau S (interruptible sleep) dengan CPU 0%, thread mengalami deadlock pada lock primitive atau koneksi jaringan tanpa read_timeout.
Watchdog Heartbeat per-Batch N
Timeout bawaan runner CI (seperti timeout-minutes: 60) tidak efektif mendeteksi macet per-iterasi karena membiarkan runner terkunci terlalu lama. Solusi deterministik adalah memasang watchdog berbasis timer yang di-reset setiap kali batch N selesai diproses.
Implementasi Watchdog Python
import os
import sys
import time
import threading
class BenchmarkWatchdog:
def __init__(self, timeout_seconds=30):
self.timeout = timeout_seconds
self.last_heartbeat = time.time()
self._stop_event = threading.Event()
self._thread = threading.Thread(target=self._monitor, daemon=True)
def start(self):
self._thread.start()
def ping(self, batch_idx):
self.last_heartbeat = time.time()
sys.stderr.write(f"[HEARTBEAT] Batch {batch_idx} processed\n")
sys.stderr.flush()
def stop(self):
self._stop_event.set()
def _monitor(self):
while not self._stop_event.is_set():
time.sleep(2)
elapsed = time.time() - self.last_heartbeat
if elapsed > self.timeout:
sys.stderr.write(f"[FATAL] Watchdog timeout: Batch stuck for {elapsed:.1f}s\n")
sys.stderr.flush()
# Kirim SIGABRT ke diri sendiri untuk menghasilkan stack dump
os.abort()
# Simulasi penggunaan
watchdog = BenchmarkWatchdog(timeout_seconds=15)
watchdog.start()
try:
for n in range(1, 100):
# Eksekusi unit benchmark
if n == 22:
time.sleep(60) # Simulasi hang
else:
time.sleep(0.5)
watchdog.ping(batch_idx=n)
finally:
watchdog.stop()Eksekusi os.abort() menghentikan proses secara langsung saat batch tertentu melebihi toleransi waktu (15 detik), menghasilkan core dump atau stack trace seketika tanpa menunggu pipeline timeout global.
Strategi Otomatis Cancel dan Safe Rollback Runner
Saat job benchmark dibatalkan paksa (baik oleh watchdog atau CI cancellation), subproses sering kali tertinggal dan menjadi zombie process atau orphaned process. Pada self-hosted runner, hal ini mengakibatkan shared memory (/dev/shm), port, atau GPU memory (VRAM) tetap terkunci, merusak eksekusi job berikutnya.
1. Isolasi Environment Menggunakan Process Group
Bungkus proses benchmark dalam process group terpisah agar signal SIGTERM dan SIGKILL dapat membersihkan seluruh child process secara tuntas.
# Jalankan proses benchmark dalam session/process group baru
set -m
python run_benchmark.py &
BENCH_PID=$!
cleanup() {
echo "[CLEANUP] Terminating process group..."
kill -TERM -"$BENCH_PID" 2>/dev/null || true
sleep 3
kill -KILL -"$BENCH_PID" 2>/dev/null || true
}
trap cleanup EXIT INT TERM
wait "$BENCH_PID"2. Safe Rollback di Level Workflow CI
Gunakan step teardown yang diproteksi kondisi always() untuk mengembalikan runner pool ke kondisi bersih:
jobs:
benchmark:
runs-on: [self-hosted, benchmark-worker]
steps:
- uses: actions/checkout@v4
- name: Run Benchmark with Wrapper
run: |
chmod +x ./scripts/run_bench.sh
./scripts/run_bench.sh
- name: Runner Sanitization and Rollback
if: always()
run: |
# Bersihkan residual zombie processes
pkill -u runner -f run_benchmark.py || true
# Reset resource GPU/Shared Memory jika digunakan
ipcs -m | awk '$3 == "runner" {print $2}' | xargs -r ipcrm -m || true
# Verifikasi ketersediaan disk & FD sebelum job selesai
df -h /tmp
ulimit -aPostmortem Ringan dan Mitigasi Regresi
Insiden benchmark macet di CI harus diakhiri dengan evaluasi penyebab mendasar (root-cause analysis):
- Audit I/O Timeouts: Pastikan setiap network client (Database driver, HTTP client, GRPC channel) memiliki limit
connect_timeoutdanread_timeouteksplisit. Nilai default tanpa batas (infinite) adalah sumber utama deadlock di batch N. - Batas Concurrency Runner: Jika batch N melibatkan beban CPU/GPU paralel, batasi concurrency worker CI (misal
pytest-xdist -n 2atau worker pool size) agar tidak memicu thread contention starvation atau OOM reaping senyap dari kernel OS. - Snapshot Artifact: Saat watchdog memicu
SIGABRT, konfigurasi runner CI untuk mengunggah log metrik FD, status thread dump, dan 100 baris log terakhir sebagai artifact pengujian agar investigasi dapat dilakukan secara asinkron tanpa mengulang eksekusi manual.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!