Keluhan insiden service jarang datang dalam bentuk kalimat yang rapi — yang sampai ke kita biasanya hanya “situsnya mati” atau “aplikasinya tidak bisa dibuka”. Padahal di dalam sistem, systemd telah merekam hampir semua kejadian: kapan service mencoba hidup, mengapa ia mati, dan siapa yang memicunya. Chapter ini melatih alur triase dari gejala di luar menuju bukti di dalam: membaca status, menambatkan jurnal pada baris penyebab, membedakan kegagalan konfigurasi dari bentrok port, lalu memverifikasi pemulihan dari sisi klien — bukan sekadar berharap restart menyelesaikan semuanya.

Alur Triase: Dari Gejala ke Bukti #

Ketika sebuah service dilaporkan mati, kita memeriksa lima hal secara berurutan — dari yang paling murah ke yang paling dalam:

  1. Tajamkan gejalanya lebih dulu. “Mati” itu kabar buruk yang kabur: apakah curl ke port-nya ditolak, halaman balik error 500, atau hanya satu user yang bermasalah? Gejala yang tidak terukur tidak bisa diverifikasi perbaikannya.
  2. Baca keadaan dengan systemctl status nama.service — terutama baris Active dan kode hasilnya.
  3. Baca jurnal dengan journalctl -u nama.service -e — opsi -e melompat ke ujung, karena pesan pemberani hampir selalu ada di baris-baris terakhir. Teknik menyaring jurnal lengkapnya sudah kita bahas di Monitoring Logs .
  4. Periksa tiga penyebab klasik pasca-edit: file konfigurasi yang salah isi, port yang bentrok dengan proses lain, dan izin file yang tidak cocok dengan User= unit.
  5. Perbaiki satu hal, restart, lalu ukur ulang dari luar dengan curl — kalau gejala awalnya “curl ditolak”, bukti pulihnya juga harus “curl berhasil”.

Dua keadaan di baris Active perlu dibedakan sejak awal. inactive (dead) berarti service memang tidak diminta jalan — bisa jadi unitnya lupa di-enable, atau dependency-nya yang lebih dulu gagal. failed berarti service mencoba hidup lalu mati oleh error — di sinilah jurnal menjadi saksi kunci. Daftar lengkap lima keadaan beserta contoh unit yang salah tulis sudah dibedah di Troubleshooting Services ; fokus kita sekarang adalah membaca jurnal korban dan memutuskan tindakan dari isinya.

Membaca Jurnal Sang Korban #

Kita simulasikan webapp.service yang mati setiap kali dihidupkan. Berikut potongan jurnalnya — persis yang ditampilkan journalctl -u webapp.service -e — yang kita simpan sebagai fixture unit.log agar bisa disaring berulang:

bash
cat > unit.log <<'AKHIR'
Jan 15 08:41:02 server-kantor systemd[1]: Starting webapp.service...
Jan 15 08:41:02 server-kantor webapp[4712]: memuat konfigurasi /etc/webapp/config.json
Jan 15 08:41:02 server-kantor webapp[4712]: fatal: /etc/webapp/config.json: Permission denied
Jan 15 08:41:02 server-kantor systemd[1]: webapp.service: Main process exited, code=exited, status=1/FAILURE
Jan 15 08:41:02 server-kantor systemd[1]: webapp.service: Failed with result 'exit-code'.
Jan 15 08:41:12 server-kantor systemd[1]: webapp.service: Scheduled restart job, restart counter is at 1.
AKHIR
grep -E 'fatal|FAILURE|Failed with result' unit.log

Output yang Dihasilkan:

text
Jan 15 08:41:02 server-kantor webapp[4712]: fatal: /etc/webapp/config.json: Permission denied
Jan 15 08:41:02 server-kantor systemd[1]: webapp.service: Main process exited, code=exited, status=1/FAILURE
Jan 15 08:41:02 server-kantor systemd[1]: webapp.service: Failed with result 'exit-code'.

Tiga baris hasil saringan itu membentuk satu cerita sebab-akibat. Baris fatal berasal dari aplikasi sendiri — inilah penyebabnya: file konfigurasi tidak bisa dibaca. Dua baris berikutnya adalah konsekuensi yang dicatat systemd: proses utama keluar dengan status=1, lalu unit ditandai gagal. Baris terakhir memperlihatkan restart counter is at 1 — Restart=on-failure sedang mencoba lagi, dan jika terus gagal, hitungan ini yang pada akhirnya menyentuh batas StartLimitBurst. Aturan bacanya sederhana: cari baris error dari aplikasi lebih dulu; baris-baris systemd hanyalah berita susulannya.

