SSD Nodes Learn 🎉 VPS เริ่ม $5.50/เดือน
คู่มือ Matt Connorโดย Matt Connor

วิธีเลือก Type= ใน systemd ให้ตรงกับ service ของคุณ

แก้ไขปัญหา service ขึ้นสถานะ active ทั้งที่ process ตายไปแล้วด้วยการตั้งค่า Type= ให้ถูกต้อง เรียนรู้วิธีระบุ main PID สำหรับ simple, forking, notify และ oneshot

เหตุใด systemd จึงรายงานว่า unit ยังคงทำงานอยู่ทั้งที่ process ตายไปแล้ว

service unit ของ systemd จะคงสถานะเป็น active ตราบเท่าที่ process ที่ systemd ระบุว่าเป็น main process ยังคงทำงานอยู่ โดยการตั้งค่าในส่วน [Service] ของ Type= จะเป็นตัวกำหนดว่า process ใดคือ main process หากระบุค่าไม่ถูกต้อง systemd จะเฝ้าติดตามเพียง shell wrapper หรือ parent process ที่ทำงานเพียงชั่วคราว ในขณะที่ daemon ที่คุณต้องการใช้งานจริงอาจตายไปแล้วภายใน unit เดียวกัน ดังนั้น unit จึงรายงานสถานะตามความเป็นจริงของ process ที่ถูกสั่งให้เฝ้าติดตาม

การเปลี่ยนนโยบายการรีสตาร์ทไม่สามารถช่วยแก้ปัญหานี้ได้ เนื่องจาก Restart= จะทำงานก็ต่อเมื่อ main process จบการทำงานลงเท่านั้น ดังนั้น Restart=always จึงไม่ทำงานตราบใดที่ main PID (process identifier) ยังคงเป็นของ process ที่ยังทำงานอยู่ คุณต้องแก้ไข Type= ให้ถูกต้องเสียก่อน ส่วนการที่ systemd จะดำเนินการอย่างไรหลังจาก main process จบการทำงานลงจริงๆ นั้น เป็นการตัดสินใจแยกต่างหาก ซึ่งอธิบายไว้ใน คู่มือการใช้งาน Restart= และ RestartSec=

Type= ตัดสินใจเรื่องอะไรบ้าง

ทุกค่าของ Type= จะตอบคำถามสองข้อพร้อมกัน คือ systemd จะพิจารณาว่า unit นี้เริ่มทำงานแล้วเมื่อใด และกระบวนการ (process) ใดคือกระบวนการหลัก

คำตอบแรกควบคุมลำดับการทำงาน unit ที่ระบุชื่อ unit ของคุณไว้ใน After= จะต้องรอจนกว่า systemd จะเรียก unit ของคุณว่าเริ่มทำงานแล้ว หาก Type= รายงานสถานะว่า "started" เร็วเกินไป จะทำให้ unit ที่ขึ้นต่อกันเริ่มทำงานก่อนที่ service ของคุณจะพร้อมตอบสนอง

คำตอบที่สองควบคุมการกำกับดูแล systemd จะนำทุกกระบวนการที่ unit สร้างขึ้นไปไว้ใน cgroup (control group) ซึ่งเป็นฟีเจอร์ของ kernel ที่ใช้จัดกลุ่มกระบวนการเพื่อให้สามารถจำกัดทรัพยากรและสั่งยุติการทำงานพร้อมกันได้ cgroup คือวิธีที่ systemctl stop ใช้ในการล้างข้อมูล โดยค่าเริ่มต้นของ KillMode= คือ control-group ดังนั้นการสั่งหยุด unit จึงเป็นการส่งสัญญาณไปยังทุกกระบวนการที่อยู่ภายในนั้น ส่วน PID หลักนั้นมีความเฉพาะเจาะจงกว่า คือเป็นกระบวนการเดียวที่หากสิ้นสุดลงจะถือว่า unit นั้นจบการทำงาน และสถานะการจบการทำงานของกระบวนการนั้นจะกลายเป็นผลลัพธ์ของ unit การอ่านค่า cgroup โดยเข้าใจผิดว่าเป็น PID หลักคือจุดเริ่มต้นของความสับสน

