Мета заняття: відпрацювати руками три поломки, які трапляються насправді, — видалена база, зупинений процес і заповнений диск — і навчитися повертати сервіс до роботи за кілька хвилин, користуючись власною письмовою інструкцією.
Замість <логін> підставляєш свій
логін виду bohdan.g9: імʼя, крапка, номер групи. Кутові дужки не пишуться.
Кабінет — сторінка, де проти кожного критерію стоїть відмітка, зарахований він чи ні.
У прикладах на цій сторінці скрізь написано bohdan.g9 — читай це як свій
логін і виправляй у кожній команді, яку копіюєш. Це важливо саме сьогодні: у розкладі
команд шляхи доведеться писати повністю, і чужий логін там просто нічого не знайде.
Ти налаштуєш щоденну копію своєї бази: невеликий скрипт зробить копію, лишить сім останніх і запускатиметься сам щоночі за розкладом, без твоєї участі. Потім ти по-справжньому перевіриш цю копію — відкриєш її окремо від робочої бази й звіриш, чи всі рядки на місці.
Далі ти власноруч зупиниш свою службу командою kill -9 і подивишся, як вона
піднімається сама. Служба — це твоя програма, яку сервер тримає запущеною постійно і сам
запускає знову після зупинки; ти зробив її на занятті 02, вона називається
club-api. Потім розберешся, як шукати й звільняти місце, коли диск заповнився.
Запишеш файл RUNBOOK.md — коротку інструкцію з командами на випадок, коли сервіс
не відповідає. Наприкінці заняття викладач видалить твою базу, і ти повернеш сервіс до роботи
за цією ж інструкцією, засікаючи час.
Твій сервіс працює три місяці. За цей час він жодного разу не зупинявся надовго, і коли щось працює саме, легко забути, що воно взагалі може зупинитися. Сьогодні розберемо три поломки. Вони не вигадані для заняття: кожну з них ти цілком можеш зустріти на своєму сервері, і всі три трапляються без жодної складної причини.
Перша: зникли або зіпсувалися дані. Уся твоя база — це один файл
~/data/app.db. Його можна видалити командою rm, набраною не в тій
теці. Його можна зіпсувати запитом DELETE FROM habits, у якому забули
WHERE, — і тоді таблиця лишиться, а рядків у ній не буде. Його можна перезаписати,
скопіювавши старий файл поверх нового по FTP — це спосіб передавати файли між своїм компʼютером
і сервером, для нього на компʼютері ставлять окрему програму-клієнт. У всіх трьох випадках
код цілий, служба працює, сторінка відкривається — і на ній порожньо.
Друга: зупинився процес.
python main.py, система створює процес:
виділяє йому памʼять, дає номер і починає виконувати код. Закриється процес — програма
перестане відповідати, навіть якщо файл із кодом нікуди не подівся.club-api, ти налаштував її на занятті 02.systemctl --user--user означає «мої служби, запущені від мого імені», а не
системні — на них у тебе немає прав. Далі на сторінці ця команда трапляється постійно:
systemctl --user status club-api показує докладний стан,
systemctl --user is-active club-api — одне слово
(active, failed або inactive).journalctljournalctl --user -u club-api -n 30 --no-pager: -u (unit) обмежує
вивід однією службою, -n 30 лишає останні 30 рядків,
--no-pager друкує їх одразу, без гортання. Саме туди потрапляють повідомлення
про помилки, які інакше ніхто б не побачив.http://127.0.0.1:8014/api/ping потрапляє до неї, а не до чужої програми. Два
процеси не можуть зайняти той самий порт: другий не стартує.Твій процес може зупинитися з трьох причин. Помилка в коді: якщо в
main.py зʼявився SyntaxError, то uvicorn — програма, яка
запускає твій застосунок і приймає для нього запити, — не зможе стартувати взагалі.
Перевищення обмежень: службі можна задати межу памʼяті рядком на кшталт
MemoryMax=200M, і якщо твій код почне складати в список мільйони рядків, система
зупинить процес, щойно він цю межу перевищить. І людина: хтось виконав команду завершення
процесу, зокрема ти сам, коли перевіряв щось у терміналі.
Третя: скінчилося місце на диску. Це найменш очевидна з трьох, бо вона
настає поступово. Файли логів стають більшими з кожним записом, копій бази щодня додається ще
одна, і в якийсь день на розділі не лишається вільних байтів. Тоді SQLite — програма, яка веде
твій файл бази, — не може записати новий рядок і відповідає помилкою
database or disk is full. Служба при цьому запущена, сторінка відкривається, читання
працює — не працює тільки додавання.
Спільне в усіх трьох поломок одне: коли вони трапляються, ти дізнаєшся про це не одразу, а коли хтось спробує скористатися сервісом. Тому сьогодні ми робимо дві речі — готуємо копію даних заздалегідь і записуємо порядок дій, поки є час подумати спокійно.
У Дарини сайт відкривається, список записів показується, а форма додавання
повертає помилку. systemctl --user status club-api показує
active (running), у журналі — рядок
sqlite3.OperationalError: database or disk is full. Що сталося?
Перше, що спадає на думку: скопіювати файл бази командою cp. Так роблять, і
здебільшого це працює, але в цьому способі є дві проблеми.
Проблема перша: одне імʼя. Команда
cp ~/data/app.db ~/backups/app.db щоразу створює файл із тим самим імʼям, тобто
затирає попередній. Через тиждень у тебе буде одна копія — учорашня. Уяви, що в суботу в коді
зʼявилася помилка й він почав записувати порожні назви. У вівторок ти це помічаєш, відкриваєш
копію — а вона зроблена в понеділок і містить ті самі порожні назви. Ось чому копій має бути
кілька, з різними іменами, і серед них — старіші за помилку.
Проблема друга: копіювання під час запису. SQLite зберігає базу не одним
шматком, а сторінками — блоками по кілька кілобайтів. Коли ти додаєш запис, змінюється відразу
кілька сторінок у різних місцях файлу. Якщо cp почне читати файл посеред такого
запису, він устигне прочитати частину сторінок у новому стані, а частину — у старому. Файл
скопіюється, розмір буде правильний, а всередині опиниться суміш двох станів. SQLite назве
такий файл malformed — пошкодженим — і відкрити його не вдасться.
Ймовірність цього невелика: запис триває мілісекунди. Але копія робиться раз на добу
протягом місяців, і рано чи пізно копіювання збіжиться із записом. Розвʼязання просте: копію
робить сам SQLite. У Python для цього є метод backup() — він працює через ту саму
бібліотеку, що й звичайні запити, тому знає про сторінки й блокування і віддає узгоджений
файл.
src = sqlite3.connect(DB) # робоча база
dst = sqlite3.connect(out) # новий файл під копію
with dst:
src.backup(dst) # копіюємо силами самої SQLiteІ ще одне про місце зберігання. Наші копії лежать у ~/backups — на тому самому
диску, що й база. Від видаленого файлу це захищає, від відмови самого диска — ні. Тому копію
корисно час від часу забирати до себе на компʼютер по FTP: тоді вона є у двох різних місцях.
Для курсу цього достатньо.
Богдан налаштував копіювання командою
cp ~/data/app.db ~/backups/app.db щодня о третій ночі. У вівторок він помітив, що
ще в суботу його код почав записувати в базу порожні назви. Чому копії йому не допоможуть?
Копія, яку жодного разу не відкривали, нічого не гарантує. У теці можуть лежати сім файлів по сорок кілобайтів, а всередині — порожні бази: скрипт відпрацьовував, але брав шлях до бази, якої вже немає, і щоразу створював новий порожній файл. Це виявляється тільки тоді, коли копію відкривають, — тобто коли вона вже потрібна. Тому копію треба хоча б раз відкрити й подивитися, що в ній.
Перевірка складається з двох питань. Перше: чи файл цілий? Друге: чи в ньому ті самі дані, що в робочій базі?
ok. Якщо
ні — перелік того, що зламано. Слово PRAGMA означає, що це не запит до даних, а
вказівка самій базі щось зробити або щось про себе розповісти.Друге питання — про кількість рядків. Якщо в робочій базі 34 записи, а в копії 34, копія свіжа й повна. Якщо в копії 12 — вона стара або зроблена не з того файлу. Якщо 0 — скрипт копіює порожнечу, і це треба виправляти сьогодні, а не тоді, коли копія знадобиться.
Саме ці два кроки виконує автоперевірка заняття: бере останній файл із
~/backups, кладе його в тимчасову теку, виконує PRAGMA integrity_check
і звіряє кількість рядків із твоєю робочою базою. Назву головної таблиці програма бере з
project.yaml, з поля main_table, — того самого, яке ти заповнював на
занятті 04.
У Софії в теці ~/backups сім файлів, кожен по 40 КБ, найсвіжіший
зроблено сьогодні вночі. Що з переліченого справді дає підставу вважати, що з цих файлів можна
відновити дані?
Копія, яку треба робити руками, рано чи пізно перестане робитися. Значить, запускати скрипт має система.
sudo тут не потрібен —
ти плануєш свої команди у своїй теці, а не системні.Рядок розкладу складається з пʼяти чисел і команди:
хвилина година день_місяця місяць день_тижня команда
17 3 * * * /home/bohdan.g9/app/backup.pyЗірочка означає «будь-яке значення». Рядок вище читається так: коли хвилина дорівнює 17, а година дорівнює 3, у будь-який день місяця, у будь-який місяць, у будь-який день тижня — запусти цю команду. Тобто щодня о 3:17 ночі.
| Рядок | Коли спрацює |
|---|---|
| 17 3 * * * | щодня о 3:17 |
| 0 * * * * | щогодини, на початку години: 0:00, 1:00, 2:00 і далі |
| */10 * * * * | кожні десять хвилин |
| 30 20 * * 1 | щопонеділка о 20:30 (день тижня: 1 — понеділок, 0 — неділя) |
| 0 6 1 * * | першого числа кожного місяця о 6:00 |
Чому саме 3:17, а не рівно третя. О рівних годинах у системі запускається багато чого одразу, і дванадцять учнівських скриптів у ту саму секунду створять непотрібне навантаження. Дивна хвилина розводить запуски в часі. Це звичайна практика, і в тебе може бути своя хвилина.
Далі — три речі, через які cron найчастіше не спрацьовує в того, хто налаштовує його вперше.
Оточення майже порожнє. Коли ти працюєш у терміналі, у тебе є змінні
оточення, зокрема PATH — список тек, де система шукає програми за іменем. cron
запускає команду з коротким PATH і без твоїх налаштувань. Тому в розкладі шляхи
пишуться повністю: /home/bohdan.g9/app/backup.py, а не backup.py і
не ~/app/backup.py.
Вивід нікуди не видно. Термінал не відкритий, тому все, що скрипт друкує, не побачить ніхто. Щоб не втратити цей вивід, його спрямовують у файл:
17 3 * * * /home/bohdan.g9/app/backup.py >> /home/bohdan.g9/logs/backup.log 2>&1Знак >> означає «дописати в кінець файлу», один знак >
означав би «затерти файл і записати заново». Запис 2>&1 додає до цього
файлу ще й потік помилок: у Unix програма пише в два окремі потоки — звичайний вивід
(номер 1) і повідомлення про помилки (номер 2), і без 2>&1 помилки
пройшли б повз лог. А саме вони цікавлять найбільше.
Файл має бути запускним. cron не здогадається, що .py треба
віддати Python. Тому скрипт починається з рядка, який називає потрібну програму, і йому
ставлять ознаку «можна запускати» командою chmod +x. Про це — на практиці.
Рядок у розкладі працює доти, доки ти його звідти не прибереш: cron виконуватиме команду в заданий час щодня, незалежно від того, чи памʼятаєш ти про неї і чи є хтось на сервері.
Оля додала в свій crontab рядок 0 4 * * * backup.py. Команда
crontab -l цей рядок показує, скрипт запущений руками працює і створює копію.
Але за три дні в ~/backups не зʼявилося жодного нового файлу. Найімовірніша
причина?
На занятті 02 ти зробив зі свого коду службу, і відтоді вона працює. Сьогодні розберемо, що саме тримає її запущеною.
club-api — одна з таких служб.~/.config/systemd/user/club-api.service. Слово «unit» тут — просто назва такого
опису.Щоб побачити це в дії, процес треба зупинити. Для цього процесу надсилають сигнал.
SIGTERM —
прохання завершитися: програма отримує його, встигає закрити файли й вийти акуратно.
SIGKILL — розпорядження зупинити процес негайно; його виконує саме ядро системи,
і програма не може ні перехопити його, ні відкласти, ні щось доробити наостанок.SIGKILL (він має номер 9). Після неї
процес зникає миттєво. Це найгрубіший спосіб зупинки — і саме тому він годиться для
перевірки: якщо служба піднімається після kill -9, вона підніметься й після
мʼякших випадків.Важлива межа, яку варто розуміти зараз. Restart=always повертає процес, коли
той зупинився з причини, зовнішньої щодо коду: сигнал, перевищення памʼяті, перезавантаження
сервера. Але якщо процес падає одразу після старту через помилку в самому коді, перезапуск
не допоможе: він запуститься і впаде знову, і так по колу. Тому в systemd є обмеження на
кількість спроб — типово пʼять спроб за десять секунд. Після них служба переходить у стан
failed і сама більше не стартує. Це зроблено навмисно, щоб зламаний код не
запускався нескінченно.
Стан failed знімається командою
systemctl --user reset-failed club-api, але спершу треба виправити код, інакше все
повториться. Скільки разів служба вже перезапускалася, показує окрема команда — вона буде на
практиці.
Раніше служба Максима завжди піднімалася сама після kill -9.
Сьогодні він поправив main.py, зробив перезапуск і пішов на перерву. Повернувся —
systemctl --user status club-api показує failed, і служба сама не
стартує. У журналі — SyntaxError: invalid syntax. Що відбулося?
200 означає «запит оброблено, ось відповідь», 404 — «такої адреси
немає», 500 — «на сервері сталася помилка під час обробки». Далі на сторінці
ми питатимемо саме це число командою curl, і 200 буде ознакою, що
сервіс відповідає.Питання, яке тут доречне: навіщо записувати те, що й так знаєш. Відповідь у тому, коли цим доведеться скористатися. Сервіс зупиняється тоді, коли зупиняється, — увечері, у вихідний, за день до показу. Розбиратися доводиться швидко й часто з телефона, а згадувати в такий момент точний синтаксис команди довго, і помилитися легко.
Тому RUNBOOK пишуть за трьома правилами.
systemctl --user status club-api --no-pager копіюється й виконується.active (running)», «має бути 200». Без цього незрозуміло, чи крок
допоміг, чи треба йти далі.Пояснень «як усе влаштовано» в RUNBOOK не пишуть: для цього є README. Тут потрібні тільки дії — команда, очікувана відповідь і що робити, коли відповідь інша.
О десятій вечора сайт не відкривається, ти дивишся в RUNBOOK з телефона. Який запис справді допоможе?
Далі — тринадцять кроків. Скрипти пишемо на Python: він уже встановлений на сервері за
шляхом /opt/club/venv — це окрема тека з Python і потрібними бібліотеками, спільна
для всіх учнів. Ставити нічого не треба, достатньо звертатися саме до цього шляху. Усі команди
виконуються на сервері, тому спершу заходимо туди.
ssh bohdan.g9@91.219.61.4Перш ніж щось налаштовувати, корисно побачити початковий стан: скільки місця на диску,
який розмір бази, чи є вже щось у теці копій. Спершу створюємо дві теки, які знадобляться
далі: якщо вони вже є, команда нічого не зробить і помилки не буде — про це й каже ключ
-p.
mkdir -p ~/logs ~/backups
df -h /home
ls -lh ~/data/
ls -lh ~/backups/
du -sh ~/app ~/data ~/logs ~/backupsЩо показує кожна команда:
df -h /home — скільки місця на розділі, де твоя домашня тека.
-h означає «у зручних одиницях»: 3.4G замість числа байтів. Перший
рядок — назви колонок; дивись на Avail (вільно) і Use% (скільки
зайнято у відсотках). Далі в RUNBOOK ми писатимемо
df -h /home | tail -1: tail -1 лишає тільки другий рядок, із
числами, бо назви колонок ти вже бачив тут.ls -lh ~/data/ — файли бази. Крім app.db тут можуть бути
app.db-wal і app.db-shm: це службові файли SQLite. У
app.db-wal SQLite спочатку записує зміни, а потім переносить їх у сам
app.db; app.db-shm допомагає це робити кільком процесам одночасно.
Копіювати їх окремо не треба — метод backup() враховує їх сам. А от при
відновленні бази з копії про них доведеться згадати: про це в кроці 13.du -sh — розмір кожної теки разом із вмістом. Це та сама команда, якою ти
шукатимеш, що зайняло місце, коли воно скінчиться.Запиши десь розмір app.db і вільне місце: наприкінці заняття корисно
порівняти.
Створюємо файл у теці ~/app, щоб він потрапив у git разом із рештою коду. git
— програма, яка зберігає історію твоїх файлів: ти налаштував її на занятті 03, і відтоді кожен
коміт (один запис в історії: набір змін, дата й короткий опис) фіксує стан теки
~/app. Файли поза цією текою в історію не потрапляють.
nano ~/app/backup.py#!/opt/club/venv/bin/python
"""Копія бази з ротацією. Заняття 11."""
import sqlite3
import sys
import time
from pathlib import Path
import yaml
APP = Path.home() / "app"
CFG = yaml.safe_load((APP / "project.yaml").read_text(encoding="utf-8"))
DB = Path.home() / "data" / CFG["database"]["file"]
DIR = Path.home() / "backups"
KEEP = 7
if not DB.is_file():
sys.exit(f"немає файлу бази {DB}: копію не роблю")
DIR.mkdir(exist_ok=True)
stamp = time.strftime("%Y-%m-%d_%H%M%S")
out = DIR / f"app-{stamp}.db"
src = sqlite3.connect(DB)
dst = sqlite3.connect(out)
with dst:
src.backup(dst)
dst.close()
src.close()
copies = sorted(DIR.glob("app-*.db"), reverse=True)
for old in copies[KEEP:]:
old.unlink()
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} {out.name} "
f"{out.stat().st_size} байт копій у теці: {len(copies[:KEEP])}")Розбір рядків, які варто розуміти:
#!/opt/club/venv/bin/python — перший рядок файлу каже системі, якою
програмою його виконувати. Завдяки йому cron зможе запустити скрипт просто за імʼям файлу,
без python попереду. Цей рядок має бути найпершим, до нього не можна ставити
навіть порожній.CFG = yaml.safe_load(...) — читаємо project.yaml і беремо
імʼя файлу бази звідти, а не пишемо app.db у скрипті. Той самий підхід, що в
db.py: назви лежать в одному місці, і якщо ти колись їх зміниш, скрипт
підхопить зміну сам.if not DB.is_file(): sys.exit(...) — найважливіші три рядки в цьому
скрипті. sqlite3.connect на неіснуючому шляху не повідомляє про помилку, а
створює новий порожній файл. Без перевірки скрипт після зникнення бази щоночі робив би
порожню копію, друкував би в лог успішний рядок і за сім ночей витер би ротацією всі
справжні копії. З перевіркою він зупиняється й пише, якого файлу немає.stamp = time.strftime("%Y-%m-%d_%H%M%S") — поточний час у вигляді
2026-09-06_031712. Такий формат обраний навмисно: імена файлів, упорядковані за
абеткою, автоматично впорядковані й за часом, бо рік іде перед місяцем, а місяць перед
днем.src.backup(dst) — копіювання силами самої SQLite, як у теорії.copies = sorted(..., reverse=True) — усі копії від найновішої до
найстарішої. copies[KEEP:] — усе, що йде після сьомої, тобто зайве.
old.unlink() видаляє файл.print надрукує рядок у лог: час, імʼя копії, розмір у байтах і
скільки копій лишилося. За цим рядком потім видно, що скрипт справді відпрацьовував
щоночі.Зберегти в nano: Ctrl+O, Enter, вийти — Ctrl+X.
chmod +x ~/app/backup.py
~/app/backup.pychmod +x ставить файлу ознаку «його можна запускати». Без неї система
відповість Permission denied, навіть якщо всередині все правильно.
Очікуваний вивід — один рядок приблизно такого вигляду:
2026-09-06 12:04:31 app-2026-09-06_120431.db 45056 байт копій у теці: 1Перевіримо, що файл справді зʼявився:
ls -lh ~/backups/Два випадки, коли треба зупинитися тут і розібратися, бо далі налаштовувати розклад немає сенсу:
немає файлу бази ... — шлях, який він склав із
project.yaml, не збігається зі справжнім файлом. Звір поле
database.file у ~/app/project.yaml з тим, що показує
ls -lh ~/data/: там має бути те саме імʼя, посимвольно.ls -lh ~/data/ і чи взагалі створювалися таблиці.Ротацію легко перевірити одразу: запустимо скрипт девʼять разів поспіль і подивимося, скільки файлів лишилося.
for i in $(seq 1 9); do ~/app/backup.py; sleep 1; done
ls -1 ~/backups/app-*.db | wc -lЗапис for i in $(seq 1 9); do ... ; done — цикл у bash: виконати команди в
дужках девʼять разів. sleep 1 робить паузу в секунду перед наступним запуском, і
вона тут обовʼязкова: у мітці часу
%Y-%m-%d_%H%M%S найдрібніша одиниця — секунда, тому два запуски в межах однієї
секунди дали б однакове імʼя файлу, і друга копія просто затерла б першу. Через паузу цикл
триватиме приблизно десять секунд.
ls -1 друкує імена в один стовпчик, wc -l рахує рядки. Маска
app-*.db тут не випадкова: скрипт видаляє старі копії саме за цією маскою, тому й
рахувати треба тільки їх, інакше сторонній файл у теці зіпсує перевірку. Разом із копією з
кроку 3 запусків було десять, а число має вийти 7. Подивись на самі
імена:
ls -1t ~/backups/-t впорядковує за часом зміни, найновіші згори. Три найстаріші копії зникли —
саме це й робить copies[KEEP:]. Часові позначки в іменах ідуть із різницею в одну
секунду, бо ми запускали скрипт підряд із паузою. Далі він працюватиме раз на добу, і різниця
буде добова.
Другий скрипт відкриває останню копію й дивиться, що всередині.
nano ~/app/check-backup.py#!/opt/club/venv/bin/python
"""Перевірка, що остання копія читається й повна. Заняття 11."""
import shutil
import sqlite3
import sys
import tempfile
from pathlib import Path
import yaml
APP = Path.home() / "app"
CFG = yaml.safe_load((APP / "project.yaml").read_text(encoding="utf-8"))
DB = Path.home() / "data" / CFG["database"]["file"]
TABLE = CFG["database"]["main_table"]
if not TABLE:
sys.exit("у project.yaml порожнє поле database.main_table — заповни його")
copies = sorted((Path.home() / "backups").glob("app-*.db"), reverse=True)
if not copies:
sys.exit("копій немає: скрипт backup.py жодного разу не відпрацював")
last = copies[0]
tmpdir = Path(tempfile.mkdtemp())
tmp = tmpdir / last.name
shutil.copy(last, tmp)
conn = sqlite3.connect(tmp)
status = conn.execute("PRAGMA integrity_check").fetchone()[0]
rows_copy = conn.execute(f"SELECT COUNT(*) FROM {TABLE}").fetchone()[0]
conn.close()
shutil.rmtree(tmpdir)
live = sqlite3.connect(DB)
rows_live = live.execute(f"SELECT COUNT(*) FROM {TABLE}").fetchone()[0]
live.close()
print(f"копія: {last.name}")
print(f"integrity_check: {status}")
print(f"рядків у копії: {rows_copy}")
print(f"рядків у базі: {rows_live}")
if status == "ok" and rows_copy == rows_live:
print("ПІДСУМОК: копія ціла й повна")
else:
print("ПІДСУМОК: розбіжність, дивись рядки вище")
sys.exit(1)Три місця, які варто пояснити окремо.
tempfile.mkdtemp() створює нову тимчасову теку і повертає шлях до неї. Ми
копіюємо туди файл копії й відкриваємо вже цю тимчасову копію, щоб сам бекап лишився
недоторканим. shutil.rmtree(tmpdir) наприкінці видаляє цю теку разом із
вмістом. Без цього рядка кожен запуск лишав би в системі ще одну копію бази — на занятті про
заповнений диск це було б дивно.f"SELECT COUNT(*) FROM {TABLE}" — назва таблиці підставляється прямо в
текст запиту. На занятті 08 ми домовилися так не робити з даними від користувача, і це
правило лишається. Тут інший випадок: імʼя таблиці не можна передати параметром — SQL
дозволяє параметри тільки для значень. А сам рядок береться з твого власного
project.yaml, куди сторонні не пишуть.sys.exit(1) в кінці — завершити скрипт із ознакою помилки. Завдяки цьому
перевірку можна буде колись поставити в deploy.sh — свій скрипт оновлення коду
на сервері — і зупиняти оновлення, якщо копія непридатна.chmod +x ~/app/check-backup.py
~/app/check-backup.pyОчікуваний вивід:
копія: app-2026-09-06_120438.db
integrity_check: ok
рядків у копії: 34
рядків у базі: 34
ПІДСУМОК: копія ціла й повнаЦе і є перевірка відновлення. Тепер про копії відомо не «файл є», а «файл відкривається, цілий, і в ньому стільки ж рядків, скільки в робочій базі».
Якщо кількість рядків відрізняється на одиницю-дві — імовірно, ти додав запис через сайт
уже після того, як зробив копію. Запусти ~/app/backup.py ще раз і повтори
перевірку.
Додамо рядок у свій crontab. Щоб не відкривати для цього редактор, скористаємося готовою командою: вона бере поточний розклад, дописує до нього рядок і повертає назад.
( crontab -l 2>/dev/null; echo "17 3 * * * /home/bohdan.g9/app/backup.py >> /home/bohdan.g9/logs/backup.log 2>&1" ) | crontab -Що тут відбувається. crontab -l друкує поточний розклад;
2>/dev/null ховає повідомлення no crontab for bohdan.g9, яке
зʼявляється, коли розкладу ще немає. echo додає новий рядок. Дужки обʼєднують
вивід обох команд в один потік, а | crontab - віддає цей потік команді
crontab, яка знаком - позначає «читай новий розклад зі стандартного
входу». Стандартний вхід — це потік, з якого програма читає дані, коли їх передають не файлом,
а напряму від іншої команди; знак | і зʼєднує вивід однієї команди зі стандартним
входом наступної.
Не запускай цю команду двічі — рядок додасться вдруге, і копія робитиметься двічі за ніч. Перевір результат:
crontab -lМаєш побачити свій рядок із повними шляхами. Двічі перечитай логін у шляхах: у
прикладі bohdan.g9, у тебе має бути твій.
crontab -e відкриває розклад у редакторі. Якщо система пропонує обрати
редактор, обирай nano. Можна задати його наперед:
export EDITOR=nano, а потім crontab -e. Видалити помилковий рядок
можна саме там.
Чекати до 3:17 ночі не будемо. Щоб переконатися, що рядок робочий, тимчасово поставимо запуск за дві хвилини від зараз. Подивись поточний час і додай рядок із потрібною хвилиною:
date +'%H:%M'export EDITOR=nano
crontab -eДопиши другий рядок, підставивши свої числа (тут — 12:47). Хвилина йде першою, година
другою. Якщо дві хвилини потрапляють на наступну годину — скажімо, зараз 12:59, а треба
13:01, — то й рядок починається з 1 13, а не з 61 12: числа більші
за 59 у полі хвилин cron не приймає.
47 12 * * * /home/bohdan.g9/app/backup.py >> /home/bohdan.g9/logs/backup.log 2>&1Через дві хвилини подивись лог і теку:
cat ~/logs/backup.log
ls -1t ~/backups/ | head -3Якщо в логу зʼявився рядок, а в теці — нова копія, розклад працює. Тепер прибери тестовий
рядок через crontab -e: щоденного о 3:17 достатньо.
Копія зберігає дані. Але корисно знати ще одне: коли саме сервіс переставав відповідати. Для
цього — маленький скрипт, який щогодини звертається до /api/ping і дописує
результат у лог.
nano ~/app/health.sh#!/usr/bin/env bash
# Погодинна перевірка, що сервіс відповідає. Заняття 11.
UNIT="$HOME/.config/systemd/user/club-api.service"
PORT=$(grep -oP '(?<=--port[ =])\d+' "$UNIT")
LOG="$HOME/logs/health.log"
mkdir -p "$HOME/logs"
if [ -z "$PORT" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') порт не знайдено в $UNIT" >> "$LOG"
exit 1
fi
CODE=$(curl -s -m 5 -o /dev/null -w '%{http_code}' "http://127.0.0.1:$PORT/api/ping")
STATE=$(systemctl --user is-active club-api)
echo "$(date '+%Y-%m-%d %H:%M:%S') ping=$CODE служба=$STATE" >> "$LOG"Пояснення:
PORT=$(grep -oP '(?<=--port[ =])\d+' "$UNIT") — дістає з unit-файла
число, що стоїть після --port. Порт у кожного свій, тому вписувати його числом
не треба. Запис [ =] означає «пробіл або знак дорівнює», тому команда працює і
з --port 8014, і з --port=8014.if [ -z "$PORT" ] — «якщо змінна порожня». Так буває, коли в
ExecStart порт записаний якось інакше: узятий зі змінної оточення або з
окремого файлу налаштувань. Без цієї перевірки curl пішов би на адресу
http://127.0.0.1:/api/ping і щогодини писав би в лог незрозумілий результат.
Якщо в логу зʼявився рядок «порт не знайдено», відкрий unit-файл, знайди в
ExecStart свій порт і впиши його в скрипт числом:
PORT=8014.curl — програма, яка робить запит на адресу з командного рядка. Ключі тут
такі: -s — не друкувати смужку поступу, -o /dev/null — саму
відповідь викинути (нам потрібен лише код), -w '%{http_code}' — надрукувати
натомість код відповіді, -m 5 — не чекати довше за пʼять секунд. Без останнього
обмеження скрипт міг би зависнути, якби сервіс приймав зʼєднання, але не відповідав.200 у логу означає, що сервіс відповів нормально.
000 — що curl не отримав відповіді взагалі: ніхто не слухає цей
порт (служба зупинена) або відповідь не прийшла за пʼять секунд. Це не код від сервера, а
позначка самого curl: сервер до відповіді навіть не дійшов. Тим
000 і відрізняється від 500, де сервіс відповів, але з помилкою
всередині.systemctl --user is-active club-api друкує одне слово:
active, failed або inactive. Зручно бачити в логу
поруч із кодом відповіді.set -euo pipefail, який ти писав у
deploy.sh. Там він потрібен, щоб зупинити оновлення на першій помилці. Тут
завдання протилежне: якщо сервіс не відповів, скрипт має дожити до кінця й записати цей
факт у лог.Дозволяємо запуск, пробуємо руками, ставимо в розклад:
chmod +x ~/app/health.sh
~/app/health.sh
cat ~/logs/health.log( crontab -l 2>/dev/null; echo "0 * * * * /home/bohdan.g9/app/health.sh" ) | crontab -
crontab -lЦю команду теж не запускай двічі — рядок додасться вдруге. Якщо це вже
сталося, прибери зайвий рядок через crontab -e.
Тепер у розкладі два рядки: копія щодня о 3:17 і перевірка щогодини. Записи з часом у
~/logs/ — це заразом критерій L08.2 із заняття 08.
Рядок Restart=always стоїть у твоєму unit-файлі з заняття 02. Сьогодні ми його
не додаємо — перевіряємо, що він на місці й ніхто його не прибрав.
grep 'Restart' ~/.config/systemd/user/club-api.serviceОчікуваний вивід — два рядки:
Restart=always
RestartSec=3Ще одна команда показує, що systemd бачить це саме так. Вона читає не файл, а те налаштування, яке зараз застосоване до служби:
systemctl --user show club-api -p Restart -p RestartUSec -p NRestartsRestart=always
RestartUSec=3s
NRestarts=0NRestarts — скільки разів служба перезапускалася відтоді, як її востаннє
запускали командою. Зараз там нуль. За хвилину це число зміниться.
Тепер зупинимо процес найгрубішим способом. Спершу дізнаємося його номер.
systemctl --user show club-api -p MainPID --valueMainPID — номер головного процесу служби, --value друкує саме
число без назви поля. Побачиш щось на кшталт 18432.
Зупиняємо і одразу дивимося стан:
kill -9 $(systemctl --user show club-api -p MainPID --value)
systemctl --user is-active club-apiЗапис $( ) виконує команду в дужках і підставляє її вивід у зовнішню команду.
Тобто kill -9 отримує номер процесу без того, щоб ти набирав його руками.
Одразу після цього is-active швидше за все відповість
activating: систему налаштовано чекати три секунди перед повторним запуском, і
саме ці секунди ти й застав. Зачекай пʼять секунд і подивись ще раз:
sleep 5
systemctl --user show club-api -p MainPID -p NRestarts
systemctl --user is-active club-apiТепер має бути active, NRestarts=1, а MainPID —
інший номер. Це новий процес: старий зник назавжди, systemd запустив на його місце новий.
Тут ми пишемо show без ключа --value, щоб у виводі лишилися назви
полів: інакше побачиш два голі числа й не зрозумієш, котре з них котре.
Перевіряємо, що сервіс справді відповідає:
PORT=$(grep -oP '(?<=--port[ =])\d+' ~/.config/systemd/user/club-api.service)
echo "порт: $PORT"
curl -s -o /dev/null -w '%{http_code}\n' "http://127.0.0.1:$PORT/api/ping"Має бути 200. Це і є критерій L11.2: після kill -9 служба
піднімається сама.
Якщо echo надрукував «порт: » і далі порожньо — у твоєму
ExecStart порт записаний інакше, ніж очікує ця команда. Відкрий unit-файл,
подивись рядок ExecStart і далі підставляй свій порт числом:
PORT=8014. Якщо порт порожній, curl звернеться в нікуди й
надрукує 000 — «відповіді не було взагалі».
Заповнити диск по-справжньому не можна: він спільний, і після тебе на ньому працюють ще сорок людей. Але вся робота при цій поломці — не в тому, щоб диск заповнити, а в тому, щоб швидко знайти, що саме займає місце, і прибрати зайве. Це й відпрацюємо на файлі, який створимо самі.
Спершу запиши, скільки вільного місця зараз:
df -h /home | tail -1Подивись на колонку Avail. Якщо вільного менше за 1G, файл не створюй —
диск спільний, і місця може не вистачити комусь іншому; тоді пропусти наступну команду й
переходь одразу до пошуку. Якщо місця вистачає, створюємо файл на 100 мегабайтів у теці
логів і одразу дивимося, як змінилося число:
dd if=/dev/zero of=~/logs/velykyi.tmp bs=1M count=100
df -h /home | tail -1dd копіює дані блоками. if=/dev/zero — джерело, з якого система
віддає скільки завгодно нулів; of= — куди писати; bs=1M count=100 —
сто блоків по мегабайту. Вільного місця в другому df має стати приблизно на
100M менше.
Тепер шукаємо, що займає місце, — так, ніби не знаємо, звідки цей файл узявся:
du -sh ~/app ~/data ~/logs ~/backups
du -sh ~/logs/* | sort -h | tail -5Перша команда показує розмір кожної теки й одразу видно, яка з них найбільша. Друга
розкладає найбільшу теку на окремі файли: sort -h впорядковує за розміром з
урахуванням літер K, M, G, а tail -5 лишає пʼять найбільших рядків. Якщо ти
створював velykyi.tmp, він має бути останнім у списку — найбільший файл у теці.
Прибираємо і звіряємо. Ключ -f у першому рядку означає «не повідомляти про
помилку, якщо файлу немає»: він потрібен тим, хто dd пропустив.
rm -f ~/logs/velykyi.tmp
ls -1t ~/backups/app-*.db | tail -n +8 | xargs -r rm --
df -h /home | tail -1Другий рядок видаляє всі копії бази, крім семи найновіших: tail -n +8 лишає
рядки, починаючи з восьмого, а xargs передає ці імена команді rm;
ключ -r означає «нічого не роби, якщо список порожній». Зараз копій рівно сім, і
команда нічого не видалить — але саме цей рядок ти виконуватимеш, коли копій набереться за
півроку. Вільного місця має стати стільки ж, скільки було на початку кроку.
Ці чотири команди — df, два du і прибирання — і є весь порядок
дій при заповненому диску. У наступному кроці ми запишемо їх у RUNBOOK, щоб потім не
пригадувати по одній.
Файл кладемо в ~/app, поруч із README.
nano ~/app/RUNBOOK.mdНижче — заготовка. Підстав свій логін і свій порт (його покаже команда
grep -oP '(?<=--port[ =])\d+' ~/.config/systemd/user/club-api.service, а якщо
вона нічого не надрукувала — візьми число з рядка ExecStart у unit-файлі), а далі
доповнюй файл тим, з чим стикався сам.
# RUNBOOK
Що робити, коли сервіс не відповідає.
Логін: bohdan.g9 · порт: 8014 · база: ~/data/app.db · копії: ~/backups/
## 1. Зрозуміти, що саме зламалося
ssh bohdan.g9@91.219.61.4
systemctl --user is-active club-api
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8014/api/ping
df -h /home | tail -1
Має бути: active, 200, вільного місця більше за 200M.
Далі йди до того розділу, який відповідає побаченому.
## 2. Служба не active
journalctl --user -u club-api -n 30 --no-pager
- SyntaxError або ImportError у main.py — код зламаний.
Повернути версію main.py, збережену в git останнім комітом
(усі незбережені зміни при цьому зникнуть):
cd ~/app && git checkout -- main.py
systemctl --user restart club-api
- Стан failed після виправлення коду:
systemctl --user reset-failed club-api
systemctl --user start club-api
## 3. Немає місця на диску
df -h /home | tail -1
du -sh ~/app ~/data ~/logs ~/backups
du -sh ~/logs/* | sort -h | tail -5
ls -1t ~/backups/app-*.db | tail -n +8 | xargs -r rm --
: > ~/logs/health.log
Після цього перезапустити службу й перевірити ping.
## 4. База зникла або порожня
ls -1t ~/backups/app-*.db | head -3
systemctl --user stop club-api
rm -f ~/data/app.db-wal ~/data/app.db-shm
cp ~/backups/ІМʼЯ_КОПІЇ.db ~/data/app.db
systemctl --user start club-api
~/app/check-backup.py
Далі відкрити сайт і переконатися, що список записів на місці.
## 5. Нічого з переліченого не допомогло
Написати викладачеві: логін, час, останні 20 рядків журналу
(journalctl --user -u club-api -n 20 --no-pager).Кілька зауважень до цієї заготовки.
.md: це Markdown — звичайний текст із кількома
домовленостями про розмітку. Рядок, що починається з #, показується як
заголовок, з - — як пункт списку, а рядок із відступом у чотири пробіли — як
команда, моноширинним шрифтом. Саме тому команди тут записані з відступом: їх зручно
виділяти цілими рядками, і жоден редактор не спробує їх переносити.git checkout -- main.py повертає не «останню робочу» версію файлу, а ту,
яка збережена в git останнім комітом. Якщо ти встиг закомітити вже зламаний код, вона буде
така сама зламана — тоді дивись git log і повертайся до давнішого коміту. І
ще: ця команда мовчки знищує всі зміни в main.py, які ти не закомітив. Перед
нею варто хоча б подивитися git diff.app.db-wal. Якщо підкласти новий app.db, поки
служба працює, SQLite може застосувати до нього -wal, залишений від старої
бази, — це або втрачені записи, або database disk image is malformed. Тому:
stop → прибрати -wal і -shm → cp →
start./var/log/journal, тому команда
journalctl --user --vacuum-size=20M на вільне місце в /home не
впливає, та й прав на ці файли у звичайного користувача зазвичай немає. Якщо
df -h /home показує, що місця немає, а всі твої теки маленькі — місце зайняв
хтось інший, і це вже до викладача.Файл має вміщатися на один-два екрани. Довшу інструкцію в потрібний момент не читають.
Перед навчальною аварією пройди відновлення сам, спокійно, з інструкцією перед очима. Спершу переконайся, що свіжа копія є:
~/app/backup.py
~/app/check-backup.pyТепер прибираємо робочу базу. Спершу зупиняємо службу, щоб вона не тримала базу відкритою,
і разом із базою прибираємо файли -wal і -shm — у них лежать
найсвіжіші зміни, і на новому файлі вони тільки нашкодять. Саму базу не видаляємо, а
перейменовуємо: якщо щось піде не так, її можна повернути.
systemctl --user stop club-api
mv ~/data/app.db ~/data/app.db.vidkladena
rm -f ~/data/app.db-wal ~/data/app.db-shm
systemctl --user start club-api
PORT=$(grep -oP '(?<=--port[ =])\d+' ~/.config/systemd/user/club-api.service)
curl -s -o /dev/null -w '%{http_code}\n' "http://127.0.0.1:$PORT/api/ping"Подивись, що показує сайт: /api/ping може й далі відповідати 200, бо він не
звертається до бази, а от список записів має видати помилку. Це той стан, у якому ти
опинишся під час аварії: служба працює, дані зникли.
Відновлюємо. Дивимося, які копії є, і беремо найновішу:
ls -1t ~/backups/app-*.db | head -3systemctl --user stop club-api
rm -f ~/data/app.db-wal ~/data/app.db-shm
cp ~/backups/app-2026-09-06_120438.db ~/data/app.db
systemctl --user start club-api
~/app/check-backup.pyІмʼя копії підстав своє з попередньої команди. Той самий порядок — зупинити, прибрати
-wal і -shm, скопіювати, запустити — записаний у розділі 4 твого
RUNBOOK. Відкрий сайт і переконайся, що записи на місці.
Прибери відкладений файл, щоб він не займав місце й не плутав:
rm ~/data/app.db.vidkladenaТепер запишемо в історію git усе, що написали сьогодні:
cd ~/app
git add backup.py check-backup.py health.sh RUNBOOK.md
git commit -m "щоденна копія бази, перевірка відновлення і RUNBOOK"
git push origin master~/backups в історію git не потрапляють: вони лежать поза
~/app. Це навмисно — база містить дані користувачів, і в комітах їм не
місце. Те саме правило діє з заняття 03 для .env і самої бази.
Чотири критерії заняття 11. Три перших перевіряє програма, четвертий зараховує викладач. Перевірка запускається за розкладом, тому відмітка в кабінеті зʼявляється не в ту саму секунду, коли ти виконав команду.
| Код | Критерій | Що це означає | Не зараховано — що робити |
|---|---|---|---|
| L11.1 | бекап свіжий і відновлюється | Програма дивиться в ~/backups: копії мають бути, найсвіжіша — не старша
за добу з невеликим запасом. Далі вона копіює цю копію в тимчасову теку, відкриває її як
базу SQLite, виконує PRAGMA integrity_check і звіряє кількість рядків
головної таблиці з робочою базою. Назву таблиці бере з поля
database.main_table у project.yaml. Тобто рахується не сам факт
наявності файлів, а те, що з них справді можна відновити дані. |
Спершу виконай свою ж перевірку: ~/app/check-backup.py — вона робить те
саме. Порожня тека означає, що backup.py не відпрацював: перевір
crontab -l і cat ~/logs/backup.log. Копія стара — рядок у
розкладі є, але команда падає; причина буде в тому ж логу, найчастіше це відносний шлях
або відсутнє chmod +x. integrity_check повернула не
ok — пошкоджена або сама копія, або робоча база. Спершу перевір базу:
/opt/club/venv/bin/python -c "import sqlite3,pathlib; print(sqlite3.connect(pathlib.Path.home()/'data'/'app.db').execute('PRAGMA integrity_check').fetchone()[0])". Якщо там ok, копію робили
звичайним cp — запусти ~/app/backup.py і перевір ще раз. Якщо
не ok, пошкоджена робоча база, і нова копія буде такою самою: тоді бери
давнішу копію, у якої перевірка проходить, і відновлюйся з неї за розділом 4 RUNBOOK.
Кількість рядків нуль — у project.yaml вказано не той файл бази. |
| L11.2 | після kill -9 служба піднімається сама | Програма дізнається номер твого процесу, зупиняє його сигналом
SIGKILL, чекає сім секунд і звертається до /api/ping. Має бути
код 200. Це той самий механізм, що перевірявся на занятті 02, — рядок
Restart=always в unit-файлі. |
Перевір рядок:
systemctl --user show club-api -p Restart --value — має надрукувати
always. Якщо там no, значить unit-файл правили: відкрий
~/.config/systemd/user/club-api.service, поверни рядок
Restart=always, потім systemctl --user daemon-reload і
systemctl --user restart club-api. Якщо рядок на місці, а служба все одно не
піднімається, дивись journalctl --user -u club-api -n 30: найімовірніше
процес падає одразу після старту через помилку в коді, і systemd припинив спроби. Тоді
виправ код, зроби systemctl --user reset-failed club-api і запусти
службу. |
| L11.3 | RUNBOOK.md описує дії при відмові | Файл ~/app/RUNBOOK.md має існувати й містити конкретні дії на випадок,
коли сервіс не відповідає: команди для перевірки стану, звернення до журналу і порядок
відновлення бази з копії. Програма дивиться, що файл непорожній і що в ньому є команди, а
не самі заголовки. |
Перевір, що файл лежить саме в ~/app і названий точно
RUNBOOK.md: великі літери мають значення, runbook.md — інший
файл. Якщо файл є, а критерій не зараховано, найчастіша причина — у ньому лише опис
словами. Додай самі команди рядками, які можна скопіювати: systemctl --user,
journalctl --user, cp з ~/backups. Заготовка — у
кроці 12 практики. |
| L11.4 | навчальна аварія пройдена за 10 хвилин | Це людський критерій: його зараховує викладач на занятті. Він видаляє твою базу (копія до цього моменту вже зроблена), а ти повертаєш сервіс до роботи й називаєш час. Понад десять хвилин — критерій не зараховується одразу, і ти доробляєш RUNBOOK, щоб з наступного разу вкластися. | Пройди відновлення заздалегідь, як у кроці 13 практики: спокійно, з секундоміром. Найбільше часу зазвичай займають дві речі — згадати, як називається тека з копіями, і знайти потрібну команду. Обидві зникають, щойно ці рядки записані в RUNBOOK. Якщо не встиг на занятті, домовся з викладачем про повторну спробу до кінця тижня. |
Основне, частина перша: підготувати демо. Наступне заняття — реліз і
показ. Виступ триває пʼять хвилин, і його треба підготувати заздалегідь. Напиши в окремому
файлі ~/app/DEMO.md три речі:
chmod +x, порожня назва, яка давала код 500. Три-чотири речення:
що побачив, де подивився, що зробив.Основне, частина друга: почати CHANGELOG.md. Це список того, що зʼявилося в проєкті, від старішого до новішого. На занятті 12 у ньому має бути щонайменше вісім записів, і писати їх зараз простіше, ніж за двадцять хвилин до демо, коли деталі вже забулися.
Найлегший спосіб згадати — переглянути власні коміти, тобто записи в історії git: кожен із них має дату й короткий опис, який ти сам написав:
cd ~/app
git log --reverse --pretty=format:'%ad %s' --date=short--reverse показує коміти від найстаріших, --pretty=format задає
вигляд рядка: дата й повідомлення. Далі перенеси в файл те, що справді додало можливостей.
nano ~/app/CHANGELOG.md# Що змінювалося
## 2026-06-28
- перша сторінка на сервері й опис проєкту в BRIEF.md
## 2026-07-05
- сервіс відповідає на /api/ping, служба піднімається сама після перезавантаження
## 2026-07-12
- історія в git, окреме сховище для неї в ~/repo.git
...Записуй те, що змінилося для того, хто користується сервісом, а не назви файлів. Рядок «додав habits.py» нічого не каже; рядок «зʼявився список звичок і кнопка додавання» — каже.
Закомить обидва файли:
git add DEMO.md CHANGELOG.md
git commit -m "план демо і перші записи CHANGELOG"
git push origin masterДодатково. Забери одну копію бази до себе на компʼютер по FTP (адреса
91.219.61.4, порт 21, шифрування «явний FTP через TLS», логін і пароль ті самі). Тека
backups, файл — найновіший. Тоді копія буде у двох різних місцях, і навіть
відмова диска на сервері не залишить тебе без даних.
Друге повідомлення не є помилкою: воно просто означає, що розкладу в тебе ще немає. Додай рядок командою з кроку 7, і повідомлення зникне.
Перше означає, що cron на сервері не встановлений. Тоді скористайся розкладом systemd — той самий механізм, що вже тримає твою службу. Потрібні два файли.
mkdir -p ~/.config/systemd/user
nano ~/.config/systemd/user/backup.service[Unit]
Description=Щоденна копія бази
[Service]
Type=oneshot
ExecStart=/home/bohdan.g9/app/backup.pynano ~/.config/systemd/user/backup.timer[Unit]
Description=Запуск копії бази о 3:17
[Timer]
OnCalendar=*-*-* 03:17:00
Persistent=true
[Install]
WantedBy=timers.targetsystemctl --user daemon-reload
systemctl --user enable --now backup.timer
systemctl --user list-timers backup.timerType=oneshot означає «запустити команду, дочекатися завершення й не тримати
службу запущеною». Persistent=true — якщо в потрібний час сервер був вимкнений,
запустити команду одразу після ввімкнення. Остання команда покаже, коли таймер спрацює
наступного разу.
Спочатку подивись лог — саме для цього ми додавали >> ~/logs/backup.log
2>&1:
cat ~/logs/backup.log
crontab -lДалі за тим, що побачив.
ls -ld ~/logs. Якщо її немає, перенаправлення >> не спрацює,
і лог не зʼявиться, хоч би скільки разів команда запускалася. Створи теку:
mkdir -p ~/logs — і чекай наступного запуску./home/bohdan.g9/app/backup.py. Записи
~/app/backup.py і просто backup.py тут не працюють.Permission denied — забув chmod +x ~/app/backup.py.bad interpreter: /opt/club/venv/bin/python^M — файл редагували у Windows і
поклали по FTP: там кінець рядка позначається двома символами. Прибрати їх:
sed -i 's/\r$//' ~/app/backup.py.Traceback — скрипт запустився й упав. Читай останній
рядок: там назва помилки. Найчастіше це FileNotFoundError з іменем
project.yaml, і причина в тому, що шлях у скрипті записаний відносно поточної
теки, а cron стартує з домашньої. У нашому скрипті всі шляхи будуються від
Path.home(), тому такого бути не повинно — звір свій файл із кроком 2.Скрипт перевірки читає назву головної таблиці з project.yaml. Подивись, що там
записано:
grep -A5 '^database:' ~/app/project.yaml-A5 означає «показати пʼять рядків після знайденого». Поле
main_table має містити назву твоєї таблиці із заняття 04, наприклад
habits. Якщо там порожні лапки, скрипт зупиниться з підказкою; заповни поле.
Повідомлення no such table: habits означає інше: поле заповнене, але такої
таблиці в базі немає. Подивись, які таблиці є насправді:
/opt/club/venv/bin/python -c "import sqlite3,pathlib; print([r[0] for r in sqlite3.connect(pathlib.Path.home()/'data'/'app.db').execute(\"SELECT name FROM sqlite_master WHERE type='table'\")])"sqlite_master — службова таблиця, у якій SQLite тримає перелік усього, що є в
базі. Виправ назву в project.yaml так, щоб вона збігалася посимвольно.
Дивимось стан і журнал:
systemctl --user status club-api --no-pager
journalctl --user -u club-api -n 30 --no-pager--no-pager друкує все одразу, без гортання. Три типові випадки.
failed, у журналі SyntaxError або
ImportError — процес падає одразу після старту, і systemd припинив спроби.
Виправ код або поверни версію, збережену в git останнім комітом:
cd ~/app && git checkout -- main.py. Май на увазі два наслідки: усі
незбережені зміни в main.py ця команда знищить без запитань, а якщо останній
коміт уже містив помилку, файл повернеться таким самим зламаним — тоді дивись
git log і бери давніший коміт.
Потім systemctl --user reset-failed club-api і
systemctl --user start club-api.start request repeated too quickly — те саме обмеження на кількість
спроб. Порядок дій той самий: спершу причина падіння, потім
reset-failed.kill: (18432) - Operation not permitted — ти надіслав сигнал чужому процесу.
Номер треба брати командою
systemctl --user show club-api -p MainPID --value, а не з переліку всіх процесів
на сервері.Перше повідомлення означає, що файл копії пошкоджений. Найчастіша причина — копію робили
командою cp у момент, коли в базу писали. Зроби копію правильним способом і
перевір ще раз:
~/app/backup.py
~/app/check-backup.pyЯкщо нова копія теж не проходить перевірку, пошкоджена сама робоча база: копія знімається з
неї, тому повторювати backup.py немає сенсу — вийде такий самий пошкоджений файл.
Тоді перебирай копії від найновішої до давніших, поки якась не дасть ok, і
відновлюйся з неї за розділом 4 свого RUNBOOK.
Друге повідомлення означає, що місця на диску немає. Подивись, скільки лишилося і що займає найбільше:
df -h /home | tail -1
du -sh ~/app ~/data ~/logs ~/backups
du -sh ~/logs/* | sort -h | tail -5sort -h впорядковує за розміром з урахуванням одиниць,
tail -5 лишає пʼять найбільших. Далі прибирай за розміром: зайві копії, великі
файли логів, усе, що ти сам колись поклав у домашню теку й забув.
ls -1t ~/backups/app-*.db | tail -n +8 | xargs -r rm --
: > ~/logs/health.log
df -h /home | tail -1Другий рядок спорожнює лог, лишаючи сам файл. Двокрапка тут — команда, яка нічого не
робить, а > перед іменем файлу затирає його вміст.
Прибирати треба саме те, що лежить у твоїй домашній теці: df -h /home рахує
місце тільки на цьому розділі. Журнал systemd лежить в іншому місці —
/var/log/journal, — тому команда journalctl --user --vacuum-size=20M
вільного місця в /home не додасть, і прав на ці файли у звичайного користувача
зазвичай немає. Якщо всі твої теки маленькі, а місця однаково немає, — його зайняв не ти,
напиши викладачеві.
Після звільнення місця перезапусти службу і перевір, що запис у базу знову працює: додай запис через форму на своєму сайті.