Модуль 28

Graceful shutdown та fault tolerance

Ось твій урок у стилі CS50. Вмикай уяву, ми починаємо! 🚀


🎓 CS50: Мистецтво падіння. Graceful Shutdown та Fault Tolerance

Привіт, друзі! Мене звати [Твоє Ім'я], і це CS50!

Сьогодні ми поговоримо про те, що неминуче трапляється з кожною програмою, кожним сервером і кожним розробником. Ми поговоримо про крах. Про момент, коли все йде шкереберть.

Але питання не в тому, чи впаде ваша програма. Питання в тому, як саме вона це зробить. Чи буде це паніка, втрата даних і розлючені користувачі? Чи це буде елегантне завершення роботи, як у пілота, що садить літак із заглушеним двигуном?

Сьогоднішня тема: Graceful Shutdown (чемне завершення) та Fault Tolerance (відмовостійкість).


1. 🔥 Вступ: Чому це питання життя і смерті (вашого сервісу)

Уявіть, що ви працюєте офіціантом у дуже популярному ресторані. Вечір, повна зала людей, всі їдять. І раптом власник ресторану вибігає в зал і кричить: «ЗАКРИВАЄМОСЬ!»

Він вимикає світло, вириває тарілки з рук клієнтів (навіть якщо вони не доїли), виштовхує всіх на вулицю і вішає замок.

Запитання до вас: 1. Чи повернуться ці клієнти завтра? 2. Чи заплатять вони за те, що встигли з’їсти?

Звісно, ні. Це — Hard Shutdown (kill -9). Це катастрофа.

А тепер уявіть інший сценарій. Власник каже: «Друзі, через 30 хвилин ми зачиняємось». * Ви перестаєте пускати нових гостей. * Тим, хто вже їсть, ви даєте час доїсти. * Ви спокійно миєте посуд, рахуєте касу, вимикаєте плити. * І тільки коли останній гість пішов, ви вішаєте замок.

Оце і є Graceful Shutdown.

Навіщо нам це в коді? Уявіть, що ваш сервер обробляє платіж. Користувач натиснув «Оплатити», гроші списалися з картки, але тут сервер перезавантажується для оновлення. Якщо ви «вимкнете світло» миттєво — гроші списані, а замовлення не створене. Користувач лютує. Підтримка плаче.

Ми маємо навчити наші програми помирати гідно.


2. 🧠 Теоретична база: Що відбувається під капотом?

Давайте розберемося, як комп'ютер каже програмі «стоп».

Коли ви натискаєте Ctrl+C у терміналі або коли Kubernetes вирішує зупинити ваш контейнер, він не просто вбиває процес (ну, зазвичай). Він надсилає Сигнал.

📡 Сигнали (Signals)

Це як SMS для вашої програми від операційної системи.

  • SIGINT (Signal Interrupt): Це ваше Ctrl+C. Це ввічливе прохання: «Ей, користувач хоче, щоб ти зупинився. Будь ласка, закругляйся».
  • SIGTERM (Signal Terminate): Стандартний сигнал від системи (наприклад, Docker). Це наказ: «Робочий день закінчено. Збирай речі».
  • SIGKILL: Це постріл у голову. Програма не може його перехопити. Вона просто зникає. Ми хочемо уникати цього.

🛡 Fault Tolerance (Відмовостійкість)

Це здатність системи продовжувати працювати (можливо, трохи повільніше), навіть якщо щось пішло не так. * База даних відпала? Ми не падаємо, а пробуємо під’єднатися знову (Retry). * Зовнішній сервіс тупить? Ми не чекаємо вічно, а скидаємо з’єднання (Timeout).

🔑 Що треба запам'ятати:

  1. Ваша програма завжди має слухати сигнали ОС (SIGTERM, SIGINT).
  2. Ви маєте чіткий план дій при отриманні сигналу: Stop accepting new -> Finish current -> Close DB -> Exit.
  3. Помилки — це норма. Не панікуйте (don't panic), а обробляйте їх.

3. 🧪 Приклади: Від хаосу до порядку

Давайте подивимось на прикладі мови Go (або уявіть це як псевдокод, логіка однакова для Python/Node.js/Java).

Приклад 1: Хаос (Hard Shutdown) ❌

func main() {
    // Нескінченний цикл роботи
    for {
        fmt.Println("Обробляю платіж клієнта...")
        time.Sleep(2 * time.Second) // Імітація роботи
        fmt.Println("✅ Платіж завершено!")
    }
}

Питання: Що буде, якщо я натисну Ctrl+C рівно посередині, коли програма "спить"? Відповідь: Ви побачите "Обробляю платіж..." і все. Програма помре. Платіж "завис". База даних може залишитись у заблокованому стані.

Приклад 2: Порядок (Graceful Shutdown) ✅

Тут ми додаємо "вуха", щоб чути операційну систему.

func main() {
    // Створюємо канал для прослуховування сигналів
    stopChan := make(chan os.Signal, 1)
    // Кажемо ОС: "Якщо буде SIGINT або SIGTERM, кинь їх сюди"
    signal.Notify(stopChan, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        for {
            fmt.Println("Обробляю платіж...")
            time.Sleep(2 * time.Second)
            fmt.Println("✅ Платіж завершено!")
        }
    }()

    // Головний потік блокується і чекає сигналу
    <-stopChan 
    fmt.Println("\n⚠️ Отримано сигнал зупинки! Завершуємо поточні справи...")

    // Тут ми б закрили БД і дочекались завершення поточних запитів
    time.Sleep(1 * time.Second) 
    fmt.Println("👋 Бувайте! Тепер можна вимикати.")
}

Що змінилось? Коли ви натискаєте Ctrl+C, програма не вмирає миттєво. Вона друкує повідомлення, виконує код очистки ("миє посуд") і тільки потім виходить.

Приклад 3: Fault Tolerance (Retries) 🛡

Уявіть, що база даних "блимнула" на 1 секунду.

Погано: Одразу повернути помилку "500 Internal Server Error". Добре: Спробувати ще раз.

func connectToDB() error {
    // Проста стратегія Retry
    for i := 0; i < 3; i++ {
        err := tryConnect()
        if err == nil {
            return nil // Успіх!
        }
        fmt.Println("БД не відповідає. Чекаємо секунду...")
        time.Sleep(1 * time.Second)
    }
    return fmt.Errorf("здаюсь, БД дійсно впала")
}

4. 🛠 Практична частина

Час забруднити руки кодом! 👐

Завдання 1: Симулятор краху Напишіть скрипт, який пише у файл рядок "Початок запису..." чекає 3 секунди, а потім пише "Кінець запису". Запустіть його і вбийте (Ctrl+C) на другій секунді. Відкрийте файл. Очікування: Там тільки "Початок запису...". Файл пошкоджено.

Завдання 2: Рятівник файлів Модифікуйте попередній скрипт. Додайте перехоплення сигналу. При отриманні SIGINT, навіть якщо робота не закінчена, запишіть у файл "АВАРІЙНЕ ЗАВЕРШЕННЯ". Мета: Зрозуміти, що ви можете виконати код після команди "стоп".

Завдання 3: The Timeout Problem У вас є функція, яка робить запит до API погоди. Іноді API відповідає 10 секунд. Реалізуйте механізм, який скасовує запит, якщо він триває довше 2 секунд, і виводить користувачеві: "Сервіс повільний, спробуйте пізніше", замість того щоб змушувати його чекати.

Завдання 4: Міні-кейс "Маркетплейс" Ви пишете сервіс розсилки email-підтверджень. Якщо ваш сервіс падає, листи губляться. Напишіть псевдокод алгоритму: що має зробити ваш сервіс, якщо під час відправки 1000 листів прийшов сигнал SIGTERM? (Підказка: не брати нові листи, запам'ятати, де зупинились).


5. 💡 Мислення як у розробника

Як відрізнити новачка від сеньйора?

Новачок думає: "Мій код працює ідеально, якщо ніхто нічого не чіпає". Сеньйор думає: "Мережа відпаде. Диск переповниться. Світло вимкнуть. Код має вижити".

Типові помилки:

  1. Ігнорування Timeouts: Чекати відповіді від бази вічно. Це вб'є ваш сервер, коли база зависне.
  2. Занадто довгий Shutdown: Kubernetes дає вам зазвичай 30 секунд на завершення (Grace period). Якщо ви "миєте посуд" 5 хвилин — вас вб'ють силою (SIGKILL). Вкладайтеся в ліміти.
  3. Retry Storm: Якщо 1000 користувачів отримали помилку і всі одночасно спробували знову — вони точно "покладуть" сервер остаточно. Робіть паузи між спробами (Backoff).

Порада з практики:

Завжди, чуєте, завжди логуйте процес вимкнення. Коли ваш сервіс перезавантажиться вночі, ви захочете знати: це був плановий рестарт чи аварія? Рядок у логах Received SIGTERM, shutting down... збереже вам купу нервів.


6. 🧩 Підсумок

Отже, що ми сьогодні вивчили?

  1. Програми не повинні вмирати раптово. Вони повинні завершуватися чемно (Graceful Shutdown).
  2. Ми слухаємо сигнали операційної системи (SIGTERM, SIGINT).
  3. Ми будуємо системи, готових до збоїв (Fault Tolerance), використовуючи повторні спроби (Retries) та таймаути.

Тепер ви вмієте: Робити свої програми надійними, професійними та стійкими до суворого реального світу серверів.

Що далі? Ви навчилися коректно вимикати одну програму. А що, якщо у вас їх сотня? І вони спілкуються між собою? Наступного разу ми зазирнемо у світ Контейнеризації та Docker, де ці принципи стають законом.

А поки що... це був CS50! 👋