EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Linux / บริการยังรันอยู่ แต่ตายไปแล้ว — systemd restart ช่วยไม่ได้
Linux

บริการยังรันอยู่ แต่ตายไปแล้ว — systemd restart ช่วยไม่ได้

โดย Ton-Or ·9 ต.ค. 2026·อ่าน 9 นาที
บริการยังรันอยู่ แต่ตายไปแล้ว — systemd restart ช่วยไม่ได้

active (running) แปลว่ากระบวนการยังไม่ตาย ไม่ได้แปลว่ามันยังทำงาน ระหว่างสองอย่างนั้นมีสถานะที่แย่กว่าการตายไปเลย คือค้างอยู่เฉย ๆ โดยไม่มีใครรู้

พอดีผมมีเดมอนตัวหนึ่งบนเซิร์ฟเวอร์ที่โรงงาน หน้าที่ของมันคือถือ session ยาวไว้กับ API ภายนอกเจ้าหนึ่ง แล้วคอยส่งข้อความแจ้งเตือนออกไปเวลามีเหตุการณ์จากระบบเซ็นเซอร์ในโรงงาน วันหนึ่งเน็ตขาออกของโรงงานหลุดยาวข้ามคืน พอเช้ามาเน็ตกลับมาปกติทุกอย่าง แต่การแจ้งเตือนเงียบสนิท ไม่มีข้อความออกเลยสักข้อความ

ที่ทำให้ผมสนใจเคสนี้เป็นพิเศษคือ ตอนผมเข้าไปดู ระบบบอกว่าทุกอย่างปกติ

systemctl status alert-bridge
# * alert-bridge.service - outbound alert bridge
#      Loaded: loaded (/etc/systemd/system/alert-bridge.service; enabled)
#      Active: active (running) since Tue 2026-10-13 03:12:41 +07; 9h ago
#    Main PID: 21884 (python3)

active (running) ครับ uptime เก้าชั่วโมง หน่วยความจำปกติ ไม่มี exit code ให้ดู ไม่มีอะไรผิดเลยในสายตาของ systemd แต่ของจริงคือมันตายไปแล้วตั้งแต่ตีสอง เรื่องนี้ผมว่าน่าเล่าเพราะมันเปลี่ยนวิธีที่ผมออกแบบการเฝ้าระวังไปเลย

ชื่อบริการ path และเลข IP ในบทความเป็นค่าตัวอย่างทั้งหมด

ไล่ log ย้อนกลับไป: ล้ม 194 รอบ แล้วเงียบ

journalctl -u alert-bridge --since '2026-10-13 01:00' --until '05:00' | tail -20
# 01:58:11 [login] Temporary failure in name resolution
# 01:58:41 [login] Temporary failure in name resolution
#   ... (วนแบบนี้ 194 รอบ)
# 03:12:41 [boot] starting, restoring session from token
# 03:12:41 [login] Logging in with token...
#   (ไม่มีบรรทัดต่อจากนี้อีกเลย)

ภาพชัดขึ้นทันที เน็ตหลุดทำให้ค้นชื่อโดเมนไม่ได้ การเชื่อมต่อใหม่เลยล้มทุกครั้ง โค้ดจับ exception ได้แล้ววนพยายามใหม่ ซึ่งเป็นพฤติกรรมที่ถูกต้อง ล้มไป 194 รอบและทุกรอบมีบรรทัด log กำกับครบ

แล้วรอบที่ 195 มันค้างกลาง ขั้นตอนที่ไม่มี timeout ครับ ไลบรารีที่ผมใช้เปิด socket ค้างไว้รอคำตอบจากปลายทางที่ตอนนั้นครึ่งหลับครึ่งตื่น ไม่ได้ปฏิเสธ ไม่ได้ตอบ แค่เงียบ โปรเซสเลยนั่งรออยู่อย่างนั้นข้ามคืน ไม่มี exception ให้จับ ไม่มีการ exit

