Продолжаем ковырять systemd
Для того чтобы потыкать
Делается это так:
Ключ
ㅤ
Теперь этот юнит будет запускаться каждый раз при старте пользовательской сессии, а таймер начнет тикать.
Эта штука создаёт символическую ссылку в
Если тебе нужно разово запустить такой юнит, то
А как отлаживать?
➡️ 1. Сначала нужно убедиться что таймер активен:
Если всё ок, то увидишь
➡️ 2. Смотрим расписание запуска
-
-
-
-
➡️ 3. Проверяем выполнялся ли сервис
Команда покажет логи выполнения скрипта. Можно сузить диапазон, например так:
По дебагу это основное. Мелочи вроде синтаксических ошибок и т.п. рассматривать не будем, тут уже от кривизны рук все зависит.
Ну а так всё что нужно тебе знать. То есть у каждого пользователя могут быть свои юниты и сервисы, а не только у рута.
🛠 #linux #tricks #debug #systemd #bash
—
✅ @bashdays / @linuxfactory / @blog
Для того чтобы потыкать
systemd не обязательно обладать рутовскими правами или судой-мудой. Да, ты не ослышался, всё можно запускать под обычным юзером.Делается это так:
systemctl --user enable bashdays.timer
Ключ
--user означает, что команда применяется в пользовательском пространстве, а не на уровне всей системы.ㅤ
Теперь этот юнит будет запускаться каждый раз при старте пользовательской сессии, а таймер начнет тикать.
Эта штука создаёт символическую ссылку в
~/.config/systemd/user/ в директории default.target.wants/Если тебе нужно разово запустить такой юнит, то
enable меняем на start. Вот и вся наука.А как отлаживать?
systemctl --user status bashdays.timer
Если всё ок, то увидишь
Active: active (waiting).systemctl --user list-timers
-
NEXT — когда следующий запуск-
LEFT — сколько осталось времени-
LAST — когда запускался в прошлый раз-
UNIT — имя таймераjournalctl --user-unit bashdays.service --since "1 hour ago"
Команда покажет логи выполнения скрипта. Можно сузить диапазон, например так:
journalctl --user-unit bashdays.service --since today
По дебагу это основное. Мелочи вроде синтаксических ошибок и т.п. рассматривать не будем, тут уже от кривизны рук все зависит.
Ну а так всё что нужно тебе знать. То есть у каждого пользователя могут быть свои юниты и сервисы, а не только у рута.
—
Please open Telegram to view this post
VIEW IN TELEGRAM
3 62
Продолжаем разбирать
ㅤ
Директива
Пример:
Этот юнит не запустится, если файл
Если путь НЕ существует,
Нахуя это надо?
1. Например, если
2. Можно создавать один юнит, который активируется только при наличии определённого модуля или плагина.
3. Иногда это используют в early boot-юнитах, чтобы запускать их только если что-то доступно (например, том LUKS).
Список основных Condition's (нажми на спойлер)
Дополнительно
Если заменить
То есть берем к примеру директиву
И получается:
Если
Статус будет такой:
Ну и все это дело можно комбинировать:
Если
Если существует, но не смонтирован — юнит заваливается.
Короче
🛠 #linux #tricks #debug #systemd #bash
—
✅ @bashdays / @linuxfactory / @blog
systemd на кирпичики. Сегодня про кандишены (условия/ситуации).ㅤ
Директива
ConditionPathExists используется в юнит-файлах systemd и позволяет задать условие для запуска юнита: он будет запущен только если указанный файл или директория существует.Пример:
[Unit]
Description=Special Service
ConditionPathExists=/etc/bashdays-config
[Service]
ExecStart=/usr/local/bin/bashdays-handler
Этот юнит не запустится, если файл
/etc/bashdays-config не существует. В статусе сервиса ты увидишь:ConditionPathExists=/etc/bashdays-config was not met
Если путь НЕ существует,
systemd не будет запускать юнит. Вместо этого он будет считаться пропущенным (skipped), а не проваленным.Нахуя это надо?
1. Например, если
bashdays-config существует, запускается сервис с особым поведением.2. Можно создавать один юнит, который активируется только при наличии определённого модуля или плагина.
3. Иногда это используют в early boot-юнитах, чтобы запускать их только если что-то доступно (например, том LUKS).
Список основных Condition's (нажми на спойлер)
ConditionPathExists — файл или каталог существует
ConditionPathIsDirectory — путь существует и это каталог
ConditionPathIsMountPoint — путь является точкой монтирования
ConditionFileIsExecutable — файл существует и он исполняемый
ConditionKernelCommandLine — есть ли параметр ядра с указанным значением
ConditionPathExistsGlob — совпадает ли хотя бы один путь по glob-шаблону
ConditionPathIsSymbolicLink — является ли путь символической ссылкой
ConditionFileNotEmpty — существует ли файл и не пуст ли он
ConditionEnvironment — установлена ли переменная окружения
ConditionArchitecture — архитектура CPU (например, x86_64, aarch64)
ConditionVirtualization — nип виртуализации (например, kvm, docker)
ConditionHost — имя хоста
ConditionMachineID — cовпадает ли machine-id
ConditionControlGroupController — есть ли указанный cgroup controller (например, cpu, memory)
ConditionNeedsUpdate — нуждается ли в обновлении (/usr или /etc)
ConditionFirstBoot — Первый ли это запуск после установки
ConditionACPower — Подключено ли питание от сети (для ноутбуков)
ConditionSecurity — Активен ли определённый LSM (например, selinux)
ConditionUser — Запускается ли от указанного пользователя
ConditionGroup — Запускается ли от указанной группы
ConditionCapability — Имеет ли процесс определённую capability
ConditionNetwork — Есть ли сеть (online, configured)
ConditionMemory — Есть ли минимум указанного объёма памяти
Дополнительно
Если заменить
Condition на Assert, условие не выполнено — юнит считается проваленным, а не пропущенным.То есть берем к примеру директиву
ConditionPathExists и меняем её на AssertPathExists. AssertPathExists=/etc/bashdays.conf
И получается:
ConditionPathExists — юнит пропускается (не считается ошибкой)AssertPathExists — юнит падает (считается ошибкой)Assert полезен, когда ты строго требуешь, чтобы ресурс (например, внешний диск, NFS, или другой том) был смонтирован перед запуском сервиса. Если его нет — это ошибка, а не «ну и похуй».[Unit]
Description=Start backup script only if /mnt/backup is mounted
AssertPathIsMountPoint=/mnt/backup
[Service]
ExecStart=/usr/local/bin/backup.sh
Если
/mnt/backup не смонтирован, systemd выдаст ошибку, и сервис не запустится.Статус будет такой:
systemd[1]: Starting backup.service...
systemd[1]: backup.service: Failed with result 'assert'.
Ну и все это дело можно комбинировать:
ConditionPathExists=/mnt/backup
AssertPathIsMountPoint=/mnt/backup
Если
/mnt/backup не существует — юнит пропускается.Если существует, но не смонтирован — юнит заваливается.
Короче
systemd не такой уж простой, как кажется с первого взгляда. На нём можно прям заебись логику построить и получить желаемое. Так что недооценивать его явно не стоит, это прям заебись комбайн.—
Please open Telegram to view this post
VIEW IN TELEGRAM
6 73
Сегодня будем убивать неугодные сервисы. Нет, тут будет не про
Короче в юните можно указать параметр
ㅤ
Удобно применять для сервисов, которые не должны жить вечно. Нет ничего вечного! Например, временные задачи, вспомогательные демоны, скрипты и т.п.
А что будет если время вышло?
Как и написал выше — будет песда!
Этот сервис будет убит через 60 секунд после запуска — даже если скрипт ещё не завершился.
Если
Ждём до 10 секунд → если не завершился →
Частый паттерн
Сервис работает максимум 5 минут, и если завис —
А нахуя тут Restart=on-failure?
Оно говорит — «если сервис завершился аварийно — перезапусти его» А завершение по
Если не указать
Важный нюанс!
Если в юните используется
Такие дела, изучай!
🛠 #linux #tricks #debug #systemd #bash
—
✅ @bashdays / @linuxfactory / @blog
kill и т.п. а будет все тот же systemd.Короче в юните можно указать параметр
RuntimeMaxSec, в нем задаём время жизни сервиса. Если сервис работает дольше указанного времени, то ему песда. Systemd принудительно завершит его.ㅤ
Удобно применять для сервисов, которые не должны жить вечно. Нет ничего вечного! Например, временные задачи, вспомогательные демоны, скрипты и т.п.
[Service]
RuntimeMaxSec=30s
0s, 5min, 1h, 2d — интервалыinfinity — отключение лимитаА что будет если время вышло?
Как и написал выше — будет песда!
Systemd пошлет SIGTERM. А если сервис сука живучий и не завершился, то в ход пойдет тяжелая артиллерия через TimeoutStopSec, тут уже будет послан SIGKILL. Но его нужно предварительно прописать.TimeoutStopSec= — это время ожидания корректного завершения сервиса после того, как systemd послал ему сигнал SIGTERM.[Service]
ExecStart=/usr/bin/python3 /opt/scripts/bashdays-task.py
RuntimeMaxSec=60
Этот сервис будет убит через 60 секунд после запуска — даже если скрипт ещё не завершился.
[Service]
ExecStart=/usr/bin/bashdays-daemon
RuntimeMaxSec=60
TimeoutStopSec=10
Если
bashdays-daemon работает дольше 60 секунд → SIGTERMЖдём до 10 секунд → если не завершился →
SIGKILLЧастый паттерн
RuntimeMaxSec=300
TimeoutStopSec=5
Restart=on-failure
Сервис работает максимум 5 минут, и если завис —
systemd подождёт 5 секунд на аккуратное завершение, а потом прибьёт его нахуй.А нахуя тут Restart=on-failure?
Оно говорит — «если сервис завершился аварийно — перезапусти его» А завершение по
SIGKILL из-за превышения времени жизни это и есть failure.Если не указать
TimeoutStopSec, будет использоваться значение по умолчанию: 90 секунд — порой это дохуя, поэтому предпочтительнее задать его руками.Важный нюанс!
Если в юните используется
Restart=always, то после убийства, сервис будет перезапущен и возможно сразу «помрёт» если не изменит своё поведение.Такие дела, изучай!
—
Please open Telegram to view this post
VIEW IN TELEGRAM
6 61
reexec VS reload
Порой народ путает команды
ㅤ
С виду вроде похожие, но нет. Спросил тут на досуге товарища — а ты знаешь чем они отличаются?
Да, ответил товариш,
Неее… так не нужно! Это хуйня! По крайней мере первая команда тебе тут не нужна для перезапуска и обновления сервисов.
Команда
После редактирования
Обычно проходится по каталогам:
То есть она перезагружает только конфигурацию юнитов, без перезапуска сервисов.
Так что не путай, в большинстве случаев тебе достаточно
🛠 #linux #tricks #debug #systemd
—
✅ @bashdays / @linuxfactory / @blog
Порой народ путает команды
systemctl daemon-reload и systemctl daemon-reexec. ㅤ
С виду вроде похожие, но нет. Спросил тут на досуге товарища — а ты знаешь чем они отличаются?
Да, ответил товариш,
reexec это старая версия перечитывания сервисов и юнитов. Я обычно делаю так:systemctl daemon-reexec
systemctl daemon-reload
systemctl enable node_exporter
systemctl start node_exporter
Неее… так не нужно! Это хуйня! По крайней мере первая команда тебе тут не нужна для перезапуска и обновления сервисов.
Команда
systemctl daemon-reexec перезапускает сам systemd, это нужно например при обновлении бинарников systemd, но никак не для перезапуска юнитов и сервисов.После редактирования
*.service / *.timer / *.mount файлов, достаточно сделать daemon-reload, эта команда перечитает unit-файлы.Обычно проходится по каталогам:
/etc/systemd/system/
/lib/systemd/system/
/usr/lib/systemd/system/
/run/systemd/system/
То есть она перезагружает только конфигурацию юнитов, без перезапуска сервисов.
Так что не путай, в большинстве случаев тебе достаточно
daemon-reload.—
Please open Telegram to view this post
VIEW IN TELEGRAM
8 70
Если ты выполняешь план, то тебе повысят план, а не зарплату!
Пытался вчера подружить taskwarrior и s3 для синхронизации, из коробки оно вроде как только с AWS работает.
Подкинул в конфиг параметры подключение к кастомному хранилищу. Ну думаю, какая в хуй разница.
Проверяю
Мде… Всёж правильно, эндпоинт пингуется, курлится, телнетится. Описывать весь момент дебага не буду, но там конкретный такой - метод тыка был.
Ну раз обычный «метод тыка» не помогает, расчехляем
Что делает команда:
И в выхлопе находим строчку:
😀 😃 😄 😁 😅 😂 🤣 😊
😇 🙂 🙃 😉 😌 😍 🥰 😘
😗 😙 😚 😋 😛 😝 😜 🤪
🤨 🧐 🤓 😎 🤩 🥳 😏 😒
😞 😔 😟 😕 🙁 ☹️ 😣 😖
😫 😩 🥺 😢 😭 😤 😠 😡
Ну ёб твою мать! А нахуй я тогда все эти приседания с конфигом устраивал, если эта падла хуй положила и по
То есть настройка
Ну хоть проблему нашли.
Отсюда вывод: с кастомным S3 напрямую taskwarrior работать не сможет. Даже если устроить подмену хостов или сделать хак через CNAME.
А как синхронизировать-то задачи? Ооо брат, я уже написал про это отдельный пост, чуть позже закину.
Хороших тебе выходных!
🛠 #strace #debug #taskwarrior
—
✅ @bashdays ✅ @linuxfactory ✅ @blog
Пытался вчера подружить taskwarrior и s3 для синхронизации, из коробки оно вроде как только с AWS работает.
Подкинул в конфиг параметры подключение к кастомному хранилищу. Ну думаю, какая в хуй разница.
sync.backend=s3
sync.aws.region=ru-7
sync.aws.bucket=taskwarrior
sync.aws.access_key_id=aed38518013b4ab
sync.aws.secret_access_key=992570bad57
sync.aws.endpoint=s3.ru-7.storage.selcloud.ru
Проверяю
task sync init, хуй там плавал, ошибка:unhandled error: dispatch failure: io error: error trying to connect: dns error: failed to lookup address information: Name or service not known: dns error: failed to lookup address information: Name or service not known: failed to lookup address information: Name or service not known
Мде… Всёж правильно, эндпоинт пингуется, курлится, телнетится. Описывать весь момент дебага не буду, но там конкретный такой - метод тыка был.
Ну раз обычный «метод тыка» не помогает, расчехляем
strace!strace -s 200 -f -e trace=network,connect,sendto,recvfrom task sync
Что делает команда:
-s 200 — печатать до 200 байт строковых аргументов (по умолчанию strace режет строки до 32 байт). Это важно, чтобы увидеть полный URL/hostname, который передаётся в syscalls.-f — следить не только за основным процессом, но и за всеми дочерними (fork/clone)-e trace=network, connect, sendto, recvfrom — ограничиваем вывод только сетевыми вызовами: socket, connect → создание сокетов и подключения (TCP/UDP). sendto / recvfrom → передача данных (обычно видно DNS-запросы, HTTP-заголовки и т.д.).И в выхлопе находим строчку:
taskwarrior.s3.ru-7.amazonaws.com.Ну ёб твою мать! А нахуй я тогда все эти приседания с конфигом устраивал, если эта падла хуй положила и по
task diagnostic никаких ошибок не вывело.То есть настройка
sync.aws.endpoint=… вообще не учитывается — клиент жёстко строит URL по схеме AWS.Ну хоть проблему нашли.
Strace все же достаточно пиздатый инструмент.Отсюда вывод: с кастомным S3 напрямую taskwarrior работать не сможет. Даже если устроить подмену хостов или сделать хак через CNAME.
А как синхронизировать-то задачи? Ооо брат, я уже написал про это отдельный пост, чуть позже закину.
Хороших тебе выходных!
—
Please open Telegram to view this post
VIEW IN TELEGRAM
13 56
Ищем баги с помощью strace
Предположим, крутится у тебя в проде какое-то приложение, это приложение было разработано криворукими обезьянами — на отъебись.
ㅤ
По итогу продакшен начинает троить и выжирать процессорное время. Хуита!
Явно требуется профилирование, но мыж с тобой не обезьяны, поэтому изучать код не будем. А сразу вооружимся
Запускаем:
Через несколько секунд жмём
Хм… эта падла активно пользуется системным вызовом
Виновника определили. То есть приложение постоянно что-то куда-то пишет, тем самым забивая 99% процессорного времени.
Важно понимать:
Здесь
Исходники у нас есть, давай посмотрим:
Что тут не так:
Из-за
Как пофиксить:
Теперь данные будут сбрасывать пачками, так как мы указали буферизацию, которая равна 1MB.
Проверяем до фикса:
Проверяем после фикса:
Количество вызовов
Как костыль и быстрофикс — сойдёт! Повторюсь — мы с тобой не обезьяны, чтобы вникать в код разработчиков и что-то в нем «правильно» править.
В большинстве случаев, ты просто находишь проблемы и уже с этими данными создаешь задачу на разработчика. Сам в код не лезь, целее будешь.
Ну и на закуску фикс, который сделали разработчики:
Как это работает:
1. StringIO хранит текст в оперативной памяти.
2. Цикл гонит туда строки.
3. Когда накопится, например, 1 MB, содержимое сбрасывается в файл одной большой порцией (
4. Буфер очищается и цикл продолжается.
Хуй знает на сколько это всё правильно, ну раз сделали через внутреннию буферизацию
Такие дела. Изучай.
🛠 #debug
—
✅ @bashdays ✅ @linuxfactory ✅ @blog
Предположим, крутится у тебя в проде какое-то приложение, это приложение было разработано криворукими обезьянами — на отъебись.
ㅤ
По итогу продакшен начинает троить и выжирать процессорное время. Хуита!
Явно требуется профилирование, но мыж с тобой не обезьяны, поэтому изучать код не будем. А сразу вооружимся
strace и посмотрим где-же узкое горлышко.Запускаем:
strace -c python3 app.py
Через несколько секунд жмём
Ctrl-C и получаем статистику:% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ------
99.82 0.413251 8 49431 write
0.07 0.000291 32 9 mmap
0.05 0.000207 25 8 mprotect
0.03 0.000129 21 6 openat
0.02 0.000090 30 3 close
......
Хм… эта падла активно пользуется системным вызовом
write().time — процент процессорного времени, потраченного на вызов.usecs/call — среднее время на один вызов (в микросекундах).calls — сколько раз вызов был сделан.Виновника определили. То есть приложение постоянно что-то куда-то пишет, тем самым забивая 99% процессорного времени.
Важно понимать:
strace показывает только то время, которое ядро тратит на обработку системных вызовов. Поэтому значения могут отличаться от того, что покажет команда time:$ time python3 app.py
real 0m7.412s
user 0m1.102s
sys 0m6.184s
Здесь
sys совпадёт с тем, что мы видели через strace -c.Ну и теперь даже без доступа к исходникам можно быстро понять, где «утекают» ресурсы.
Исходники у нас есть, давай посмотрим:
with open("tmp.txt", "w") as f:
while True:
f.write("Привет супчики! Привет от BashDays!")
f.flush()Что тут не так:
Из-за
flush() Python гонит строку сразу в файловую систему, без буферизации.Как пофиксить:
# fixed.py
with open("tmp.txt", "w", buffering=1024*1024) as f:
while True:
f.write("Привет супчики! Привет от BashDays!\n")
Теперь данные будут сбрасывать пачками, так как мы указали буферизацию, которая равна 1MB.
Проверяем до фикса:
$ strace -c python3 app.py
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ------
99.8 0.413251 8 49431 write
Проверяем после фикса:
$ strace -c python3 app-fixed.py
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ------
98.9 0.072111 450 160 write
Количество вызовов
write() резко сократилось, нагрузка на ядро упала.Как костыль и быстрофикс — сойдёт! Повторюсь — мы с тобой не обезьяны, чтобы вникать в код разработчиков и что-то в нем «правильно» править.
В большинстве случаев, ты просто находишь проблемы и уже с этими данными создаешь задачу на разработчика. Сам в код не лезь, целее будешь.
Ну и на закуску фикс, который сделали разработчики:
import io
buffer = io.StringIO()
with open("tmp.txt", "w") as f:
while True:
buffer.write("Привет супчики! Привет от BashDays\n")
if buffer.tell() > 1024 * 1024:
f.write(buffer.getvalue())
f.flush()
buffer.seek(0)
buffer.truncate(0)
Как это работает:
1. StringIO хранит текст в оперативной памяти.
2. Цикл гонит туда строки.
3. Когда накопится, например, 1 MB, содержимое сбрасывается в файл одной большой порцией (
write + flush).4. Буфер очищается и цикл продолжается.
Хуй знает на сколько это всё правильно, ну раз сделали через внутреннию буферизацию
StringIO, значит так правильно.Такие дела. Изучай.
—
Please open Telegram to view this post
VIEW IN TELEGRAM
108 75
Как не стремись к автоматизации, всегда найдется какой-нибудь легаси сервис, который требует ручного обслуживания.
Был у меня такой сервис и работал он только тогда, когда все его файлы и каталоги принадлежали определенному пользователю.
ㅤ
Доступ к сервису имели многие, поэтому люди порой троили и запускали команды в каталоге сервиса от root. Сервису было на это поебать, но до момента его перезапуска.
Обычно это чинилось очень легко, через
Казалось бы, есть масса способов предотвратить такие ошибки: правильные права на файлы, ACL’ы, SELinux.
Но веселья в этом мало! Поэтому я решил заебенить свой собственный мониторинг файловой системы. Скиллов то предостаточно, хули нет.
Спойлер:Я залез в кроличью нору и знатно так хлебнул гавна.
Попытка намбер 1
В Linux есть API под названием fanotify, с его помощью можно получать события о действиях с файлами в юзерспейс.
Всё просто: инициализируем fanotify_init, указываем каталоги через fanotify_mark и читаем события из дескриптора.
Но тут же вылез огромный хуй:
- нельзя отслеживать каталоги рекурсивно (только целый маунт)
-
Решение вполне рабочее, но громоздкое. Я такие не люблю, этож думать надо, вайбкодингом не решается.
Попытка намбер 2
Вспоминаем что есть
В
То есть единая точка входа. Там можно отлавливать события и фильтровать их, не гоняя лишние переключения контекста.
Но и тут вылез хуй:
-
- фильтрацию приходится писать прямо в
Да блядь…
Как я победил рекурсивный обход
Чтобы понять, что именно меняется в каталоге сервиса, пришлось использовать структуру
Но так как в
На практике этого вполне достаточно. Глубина каталогов мне известна. Ну и конечно, пришлось аккуратно работать с RCU-локами, чтобы дерево не поменялось в момент обхода.
Как можно улучшить
В идеале использовать не VFS-хуки, а LSM hooks (Linux Security Module).
Они стабильнее, понятнее и позволяют сразу работать с путями. Там можно красиво получить path и сразу преобразовать его в строку, а потом делать поиск подстроки.
Но в моём ядре этих хуков не было, хуй знает почему, видимо дистрибутив слишком древний. Надо попробовать на новых, чем черт не шутит.
Итоги
Эта поделка как и предполагалась, погрузила меня в печаль, душевные страдания, НО стала отличным тренажером для прокачки:
- Внутренностей Linux ядра
- Работы с eBPF
- И кучу другого с kernel-space
Информации по нему много, но вся она разбросана. Собрать всё это в кучу было отдельным квестом.
Мораль?
Иногда самое простое решение — это
🛠 #linux #debug #dev
—
✅ @bashdays ✅ @linuxfactory ✅ @blog
Был у меня такой сервис и работал он только тогда, когда все его файлы и каталоги принадлежали определенному пользователю.
ㅤ
Доступ к сервису имели многие, поэтому люди порой троили и запускали команды в каталоге сервиса от root. Сервису было на это поебать, но до момента его перезапуска.
Обычно это чинилось очень легко, через
chown -R. Все это знали и никого это не смущало. Короче костыль ебаный.Казалось бы, есть масса способов предотвратить такие ошибки: правильные права на файлы, ACL’ы, SELinux.
Но веселья в этом мало! Поэтому я решил заебенить свой собственный мониторинг файловой системы. Скиллов то предостаточно, хули нет.
Спойлер:
Попытка намбер 1
В Linux есть API под названием fanotify, с его помощью можно получать события о действиях с файлами в юзерспейс.
Всё просто: инициализируем fanotify_init, указываем каталоги через fanotify_mark и читаем события из дескриптора.
Но тут же вылез огромный хуй:
- нельзя отслеживать каталоги рекурсивно (только целый маунт)
-
anotify даёт только PID процесса, который что-то сделал. А чтобы узнать UID/GID — нужно лезть в /proc/<pid>/status. То есть на каждое событие приходится открывать и парсить файлы в /proc.Решение вполне рабочее, но громоздкое. Я такие не люблю, этож думать надо, вайбкодингом не решается.
Попытка намбер 2
Вспоминаем что есть
eBPF. Это штука позволяет запускать программы прямо в ядре Linux. Они компилируются в байткод, проходят проверку, а потом гоняются через JIT почти с нативной скоростью.Что такое eBPF можешь почитать тут и тут.
В
eBPF заебись то, что можно цепляться за разные функции ядра. Например, можно подцепиться к vfs_mkdir или vfs_create — это общий слой для работы с файлами.То есть единая точка входа. Там можно отлавливать события и фильтровать их, не гоняя лишние переключения контекста.
Но и тут вылез хуй:
-
kprobes на функции VFS нестабильны, в новых ядрах сигнатуры могут меняться или функции вообще исчезнуть.- фильтрацию приходится писать прямо в
eBPF, а там свои ограничения. Нет бесконечных циклов, стек всего ~512 байт.Да блядь…
Как я победил рекурсивный обход
Чтобы понять, что именно меняется в каталоге сервиса, пришлось использовать структуру
dentry и подниматься по дереву до родителя.Но так как в
eBPF нельзя сделать «бесконечный» цикл, я ограничил глубину с помощью MAX_DEPTH.На практике этого вполне достаточно. Глубина каталогов мне известна. Ну и конечно, пришлось аккуратно работать с RCU-локами, чтобы дерево не поменялось в момент обхода.
➡️ Кусок кода в первом комментарии, сюда не влез.
Как можно улучшить
В идеале использовать не VFS-хуки, а LSM hooks (Linux Security Module).
Они стабильнее, понятнее и позволяют сразу работать с путями. Там можно красиво получить path и сразу преобразовать его в строку, а потом делать поиск подстроки.
Но в моём ядре этих хуков не было, хуй знает почему, видимо дистрибутив слишком древний. Надо попробовать на новых, чем черт не шутит.
Итоги
Эта поделка как и предполагалась, погрузила меня в печаль, душевные страдания, НО стала отличным тренажером для прокачки:
- Внутренностей Linux ядра
- Работы с eBPF
- И кучу другого с kernel-space
eBPF — мощнейший инструмент, но очень тонкий. Ошибёшься — будешь выебан в жопу.
Информации по нему много, но вся она разбросана. Собрать всё это в кучу было отдельным квестом.
Мораль?
Иногда самое простое решение — это
chown -R. Но куда интереснее — написать свой велосипед и заглянуть в кроличью нору Linux ядра.—
Please open Telegram to view this post
VIEW IN TELEGRAM
9 47
Настройка core dump в Docker
Цель этого поста — дать тебе общее руководство по включению и сбору core dumps для приложений, работающих в docker контейнере.
Настраиваем хост для сохранения дампов
Для начала надо сконфигурировать хостовую машину, чтобы сохранять такие дампы в нужное место. Нет, не в жопу.
ㅤ
Универсальный способ — задать шаблон core pattern. Шаблон определяет путь и информацию о процессе который наебнулся.
Кратенько:
Как вариант, можно настроить host изнутри контейнера через CMD или ENTRYPOINT. Но контейнер в этом случае должен запускаться в privileged mode, что хуева с точки зрения безопасности.
Пример хуёвого приложения
После компиляции и запуска, приложение наебнется с ошибкой.
Пишем Dockerfile для этого приложения
Запускаем контейнер с нужными опциями:
Разбираем опции:
Здесь важно: путь
После того как приложение наебнется, core dump будет сохранён на хостовой машине в директории
Анализируем дамп с помощью gdb
Такс, мы получили core dump и он у нас лежит на хостовой машине, но рекомендуется открыть его внутри контейнера. Контейнер должен быть из того же образа, в котором компилилось приложение.
Это поможет убедиться, что все зависимости (библиотеки и прочая хуитень) доступны.
Если в образе нет исходного кода, можно допом смаунтить исходники:
Теперь внутри контейнера запускаем:
После загрузки дампа можно выполнить команду
Команда поможет определить, где именно произошел факап.
Давай быстренько разберем ошибку.
В исходнике было:
То есть ошибка здесь не баг компиляции или рантайма, а намеренно вставленный вызов
Если у тебя docker-compose, то все флаги (
Хуй знает чё еще написать, завтра тему дебага продолжим, чет в конце года много траблшутинга навалило разнообразного.
🛠 #linux #debug #dev
—
✅ @bashdays ✅ @linuxfactory ✅ @blog
Цель этого поста — дать тебе общее руководство по включению и сбору core dumps для приложений, работающих в docker контейнере.
Настраиваем хост для сохранения дампов
Для начала надо сконфигурировать хостовую машину, чтобы сохранять такие дампы в нужное место. Нет, не в жопу.
ㅤ
Универсальный способ — задать шаблон core pattern. Шаблон определяет путь и информацию о процессе который наебнулся.
echo '/tmp/core.%e.%p' | sudo tee /proc/sys/kernel/core_pattern
Кратенько:
%e — имя процесса %p — pid процессаБолее подробно о конфигурации core pattern можешь почитать в man-странице ядра Linux.
Как вариант, можно настроить host изнутри контейнера через CMD или ENTRYPOINT. Но контейнер в этом случае должен запускаться в privileged mode, что хуева с точки зрения безопасности.
Пример хуёвого приложения
#include <cstdlib>
void foo() {
std::abort();
}
int main() {
foo();
return 0;
}
После компиляции и запуска, приложение наебнется с ошибкой.
Пишем Dockerfile для этого приложения
FROM ubuntu:22.04
# Install tools
RUN apt update \
&& apt -y install \
build-essential \
gdb \
&& rm -rf /var/lib/apt/lists/*
# Build the application
COPY ./ /src/
WORKDIR /src/
RUN g++ main.cpp -o app
CMD ["/src/app"]
Тот кто в теме, сможет его прочитать и понять. Если не понятно, гугли и разбирай, качнешь свой девопсовый скиллз.
Запускаем контейнер с нужными опциями:
docker run \
--init \
--ulimit core=-1 \
--mount type=bind,source=/tmp/,target=/tmp/ \
application:latest
Разбираем опции:
--init — гарантирует корректную обработку сигналов внутри контейнера--ulimit — устанавливает неограниченный размер core dump для процессов внутри контейнера--mount — монтирует /tmp из хоста в контейнер, чтобы дампы, создаваемые внутри контейнера, были доступны после его остановки или удаленияЗдесь важно: путь
source на хосте должен совпадать с тем, который задан в шаблоне core pattern.После того как приложение наебнется, core dump будет сохранён на хостовой машине в директории
/tmp. ls /tmp/core*
# /tmp/core.app.5
Анализируем дамп с помощью gdb
Такс, мы получили core dump и он у нас лежит на хостовой машине, но рекомендуется открыть его внутри контейнера. Контейнер должен быть из того же образа, в котором компилилось приложение.
Это поможет убедиться, что все зависимости (библиотеки и прочая хуитень) доступны.
docker run \
-it \
--mount type=bind,source=/tmp/,target=/tmp/ \
application:latest \
bash
Если в образе нет исходного кода, можно допом смаунтить исходники:
docker run \
-it \
--mount type=bind,source=/tmp/,target=/tmp/ \
--mount type=bind,source=<путь_к_исходникам_на_хосте>,target=/src/ \
application:latest \
bash
Теперь внутри контейнера запускаем:
gdb app /tmp/core.app.5
После загрузки дампа можно выполнить команду
bt (backtrace), чтобы увидеть трассировку стека:(gdb) bt
#0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
#1 0x00007f263f378921 in __GI_abort () at abort.c:79
#2 0x000055f9a9d16653 in foo() ()
#3 0x000055f9a9d1665c in main ()
Команда поможет определить, где именно произошел факап.
Давай быстренько разберем ошибку.
#0 и #1 показывают, что процесс получил сигнал 6 (SIGABRT) и завершился через abort()#2 — вызов произошёл внутри функции foo()#3 — main() вызвал foo()В исходнике было:
void foo() {
std::abort();
}То есть ошибка здесь не баг компиляции или рантайма, а намеренно вставленный вызов
std::abort(), который и приводит к аварийному завершению и генерации core dump.Если у тебя docker-compose, то все флаги (
--init, --ulimit, --mount и т.д.) применимы и для него. То есть отладку можно легко адаптировать.Хуй знает чё еще написать, завтра тему дебага продолжим, чет в конце года много траблшутинга навалило разнообразного.
—
Please open Telegram to view this post
VIEW IN TELEGRAM
5 45
Здесь: я как-то поднимал проблему с торможением 1c на postgres.
🔤 🔤 🔥 🔤 🔤 🔤 🔤
Благодаря нашему коллеге @ovchinnikovmy, дело сдвинулось с мертвой точки. Спасибо ему большое за консультации и рекомендации по PG.
ㅤ
Мы начали попытки оптимизировать работу postgres для нашей задачи. И сразу столкнулись с проблемой. Ну, оптимизировали. А насколько?
Улучшение есть, а кто был виноват в тормозах PG или 1С?
Все может прекрасно работать в тестах, и становится колом, когда идет интенсивная работа в нескольких базах одновременно. Где горлышко бутылки - число ядер, частота или скорость диска, или может пора памяти добавить?
Там маленькая конторка. Фактически один сервак. Не будешь же zabbix ради этого ставить.
Онлайн можно посмотреть через
Остановился на пакете
Он собирает статистику каждые 10 сек по двум пользователям postgres (PG) и usr1cv83 (1С) в csv-лог (разделитель пробел, но это можно исправить).
Поскольку лог текстовый, дальше его можно вертеть с помощью awk/sort или просто в LibreOffice Calc.
pidstat ключи:
-r - память
-l - командная строка процесса
-d - диск
-h - табличный режим
-H - время unix
-U - username
-u - проц
gawk ключи:
🛠 #debug #linux
—
✅ @bashdays ✅ @linuxfactory ✅ @blog
Благодаря нашему коллеге @ovchinnikovmy, дело сдвинулось с мертвой точки. Спасибо ему большое за консультации и рекомендации по PG.
ㅤ
Мы начали попытки оптимизировать работу postgres для нашей задачи. И сразу столкнулись с проблемой. Ну, оптимизировали. А насколько?
Улучшение есть, а кто был виноват в тормозах PG или 1С?
Все может прекрасно работать в тестах, и становится колом, когда идет интенсивная работа в нескольких базах одновременно. Где горлышко бутылки - число ядер, частота или скорость диска, или может пора памяти добавить?
Там маленькая конторка. Фактически один сервак. Не будешь же zabbix ради этого ставить.
Онлайн можно посмотреть через
nmon, top/htop. nmon даже позволяет записывать данные в лог, и есть программа, которая позволяет генерить html с отчетами, но там все интегрально. По системе. А хочется по процессам.Остановился на пакете
sysstat. Это такой консольный zabbix. Он позволяет собирать статистику по процессам. Анализировать можно память, проц, диск, стэк. Причем по каждому PID в отдельности и прямо в консоли. В общем, все, что нужно. Для большего удобства я набросал скрипт.#!/bin/bash
# 20251005
# apt install sysstat gawk
# работа с 9 до 18, запись с 8:30 до 18:30
# запуск через cron
# 30 8 * * * /root/work/stat/stat.sh &
declare -x PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
declare -i INTERVAL_SEC=10
declare -i COUNT=3600 # итого 10 часов
declare -i WEEK_DAY;printf -v WEEK_DAY "%(%-u)T"
declare LOG="$0_${WEEK_DAY}.csv"
pidstat -r -l -d -H -h -U -u $INTERVAL_SEC $COUNT |
gawk 'NR<4;$2=="usr1cv83"||$2=="postgres"{$1=strftime("%Y%m%d_%H%M%S",$1);print}'>"$LOG"
Он собирает статистику каждые 10 сек по двум пользователям postgres (PG) и usr1cv83 (1С) в csv-лог (разделитель пробел, но это можно исправить).
Поскольку лог текстовый, дальше его можно вертеть с помощью awk/sort или просто в LibreOffice Calc.
pidstat ключи:
-r - память
-l - командная строка процесса
-d - диск
-h - табличный режим
-H - время unix
-U - username
-u - проц
gawk ключи:
NR<4 - заголовок (легенда) из трех строк$2=="usr1cv83"||$2=="postgres" - фильтрация по username$1=strftime("%Y%m%d_%H%M%S",$1) - удобный формат времени.LOG="$0_${WEEK_DAY}.csv" - Недельная ротация. По одному на день.—
Please open Telegram to view this post
VIEW IN TELEGRAM
Фиксим кривой Exit Code в docker
Во время работы с docker контейнером наткнулся на неочевидный код выхода (exit code).
ㅤ
Суть проблемы: Когда программа внутри контейнера падает с
То есть контейнер маскирует реальную причину падения приложения. Соответственно дальнейший дебаг не имеет смысла, потому что код выхода не соответствует действительности.
Давай проверим:
Пишем Dockerfile:
Собираем и запускаем:
А на выходе у нас код:
В примере выше код выхода — 139 = 128 + 11, где
Чтобы это пофиксить, нужно захерачить хак:
После этого контейнер будет возвращать корректный код
Вариант рабочий, но костыльный. Правильнее использовать ключ
Если запустить контейнер с флагом
Почему init все починил?
Давай копнём глубже. В Linux процесс с PID 1 (init) имеет нестандартную семантику сигналов:
- Если у PID 1 для сигнала стоит действие «по умолчанию» (никакого обработчика), то сигналы с действием
- PID 1 должен забирать зомби-процессы (делать
- PID 1 обычно пробрасывает сигналы дальше — тому «настоящему» приложению, которое оно запускает.
Когда мы запускаем контейнер без
Наш сценарий с
Для обычного процесса с PID ≠ 1 это приводит к завершению с кодом
Ну а дальше вступают в силу детали реализации рантайма/сишной библиотеки, как именно контейнерный рантайм считывает статус.
На практике это может приводить к тому, что ты видишь
И тут проблема не в docker, а в том, что приложение неожиданно оказалось в роли init-процесса и попало под его особые правила.
Вот и вся наука. Изучай.
🛠 #docker #devops #linux #debug
—
✅ @bashdays ✅ @linuxfactory ✅ @blog
Во время работы с docker контейнером наткнулся на неочевидный код выхода (exit code).
Про exit codes (коды выхода) писал тут
ㅤ
Суть проблемы: Когда программа внутри контейнера падает с
abort(), Docker возвращает неправильный код выхода. Вместо ожидаемого 134 (128 + SIGABRT), контейнер отдаёт 139 (128 + SIGSEGV).То есть контейнер маскирует реальную причину падения приложения. Соответственно дальнейший дебаг не имеет смысла, потому что код выхода не соответствует действительности.
Давай проверим:
#include <cstdlib>
int main() {
std::abort();
return 0;
}
Пишем Dockerfile:
FROM ubuntu:22.04
RUN apt-get update \
&& apt-get -y install \
build-essential \
&& rm -rf /var/lib/apt/lists/*
COPY ./ /src/
WORKDIR /src/
RUN g++ main.cpp -o app
WORKDIR /
CMD ["/src/app"]
Собираем и запускаем:
docker build -f ./Dockerfile -t sigabort_test:latest .
docker run --name test sigabort_test:latest ; echo $?
А на выходе у нас код:
139.В примере выше код выхода — 139 = 128 + 11, где
11 соответствует SIGSEGV (ошибка сегментации), а не 134 = 128 + 6, что был бы SIGABRT (аварийное завершение).Чтобы это пофиксить, нужно захерачить хак:
CMD ["bash", "-c", "/src/app ; exit $(echo $?)"]
docker run --name test sigabort_test:latest ; echo $?
bash: line 1: 6 Aborted /src/app
134
После этого контейнер будет возвращать корректный код
134.Вариант рабочий, но костыльный. Правильнее использовать ключ
--init.Если запустить контейнер с флагом
--init, используя исходную команду CMD ["/src/app"], мы получим ожидаемый 134 код. Что нам и нужно.docker run --init --name test sigabort_test:latest ; echo $?
134
Почему init все починил?
Давай копнём глубже. В Linux процесс с PID 1 (init) имеет нестандартную семантику сигналов:
- Если у PID 1 для сигнала стоит действие «по умолчанию» (никакого обработчика), то сигналы с действием
terminate игнорируются ядром. Это сделано, чтобы случайным SIGTERM/SIGINT нельзя было «уронить» init.- PID 1 должен забирать зомби-процессы (делать
wait() за умершими детьми). Если этого не делать, накопятся зомби.- PID 1 обычно пробрасывает сигналы дальше — тому «настоящему» приложению, которое оно запускает.
Когда мы запускаем контейнер без
--init, приложение становится PID 1.Большинство обычных приложений (на C/C++/Go/Node/Java и т.д.) не написаны как «инит-системы», они не настраивают обработку всех сигналов, не занимаются «реапингом» детей и не пробрасывают сигналы. В результате вылазиют баги.
Наш сценарий с
abort() (который поднимает SIGABRT) упирается именно в правила для PID 1. abort() внутри процесса поднимает SIGABRT.Для обычного процесса с PID ≠ 1 это приводит к завершению с кодом
128 + 6 = 134. Но если процесс — PID 1, ядро игнорирует «терминирующие» сигналы при действии по умолчанию. В результате стандартные ожидания вокруг SIGABRT ломаются.Ну а дальше вступают в силу детали реализации рантайма/сишной библиотеки, как именно контейнерный рантайм считывает статус.
На практике это может приводить к тому, что ты видишь
139 (SIGSEGV) вместо ожидаемого 134 (SIGABRT).И тут проблема не в docker, а в том, что приложение неожиданно оказалось в роли init-процесса и попало под его особые правила.
Вот и вся наука. Изучай.
—
Please open Telegram to view this post
VIEW IN TELEGRAM
5 67