Type=simple รายงานว่าเริ่มทำงานก่อนที่ไฟล์ binary จะรันจริง

Type=simple คือค่าเริ่มต้นเมื่อมีการตั้งค่า ExecStart= และไม่มีทั้ง Type= หรือ BusName= อยู่ systemd จะสร้างกระบวนการทำงานขึ้นมา ถือว่า unit เริ่มทำงานทันที และนับกระบวนการนั้นเป็น PID หลัก ส่งผลให้ unit ลำดับถัดไปเริ่มทำงานทันที ก่อนที่ไฟล์ binary ของบริการจะถูกเรียกใช้งานจริงเสียอีก

รายละเอียดสุดท้ายนี้อธิบายถึงสิ่งที่มักสร้างความประหลาดใจอยู่บ่อยครั้ง หากระบุ path ใน ExecStart= ผิดพลาด งานที่สั่งให้เริ่มทำงาน (start job) ก็ยังคงแสดงสถานะว่าสำเร็จ แต่ความล้มเหลวจะเกิดขึ้นในเวลาต่อมาเมื่อการเรียกใช้งานจริงไม่สำเร็จ systemd จะบันทึกกรณีดังกล่าวด้วย exit code 203 ซึ่งตารางของ systemd เองระบุชื่อว่า EXEC และนิยามว่าเป็นความล้มเหลวในการเรียกใช้งานไฟล์ binary ของบริการ ดังนั้นการที่ systemctl start ส่งค่ากลับโดยไม่มีข้อผิดพลาด จึงไม่ได้เป็นเครื่องพิสูจน์ว่าไฟล์ binary ของคุณมีอยู่จริง

ให้ใช้ simple สำหรับโปรแกรมที่ทำงานใน foreground และไม่มีการย้ายตัวเองไปทำงานใน background ซึ่งครอบคลุมถึง daemon สมัยใหม่ส่วนใหญ่และเกือบทุกโปรแกรมที่คุณเขียนขึ้นเอง

Type=exec จะรอให้โปรแกรมเริ่มทำงานจริง

Type=exec คือ simple ที่เพิ่มขั้นตอนขึ้นมาอีกหนึ่งขั้น systemd จะถือว่า unit เริ่มทำงานก็ต่อเมื่อทั้งการ fork และการเรียกใช้ binary สำเร็จเท่านั้น หากหา binary ไม่พบหรือ User= ไม่สามารถแก้ไขได้ งานการเริ่มบริการจะล้มเหลวทันที แทนที่จะรายงานว่าสำเร็จแล้วค่อยล้มเหลวในภายหลัง

Type=exec เริ่มมีใน systemd 240 ดังนั้น server distribution ในปัจจุบันทุกตัวจึงรองรับ Ubuntu 24.04 มาพร้อมกับ systemd 255 และ Debian 13 มาพร้อมกับ systemd 257 ณ เดือนสิงหาคม 2026 ตรวจสอบเวอร์ชันของคุณด้วย systemctl --version

สิ่งที่ต้องแลกคือขั้นตอนการประสานงานที่เพิ่มขึ้นหนึ่งขั้นในตอนเริ่มต้น แต่สิ่งที่ได้รับคือสถานะการออก (exit status) ที่ตรงไปตรงมาของ systemctl start สำหรับโปรแกรมที่ทำงานใน foreground ควรเลือกใช้ exec แทน simple

Type=forking และปัญหาการหา PID หลักไม่พบ

Type=forking เป็นการแจ้งให้ systemd ทราบว่ากระบวนการใน ExecStart= จะทำการ fork กระบวนการลูกออกมาแล้วจบการทำงานลงโดยเจตนา systemd จะรอให้กระบวนการแรกนั้นจบลงก่อน แล้วจึงถือว่า unit นั้นเริ่มทำงานแล้ว กระบวนการลูกที่เหลืออยู่คือ daemon ที่แท้จริง

