Nitro: ระบบ init และตัวควบคุมดูแลโปรเซสขนาดเล็กและยืดหยุ่น
(git.vuxu.org)- Nitro คือ ตัวควบคุมดูแลโปรเซสและระบบ init ขนาดจิ๋วมาก ที่ใช้งานได้กับทั้งระบบฝังตัว เซิร์ฟเวอร์ เดสก์ท็อป และคอนเทนเนอร์
- เก็บสถานะของระบบไว้ใน RAM เท่านั้น จึงทำงานได้อย่างราบรื่นแม้บนระบบไฟล์แบบอ่านอย่างเดียว พร้อมมอบ สถาปัตยกรรมแบบ event-driven ที่รวดเร็วและมีประสิทธิภาพ
- รูปแบบการตั้งค่าคือโครงสร้างไดเรกทอรีสคริปต์ที่เรียบง่าย จึงจัดการบริการได้โดยไม่ต้องมีไฟล์ตั้งค่าที่ซับซ้อนหรือขั้นตอน build เพิ่มเติม
- รองรับ บริการแบบพารามิเตอร์ การรีสตาร์ตที่ทนทาน และความสามารถด้าน logging ที่เชื่อถือได้สำหรับแต่ละบริการ รวมถึง ฟีเจอร์ที่เหมาะกับคอนเทนเนอร์และสภาพแวดล้อมแบบฝังตัว
- รับประกัน ความยืดหยุ่นและการควบคุมระดับสูง ด้วย การควบคุมจากระยะไกลผ่านเครื่องมือ nitroctl และการควบคุมการทำงานด้วยสัญญาณ
ภาพรวม
Nitro คือ ตัวควบคุมดูแลโปรเซสขนาดจิ๋วมาก ที่สามารถใช้เป็น pid 1 บน Linux ได้
ขอบเขตการใช้งานหลักมีดังนี้
- init สำหรับเครื่อง Linux หลากหลายประเภท เช่น ระบบฝังตัว เดสก์ท็อป และเซิร์ฟเวอร์
- init ของ Linux initramfs
- init สำหรับสภาพแวดล้อมคอนเทนเนอร์ เช่น Docker/Podman/LXC/Kubernetes
- เดมอน supervision ที่ทำงานได้โดยไม่ต้องใช้สิทธิ์บนระบบ POSIX
การตั้งค่าใช้โครงสร้างสคริปต์แบบอิงไดเรกทอรี โดยตำแหน่งเริ่มต้นคือ /etc/nitro
ข้อกำหนด
- ต้องการการรองรับ Unix socket ของเคอร์เนล
- ต้องมี
tmpfsหรือ ไดเรกทอรี/runที่เขียนได้
ข้อดีเมื่อเทียบกับระบบอื่น
- ข้อมูลสถานะทั้งหมด ถูกเก็บไว้ใน RAM เท่านั้น จึงทำงานได้บน root filesystem แบบอ่านอย่างเดียวโดยไม่ต้องใช้เทคนิคพิเศษ
- ให้ประสิทธิภาพด้วย รูปแบบการทำงานแบบ event-driven ที่ไม่มีการ polling
- ไม่มีการจัดสรรหน่วยความจำแบบไดนามิก ระหว่างรันไทม์
- ไม่มีการใช้ file descriptor เพิ่มขึ้นอย่างไม่จำกัด
- ต้องการเพียง ไบนารีแบบ self-contained ตัวเดียว (อาจมีไบนารีควบคุมเพิ่มเป็นตัวเลือก)
- ไม่ต้องแปลงหรือคอมไพล์ไฟล์ตั้งค่า บริการเป็นเพียง ไดเรกทอรีธรรมดาที่มีสคริปต์อยู่ภายใน
- รองรับ การรีสตาร์ตบริการและสายโซ่ logging
- ทำงานได้ตามปกติแม้นาฬิการะบบจะไม่แม่นยำ
- สามารถรันผ่าน
/etc/ttysบน FreeBSD ได้ - สามารถสร้าง static binary ขนาดเล็กมากได้เมื่อใช้ musl libc
การจัดการบริการ
-
แต่ละไดเรกทอรีบริการ (ค่าเริ่มต้นอยู่ภายใน
/etc/nitro) อาจมีไฟล์ต่อไปนี้setup: สคริปต์ (ทางเลือก) ที่รันก่อนเริ่มบริการ จะเริ่มบริการได้ก็ต่อเมื่อจบแบบปกติ (0) เท่านั้นrun: สคริปต์สำหรับการทำงานของบริการ ตราบใดที่ยังไม่จบจะถือว่าบริการยังทำงานอยู่ หากยังไม่ถูกติดตั้งจะถือเป็นบริการแบบ one-shotfinish: สคริปต์ (ทางเลือก) ที่รันหลังrunจบลง โดยส่งสถานะการจบและค่าสัญญาณเป็นอาร์กิวเมนต์log: symbolic link ที่ชี้ไปยังไดเรกทอรีบริการอื่น โดยจะ pipe เอาต์พุตของ run ไปเป็นอินพุตของบริการนั้น (ใช้ทำ logging chain ได้)down: หากไฟล์นี้มีอยู่ nitro จะไม่ยกบริการนี้ขึ้นโดยค่าเริ่มต้น- หากชื่อไดเรกทอรีลงท้ายด้วย '@' จะถูกมองข้ามและนำไปใช้เป็นบริการแบบพารามิเตอร์ได้
- ชื่อบริการต้องยาวน้อยกว่า 64 ตัวอักษร และห้ามมีอักขระ
/,,, ขึ้นบรรทัดใหม่
-
ยูทิลิตี
chpstของ runit มีประโยชน์ในการเขียนสคริปต์run
บริการพิเศษ
LOG: บริการค่าเริ่มต้นสำหรับบันทึก log ของทุกบริการที่ไม่มีลิงก์ logSYS:SYS/setupจะถูกรันก่อนเริ่มทุกบริการ จึงใช้สร้างลำดับการเริ่มบริการได้SYS/finish: รันก่อนเข้าสู่ขั้นตอนปิดระบบทั้งหมดSYS/final: รันหลังจากทุกโปรเซสถูกปิดแล้วSYS/fatal: รันแทนการปิดเมื่อเกิดข้อผิดพลาดร้ายแรง (ถ้ามี)SYS/reincarnate: รันแทน shutdown และอาจใช้สำหรับการสร้าง initramfs ขึ้นใหม่ เป็นต้น
บริการแบบพารามิเตอร์
- ไดเรกทอรีบริการที่ลงท้ายด้วย '@' จะถูก nitro มองข้าม แต่สามารถระบุใช้งานได้โดยตรงผ่าน symbolic link หรือคำสั่ง
nitroctl - พารามิเตอร์ที่ต่อท้ายหลัง '@' จะถูกส่งเป็นอาร์กิวเมนต์ตัวแรกให้แต่ละสคริปต์
- ตัวอย่าง: หากมี symbolic link
agetty@/runและagetty@tty1จะมีการรันagetty@/run tty1 - เมื่อป้อน
nitroctl up agetty@tty2ก็สามารถรันagetty@/run tty2ได้ (ไม่ขึ้นกับว่ามีไดเรกทอรีอยู่หรือไม่)
- ตัวอย่าง: หากมี symbolic link
โหมดการทำงาน
- วงจรชีวิตทั้งหมดประกอบด้วย 3 ขั้นตอนคือ บูต รันบริการ (supervision) และปิดระบบ
- บูต: หากมีบริการพิเศษ
SYSจะรันตั้งแต่setupก่อน แล้วจึงรันทุกบริการที่ไม่ใช่ down - หากบริการจบลงจะถูกรีสตาร์ตใหม่ แต่ถ้าเพิ่งรีสตาร์ตไปไม่นานจะรอ 2 วินาที
- สามารถส่งสัญญาณปิดระบบได้ด้วย
nitroctl RebootหรือShutdown- ในกรณีนี้จะเป็นลำดับ
SYS/finish→ ส่ง SIGTERM ให้ทุกบริการ (รอได้สูงสุด 7 วินาที) → SIGKILL →SYS/final→ ลำดับการปิด
- ในกรณีนี้จะเป็นลำดับ
- หากใช้กับคอนเทนเนอร์หรือ supervisor ที่ไม่ใช้สิทธิ์ จะปิดเฉพาะโปรเซสเท่านั้น
- บูต: หากมีบริการพิเศษ
การควบคุมด้วย nitroctl
- เครื่องมือ CLI nitroctl ใช้ควบคุม nitro จากระยะไกลได้
ตัวอย่างคำสั่ง:
- list: แสดงรายการบริการ สถานะ PID uptime และสถานะการจบล่าสุด
- up/down/start/stop/restart: ควบคุมการเริ่ม·หยุด·รีสตาร์ตบริการ เป็นต้น
- การส่งสัญญาณ: p(SIGSTOP), c(SIGCONT), h(SIGHUP), a(SIGALRM), i(SIGINT), q(SIGQUIT), 1(SIGUSR1), 2(SIGUSR2), t(SIGTERM), k(SIGKILL)
- pidof: แสดง PID ของบริการที่ระบุ
- rescan: อ่านไดเรกทอรีบริการใหม่ และสะท้อนบริการที่ถูกเพิ่มหรือลบ
- Shutdown/Reboot: ปิดหรือรีบูตทั้งระบบ
การควบคุมผ่านสัญญาณ
- สามารถควบคุมได้ด้วยการส่งสัญญาณตรงไปยังโปรเซส nitro
- SIGHUP: สแกนบริการใหม่ (rescan)
- SIGINT: รีบูต
- SIGTERM: ปิดระบบ (หาก nitro ไม่ใช่ pid 1)
Nitro ในฐานะ init บน Linux
- Nitro เป็นไบนารีแบบบรรจุครบในตัวและสามารถบูตเป็น Linux pid 1 ได้โดยตรง
- จะเมานต์
/dev,/runเมื่อต้องการ และให้จัดการการทำงานอื่น ๆ ในSYS/setup - อีเวนต์ Ctrl-Alt-Del จะทริกเกอร์การรีบูตอย่างเป็นระเบียบ
การใช้ Nitro เป็น init ในคอนเทนเนอร์ Docker
- Nitro สามารถ build แบบ static และใส่ลงในคอนเทนเนอร์ได้อย่างง่ายดาย
- ต้องมี
/runอยู่ในคอนเทนเนอร์เพื่อใช้พาธซ็อกเก็ตเริ่มต้น - หากทำ bind mount ให้ control socket ก็จะควบคุมจากภายนอกด้วย nitroctl จากระยะไกลได้
Nitro บน FreeBSD
- สามารถให้ FreeBSD init ดูแล nitro ได้ด้วยการเพิ่มบรรทัดต่อไปนี้ใน
/etc/ttys/etc/nitro "/usr/local/sbin/nitro" "" on
ผู้เขียน
- Leah Neukirchen leah@vuxu.org
คำขอบคุณ
- พัฒนาขึ้นจากการวิเคราะห์อย่างละเอียดของ ระบบควบคุมดูแลโปรเซส ที่มีอยู่เดิม เช่น daemontools, freedt, runit, perp, s6 เป็นต้น
สัญญาอนุญาต
- สัญญาอนุญาต 0BSD (ดูรายละเอียดในไฟล์ LICENSE)
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
อยากเห็นการเปรียบเทียบกับ runit เพราะ runit เป็น init system ที่มินิมอลสุด ๆ แต่ก็เกือบครบถ้วนมาก มีหลายอย่างคล้ายกัน เช่น control directory, dependency ที่ไม่เป็นแบบ declarative, โครงสร้างสคริปต์คล้ายกัน, แนวทางด้าน logging แม้แต่ในหน้าคำอธิบายก็พูดถึง runit อยู่เล็กน้อยและแนะนำให้ใช้ร่วมกับยูทิลิตี
chpstจุดที่ต่างกันคือโครงสร้างที่ใช้ service directory เดียวเพื่อจัดการหลายโปรเซสที่คล้ายกันโดยใส่พารามิเตอร์ได้ (เช่นagetty) ซึ่งผมคิดว่าดี อีกอย่างคือสามารถสั่ง reboot หรือ shutdown ได้ตรงผ่านไบนารีเดียว (nitroctl) ในขณะที่ runit แยกเป็นหลายไบนารีตอนปลดระวางเซิร์ฟเวอร์ชุดสุดท้ายที่ยังใช้ runit จัดการโปรเซสเมื่อปีที่แล้ว รู้สึกเสียดายมาก ตอนที่เขียน service ของ runit เองครั้งแรกเมื่อราว 15 ปีก่อน ผมเชื่อว่านี่แหละคือวิธีมาตรฐานในการจัดการ service บน Linux หลังจากนั้นผมห่างจาก Linux ไป 5 ปี พอกลับมาก็พบว่า systemd กลายเป็นค่าเริ่มต้นไปแล้ว เคยได้ยินคำวิจารณ์แย่ ๆ มาหลายครั้ง แต่ภายหลังก็พบว่าความรู้สึกต่อต้านจำนวนมากค่อนข้างบิดเบือน ทุกวันนี้ผมรัน service สำหรับสตรีมกล้องและข้อมูลอุณหภูมิบน Pi Zero ใน vivarium ของสัตว์เลื้อยคลาน และการตั้งค่าด้วย systemd นั้นง่ายมาก บนเดสก์ท็อป OpenSuse และโน้ตบุ๊กทำงานก็จัดการ service ต่าง ๆ ด้วย systemd ได้ง่ายเช่นกัน เลยรู้สึกว่า “การมีมาตรฐานก็นับว่าเป็นเรื่องดี”
มีการเปรียบเทียบแบบมินิมอลที่เหมาะสมระหว่าง runit กับ nitro อยู่ในสไลด์นำเสนอ (PDF) ของ Leah Neukirchen ที่เผยแพร่ในปี 2024
https://leahneukirchen.org/talks/#nitroyetanotherinitsy
Leah Neukirchen เป็นคนที่มีบทบาทมากในชุมชน Void Linux เลยคาดว่าโปรเจกต์นี้จะเชื่อมโยงกับ Void อย่างใกล้ชิด อยากให้มีบทความอย่างเป็นทางการมากกว่านี้เกี่ยวกับวิธีใช้ nitro บน Void
สงสัยว่าเรื่อง “ไม่มี dependency แบบ declarative” นี่ถูกมองว่าเป็นข้อดีหรือเปล่า ผมเคยได้ยินคำวิจารณ์ systemd ในฐานะ init มามาก แต่แทบไม่ค่อยเห็นใครวิจารณ์แนวทาง declarative design โดยตรง อยากฟังเหตุผลแบบละเอียดว่าทำไม
ผมรู้จัก runit ผ่าน Void Linux และใช้งานเป็น init system ได้ดี แต่รู้สึกเสียดายที่ UI และเอกสารยังไม่ดีพอ โดยเฉพาะการตั้งค่า logging ที่ยากมาก อยากลองทางเลือกที่เรียบง่ายคล้ายกัน แต่มีค่าเริ่มต้นที่สมเหตุสมผลกว่า UI ที่เข้าใจง่ายกว่า และเอกสารที่ดีกว่า
ทุกครั้งที่เห็นคนพูดถึงการรัน init system ในคอนเทนเนอร์ ผมมักลังเลเสมอ บางครั้งมันก็เป็นการออกแบบตามความจำเป็นจริง แต่หลายครั้งกลับดูเหมือนทำให้ระบบซับซ้อนเกินไป (โดยเฉพาะในสภาพแวดล้อม Kubernetes และคลาวด์ ที่จริงควรแยกส่วนให้ดีกว่านี้ตั้งแต่ต้น) เหมือนปรากฏการณ์ “ไหน ๆ ทุกคนก็ใช้แบบนี้กัน” มากกว่า เลยยังไม่แน่ใจว่าการพยายาม “ทำมันให้ดีขึ้น” แล้วช่วยให้ปัญหาแพร่หลายต่อไปนั้นดีกว่า หรือควรปล่อยให้คนล้มเหลวกับโซลูชันเดิมแบบชัด ๆ ไปเลยจะดีกว่า
ผมคิดว่าคอนเทนเนอร์ของแอปพลิเคชันควรยึดตามปรัชญา Unix คือ “ทำอย่างเดียวแต่ทำให้ดี” แต่ถ้าคอนเทนเนอร์มีการ
forkไม่ว่าด้วยเหตุผลใด ผมมองว่าควรมี init ของจริงอยู่ที่ PID 1จากที่เจอในสาย robotics คอนเทนเนอร์จำนวนมากเป็นเพียงการย้ายระบบซับซ้อนที่เดิมรันบน bare metal เข้ามาอยู่ในคอนเทนเนอร์ ระบบพวกนี้มี RPC แบบไร้โครงสร้างระหว่างโปรเซสเยอะมาก จึงไม่ได้มีประโยชน์มากนักที่จะหั่นเป็นหลายคอนเทนเนอร์แยก ภายในคอนเทนเนอร์แอปแบบ monolithic ถ้าจะรันหลายโปรเซส ก็มีตัวเลือกสารพัดที่ใช้กันอยู่ ตั้งแต่ supervisor, runit, systemd ไปจนถึง
tmuxผมเคยใช้โฮสติ้งที่คิดค่าบริการเป็นรายคอนเทนเนอร์ เช่น Fly.io, Render, Google Cloud Run บางครั้งเพราะเรื่องราคา จึงจำเป็นต้องรันหลายโปรเซสในคอนเทนเนอร์เดียว
ฟีเจอร์ใหม่ของ NixOS อย่าง modular-services ถูกนำเข้า Nixpkgs แล้ว การพอร์ต NixOS ไปยัง init system ใหม่หรือเคอร์เนลใหม่จะง่ายขึ้นมาก เลยคิดว่านี่เป็นช่วงเวลาที่ดีในการทดลองอะไรแบบ nitro
อยากเห็นการเปรียบเทียบระหว่าง dinit ที่ใช้ใน Chimera Linux กับ nitro พอไล่อ่าน readme อย่างเร็ว ๆ แล้วดูเหมือนว่ายังไม่มีการจัดการ service dependency
dinit: https://github.com/davmac314/dinit
Nitro ไม่จัดการ service dependency แบบ declarative คุณจึงไม่สามารถดูกราฟ dependency ระหว่าง service แบบสวย ๆ ได้ด้วยคำสั่งเดียว แต่ถ้าระบุ service ที่ต้องใช้ไว้ใน setup script มันจะเช็กว่า service นั้นขึ้นหรือยัง แล้วรอและ retry ให้อัตโนมัติ ถ้าอยากดูกราฟ dependency ก็แทบไม่มีทางนอกจากเขียนสคริปต์เองด้วยอะไรอย่าง
grepในทางกลับกัน เวลา service ตาย ก็ลืมได้ง่ายว่าควรปิด service ที่พึ่งพามันลงตามไปด้วย และตัว nitro เองก็ไม่มีวิธีที่สะดวกในการตรวจจับเรื่องนี้ผมเคยใช้ dinit บน Artix Linux มันเบาและน่าประทับใจมาก
Artix FAQ: https://artixlinux.org/faq.php
เวลาเห็นโปรเจกต์ระดับล่างแบบนี้ทีไร ผมรู้สึกว่าน่าสนใจมาก systemd มีจุดที่ดีตรงมันก้าวข้ามกรอบ SysV·POSIX แบบดั้งเดิม และใช้ประโยชน์จากความสามารถเฉพาะของ Linux kernel ได้อย่างดี แต่ผมก็หวังว่านั่นจะไม่ใช่จุดจบ และอยากเห็นไอเดียใหม่ ๆ กับนวัตกรรมเกิดขึ้นต่อไป ไม่นานมานี้ผมเพิ่งทำระบบสำหรับงาน automation ในการผลิต ที่บูต Linux kernel ผ่าน netboot ตรงจาก UEFI firmware แล้วฝังเพียงไบนารี init ตัวเดียวที่เขียนเองด้วย Go การควบคุมสภาพแวดล้อมของ OS ได้ทั้งหมดด้วยโค้ดที่ดึงมาเองและภาษาระดับสูง โดยไม่ต้องจัดการ subprocess จำนวนมากและไฟล์ config แบบข้อความสารพัด ทำให้รู้สึกเป็นอิสระมาก
เมื่อราว 13 ปีก่อน ผมเคยสร้าง init system เองด้วย C ต้องใช้ความพยายามมากกว่าที่คาดไว้มาก และนำไปใช้เพื่อบูต GUI กับ backend ให้เร็วบนฮาร์ดแวร์สเปกต่ำ มันเป็นแบบฝึกหัดการเขียนโปรแกรมที่สนุก แต่ภายหลังก็เริ่มตระหนักว่าอาจมีโซลูชันคล้าย ๆ กันอยู่แล้ว เพื่อนร่วมงานคนหนึ่งในบริษัทเดียวกันก็ไปสร้าง init อีกตัวขึ้นมา ทำให้เวอร์ชันแรกของผมเบามากและแทบไม่มี dependency นอกจาก libc ขณะที่ของเพื่อนใช้ libevent เป็นฐานและมีฟีเจอร์ขั้นสูงกว่า
ผมกังวลที่ชื่อมันไปทับทั้งชื่อและหน้าที่กับ AWS Nitro
https://docs.aws.amazon.com/whitepapers/latest/security-design-of-aws-nitro-system/the-nitro-system-journey.html
ชื่ออาจซ้ำกัน แต่ init system กับ hypervisor เป็นคนละอย่างกันโดยสิ้นเชิง
ผมว่าแทบจะไม่เกิดปัญหาอะไร ตัวหนึ่งเป็น init system ที่ใคร ๆ ก็ใช้ได้ ส่วน AWS Nitro เป็น KVM fork ที่ใช้ภายในบริษัทเท่านั้น
สงสัยว่าเทียบกับ s6 แล้ว nitro เป็นอย่างไร ช่วงหลังผมเพิ่งลองตั้งค่า init system ใน Docker container ด้วย s6 แต่กับ
s6-overlayต้องสร้างไฟล์เองหลายไฟล์ และไม่ได้รู้สึกว่าเข้าใจง่ายอย่างที่คิดtini ก็น่าลองดู: https://github.com/krallin/tini
ที่ Distrust เราเคยเขียน init system แบบเรียบง่ายมาก ๆ ด้วย rust ให้สั้นกว่า 500 บรรทัด และตอนนี้มีลูกค้าบางรายใช้งานจริงในสภาพแวดล้อม enclave ที่ต้องการความปลอดภัยสูง เราใช้แค่ rust standard library จึงทำให้การ audit ง่ายมาก
https://git.distrust.co/public/nit
nitอยู่ 33%) อย่างไรก็ตาม readme มีแค่วิธี build และไม่ได้อธิบาย interface หรือวิธีทำงานจริงเอาไว้ระบุ dependency ไม่ได้, ไม่มีการตั้งค่า user/group, ต้องกำหนดลำดับเอง, ไม่มีการรัน service แบบขนาน, ไม่มีการจัดการทรัพยากร ถ้าขาดสิ่งเหล่านี้ไป ผมไม่อยากเรียกระบบแบบนี้ว่า init system เลย มันเป็นแค่ bare-bones process supervisor