ทางที่มันเดินไปเน็ตหลุด->DNS ล้มวน 194 รอบ->ค้างกลาง loginไม่มี timeout->process ยังอยู่ไม่ exit->systemd เงียบไม่มีอะไรให้ restart

ทำไม systemd ไม่ restart ให้ — เพราะมันไม่มีสิทธิ์รู้

หน่วยบริการของผมตั้งไว้แบบมาตรฐานที่ทุกคนเขียนกัน

[Unit]
Description=outbound alert bridge
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=svcapp
ExecStart=/opt/venv/bin/python /opt/alert-bridge/bridge.py
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target

Restart=on-failure ทำงานถูกต้องทุกประการ มันคอยดูว่าโปรเซสจบด้วยรหัสที่ไม่ใช่ศูนย์เมื่อไร แล้วสตาร์ตใหม่ให้ ปัญหาคือ กรณีนี้ไม่มีการจบ โปรเซสยังอยู่ครบถ้วน กิน CPU ศูนย์เปอร์เซ็นต์ นั่งรออยู่ใน syscall เดียวตลอดกาล ต่อให้ตั้ง Restart=always ก็ไม่ต่างกัน เพราะเงื่อนไขที่จะทำให้มันทำงานคือ "โปรเซสจบ" ซึ่งไม่เคยเกิด

สิ่งที่ Restart= ทำและไม่ได้ทำ

มันคือปฏิกิริยาต่อ การจบของโปรเซส เท่านั้น ไม่ใช่การเฝ้าดูว่าบริการทำงานได้ผลหรือเปล่า บริการที่ค้าง ติด deadlock วนลูปไม่รู้จบ หรือทำงานอยู่แต่ผลลัพธ์ผิดหมด จะได้สถานะ active (running) เหมือนกันทุกตัว ถ้าระบบเฝ้าระวังของเราดูแค่ systemctl is-active เราจะไม่มีวันเห็นเคสพวกนี้เลย

ทางที่ดูน่าใช้แต่ผมไม่เอา: ยิงเช็ค session ถี่ ๆ

ความคิดแรกที่ทุกคนจะนึกออกคือ ให้ตัวเดมอนคอยยิงคำสั่งเบา ๆ ไปที่ API ปลายทางทุกสองสามนาที ถ้าไม่ตอบก็ถือว่าตาย แล้วสั่งตัวเองเชื่อมต่อใหม่ ฟังดูเข้าท่าแต่ผมเคยทำแล้วได้ผลตรงข้าม

ปลายทางที่เป็นบริการภายนอกส่วนใหญ่มีกลไกป้องกันการเรียกถี่ ๆ อยู่ พอเราส่ง keepalive สม่ำเสมอเกินไป มันตีความว่าผิดปกติ แล้วเริ่มหน่วงหรือตัด session ทิ้ง ผลคือ session ค้างบ่อยกว่าเดิม คือเราสร้างปัญหาที่กำลังจะแก้ขึ้นมาเอง

ที่ผมเลิกทำ

keepalive ยิงออกนอกถี่ ๆ

ทุกครั้งที่เช็คคือการใช้โควตาจริงกับปลายทางที่เราไม่ได้คุม ถี่เกินไปเสี่ยงโดนหน่วงหรือถูกตัด และตอนปลายทางล่มชั่วคราวเราจะสั่งตัวเองเชื่อมต่อใหม่รัว ๆ ทั้งที่ควรรอเฉย ๆ

ที่ผมใช้แทน

อ่านสถานะภายในตัวเอง

เปิดช่องเล็ก ๆ ที่ 127.0.0.1 ให้ตอบว่าเธรดรับข้อมูลยังเดินอยู่ไหม ส่งสำเร็จครั้งสุดท้ายเมื่อไร ไม่มีการยิงออกนอกเลยสักครั้ง ตัวเฝ้าข้างนอกอ่านค่านี้แทน

ถ้าแก้โค้ดได้ ใช้ WatchdogSec ของ systemd ตรง ๆ