ความยากอยู่ที่การระบุตัวตน เนื่องจากกระบวนการที่ systemd สั่งเริ่มทำงานได้จบไปแล้ว systemd จึงต้องหาว่ากระบวนการใดที่เหลืออยู่คือกระบวนการหลัก ให้กำหนดค่า PIDFile= ไปยังไฟล์ที่ daemon เขียนไว้ ซึ่งปกติจะเป็น path ภายใต้ /run แล้ว systemd จะอ่านค่า PID จากไฟล์นั้น นอกจากนี้ systemd จะตรวจสอบด้วยว่า PID ในไฟล์ดังกล่าวเป็นของกระบวนการที่อยู่ใน service นี้จริงหรือไม่ ดังนั้นหากเป็นไฟล์เก่าที่ระบุถึงกระบวนการที่ไม่เกี่ยวข้อง systemd จะปฏิเสธการใช้งานแทนที่จะเชื่อถือไฟล์นั้น

หากไม่มี PIDFile= ค่า GuessMainPID= จะถูกนำมาใช้ ซึ่งค่าเริ่มต้นคือ yes การคาดเดาจะเชื่อถือได้ก็ต่อเมื่อ service นั้นทำงานเหลือเพียงกระบวนการเดียวเท่านั้น คู่มือระบุข้อจำกัดไว้อย่างชัดเจนว่า หาก daemon ประกอบด้วยกระบวนการมากกว่าหนึ่งกระบวนการ การคาดเดาอาจผิดพลาดและระบบตรวจจับความล้มเหลวจะหยุดทำงาน นอกจากนี้ unit อาจจบลงด้วย PID หลักเป็น 0 ซึ่งหมายความว่า systemd ไม่มีกระบวนการใดให้คอยดูแลเลย

daemon ส่วนใหญ่ที่ใช้วิธี fork มักจะมีตัวเลือก (switch) ที่ช่วยให้โปรแกรมทำงานใน foreground ได้ ให้ใช้ตัวเลือกนั้นร่วมกับ Type=exec แล้วลบบรรทัด PIDFile= ออก การลดส่วนประกอบที่ซับซ้อนลงจะช่วยลดโอกาสที่จะสูญเสียการติดตาม PID ได้

Type=oneshot สำหรับงานที่ต้องทำงานจนเสร็จสิ้น

Type=oneshot คาดหวังให้กระบวนการทำงานจนจบและออกจากโปรแกรม systemd จะถือว่า unit นั้นเริ่มทำงานเสร็จสมบูรณ์ก็ต่อเมื่อกระบวนการดังกล่าวออกจากโปรแกรมแล้วเท่านั้น ซึ่งทำให้ oneshot เป็นรูปแบบที่เหมาะสมสำหรับงานใดๆ ที่ unit อื่นจำเป็นต้องรอให้เสร็จสิ้น นอกจากนี้ยังเป็นค่าเริ่มต้นโดยนัยเมื่อ unit ไม่ได้ระบุทั้ง Type= หรือ ExecStart= ไว้

พฤติกรรมสองประการที่เฉพาะเจาะจงสำหรับ oneshot คือ เป็น type เพียงประเภทเดียวที่ยอมรับบรรทัด ExecStart= ได้มากกว่าหนึ่งบรรทัด และบรรทัดเหล่านั้นจะทำงานตามลำดับที่กำหนด ค่าเริ่มต้นของระยะเวลา timeout ในการเริ่มทำงานจะถูกปิดใช้งานไว้ ดังนั้น oneshot ที่ค้างอยู่จะรอไปเรื่อยๆ จนกว่าคุณจะกำหนด TimeoutStartSec= ด้วยตนเอง

