לדלג לתוכן

הפעלה ופיקוח על חיי שירות

התחלה אינה מוכנות

נניח שתהליך נוצר בהצלחה, אבל עדיין טוען קובץ גדול ובונה אינדקס. מבחינת הקרנל הוא חי. מבחינת הלקוח הוא עדיין אינו מסוגל לשרת בקשה. אם מנהל השירותים מפעיל תהליך תלוי מיד לאחר יצירת התהליך הראשון, נוצר מרוץ בין האתחול לבין הבקשה הראשונה. המתנה שרירותית של שתי שניות רק מסתירה את המרוץ במחשב מסוים.

נגדיר מצבי חיים מפורשים: הכנה, מוכנות, עבודה, ניקוז וסיום. לכל מעבר נבחר אירוע שאפשר לצפות בו. מנהל השירותים זקוק להודעת מוכנות שהיישום שולח אחרי שההכנה הנדרשת באמת הושלמה. היישום זקוק לבקשת עצירה ולזמן מוגבל כדי לסיים עבודה שכבר התקבלה. שני הצדדים צריכים להסכים מה נחשב כשל ומה ניתן לנסות מחדש.

יחידה וקשרי תלות

קובץ יחידה של systemd מתאר דרך להפעיל תהליך ולפקח עליו. ההגדרה After קובעת סדר כאשר היחידות משתתפות בפעולת ההפעלה. היא אינה מבקשת בעצמה להפעיל את היחידה האחרת. ההגדרה Wants מוסיפה תלות חלשה יותר, וההגדרה Requires מוסיפה קשר דרישה בעל התנהגות כשל מוגדרת. לעיתים נדרשות גם דרישה וגם הוראת סדר, משום שמדובר בשתי שאלות שונות.

גם קשר נכון בין יחידות אינו מבטיח שמשאב מרוחק יישאר זמין. שרת מסד נתונים יכול להתנתק אחרי שהשירות שלנו התחיל. לכן ניהול תלות בזמן האתחול אינו מחליף טיפול בשגיאות במהלך העבודה. נבחן זמינות בזמן כל פעולה ונגדיר התנהגות כאשר התלות נעלמת.

תוכנית הודעות קטנה

ניצור פרויקט בשם notify_probe עם התלות libc = "0.2". נתקין במכונת המעבדה את חבילת הפיתוח של libsystemd בהתאם להפצה, כדי שהקישור לספרייה יצליח. הקוד משתמש בממשק הספרייה לשליחת הודעות ובממשק אותות סינכרוני של לינוקס. הוא חוסם את אות העצירה לפני תחילת העבודה ומקבל אותו דרך המתנה, בלי לבצע פעולות מורכבות מתוך מטפל אותות.

use std::ffi::CString;
use std::io;
use std::time::{Duration, Instant};

#[link(name = "systemd")]
unsafe extern "C" {
    fn sd_notify(unset_environment: libc::c_int,
        state: *const libc::c_char) -> libc::c_int;
    fn sd_watchdog_enabled(unset_environment: libc::c_int,
        usec: *mut u64) -> libc::c_int;
}

fn notify(state: &str) -> io::Result<()> {
    let message = CString::new(state).unwrap();
    let result = unsafe { sd_notify(0, message.as_ptr()) };
    if result < 0 { return Err(io::Error::from_raw_os_error(-result)); }
    if result == 0 { return Err(io::Error::other("notification socket missing")); }
    Ok(())
}

fn main() -> io::Result<()> {
    let mut signals: libc::sigset_t = unsafe { std::mem::zeroed() };
    unsafe { libc::sigemptyset(&mut signals); libc::sigaddset(&mut signals, libc::SIGTERM); }
    let result = unsafe { libc::pthread_sigmask(libc::SIG_BLOCK, &signals, std::ptr::null_mut()) };
    if result != 0 { return Err(io::Error::from_raw_os_error(result)); }
    notify("STATUS=Preparing lab state")?;
    std::thread::sleep(Duration::from_secs(2));
    let mut watchdog_us = 0_u64;
    let watchdog = unsafe { sd_watchdog_enabled(0, &mut watchdog_us) };
    if watchdog < 0 { return Err(io::Error::from_raw_os_error(-watchdog)); }
    let tick = if watchdog > 0 {
        Duration::from_micros((watchdog_us / 2).max(1))
    } else { Duration::from_secs(1) };
    notify("READY=1\nSTATUS=Lab loop ready")?;
    let started = Instant::now();
    let stall = std::env::args().any(|arg| arg == "stall");
    loop {
        if stall && started.elapsed() >= Duration::from_secs(3) {
            std::thread::sleep(Duration::from_secs(60));
        }
        if watchdog > 0 { notify("WATCHDOG=1")?; }
        let timeout = libc::timespec {
            tv_sec: tick.as_secs() as libc::time_t,
            tv_nsec: tick.subsec_nanos() as libc::c_long,
        };
        let signal = unsafe { libc::sigtimedwait(&signals,
            std::ptr::null_mut(), &timeout) };
        if signal == libc::SIGTERM { break; }
        if signal == -1 {
            let error = io::Error::last_os_error();
            if !matches!(error.raw_os_error(), Some(libc::EAGAIN | libc::EINTR)) {
                return Err(error);
            }
        }
    }
    notify("STOPPING=1\nSTATUS=Draining lab state")?;
    std::thread::sleep(Duration::from_millis(200));
    println!("drained");
    Ok(())
}