Skrip Triase Pesan Error Umum #

Pola di atas bisa dibungkus menjadi skrip triase mini: saring baris mencurigakan, lalu petakan pesan ke tersangka dan tindakan pertamanya. Blok berikut menulis ulang fixture unit.log lebih dulu supaya tetap berdiri sendiri:

bash
cat > unit.log <<'AKHIR'
Jan 15 08:41:02 server-kantor systemd[1]: Starting webapp.service...
Jan 15 08:41:02 server-kantor webapp[4712]: memuat konfigurasi /etc/webapp/config.json
Jan 15 08:41:02 server-kantor webapp[4712]: fatal: /etc/webapp/config.json: Permission denied
Jan 15 08:41:02 server-kantor systemd[1]: webapp.service: Main process exited, code=exited, status=1/FAILURE
Jan 15 08:41:02 server-kantor systemd[1]: webapp.service: Failed with result 'exit-code'.
Jan 15 08:41:12 server-kantor systemd[1]: webapp.service: Scheduled restart job, restart counter is at 1.
AKHIR
cat > triase.sh <<'AKHIR'
#!/bin/bash
# Triase jurnal service: saring error, usul tersangka.
grep -i 'fatal\|error\|failed' unit.log | tail -n 3
echo "---"
if grep -q 'Permission denied' unit.log; then
  echo "tersangka : izin file — cocokkan User= unit dengan ls -l file config"
elif grep -q 'Address already in use' unit.log; then
  echo "tersangka : port bentrok — periksa dengan ss -tlnp"
elif grep -q 'No such file or directory' unit.log; then
  echo "tersangka : path salah — cocokkan ExecStart dengan isi disk"
else
  echo "tersangka : belum jelas — baca konteks penuh di journalctl -e"
fi
AKHIR
bash triase.sh

Output yang Dihasilkan:

text
Jan 15 08:41:02 server-kantor webapp[4712]: fatal: /etc/webapp/config.json: Permission denied
Jan 15 08:41:02 server-kantor systemd[1]: webapp.service: Failed with result 'exit-code'.
---
tersangka : izin file — cocokkan User= unit dengan ls -l file config

Mari kita bedah skrip ini baris-per-baris:

  1. Baris 3 (grep -i ... | tail -n 3): saringan mengambil semua baris berisi fatal, error, atau failed tanpa peduli besar-kecil huruf; tail -n 3 memangkas ke tiga terakhir agar layar tidak dibanjiri crash berulang.
  2. Baris 5 (if grep -q 'Permission denied' ...): rantai keputusan memeriksa pesan paling khas lebih dulu; grep -q tidak mencetak apa pun — ia hanya mengembalikan exit code untuk diuji if.
  3. Baris 12-13 (else …): cabut penutup untuk pesan yang belum dikenali — skrip yang jujur mengaku butuh data lebih banyak lebih berguna daripada skrip yang menebak nekat.

Untuk kasus Permission denied, langkah lanjutnya berlanjut ke luar jurnal: ls -l /etc/webapp/config.json menunjukkan pemilik dan izinnya, systemctl cat webapp.service menunjukkan User= yang dipakai — dan kalau keduanya tidak bersinggungan, itulah akar masalahnya.

Insiden Mini: Service Mati, Terverifikasi dari Luar #

Alur triase menuntut verifikasi dari luar, dan pola itu bisa dilatih dengan server lokal sebagai korban: python3 -m http.server kita perlakukan sebagai “service”, curl sebagai klien — alamat tujuannya ditulis tanpa skema karena curl menganggap alamat tanpa skema sebagai HTTP biasa. Blok berikut memainkan satu siklus insiden utuh — service hidup, service kedua bentrok port, service utama mati, lalu hidup kembali:

bash
python3 -m http.server 8447 --bind 127.0.0.1 >/dev/null 2>&1 &
server=$!
sleep 1
curl -s -o /dev/null -w 'sebelum insiden : HTTP %{http_code}\n' 127.0.0.1:8447/
python3 -m http.server 8447 --bind 127.0.0.1 2>&1 | tail -n 1
kill "$server"; wait "$server" 2>/dev/null
sleep 1
curl -sS 127.0.0.1:8447/ > berkas-kesalahan 2>&1
status=$?
sed 's/after [0-9]* ms/after N ms/' berkas-kesalahan
echo "curl keluar dengan exit code $status"
python3 -m http.server 8447 --bind 127.0.0.1 >/dev/null 2>&1 &
server=$!
sleep 1
curl -s -o /dev/null -w 'setelah restart  : HTTP %{http_code}\n' 127.0.0.1:8447/
kill "$server"; wait "$server" 2>/dev/null
sleep 1
ss -tln | awk '$4 ~ /:8447$/ {n++} END {print "pendengar tersisa di port 8447 :", n+0}'