หลังจากกระบวนการออกจากโปรแกรมแล้ว unit จะกลับสู่สถานะ inactive แต่ RemainAfterExit=yes จะคงสถานะไว้เป็น active โดยไม่มีกระบวนการใดๆ ทำงานอยู่เลย นี่คือรูปแบบที่ตั้งใจให้เป็นของอาการที่กล่าวถึงที่ด้านบนของหน้านี้ และเป็นสิ่งที่ถูกต้องเมื่อหน้าที่ของ unit คือการทิ้งสถานะบางอย่างไว้เบื้องหลังแทนที่จะคงให้มีบางอย่างทำงานอยู่ตลอด เช่น การโหลดชุดกฎของ firewall หรือการเริ่มทำงานของ container stack นี่คือรูปแบบที่อยู่เบื้องหลัง Docker Compose stack ที่กลับมาทำงานใหม่หลังรีบูต โดยที่ unit จะรันคำสั่ง compose จากนั้นออกจากโปรแกรม และคงสถานะ active ไว้เนื่องจาก container ที่มันเริ่มทำงานนั้นยังคงทำงานอยู่หลังจากที่ตัว unit จบการทำงานไปแล้ว นอกจากนี้ unit แบบ oneshot ยังเป็นสิ่งที่ตัวกำหนดเวลาเรียกใช้งาน ซึ่งเป็นอีกส่วนหนึ่งของ การรันงานบน systemd timer แทนการใช้ cron

Type=notify ช่วยให้ service แจ้งสถานะเมื่อพร้อมทำงาน

Type=notify เป็นการย้ายการตัดสินใจไปไว้ที่ตัว service เอง โดย systemd จะคงสถานะการเริ่มงานไว้จนกว่า process จะส่ง READY=1 ผ่าน Unix socket ซึ่งได้รับ path มาในตัวแปร environment NOTIFY_SOCKET สำหรับ C interface นั้นคือ sd_notify(3) ซึ่งเซิร์ฟเวอร์จำนวนมากรองรับการใช้งานนี้อยู่แล้ว

นี่คือคำตอบที่แม่นยำสำหรับคำถามที่ว่า "บริการเริ่มทำงานแล้วหรือยัง" เพราะ simple และ exec จะรายงานว่าเริ่มทำงานแล้วตั้งแต่ก่อนที่ service จะอ่านค่า configuration หรือเปิด listening socket ทำให้ unit ที่ขึ้นต่อกันอาจเริ่มทำงานเร็วเกินไปและล้มเหลวในการเชื่อมต่อครั้งแรก ในขณะที่ notify จะรายงานว่าเริ่มทำงานแล้วในวินาทีที่ service แจ้งว่าพร้อมใช้งานจริง

systemd จะยอมรับข้อความดังกล่าวจาก main process เท่านั้น ซึ่งเป็นความหมายของ NotifyAccess=main และ Type=notify ก็สื่อถึงเรื่องนี้เช่นกัน หากข้อความถูกส่งมาจาก child process หรือ helper process ให้ตั้งค่า NotifyAccess=all สคริปต์ shell สามารถเรียกใช้ systemd-notify --ready ได้ แต่เนื่องจากเป็นการทำงานของ process ระยะสั้นที่แยกออกมา จึงจำเป็นต้องใช้ NotifyAccess=all และ systemd อาจไม่สามารถระบุที่มาของข้อความได้หากผู้ส่งจบการทำงานไปแล้ว ดังนั้น service ที่สื่อสารผ่านโปรโตคอลนี้โดยตรงจึงมีความน่าเชื่อถือมากกว่า

มีการตั้งค่าที่เกี่ยวข้องอีกสองรายการที่ควรทราบ คือ Type=notify-reload ซึ่งมีให้ใช้งานตั้งแต่ systemd 253 เป็นต้นไป โดยขยายการทำ handshake นี้ไปถึงการ reload ด้วย ทำให้ systemctl reload จะส่งค่ากลับเมื่อ service รายงานว่าการ reload เสร็จสิ้นแล้ว แทนที่จะส่งค่ากลับทันทีที่ส่งสัญญาณไป ส่วน WatchdogSec= จะเป็นการสั่งให้ service ที่ใช้การแจ้งสถานะส่งข้อความ keep-alive ตามช่วงเวลาที่กำหนด และ systemd จะถือว่าการไม่ส่งข้อความตามกำหนดเวลาคือความล้มเหลว

Type=dbus และ Type=idle

