Container Lifecycle: state machine від Created до Removed¶
Written by:
Igor Gorovyy
DevOps Engineer Lead & Senior Solutions Architect
Контейнер проходить через чіткі стани. Як і в 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.
Ресурси¶
- OCI Runtime Spec — життєвий цикл і стани контейнера
- signal(7) — POSIX-сигнали SIGTERM/SIGKILL
- Docker container lifecycle — довідник по start/stop/kill/rm
- State pattern — пояснення патерну
Вихідний код циклу: github.com/igorgorovoy/sheep-shepherd-meadow
Попередня: Image Management | Наступна: Docker CLI