ההמתנה הראשונה מדמה הכנה כדי שנוכל לראות את מצב האתחול. בשירות אמיתי נחליף אותה בפתיחת משאבים, בדיקת תצורה והקמת עובדים. ההמתנה האחרונה מדמה ניקוז, ואינה הוכחה שכל בקשה הסתיימה. בפרויקט נחליף אותה בהמתנה למונה עבודה ולבעלות על משאבים כפי שעשינו בשרת של הפרק הקודם.

ההתאמה בתוך matches! משתמשת בתבנית חלופות, כך שכל אחת משתי השגיאות נחשבת סיבה להמשך הלולאה. פונקציית החסימה של האותות מחזירה מספר שגיאה ישירות, בעוד המתנת האותות משתמשת במנגנון השגיאה הרגיל. ההבדל הזה נשאר חשוב גם כאשר שתי הפונקציות מגיעות מאותה ספרייה.

יחידת המעבדה

נבנה את הפרויקט ונעתיק את הקובץ המתקבל לנתיב /usr/local/libexec/amit-notify-lab במכונה הווירטואלית. ניצור את הקובץ /etc/systemd/system/amit-notify-lab.service עם התוכן הבא. שמות המעבדה מיועדים לשימוש שלנו בלבד, ולא להחלפת שירות קיים.

[Unit]
Description=Amit notification lab
StartLimitIntervalSec=30
StartLimitBurst=3

[Service]
Type=notify
NotifyAccess=main
ExecStart=/usr/local/libexec/amit-notify-lab
DynamicUser=yes
NoNewPrivileges=yes
UMask=0077
TimeoutStartSec=5
TimeoutStopSec=3
WatchdogSec=2
Restart=on-failure
RestartSec=1

נבקש ממנהל המערכת לטעון את התיאור, נתחיל את השירות ונבדוק את מצבו. הפקודה המתחילה עשויה להמתין להודעת המוכנות, בהתאם לפעולה שביקשנו.

sudo systemctl daemon-reload
sudo systemctl start amit-notify-lab.service
systemctl show amit-notify-lab.service -p ActiveState -p SubState -p Result
journalctl -u amit-notify-lab.service --no-pager -n 30
sudo systemctl stop amit-notify-lab.service

האפשרות לטעינת יחידות אינה טוענת מחדש את התצורה הפנימית של היישום. היא אומרת למנהל השירותים לקרוא את קובצי היחידות. בהמשך נבנה מנגנון טעינה נפרד לתוכן שהתוכנית מנהלת בעצמה.

מה פעימת חיים באמת מוכיחה

הודעת פעימת החיים נשלחת מתוך הלולאה שאנו רוצים לבדוק. תהליכון עזר ששולח פעימות בזמן שכל עובדי השירות תקועים יכול להטעות את המפקח. נשאל איזה נתיב חייב להתקדם כדי שתישלח הפעימה. בשירות בעל כמה רכיבים נאגד התקדמות של הרכיבים הנדרשים, במקום להסתפק בכך שמנגנון התזמון של תהליכון בודד עדיין עובד.

נוסיף את המילה stall לשורת ההפעלה ביחידת המעבדה, נטען את היחידה מחדש ונפעיל אותה. אחרי שלוש שניות של עבודה התוכנית תפסיק לשלוח פעימות למשך זמן ארוך. מנהל השירותים אמור לזהות את החריגה ולסיים את השירות לפי המדיניות, אך הזמן המדויק תלוי בתזמון ובתצורת המערכת. נשמור את ערך Result ואת היומן כדי להבחין בכשל פיקוח ולא לייחס אותו לאות עצירה רגיל.

לולאת קריסות ושימור ראיות

הפעלה חוזרת אוטומטית יכולה להחזיר שירות לאחר כשל זמני, אבל אינה מתקנת תצורה שבורה. שירות שנופל מיד וצומח שוב עלול לייצר עומס ולדרוס את הראיות הראשונות. לכן היחידה מגבילה את מספר ניסיונות ההתחלה בחלון זמן. נבדוק גם את מונה ההפעלות ואת הסיבה לסיום, ולא רק האם קיים כרגע מזהה תהליך.

ניקוז חייב להיות מוגבל בזמן. עם קבלת עצירה נפסיק לקבל עבודה, נחליט מה מותר להשלים ונשמור מספיק זמן לסגירת המשאבים. לאחר תום התקציב מנהל השירות יכול לסיים בכוח את הקבוצה. ההבטחה ללקוח חייבת להתאים לאפשרות הזאת, במיוחד כאשר פעולה עסקית כבר בוצעה והתשובה טרם נשלחה.

הפרטים של סוג השירות, הודעות המוכנות והפיקוח מופיעים בתיעוד השירותים. נבדוק את גרסת המערכת המקומית ואת התיעוד המותקן כאשר אפשרות אינה מזוהה. בתום המעבדה נעצור את היחידה, נסיר רק את קובץ היחידה ואת קובץ ההרצה שיצרנו, ונבצע שוב טעינת יחידות.