Type=dbus จะรอจนกว่าบริการจะลงทะเบียนชื่อบน D-Bus ซึ่งเป็น message bus ที่บริการของระบบและเดสก์ท็อปใช้สื่อสารกัน ค่านี้จำเป็นต้องใช้ BusName= และจะกลายเป็นค่าเริ่มต้นทันทีที่มีการตั้งค่า BusName= ให้ใช้ค่านี้เฉพาะกับบริการที่มีการลงทะเบียนชื่อบน bus จริงๆ เท่านั้น

Type=idle มีการทำงานคล้ายกับ simple แต่จะหน่วงเวลาการรันโปรแกรมไว้จนกว่างานที่อยู่ในคิวจะถูกส่งออกไป โดยจำกัดเวลาไว้ที่ 5 วินาที ค่านี้มีไว้เพื่อให้ข้อความที่แสดงบนคอนโซลระหว่างการบูตไม่ปะปนกับข้อความสถานะของระบบ ไม่ใช่เครื่องมือสำหรับจัดการลำดับการทำงาน และไม่ควรนำไปใช้กับบริการทั่วไป

เหตุใด wrapper script จึงทำให้ systemd ติดตาม PID ผิดพลาด

นี่คือรูปแบบที่ทำให้เกิดอาการดังกล่าว

[Service]
Type=simple
ExecStart=/opt/app/run.sh
#!/bin/bash
export APP_ENV=production
/opt/app/bin/server --config /etc/app.yaml &
/opt/app/bin/exporter --port 9101

systemd บันทึก shell ไว้เป็น main PID โดย shell จะยังคงทำงานอยู่ขณะที่ exporter ทำงานใน foreground หาก server หยุดทำงาน shell จะไม่รับรู้ ดังนั้น main PID จึงยังคงทำงานอยู่ สถานะของ unit จึงยังคงเป็น active และ Restart= ก็ไม่มีสิ่งใดให้ดำเนินการ ทั้งสองกระบวนการยังคงอยู่ใน cgroup ของ unit ตลอดเวลา ดังนั้น systemctl stop จึงยังคงทำความสะอาดได้อย่างถูกต้อง สิ่งที่ล้มเหลวคือการกำกับดูแล (supervision) ไม่ใช่การทำความสะอาด

วิธีแก้ไขขึ้นอยู่กับจำนวนกระบวนการที่ทำงานต่อเนื่องใน unit นั้น

หากมีเพียงกระบวนการเดียว ให้แทนที่ shell ด้วยกระบวนการนั้น

#!/bin/bash
export APP_ENV=production
exec /opt/app/bin/server --config /etc/app.yaml

exec จะแทนที่ shell ด้วยโปรแกรมที่ระบุและใช้ PID เดิม ดังนั้น PID ที่ systemd บันทึกไว้จึงเป็นของ daemon โดยตรง วิธีที่ดีกว่าคือการลบ wrapper ออก Environment= และ EnvironmentFile= สามารถส่งผ่านตัวแปรต่างๆ ได้ และ ExecStartPre= สามารถจัดการขั้นตอนการตั้งค่าได้ systemd จึงสามารถเรียกใช้ daemon ได้โดยตรงและทราบ PID ที่แท้จริงตั้งแต่เริ่มทำงาน

หากมีสองกระบวนการ จะไม่มี PID เดียวที่แสดงถึง unit นั้นได้ ให้แยกออกเป็นสอง unit และกำหนดลำดับด้วย After= และ Wants= การจัดรูปแบบให้หนึ่ง unit ต่อหนึ่งกระบวนการคือสิ่งที่ systemd กำกับดูแลได้ดีที่สุด และเป็นวิธีเดียวที่แต่ละกระบวนการจะมีพฤติกรรมการ restart เป็นของตนเอง

การเปลี่ยนแปลงของ ExitType=cgroup

ExitType= ถูกเพิ่มเข้ามาใน systemd 250 ค่าเริ่มต้นคือ main ซึ่งหมายความว่า unit จะถือว่าหยุดทำงานเมื่อ process หลักจบการทำงานลง แต่ด้วย ExitType=cgroup unit จะยังคงถือว่าทำงานอยู่ตราบเท่าที่ยังมี process ใดๆ ใน cgroup นั้นทำงานอยู่