สำหรับบริการที่เราเขียนเองและแก้โค้ดได้ ทางที่สะอาดที่สุดคือให้ตัวโปรแกรมรายงานตัวกับ systemd เป็นระยะ ถ้าเงียบเกินกำหนดเมื่อไร systemd จะถือว่าล้มเหลวแล้วจัดการให้เอง ไม่ต้องมีตัวเฝ้าเพิ่ม

[Service]
Type=notify
NotifyAccess=main
WatchdogSec=90
Restart=always
RestartSec=10
ExecStart=/opt/venv/bin/python /opt/alert-bridge/bridge.py

ฝั่งโปรแกรมต้องเรียกแจ้งกลับจาก ลูปหลักที่ทำงานจริง ไม่ใช่จากเธรดแยกที่เดินอิสระ เพราะถ้าแจ้งจากเธรดที่ไม่เกี่ยวกับงาน ลูปงานค้างไปแล้วเราก็ยังรายงานว่าปกติอยู่ดี ซึ่งกลับไปที่ปัญหาเดิมเป๊ะ

import socket, os

def notify(state: str) -> None:
    addr = os.environ.get('NOTIFY_SOCKET')
    if not addr:
        return
    if addr.startswith('@'):
        addr = '\0' + addr[1:]
    with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) as s:
        s.connect(addr)
        s.send(state.encode())

notify('READY=1')
while True:
    msg = inbox.get(timeout=30)      # ลูปงานจริง
    handle(msg)
    notify('WATCHDOG=1')             # แจ้งจากตรงนี้เท่านั้น

ถ้าแก้โค้ดไม่ได้: ช่องสถานะในตัว + ตัวเฝ้าข้างนอก

เดมอนหลายตัวเราแก้ไม่ได้ หรือแก้ได้แต่ไม่อยากไปยุ่งกับลูปหลักที่ทำงานดีอยู่ ทางที่ผมใช้คือให้มันเขียนสถานะภายในออกมาเป็นไฟล์หรือเปิดช่อง HTTP ที่ฟังเฉพาะ 127.0.0.1 แล้วมีตัวเฝ้าเล็ก ๆ ข้างนอกคอยอ่าน

#!/usr/bin/env python3
# /opt/alert-bridge/watchdog.py — อ่านสถานะภายใน ไม่ยิงออกนอก
import json, subprocess, urllib.request, pathlib

STATE = pathlib.Path('/run/alert-bridge-fail')
LIMIT = 3

def healthy() -> bool:
    try:
        with urllib.request.urlopen('http://127.0.0.1:3847/health', timeout=5) as r:
            h = json.load(r)
    except Exception:
        return False
    # อายุการทำงานพอสมควรแล้วแต่ลูปรับข้อมูลไม่เดิน = ค้าง
    return not (h.get('uptime_s', 0) > 180 and not h.get('loop_alive'))

n = 0 if healthy() else int(STATE.read_text() or 0) + 1 if STATE.exists() else 1
STATE.write_text(str(n))
if n >= LIMIT:
    subprocess.run(['systemctl', 'restart', 'alert-bridge'], check=False)
    STATE.write_text('0')
# /etc/systemd/system/alert-bridge-watchdog.timer
[Unit]
Description=check alert-bridge every minute

[Timer]
OnBootSec=3min
OnUnitActiveSec=60s
AccuracySec=5s

[Install]
WantedBy=timers.target
systemctl enable --now alert-bridge-watchdog.timer
systemctl list-timers alert-bridge-watchdog.timer
# NEXT                        LEFT  LAST                        PASSED  UNIT
# Fri 2026-10-17 09:41:12 +07  38s  Fri 2026-10-17 09:40:12 +07  21s ago ...

