Перейти до змісту

Container Lifecycle: state machine від Created до Removed

Container Lifecycle: state machine від Created до Removed

Written by:

Igor Gorovyy
DevOps Engineer Lead & Senior Solutions Architect

LinkedIn


Контейнер проходить через чіткі стани. Як і в Docker, кожен стан визначає, що можна робити далі. Ось state machine контейнера в Sheep.

stateDiagram-v2
    [*] --> created : Manager.Create()
    created --> running : Manager.Start()
    running --> stopped : Manager.Stop()
    running --> stopped : Процес завершився
    stopped --> running : Manager.Start()
    stopped --> [*] : Manager.Remove()
    created --> [*] : Manager.Remove()

Три стани

type State string

const (
    StateCreated State = "created"
    StateRunning State = "running"
    StateStopped State = "stopped"
)

Created - overlay змонтований, стан збережений, процес не запущений. Running - namespace'и активні, cgroups застосовані, мережа налаштована, PID відстежується. Stopped - процес зупинений, cgroups прибрані, overlay все ще змонтований, exit code записаний.

Create - створення контейнера

func (m *Manager) Create(opts RunOpts) (*Container, error) {
    m.mu.Lock()
    defer m.mu.Unlock()

    id := GenerateID()

    // Перевірка унікальності імені
    for _, c := range m.containers {
        if c.Name == opts.Name {
            return nil, fmt.Errorf(
                "container name %q already in use", opts.Name)
        }
    }

    // Знаходимо образ
    img, err := m.images.Get(opts.Image, "latest")
    if err != nil {
        return nil, fmt.Errorf("image not found: %w", err)
    }

    // Налаштовуємо overlay
    rootfs, err := m.setupOverlay(id, img.RootFS)
    if err != nil {
        return nil, fmt.Errorf("setup overlay: %w", err)
    }

    c := &Container{
        ID: id, Name: opts.Name, Image: opts.Image,
        Command: opts.Command, State: StateCreated,
        CreatedAt: time.Now(), RootFS: rootfs,
        Config: Config{
            Hostname: hostname, Env: opts.Config.Env,
            Memory: opts.Config.Memory,
            CPUShares: opts.Config.CPUShares,
            PidsLimit: opts.Config.PidsLimit,
        },
        Mounts: opts.Mounts,
    }

    m.containers[id] = c
    m.saveState(c)
    return c, nil
}

Start - запуск

func (m *Manager) Start(id string) error {
    m.mu.Lock()
    c, ok := m.containers[id]
    if c.State == StateRunning {
        return fmt.Errorf("container %s already running",
            ShortID(id))
    }
    m.mu.Unlock()

    pid, err := startContainer(c) // namespace'и, cgroups, мережа
    if err != nil {
        return fmt.Errorf("start container: %w", err)
    }

    m.mu.Lock()
    c.Pid = pid
    c.State = StateRunning
    c.StartedAt = time.Now()
    m.mu.Unlock()

    return m.saveState(c)
}

Stop - зупинка

func (m *Manager) Stop(id string) error {
    // ... перевірки ...

    exitCode, err := stopContainer(c)

    m.mu.Lock()
    c.State = StateStopped
    c.ExitCode = exitCode
    c.StoppedAt = time.Now()
    c.Pid = 0
    m.mu.Unlock()

    return m.saveState(c)
}

Зупинка контейнера - SIGTERM, потім SIGKILL:

func stopContainer(c *Container) (int, error) {
    proc, _ := os.FindProcess(c.Pid)
    proc.Signal(syscall.SIGTERM)
    proc.Signal(syscall.SIGKILL)
    state, _ := proc.Wait()
    cleanupCgroups(c)
    if state != nil {
        return state.ExitCode(), nil
    }
    return 0, nil
}

Remove - видалення

func (m *Manager) Remove(id string) error {
    m.mu.Lock()
    c, ok := m.containers[id]
    if c.State == StateRunning {
        return fmt.Errorf(
            "container %s is running, stop it first",
            ShortID(id))
    }
    delete(m.containers, id)
    m.mu.Unlock()

    m.cleanupOverlay(id)
    os.RemoveAll(filepath.Join(m.baseDir, "containers", id))
    return nil
}

Не можна видалити запущений контейнер - потрібно спочатку зупинити.

Персистентність стану

Кожна зміна стану зберігається в state.json:

func (m *Manager) saveState(c *Container) error {
    dir := filepath.Join(m.baseDir, "containers", c.ID)
    os.MkdirAll(dir, 0755)
    data, _ := json.MarshalIndent(c, "", "  ")
    return os.WriteFile(
        filepath.Join(dir, "state.json"), data, 0644)
}

При старті Sheep завантажує існуючі контейнери:

func (m *Manager) loadExisting() error {
    dir := filepath.Join(m.baseDir, "containers")
    entries, _ := os.ReadDir(dir)

    for _, e := range entries {
        data, _ := os.ReadFile(
            filepath.Join(dir, e.Name(), "state.json"))
        var c Container
        json.Unmarshal(data, &c)

        // Перевіряємо, чи раніше запущені контейнери живі
        if c.State == StateRunning && c.Pid > 0 {
            if !isProcessAlive(c.Pid) {
                c.State = StateStopped
                c.Pid = 0
            }
        }
        m.containers[c.ID] = &c
    }
    return nil
}

Дивись, яка штука: якщо Sheep перезапустився, а контейнер ще працює - ми перевіряємо це через signal 0:

func isProcessAlive(pid int) bool {
    proc, _ := os.FindProcess(pid)
    err := proc.Signal(syscall.Signal(0))
    return err == nil
}

Signal 0 не відправляє сигнал, але перевіряє, чи процес існує.

Пошук контейнера

func (m *Manager) Get(id string) (*Container, error) {
    // Точний збіг
    if c, ok := m.containers[id]; ok { return c, nil }
    // Prefix match (як docker - перші символи ID)
    for cid, c := range m.containers {
        if len(id) >= 4 && cid[:len(id)] == id {
            return c, nil
        }
    }
    // Пошук за іменем
    for _, c := range m.containers {
        if c.Name == id { return c, nil }
    }
    return nil, fmt.Errorf("container %s not found", id)
}

Як у Docker - можна вказати перші кілька символів ID або ім'я.

Де граблі

Якщо Sheep впаде, поки контейнер running - state.json каже running, але PID може бути вже іншого процесу (PID reuse). isProcessAlive() поверне true для чужого процесу. Docker вирішує це через containerd-shim, який тримає зв'язок з контейнером.

Спробуй сам

sudo ./sheep create --name lifecycle-test minimal /bin/sh
sudo ./sheep ps -a          # state: created
sudo ./sheep start lifecycle-test
sudo ./sheep ps              # state: running
sudo ./sheep stop lifecycle-test
sudo ./sheep ps -a          # state: stopped, exit code
sudo ./sheep rm lifecycle-test

Lifecycle зрозумілий. Далі - як побудувати Docker-like CLI за 500 рядків Go.

Ресурси

Вихідний код циклу: github.com/igorgorovoy/sheep-shepherd-meadow

Попередня: Image Management | Наступна: Docker CLI