[Service]
Type=simple
ExitType=cgroup
ExecStart=/opt/app/launcher

การตั้งค่านี้ช่วยแก้ปัญหาเฉพาะอย่างหนึ่ง หากมีตัวเรียกใช้งาน (launcher) ที่เริ่มการทำงานจริงแล้วจบการทำงานลง ภายใต้ ExitType=main ระบบ systemd จะถือว่า unit นั้นหยุดทำงานและสั่งฆ่า process ที่เหลืออยู่ทั้งหมด แต่ด้วย ExitType=cgroup unit จะติดตามสถานะของทั้งกลุ่มแทน

ต้องทำความเข้าใจให้ชัดเจนว่าสิ่งนี้ไม่ได้แก้ปัญหาอะไรบ้าง ExitType=cgroup จะรักษา unit ให้ทำงานอยู่ตราบเท่าที่มีอย่างน้อยหนึ่ง process ยังคงอยู่ ดังนั้นหาก unit หนึ่งดูแล daemon สองตัว unit นั้นจะยังคงทำงานอยู่แม้ว่าตัวหนึ่งจะตายไป การตั้งค่านี้แก้ปัญหาในกรณีของ launcher เท่านั้น ไม่ได้เปลี่ยน unit ให้กลายเป็นตัวควบคุม (supervisor) ของ process อิสระหลายตัว นอกจากนี้ ExitType= ยังไม่สามารถใช้งานร่วมกับ Type=oneshot ได้

cgroup ยังเป็นจุดที่ใช้ในการบันทึกการใช้ทรัพยากร ดังนั้นข้อจำกัดอย่าง MemoryMax= และ CPUQuota= จึงมีผลกับทุก process ที่ unit นั้นสร้างขึ้น ไม่ว่า Type= จะระบุเกี่ยวกับ main PID ไว้อย่างไรก็ตาม ในส่วนนี้สามารถดูรายละเอียดเพิ่มเติมได้ที่ การจำกัดหน่วยความจำและ CPU ของ service ด้วย systemd

วิธีค้นหา process ที่ systemd กำลังเฝ้าติดตามอยู่จริง

ให้ดำเนินการตามขั้นตอนต่อไปนี้กับ unit ที่คุณกำลังแก้ไขปัญหา โดยเริ่มจากการอ่านสิ่งที่ systemd โหลดขึ้นมา จากนั้นดูสิ่งที่ระบบติดตามอยู่ แล้วจึงเปรียบเทียบกับตาราง process

systemctl cat app.service
systemctl show -p Type,ExitType,GuessMainPID,PIDFile,MainPID,RemainAfterExit app.service

systemctl cat จะแสดงไฟล์ unit พร้อมกับ drop-in ทุกรายการที่เกี่ยวข้อง เพื่อให้คุณได้อ่านสิ่งที่ systemd โหลดจริงแทนที่จะเป็นไฟล์ที่คุณจำได้ว่าเคยแก้ไข systemctl show จะแสดงค่าที่มีผลจริง รวมถึงค่าเริ่มต้นที่คุณไม่ได้ระบุไว้ในไฟล์ ให้จดบันทึกค่าของ MainPID ไว้ก่อนดำเนินการในขั้นตอนถัดไป

systemd-cgls --unit=app.service
ps -o pid,ppid,stat,etime,args -p "$(systemctl show -p MainPID --value app.service)"

systemd-cgls จะแสดงรายการ process ทั้งหมดที่อยู่ใน cgroup ของ unit นั้น บรรทัด ps จะอธิบายถึง process เดียวที่ systemd กำลังควบคุมดูแลอยู่ ให้พิจารณาทั้งสองส่วนประกอบกัน หาก MainPID เป็น 0 หมายความว่า systemd ไม่มี process ใดให้เฝ้าติดตาม หาก MainPID ชี้ไปยัง shell ในขณะที่ cgroup ยังคงมี daemon ของคุณอยู่ แสดงว่าเป็นกรณีของ wrapper ที่กล่าวถึงข้างต้น หาก cgroup มีจำนวน process มากกว่าที่คาดไว้ แสดงว่ามีการใช้ launcher หรือ daemon แบบ forking เข้ามาเกี่ยวข้อง