จุดที่ผมย้ำคือ ต้องล้มติดกันหลายรอบก่อนถึงจะสั่ง restart ตัวเลข 3 รอบที่ห่างกันนาทีละครั้งแปลว่าบริการต้องผิดปกติจริงราวสามนาทีเต็มถึงจะโดนสตาร์ตใหม่ ถ้าสั่งทันทีที่เช็คพลาดครั้งเดียว เราจะได้เครื่องที่ restart ตัวเองทุกครั้งที่เครือข่ายสะดุดหนึ่งวินาที ซึ่งทำให้ระบบไม่เสถียรยิ่งกว่าเดิม

ตัวเฝ้ารันทุก 60 วินาที ต้องล้มติดกัน 3 รอบก่อน restartเครือข่ายสะดุดครั้งเดียว: ตัวนับรีเซ็ตOK n=0FAIL n=1OK n=0OK n=0OK n=0ไม่ restartค้างต่อเนื่อง: ตัวนับสะสมถึง 3OK n=0FAIL n=1FAIL n=2FAIL n=3restartเวลาแต่ละช่องคือการเช็กหนึ่งครั้ง ห่างกัน 60 วินาทีบริการต้องผิดปกติจริงราว 3 นาทีเต็มถึงจะโดนสั่งใหม่
แถวบนคือเครือข่ายสะดุดรอบเดียวแล้วกลับมาปกติ ตัวนับกลับเป็นศูนย์ แถวล่างคือบริการค้างจริง ตัวนับขึ้นถึง 3 แล้วค่อยสั่ง restart

เปลี่ยนตัวชี้วัดจาก "process อยู่" เป็น "ข้อมูลสด"

ส่วนที่ผมคิดว่าสำคัญที่สุดของทั้งเรื่องอยู่ตรงนี้ครับ ต่อให้มีตัวเฝ้าแล้ว ถ้าสิ่งที่เราเฝ้าคือ "โปรเซสยังอยู่ไหม" เราก็ยังตอบคำถามที่ผิดอยู่ดี คำถามที่ถูกคือ ข้อมูลที่บริการนี้ควรผลิตออกมา ชิ้นล่าสุดเก่าแค่ไหน

ผมเลยให้ทุกงานที่ทำงานเป็นรอบ เขียนไฟล์สถานะสั้น ๆ ทิ้งไว้หนึ่งไฟล์ แล้วให้หน้าเว็บกับตัวเฝ้าอ่านไฟล์เดียวกันนี้

{
  "ts": 1760671283,
  "loop_alive": true,
  "last_sent_ts": 1760671201,
  "last_error": null,
  "queued": 0
}

เกณฑ์ที่ผมใช้คือดูอายุของค่า ไม่ใช่ดูว่ามีไฟล์อยู่ไหม เพราะไฟล์เก่าที่ค้างอยู่คือหน้าตาของความล้มเหลวแบบเงียบพอดี

194รอบที่ล้ม
9 ชม.ที่เงียบไป
60 วิรอบตัวเฝ้า
3 รอบก่อนสั่งใหม่
# ข้อมูลเก่ากว่า 5 นาที = ถือว่าตาย ไม่ว่า process จะยังอยู่หรือไม่
AGE=$(( $(date +%s) - $(jq -r '.ts' /run/alert-bridge/health.json) ))
[ "$AGE" -gt 300 ] && echo "[ERR] stale ${AGE}s" || echo "[OK] fresh ${AGE}s"

กับดักที่เกือบทำให้ตัวเฝ้าตายเงียบเสียเอง

เรื่องนี้ผมเจอกับตัวและอยากให้ทุกคนระวัง ตัวเฝ้ากับไฟล์สถานะต้องอยู่ในมือของผู้ใช้คนเดียวกัน ผมเคยเผลอสร้างไฟล์สถานะตอนทดสอบในนามผู้ใช้ระดับสูง แล้วปล่อยไฟล์นั้นค้างไว้ พอถึงเวลาจริงที่งานตามเวลารันในนามผู้ใช้ธรรมดา มันเขียนทับไฟล์เดิมไม่ได้เพราะไม่มีสิทธิ์