Output yang Dihasilkan:

text
sebelum insiden : HTTP 200
OSError: [Errno 98] Address already in use
curl: (7) Failed to connect to 127.0.0.1 port 8447 after N ms: Could not connect to server
curl keluar dengan exit code 7
setelah restart  : HTTP 200
pendengar tersisa di port 8447 : 0

Kita bedahkan peristiwa-peristiwa pentingnya:

  1. Baris 5 (curl -s -o /dev/null -w ...): pengukuran awal — baseline. Gejala “mati” kini punya definisi terbaliknya: HTTP 200.
  2. Baris 6 (python3 -m http.server 8447 ... | tail -n 1): service kedua gagal jalan karena port sudah dipakai — pesan Address already in use inilah wujud OSError yang sama yang akan muncul di jurnal service nyata; di lapangan kita mencarinya dengan ss -tlnp lalu memilih: usir proses lama, atau ganti port di konfigurasi.
  3. Baris 8-11 (kill lalu curl -sS): proses utama dibunuh — di produksi bisa jadi OOM killer — dan curl kini ditolak. Exit code 7 berarti connection refused: tidak ada yang mendengarkan; exit code 28 berarti timeout — keduanya penyakit berbeda yang tampak sama dari luar.
  4. Baris 12 (sed 's/after [0-9]* ms/after N ms/'): pesan galat curl mencantumkan durasi dalam milidetik yang berbeda-beda di setiap mesin; sed di sini membekukannya agar output materi selalu sama — angka persisnya tidak penting untuk diagnosis.
  5. Baris 14-17 (start ulang lalu ukur lagi): pemulihan hanya sah bila pengukuran yang sama dengan baseline kini kembali HTTP 200 — dan baris terakhir membuktikan tidak ada proses latihan yang tertinggal mendengarkan port.

Pola Penyakit Umum #

Di lapangan, kegagalan service berulang dalam pola yang sama. Tabel ini memetakan gejala di status dan jurnal menuju penyebab serta tindakan pertamanya:

GejalaPenyebab paling umumTindakan pertama
inactive (dead) padahal layanan dibutuhkanunit tidak di-enable, atau dependency gagalsystemctl is-enabled; systemctl list-dependencies
failed + Permission denied di jurnalUser= unit tak berhak ke file/portls -l file terkait; sesuaikan pemilik atau User=
failed + Address already in useport dipakai proses lainss -tlnp; usir proses lama atau ganti port
failed + No such file or directorypath di ExecStart atau config salahcocokkan path; uji config sebelum restart
start request repeated too quicklycrash loop menyentuh StartLimitBurstsystemctl reset-failed, lalu perbaiki akarnya
gagal hanya di satu usersystemd user service yang bermasalahsystemctl --user status nama

Dua baris terakhir layak dijelaskan. reset-failed hanya menghapus status gagal beserta hitungan restart supaya systemd mau mencoba lagi — ia membuka pintu, bukan memperbaiki mesinnya; tanpa perbaikan di akar, crash loop akan kembali menghantam batas yang sama. Untuk baris terakhir: service yang dinyalakan per-user (systemctl --user) punya jurnal sendiri, dan diagnosa sering keliru karena kita memeriksa systemctl system-wide padahal yang rusak ada di sisi user.

Validasi konfigurasi sebelum restart adalah kebiasaan yang menghemat malam minggu kita: nginx -t dan apachectl configtest menguji sintaks, sshd -t menguji file sshd_config, bash -n menguji skrip ExecStart tanpa menjalankannya — semua murah, semua instan. Pengelolaan service yang lebih dalam — menulis unit, mengatur dependency antar unit, target boot — adalah wilayah Managing Services dan Service Dependencies . Namun service yang sudah hidup pun belum tentu sehat: kadang ia justru hidup terlalu serak — menggerus CPU dan memori sampai mesin sekelilingnya ikut terseret. Cara menangkap proses rakus semacam itu adalah materi chapter berikutnya.

Referensi resminya: opsi-opsi status, reset-failed, dan is-enabled di systemctl(1) , penyaringan per-unit dengan -u dan -e di journalctl(1) , serta pembacaan socket dengan ss di ss(8) .