systemctl status app.service
journalctl -u app.service -b

systemctl status จะแสดงบรรทัดสถานะและโครงสร้าง cgroup พร้อมกัน ซึ่งมักจะตอบคำถามทั้งสองข้อได้ในคราวเดียว journalctl -u ที่จำกัดขอบเขตเฉพาะการบูตครั้งนี้ด้วย -b จะแสดงเหตุการณ์การเริ่มและหยุดทำงานที่ systemd บันทึกไว้สำหรับ unit นั้น พร้อมกับ exit code ที่ระบบตรวจพบ หาก daemon เขียน log ลงไฟล์ของตัวเองแทนที่จะส่งเข้า journal ให้คุณอ่านไฟล์นั้นด้วย เพราะ systemd สามารถบันทึกได้เฉพาะสิ่งที่ส่งมาถึงตัวมันเท่านั้น

เมื่อคุณเปลี่ยนแปลง Type= ให้ทำการ reload และ restart เสมอ

systemd-analyze verify /etc/systemd/system/app.service
sudo systemctl daemon-reload
sudo systemctl restart app.service

systemd-analyze verify จะทำการตรวจสอบไฟล์และรายงานการตั้งค่าที่ไม่สามารถยอมรับได้ daemon-reload จะสั่งให้ systemd อ่านไฟล์ unit จากดิสก์ใหม่อีกครั้ง การเปลี่ยนแปลงใน Type= จะไม่มีผลกับ unit ที่กำลังทำงานอยู่ ดังนั้นการ restart จึงเป็นสิ่งที่จำเป็น ไม่ใช่ทางเลือก

จากนั้นให้ทดสอบการเปลี่ยนแปลง โดยนำ PID ของ process ที่คุณต้องการตรวจสอบจาก systemd-cgls แล้วสั่ง kill process นั้น จากนั้นให้รัน systemctl is-active app.service ทันที หาก Type= ถูกต้อง unit จะออกจากสถานะ active หากสถานะยังคงเป็น active อยู่ แสดงว่า systemd ยังคงเฝ้าติดตาม process อื่นอยู่อีก

คุณควรใช้ Type= ใดสำหรับ systemd service

  • โปรแกรมที่ทำงานใน foreground: Type=exec
  • โปรแกรมที่รองรับการแจ้งเตือนสถานะพร้อมใช้งาน: Type=notify และ notify-reload หากโปรแกรมนั้นยืนยันการโหลดซ้ำ (reload) ด้วย
  • daemon ที่ต้องการย้ายตัวเองไปทำงานใน background: Type=forking ร่วมกับ PIDFile= หรือใช้สวิตช์สำหรับทำงานใน foreground ของโปรแกรมนั้นร่วมกับ Type=exec
  • สคริปต์ที่ทำงานจนเสร็จแล้วจบการทำงาน: Type=oneshot และใช้ RemainAfterExit=yes เมื่อต้องการให้สถานะคงอยู่หลังจากจบการทำงาน
  • ตัวเรียกใช้งาน (launcher) ที่จบการทำงานในขณะที่กระบวนการลูกยังคงทำงานอยู่: Type=simple ร่วมกับ ExitType=cgroup

หากคุณไม่แน่ใจว่า daemon ของบุคคลที่สามต้องใช้ค่าใด ให้ตรวจสอบไฟล์ unit ที่มาพร้อมกับแพ็กเกจก่อน การรันคำสั่ง systemctl cat บน unit ที่มาพร้อมกับ distribution จะแสดงค่า Type= ที่ผู้พัฒนาเลือกไว้ ซึ่งเป็นค่าที่ผ่านการทดสอบจากผู้ใช้งานจำนวนมากมาแล้ว

FAQ

ทำไม systemd unit ของฉันถึงยังคงสถานะ active ทั้งที่ process ตายไปแล้ว?