ผลคือไฟล์สถานะหยุดอัปเดตแต่ยังมีอยู่ ค่าข้างในค้างอยู่ที่วินาทีสุดท้ายที่เขียนสำเร็จ และตัวเฝ้าที่อ่านไฟล์นั้นก็รายงานค่าเดิมซ้ำไปเรื่อย ๆ อย่างสงบ ระบบเฝ้าระวังของเรากลายเป็นสิ่งที่ล้มเหลวแบบเงียบเสียเอง ซึ่งอันตรายกว่าไม่มีระบบเฝ้าเลย เพราะตอนนี้เรามีเหตุผลที่จะเชื่อว่าทุกอย่างปกติ

ls -l /run/alert-bridge/health.json
# -rw-r--r-- 1 root root 118 Oct 13 01:57 health.json   <- เจ้าของผิด

install -d -o svcapp -g svcapp -m 0755 /run/alert-bridge
chown svcapp:svcapp /run/alert-bridge/health.json
sudo -u svcapp touch /run/alert-bridge/health.json   # ต้องไม่มี Permission denied
  1. ให้ไฟล์สถานะมีเจ้าของเป็นผู้ใช้ที่รันงานจริงเสมอ และทดสอบเขียนในนามผู้ใช้นั้นหนึ่งครั้งตอนติดตั้ง อย่าทดสอบด้วยสิทธิ์สูงสุดแล้วสรุปว่าผ่าน เพราะสิทธิ์สูงสุดเขียนได้ทุกที่อยู่แล้ว
  2. ใช้ /run สำหรับไฟล์สถานะ พร้อมประกาศ RuntimeDirectory ในหน่วยบริการ ระบบจะสร้างโฟลเดอร์พร้อมเจ้าของที่ถูกต้องให้ทุกครั้งที่บูต ไม่ต้องพึ่งโฟลเดอร์ที่ใครสักคนเคย mkdir ทิ้งไว้
  3. ให้ตัวเฝ้ามีตัวเฝ้าอีกชั้น อย่างน้อยที่สุดคือเช็คว่า timer ยังทำงานอยู่ และเวลาที่มันรันครั้งล่าสุดไม่เกินสองเท่าของรอบที่ตั้งไว้ ถ้าเกินให้ส่งเสียงออกไปทางช่องทางที่ไม่ได้พึ่งบริการที่กำลังมีปัญหา

สรุปเป็นกฎสามข้อที่ผมใช้กับทุกบริการตั้งแต่นั้น

ข้อแรก สถานะของ systemd ไม่ใช่คำตอบว่าบริการทำงานอยู่หรือเปล่า มันบอกแค่ว่าโปรเซสยังไม่จบ ถ้าจะรู้มากกว่านั้นต้องมีสัญญาณจากข้างในบริการเอง ข้อสอง ทุกการรอในเครือข่ายต้องมีเวลากำกับ ไม่ว่าจะเชื่อมต่อ อ่าน หรือรอคำตอบ เพราะการรอที่ไม่มีวันหมดอายุคือวิธีที่โปรแกรมตายโดยไม่ตาย ข้อสาม วัดที่ผลงาน ไม่ใช่ที่ชีพจร ถามว่าข้อมูลชิ้นล่าสุดเก่ากี่นาทีแล้ว ไม่ใช่ถามว่ายังหายใจอยู่ไหม

ส่วนเรื่องว่าเมื่อจับได้แล้วจะแจ้งใครบ้าง แจ้งด้วยความรุนแรงระดับไหน ผมเขียนแยกไว้ที่ แบ่งระดับการแจ้งเตือนไม่ให้คนเลิกอ่าน เพราะระบบที่จับได้ทุกอย่างแล้วส่งเสียงตลอดเวลา สุดท้ายจะไม่มีใครฟัง ซึ่งให้ผลเท่ากับไม่มีระบบเฝ้าเหมือนกัน ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

อยากให้เราทำให้ไหม?

ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า

คุยเรื่องงานกับเรา

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย