Налагоджуємо розгортання ПЗ зі strace

Налагоджуємо розгортання ПЗ зі strace

Моя основна робота — це, здебільшого, розгортання систем ПЗ, тобто багато часу я витрачаю, намагаючись відповісти на такі питання:

  • У розробника це програмне забезпечення працює, а у мене немає. Чому?
  • Вчора це програмне забезпечення у мене працювало, а сьогодні ні. Чому?

Це — свого роду налагодження, яке трохи відрізняється від звичайного налагодження програмного забезпечення. Звичайне налагодження - це про логіку коду, а от налагодження розгортання - це про взаємодію коду та середовища. Навіть якщо корінь проблеми — логічна помилка, той факт, що на одній машині все працює, а на іншій — ні, це означає, що справа якимось чином у середовищі.

Тому замість звичайних інструментів для налагодження начебто gdb я маю інший набір інструментів для налагодження розгортання. І мій улюблений інструмент для боротьби з проблемою типу «Чому це програмне забезпечення у мене не оре?» називається страйк.

Що таке strace?

страйк - Це інструмент для «трасування системного виклику». Спочатку створювався під Linux, але ті ж фішки налагодження можна провертати і з інструментами для інших систем (DTrace або ktrace).

Основне вживання дуже просто. Потрібно лише запустити strace з будь-якою командою і він відправить у дамп усі системні виклики (правда, спершу, напевно, доведеться встановити сам страйк):

$ strace echo Hello
...Snip lots of stuff...
write(1, "Hellon", 6)                  = 6
close(1)                                = 0
close(2)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++

Що це за системні дзвінки? Це щось на зразок API для ядра операційної системи. Давним-давно ПЗ мав прямий доступ до «заліза», на якому воно працювало. Якщо, наприклад, потрібно було відобразити що-небудь на екрані, воно грало з портами або регістрами для відео-пристроїв, що відображаються в пам'ять. Коли стали популярні багатозадачні комп'ютерні системи, запанував хаос, тому що різні програми билися за «залізо». Помилки в одному додатку могли обрушити роботу інших, якщо не всю систему цілком. Тоді в ЦПУ з'явилися режими привілеїв (або «кільцевий захист»). Найбільш привілейованим ставало ядро: воно отримувало повний доступ до «заліза», плодячи менш привілейовані програми, яким вже доводилося вимагати доступ у ядра для взаємодії з «залізом» через системні виклики.

На бінарному рівні системний виклик трохи відрізняється від простого виклику функції, проте більшість програм використовують обгортку у стандартній бібліотеці. Тобто. стандартна бібліотека POSIX C містить виклик функції write ()яка містить весь залежний від архітектури код для системного виклику запис.

Налагоджуємо розгортання ПЗ зі strace

Якщо коротко, то будь-яка взаємодія програми зі своїм середовищем (комп'ютерними системами) здійснюється через системні виклики. Тому коли ПЗ на одній машині працює, а на іншій ні, добре зазирнути в результати трасування системних викликів. Якщо ж конкретніше, то вам список типових моментів, які можна проаналізувати за допомогою трасування системного виклику:

  • Консольне введення-виведення
  • Мережеве введення-виведення
  • Доступ до файлової системи та файлове введення-виведення
  • Управління терміном життя процесу потоку
  • Низькорівневе управління пам'яттю
  • Доступ до драйверів спеціальних пристроїв

Коли використовувати strace?

У теорії, страйк використовується з будь-якими програмами в просторі користувача, адже будь-яка програма в просторі користувача повинна робити системні виклики. Він ефективніше працює з компілюваними, низькорівневими програмами, але і з високорівневими мовами на зразок Python теж працює, якщо вийде прорватися через додатковий шум від середовища виконання та інтерпретатора.

В усій красі страйк проявляє себе під час налагодження ПЗ, яке добре працює на одній машині, а на іншій раптом перестає працювати, видаючи невиразні повідомлення про файли, дозволи чи невдалі спроби виконати якісь команди або ще що… Шкода, але не так добре він поєднується з високорівневими проблемами типу помилки перевірки сертифіката. Зазвичай тут потрібна комбінація страйк, іноді ltrace та інструментів вищого рівня (на зразок інструмента командного рядка OpenSSL для налагодження сертифікату).

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

Приклад простого налагодження

Скажімо, хочете ви запустити приголомшливий серверний додаток foo, а виходить ось що:

$ foo
Error opening configuration file: No such file or directory

Очевидно, йому не вдалося знайти написаний вами конфігураційний файл. Так відбувається, тому що іноді, коли менеджери пакетів, компілюючи програму, перевизначають очікуване розташування файлів. І якщо слідувати посібнику з установки для одного дистрибутива, в іншому знаходиш файли зовсім не там, де чекав. Вирішити проблему можна було б за пару секунд, якби повідомлення про помилку говорило, де слід шукати конфігураційний файл, але ж воно не говорить. То де шукати?

Якщо доступ до вихідного коду, можна прочитати його і все з'ясувати. Гарний запасний план, але не найшвидше рішення. Можна вдатися до покрокового відладчика начебто gdb та подивитися, що робить програма, але куди ефективніше використовувати інструмент, який спеціально спроектований показувати взаємодію із середовищем: страйк.

Висновок страйк може здатися надмірним, але хороші новини в тому, що більшу його частину можна сміливо ігнорувати. Часто корисно використовувати оператор -o, щоб зберігати результати трасування в окремий файл:

$ strace -o /tmp/trace foo
Error opening configuration file: No such file or directory
$ cat /tmp/trace
execve("foo", ["foo"], 0x7ffce98dc010 /* 16 vars */) = 0
brk(NULL)                               = 0x56363b3fb000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=25186, ...}) = 0
mmap(NULL, 25186, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f2f12cf1000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113 3 > 1 260A2 "..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1824496, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f2f12cef000
mmap(NULL, 1837056, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f2f12b2e000
mprotect(0x7f2f12b50000, 1658880, PROT_NONE) = 0
mmap(0x7f2f12b50000, 1343488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x22000) = 0x7f2f12b50000
mmap(0x7f2f12c98000, 311296, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x16a000) = 0x7f2f12c98000
mmap(0x7f2f12ce5000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b6000) = 0x7f2f12ce5000
mmap(0x7f2f12ceb000, 14336, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f2f12ceb000
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7f2f12cf0500) = 0
mprotect(0x7f2f12ce5000, 16384, PROT_READ) = 0
mprotect(0x56363b08b000, 4096, PROT_READ) = 0
mprotect(0x7f2f12d1f000, 4096, PROT_READ) = 0
munmap(0x7f2f12cf1000, 25186)           = 0
openat(AT_FDCWD, "/etc/foo/config.json", O_RDONLY) = -1 ENOENT (No such file or directory)
dup(2)                                  = 3
fcntl(3, F_GETFL)                       = 0x2 (flags O_RDWR)
brk(NULL)                               = 0x56363b3fb000
brk(0x56363b41c000)                     = 0x56363b41c000
fstat(3, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x8), ...}) = 0
write(3, "Error opening configuration file"..., 60) = 60
close(3)                                = 0
exit_group(1)                           = ?
+++ exited with 1 +++

Приблизно вся перша сторінка виводу страйк це зазвичай низькорівнева підготовка до запуску. (Багато викликів mmap, mprotect, битий для речей типу виявлення низькорівневої пам'яті та відображення динамічних бібліотек.) Загалом під час налагодження висновки страйк краще читати із самого кінця. Внизу буде виклик запис, що виводить повідомлення про помилку. Дивимося вище, і бачимо перший помилковий системний виклик - виклик openat, що видає помилку ЕНОЕНТ («файл або каталог не знайдено»), який намагався відкрити /etc/foo/config.json. Ось тут і повинен лежати конфігураційний файл.

Це був просто приклад, але я сказав би, що 90% часу, що я користуюся страйк, нічого складніше цього виконувати і годі було. Нижче повний покроковий посібник з налагодження:

  • Засмутитись через невиразне повідомлення про system-y помилку від програми
  • Заново запустити програму зі страйк
  • Знайти у результатах трасування повідомлення про помилку
  • Іти вище, поки не уткнетеся в перший невдалий системний виклик

Цілком ймовірно, що системний виклик в 4-му кроці покаже, що пішло не так.

підказки

Перш ніж показати приклад більш складного налагодження, відкрию вам кілька прийомів для ефективного використання страйк:

man - ваш друг

На багатьох *nix системах повний список системних викликів до ядра можна отримати, запустивши man syscalls. Ви побачите речі начебто brk(2), а значить, більше інформації можна отримати, запустивши man 2 brk.

Невеликі граблі: man 2 fork показує мені сторінку для оболонки вилка () в GNU libcяка, виявляється, яка реалізована за допомогою виклику клон (). Семантика виклику вилка залишається тією ж, якщо написати програму, що використовує вилка (), і запустити трасування - я не знайду викликів вилказамість них будуть клон (). Такі граблі тільки плутають, якщо почати порівнювати вихідний вихід з висновком страйк.

Використовуйте -о, щоб зберегти вихід у файл

страйк може згенерувати великий висновок, тому часто корисно зберігати результати трасування в окремих файлах (як у наведеному вище прикладі). А ще це допомагає не сплутати програмний висновок із висновком страйк у консолі.

Використовуйте -s, щоб переглянути більше даних аргументу

Ви, напевно, помітили, що друга половина повідомлення про помилку не показана у наведеному вище прикладі трасування. Це тому що страйк за замовчуванням показує лише перші 32 байти аргументу рядка. Якщо хочете бачити більше, додайте щось на зразок -с 128 до виклику страйк.

-у полегшує відстеження файлів сокетів та ін.

«Всі — файл» означає, що *nix системи виконують усі вводи-виводи, використовуючи файлові дескриптори, чи застосовно саме до файлу чи мережі, чи міжпроцесних каналів. Це зручно для програмування, але заважає стежити за тим, що відбувається насправді, коли бачите загальні зчитування и запис результати трасування системного виклику.

Додавши оператор , ви змусите страйк анотувати кожен файловий дескриптор у виведенні з приміткою, що він вказує.

Прикріпіть до вже запущеного процесу з -p**

Як видно з наведеного нижче прикладу, іноді потрібно проводити трасування програми, яка вже запущена. Якщо відомо, що вона запущена як процес 1337 року (скажімо, з висновків ps), то можна здійснити трасування її ось так:

$ strace -p 1337
...system call trace output...

Можливо вам потрібні root-права.

Використовуйте -f, щоб стежити за дочірніми процесами

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

Якщо вважаєте, що помилка в дочірньому процесі, використовуйте оператор -fце включить його трасування. Мінус цього в тому, що висновок ще більше зб'є вас з пантелику. Коли страйк трасує один процес або одну гілку, він показує єдиний потік подій дзвінків. Коли він трасує відразу кілька процесів, то, можливо, побачите початок виклику, перерваного повідомленням , потім - купку викликів для інших гілок виконання, і тільки потім - закінчення першого з <… foocall resumed>. Або розділіть усі результати трасування різними файлами, використовуючи також оператор -ff (Деталі - в керівництві по страйк).

Фільтруйте трасування за допомогою -e

Як бачите, результат трасування – реальна купа всіх можливих системних викликів. Прапором -e можна відфільтрувати трасування (див. керівництво по страйк). Головна перевага в тому, що запускати трасування з фільтрацією швидше, ніж робити повне трасування, а потім GREPСать. Якщо чесно, то мені майже завжди пофіг.

Не всі помилки погані

Простий і поширений приклад - програма, що шукає файл відразу в декількох місцях, на кшталт оболонки, що шукає, в якій кошика каталогу міститься виконуваний файл:

$ strace sh -c uname
...
stat("/home/user/bin/uname", 0x7ffceb817820) = -1 ENOENT (No such file or directory)
stat("/usr/local/bin/uname", 0x7ffceb817820) = -1 ENOENT (No such file or directory)
stat("/usr/bin/uname", {st_mode=S_IFREG|0755, st_size=39584, ...}) = 0
...

Евристика типу останній невдалий запит перед повідомленням про помилку хороша в пошуку релевантних помилок. Як би там не було, логічно починати із самого кінця.

Зрозуміти системні виклики добре допомагають посібники з програмування мовою С

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

Приклад налагодження складніший

Я вже казав, що приклад простої налагодження - це приклад того, з чим мені, здебільшого, доводиться мати справу в роботі з страйк. Однак часом потрібно справжнє розслідування, так що ось вам реальний приклад налагодження складніше.

bcron - планувальник обробки завдань, ще одна реалізація демона *nix крон. Він встановлений на сервері, але коли хтось намагається редагувати розклад, відбувається ось що:

# crontab -e -u logs
bcrontab: Fatal: Could not create temporary file

Ладненько, значить, bcron спробував написати якийсь файл, але в нього не вийшло, і він не визнається чому. Розчехляємо страйк:

# strace -o /tmp/trace crontab -e -u logs
bcrontab: Fatal: Could not create temporary file
# cat /tmp/trace
...
openat(AT_FDCWD, "bcrontab.14779.1573691864.847933", O_RDONLY) = 3
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f82049b4000
read(3, "#Ansible: logsaggn20 14 * * * lo"..., 8192) = 150
read(3, "", 8192)                       = 0
munmap(0x7f82049b4000, 8192)            = 0
close(3)                                = 0
socket(AF_UNIX, SOCK_STREAM, 0)         = 3
connect(3, {sa_family=AF_UNIX, sun_path="/var/run/bcron-spool"}, 110) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f82049b4000
write(3, "156:Slogs #Ansible: logsaggn20 1"..., 161) = 161
read(3, "32:ZCould not create temporary f"..., 8192) = 36
munmap(0x7f82049b4000, 8192)            = 0
close(3)                                = 0
write(2, "bcrontab: Fatal: Could not creat"..., 49) = 49
unlink("bcrontab.14779.1573691864.847933") = 0
exit_group(111)                         = ?
+++ exited with 111 +++

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

Якщо подивитися на людина 2 читає, то можна побачити, що перший аргумент (3) - це файловий дескриптор, який *nix і використовує для всіх обробок вводу-виводу. Як дізнатися, що є файловий дескриптор 3? У цьому конкретному випадку можна запустити страйк з оператором (див. вище), і він автоматично розповість, проте, щоб обчислювати подібні штуки, корисно знати, як читати та аналізувати результати трасування.

Джерелом файлового дескриптора може бути один із багатьох системних викликів (все залежить від того, для чого дескриптор — для консолі, мережевого сокету, власне файлу або чогось іншого), але як би там не було, виклики ми шукаємо, повертаючи 3 (т тобто шукаємо «= 3» у результатах трасування). У цьому результаті їх 2: openat у самому верху і розетка у середині. openat відкриває файл, але близько(3) після цього покаже, що він знову закривається. (Граблі: файлові дескриптори можуть використовуватися заново, коли їх відкривають та закривають). Виклик socket () підходить, оскільки він останній перед читати (), і виходить, що bcrontab працює з чимось через сокет. Наступний рядок показує, що файловий дескриптор пов'язаний з unix domain socket по дорозі /var/run/bcron-spool.

Отже, треба знайти процес, прив'язаний до unix socket з іншого боку. Для цієї мети є парочка витончених трюків, і обидва нагоді для налагодження серверних розгортань. Перший - використовувати NetStat або новіший ss (Socket status). Обидві команди показують активні мережеві з'єднання системи та беруть оператор -l для опису слухаючих сокетів, а також оператор -p для відображення програм, підключених до сокету як клієнт. (Корисних опцій набагато більше, але для цього завдання достатньо цих двох.)

# ss -pl | grep /var/run/bcron-spool
u_str LISTEN 0   128   /var/run/bcron-spool 1466637   * 0   users:(("unixserver",pid=20629,fd=3))

Це говорить про те, що слухач – це команда inixserver, що працює з ID процесу 20629. (І, за збігом, вона використовує файловий дескриптор 3 як сокет.)

Другий реально корисний інструмент для знаходження тієї ж інформації називається також. Він перераховує всі октриті файли (або файлові дескриптори) у системі. Або ж можна отримати інформацію про один конкретний файл:

# lsof /var/run/bcron-spool
COMMAND   PID   USER  FD  TYPE  DEVICE              SIZE/OFF  NODE    NAME
unixserve 20629 cron  3u  unix  0x000000005ac4bd83  0t0       1466637 /var/run/bcron-spool type=STREAM

Процес 20629 - це довгоживучий сервер, так що можна прикріпити до нього страйк за допомогою чогось на зразок strace -o /tmp/trace -p 20629. Якщо редагувати завдання cron в іншому терміналі — отримаємо виведення результатів трасування з помилкою, що виникає. А ось і результат:

accept(3, NULL, NULL)                   = 4
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7faa47c44810) = 21181
close(4)                                = 0
accept(3, NULL, NULL)                   = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21181, si_uid=998, si_status=0, si_utime=0, si_stime=0} ---
wait4(0, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WNOHANG|WSTOPPED, NULL) = 21181
wait4(0, 0x7ffe6bc36764, WNOHANG|WSTOPPED, NULL) = -1 ECHILD (No child processes)
rt_sigaction(SIGCHLD, {sa_handler=0x55d244bdb690, sa_mask=[CHLD], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7faa47ab9840}, {sa_handler=0x55d244bdb690, sa_mask=[CHLD], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7faa47ab9840}, 8) = 0
rt_sigreturn({mask=[]})                 = 43
accept(3, NULL, NULL)                   = 4
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7faa47c44810) = 21200
close(4)                                = 0
accept(3, NULL, NULL)                   = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21200, si_uid=998, si_status=111, si_utime=0, si_stime=0} ---
wait4(0, [{WIFEXITED(s) && WEXITSTATUS(s) == 111}], WNOHANG|WSTOPPED, NULL) = 21200
wait4(0, 0x7ffe6bc36764, WNOHANG|WSTOPPED, NULL) = -1 ECHILD (No child processes)
rt_sigaction(SIGCHLD, {sa_handler=0x55d244bdb690, sa_mask=[CHLD], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7faa47ab9840}, {sa_handler=0x55d244bdb690, sa_mask=[CHLD], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7faa47ab9840}, 8) = 0
rt_sigreturn({mask=[]})                 = 43
accept(3, NULL, NULL

(Останній accept () не буде завершено при трасуванні.) І знову, як не шкода, але цей результат не містить помилки, яку ми шукаємо. Ми не бачимо жодного повідомлення, яке б bcrontag посилав сокету або приймав від нього. Замість них суцільне управління процесом (клон, чекати4, SIGCHLD та ін.) Цей процес породжує дочірній процес, який, як можна здогадатися, і виконує справжню роботу. І якщо треба впіймати її слід, додайте до виклику strace -f. Ось що ми знайдемо, пошукавши повідомлення про помилку в новому результаті зі strace -f -o /tmp/trace -p 20629:

21470 openat(AT_FDCWD, "tmp/spool.21470.1573692319.854640", O_RDWR|O_CREAT|O_EXCL, 0600) = -1 EACCES (Permission denied) 
21470 write(1, "32:ZCould not create temporary f"..., 36) = 36
21470 write(2, "bcron-spool[21470]: Fatal: logs:"..., 84) = 84
21470 unlink("tmp/spool.21470.1573692319.854640") = -1 ENOENT (No such file or directory)
21470 exit_group(111)                   = ?
21470 +++ exited with 111 +++

Ось це вже дещо. Процес 21470 отримує помилку «відмовлено у доступі» при спробі створити файл на шляху tmp/spool.21470.1573692319.854640 (що відноситься до поточного робочого каталогу). Якби ми просто знали поточний робочий каталог, ми знали б і повний шлях і змогли б з'ясувати, чому процес не може створити в ньому свій тимчасовий файл. На жаль, процес вже вийшов, тому не вийде просто використовувати lsof-p 21470 для того, щоб знайти поточний каталог, але можна попрацювати у зворотній бік - пошукати системні виклики PID 21470, що змінюють каталог. (Якщо таких немає, PID 21470, мабуть, успадкували їх від батька, і це вже через lsof -p не з'ясувати.) Цей системний виклик - чдір (що неважко дізнатися за допомогою сучасних мережевих пошукових двигунів). А ось і результат зворотних пошуків за результатами трасування, до сервера PID 20629:

20629 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7faa47c44810) = 21470
...
21470 execve("/usr/sbin/bcron-spool", ["bcron-spool"], 0x55d2460807e0 /* 27 vars */) = 0
...
21470 chdir("/var/spool/cron")          = 0
...
21470 openat(AT_FDCWD, "tmp/spool.21470.1573692319.854640", O_RDWR|O_CREAT|O_EXCL, 0600) = -1 EACCES (Permission denied) 
21470 write(1, "32:ZCould not create temporary f"..., 36) = 36
21470 write(2, "bcron-spool[21470]: Fatal: logs:"..., 84) = 84
21470 unlink("tmp/spool.21470.1573692319.854640") = -1 ENOENT (No such file or directory)
21470 exit_group(111)                   = ?
21470 +++ exited with 111 +++

(Якщо губитеся, то вам, можливо, варто прочитати мою попередню посаду про управління процесом *nix та оболонки.) Отже, сервер PID 20629 не отримав дозволу створити файл на шляху /var/spool/cron/tmp/spool.21470.1573692319.854640. Найімовірніше, причиною цього є класичні налаштування дозволів файлової системи. Перевіримо:

# ls -ld /var/spool/cron/tmp/
drwxr-xr-x 2 root root 4096 Nov  6 05:33 /var/spool/cron/tmp/
# ps u -p 20629
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
cron     20629  0.0  0.0   2276   752 ?        Ss   Nov14   0:00 unixserver -U /var/run/bcron-spool -- bcron-spool

Ось де собака заритий! Сервер працює як cron, але тільки у root є дозвіл записувати в каталог /var/spool/cron/tmp/. Проста команда chown cron /var/spool/cron/tmp/ змусить bcron працювати правильно. (Якщо проблема полягала не в цьому, то наступний найімовірніший підозрюваний — модуль безпеки ядра типу SELinux або AppArmor, тому я б перевірив журнал повідомлень ядра за допомогою dmesg.)

Разом

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

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

Джерело: habr.com

Додати коментар або відгук