เพราะ process ที่ systemd ถือว่าเป็น process หลักยังคงทำงานอยู่ systemd จะเฝ้าดูเพียง PID เดียวต่อหนึ่ง service ตามที่กำหนดไว้ใน Type= ไม่ได้เฝ้าดูทุก process ใน cgroup ของ unit นั้น สาเหตุที่พบบ่อยคือการใช้ wrapper script ที่เริ่มด้วย Type=simple โดย shell จะกลายเป็น PID หลัก ดังนั้น unit จึงยังคงสถานะ active แม้ daemon ที่ shell เรียกขึ้นมาทำงานเบื้องหลังจะจบการทำงานไปแล้ว ให้รัน systemctl show -p MainPID app.service จากนั้นแสดงรายการ cgroup ของ unit ด้วย systemd-cgls --unit=app.service แล้วนำมาเปรียบเทียบกัน

Type=simple และ Type=exec ต่างกันอย่างไร?

Type=simple จะถือว่า unit เริ่มทำงานทันทีที่ systemd สร้าง process ขึ้นมา ก่อนที่ binary จะถูกเรียกใช้งานจริง ดังนั้นหากระบุ path ใน ExecStart= ผิด ก็จะยังคงได้รับสถานะว่า start job สำเร็จก่อนที่จะตามมาด้วยความล้มเหลวในภายหลัง ส่วน Type=exec จะรอจนกว่าการเรียกใช้งานจะสำเร็จ ดังนั้นความล้มเหลวจะถูกรายงานโดย start job นั้นโดยตรง ทั้งสองแบบถือว่า process เดียวกันเป็น PID หลัก และ Type=exec จำเป็นต้องใช้ systemd เวอร์ชัน 240 ขึ้นไป

ฉันยังจำเป็นต้องใช้ PIDFile= เมื่อใช้ Type=forking หรือไม่?

จำเป็น ในกรณีที่ daemon มีการเขียนไฟล์ดังกล่าว หากไม่มี PIDFile= ระบบ systemd จะใช้วิธี GuessMainPID= ซึ่งเป็นการคาดเดาและเชื่อถือได้เฉพาะกับ service ที่ทำงานเป็น process เดียวเท่านั้น เมื่อการคาดเดาผิดพลาดหรือไม่สามารถทำได้ การตรวจจับความล้มเหลวและการรีสตาร์ทอัตโนมัติสำหรับ unit นั้นจะหยุดทำงาน ให้ชี้ PIDFile= ไปยัง path ที่ daemon เขียนไฟล์จริง ซึ่งโดยปกติจะอยู่ภายใต้ /run

เมื่อใดที่ควรใช้ RemainAfterExit=yes?

ควรใช้เมื่อจุดประสงค์ของ unit คือการเปลี่ยนสถานะของระบบ ไม่ใช่การรักษาให้ process ทำงานอยู่ตลอดเวลา unit ประเภท Type=oneshot ที่โหลดกฎ firewall หรือเริ่ม container stack จะจบการทำงานทันทีที่งานเสร็จสิ้น หากไม่มี RemainAfterExit=yes ตัว unit จะเปลี่ยนสถานะเป็น inactive ซึ่งทำให้ systemctl stop ไม่มีอะไรให้หยุดทำงานและไม่มีวิธีเรียกใช้การล้างข้อมูลใน ExecStop= ได้ แต่หากใช้ RemainAfterExit=yes ตัว unit จะยังคงสถานะ active แม้จะไม่มี process หลงเหลืออยู่ ซึ่งเป็นพฤติกรรมที่ต้องการในกรณีนี้

การเปลี่ยน Type= จำเป็นต้องทำ daemon-reload หรือไม่?

จำเป็น และต้องรีสตาร์ท unit นั้นด้วย systemctl daemon-reload จะทำให้ systemd อ่านไฟล์ unit บนดิสก์ใหม่อีกครั้ง แต่ instance ที่กำลังทำงานอยู่จะยังคงใช้ Type= เดิมที่เริ่มไว้ตั้งแต่แรก ให้รัน sudo systemctl daemon-reload ตามด้วย sudo systemctl restart app.service ก่อนเริ่มทดสอบ มิฉะนั้นคุณจะยังคงเฝ้าดูพฤติกรรมการควบคุมแบบเดิมอยู่