Проверка времени отклика сервисов
Когда сервис работает, но пользователи жалуются на медлительность, нужно мерить не аптайм, а отклик. Это легко сделать обычным curl.
▪️ Самый простой замер
Показывает общее время выполнения запроса и быстро понять, тормозит или нет.
▪️ Точнее: только сетевое время
Полезные метрики:
Пример:
▪️ Проверка нескольких сервисов
▪️ Таймаут обязателен
Без таймаута любой мониторинг бесполезен.
BashTex📱 #bash #utils
Когда сервис работает, но пользователи жалуются на медлительность, нужно мерить не аптайм, а отклик. Это легко сделать обычным curl.
time curl -s https://bashtex.com > /dev/null
Показывает общее время выполнения запроса и быстро понять, тормозит или нет.
curl -s -o /dev/null -w "time_total: %{time_total}\n" https://bashtex.com
Полезные метрики:
time_namelookuptime_connecttime_starttransfertime_totalПример:
curl -w "DNS:%{time_namelookup} CONNECT:%{time_connect} TTFB:%{time_starttransfer} TOTAL:%{time_total}\n" \
-o /dev/null -s https://bashtex.com
for url in https://a.ru https://b.ru; do
curl -o /dev/null -s -w "$url %{time_total}\n" "$url"
done
curl --connect-timeout 3 --max-time 5 https://bashtex.com
Без таймаута любой мониторинг бесполезен.
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
PIPESTATUS: как узнать, какая команда в пайпе реально упалаВ Bash есть неприятная особенность: после пайпа
$? показывает статус только последней команды.Например:
false | grep test
echo $?
Здесь
grep успешно отработал и вернул 0, поэтому ошибка false потерялась.PIPESTATUSСразу после выполнения пайпа:
false | grep test
echo "${PIPESTATUS[@]}"
Получим:
1 1
Каждое значение соответствует своей команде.
false → 1
grep test → 1
cat /missing/file | grep ERROR | sort
echo "${PIPESTATUS[@]}"
Можно получить:
1 1 0
То есть
cat завершился с ошибкой, grep ничего не получил, а sort технически выполнился успешно.PIPESTATUS очень легко потерять:false | true
echo "${PIPESTATUS[@]}"
работает.
Но если между пайпом и проверкой выполнить другую команду:
false | true
echo "check"
echo "${PIPESTATUS[@]}"
теперь
PIPESTATUS относится уже к echo "check".Поэтому сохраняйте его сразу:
false | true
status=("${PIPESTATUS[@]}")
echo "${status[@]}"
Есть:
set -o pipefail
Теперь:
false | true
echo $?
вернёт ненулевой код.
Но
pipefail отвечает только на вопрос «пайп завершился успешно?», а PIPESTATUS позволяет понять «какая именно команда вернула ошибку?».В сложных Bash-конвейерах последняя команда может успешно завершиться даже тогда, когда предыдущая уже упала.
PIPESTATUS позволяет не терять эти ошибки и точно определить проблемный этап.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👨💻5
/proc/PID/fd: как узнать, что на самом деле держит процессps показывает процесс, lsof умеет искать открытые файлы. Но иногда быстрее посмотреть непосредственно в /proc.У каждого процесса есть каталог:
ls -l /proc/1234/fd
Внутри находятся символические ссылки на все открытые файловые дескрипторы процесса.
Например:
0 -> /dev/null
1 -> /var/log/app.log
2 -> /var/log/app.err
5 -> /tmp/data.db
0, 1 и 2 — стандартные stdin, stdout и stderr. Остальные дескрипторы приложение открыло самостоятельно.Особенно полезен такой поиск:
ls -l /proc/1234/fd | grep deleted
Можно обнаружить:
7 -> /var/log/app.log (deleted)
Файл уже отсутствует в каталоге, но процесс всё ещё держит его открытым.
readlink /proc/1234/fd/7
Результат может быть:
socket:[48192]
или:
pipe:[72831]
или:
/run/app.sock
То есть через один каталог можно увидеть не только обычные файлы, но и сокеты, pipes и другие объекты ядра.
for fd in /proc/1234/fd/*; do
printf '%s -> %s\n' "${fd##*/}" "$(readlink "$fd")"
done
Получается компактная карта того, с какими объектами взаимодействует процесс.
Если сервис ведёт себя странно, полезно посмотреть не только его аргументы и память, но и то, что он реально держит открытым.
/proc/PID/fd помогает быстро найти удалённые логи, неожиданные файлы, UNIX-сокеты и каналы IPC - причём без отдельного инструмента анализа.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10🔥1
/proc/PID/environ: какие переменные реально получил процессenv показывает окружение текущего shell. Но для диагностики сервиса этого недостаточно: после запуска процесс мог получить совсем другой набор переменных.Linux хранит окружение каждого процесса здесь:
cat /proc/1234/environ
Значения разделены не переносами строк, а нулевыми байтами, поэтому удобнее:
tr '\0' '\n' < /proc/1234/environ
tr '\0' '\n' < /proc/1234/environ | grep '^PATH='
Или проверить настройки прокси:
tr '\0' '\n' < /proc/1234/environ |
grep -Ei 'proxy|http_proxy|https_proxy'
Например, приложение запущено вручную и через systemd:
tr '\0' '\n' < /proc/1234/environ | sort > /tmp/app.env
tr '\0' '\n' < /proc/5678/environ | sort > /tmp/service.env
diff -u /tmp/app.env /tmp/service.env
Так можно быстро найти переменную, из-за которой одна версия работает, а другая нет.
env может вводить в заблуждениеВы выполняете:
echo "$PATH"
и видите одно значение.
Но процесс, запущенный несколько часов назад, мог получить старый
PATH. Изменение конфигурации shell не меняет окружение уже работающего процесса.То же касается:
HTTP_PROXY
LD_LIBRARY_PATH
LANG
HOME
JAVA_HOME
Чтение
/proc/PID/environ требует соответствующих прав доступа. Для чужих процессов доступ может быть ограничен настройками безопасности системы.И главное: вывод нельзя бездумно отправлять в логи. В окружении могут находиться токены, пароли и другие секреты.
Когда сервис работает “вручную”, но ломается под systemd, cron или другим менеджером процессов, часто проверяют конфигурационные файлы, но забывают про окружение.
/proc/PID/environ показывает именно то, что получил уже запущенный процесс - а не то, что, как кажется, должно было быть передано ему.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥6👍1
setsid: как действительно отделить процесс от SSH-сессииnohup часто используют, чтобы процесс пережил закрытие терминала:nohup ./backup.sh &Но
nohup в основном защищает от SIGHUP. Сам процесс всё ещё может оставаться связанным с текущей session.Для полного создания новой сессии существует
setsid.После подключения по SSH можно посмотреть:
ps -o pid,ppid,sid,tty,cmd
У процессов будут общие
SID и управляющий терминал.При закрытии SSH это может иметь значение для поведения процессов.
setsidsetsid ./backup.sh
Процесс становится лидером новой session и получает новый SID.
Проверить:
ps -o pid,ppid,sid,tty,cmd -C backup.sh
Можно увидеть, что SID процесса больше не связан с вашей SSH-сессией.
& недостаточно./backup.sh &
Фоновый процесс всё равно является частью текущей shell-сессии.
& лишь не блокирует терминал ожиданием процесса.nohup?nohup ./backup.sh &
Это уже лучше для сценария с закрытием терминала, но
nohup и setsid решают разные задачи.nohup изменяет обработку SIGHUP и обычно перенаправляет стандартный вывод.setsid создаёт новую session и отделяет процесс от текущей session.Для простого запуска полностью отдельно:
setsid ./backup.sh >/tmp/backup.log 2>&1 < /dev/null &
Теперь процесс не зависит от стандартного ввода текущего терминала и находится в отдельной session.
• запуск долгих задач из SSH;
• анализ поведения daemon-like процессов;
• shell-обвязки для сервисов;
• понимание
PID, PPID, SID и TTY.&, nohup и setsid часто воспринимают как взаимозаменяемые способы “запустить в фоне”. На самом деле они меняют разные свойства процесса.Если нужно понять, почему программа продолжает зависеть от SSH или терминала, смотреть стоит не только на
PID, но и на SID, PPID и управляющий TTY.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8🔥2
/proc/PID/cwd: когда процесс работает не из того каталогаУ каждого Linux-процесса есть текущий рабочий каталог. Обычно мы видим его через shell, но для уже запущенного процесса можно посмотреть напрямую через
/proc.readlink /proc/1234/cwd
Например:
/var/lib/app
Это каталог, относительно которого процесс разрешает относительные пути.
cwdfor p in /proc/[0-9]*; do
pid=${p##*/}
cwd=$(readlink "$p/cwd" 2>/dev/null) || continue
printf '%-8s %s\n' "$pid" "$cwd"
done
Теперь можно быстро увидеть, какие процессы находятся в
/tmp, /dev/shm, домашних каталогах или других необычных местах.Приложение может запускаться из:
/opt/myapp
но после работы оказаться в другом каталоге из-за
chdir().Особенно интересны процессы с:
/tmp
/dev/shm
/home/user
если для конкретного сервиса такое поведение не ожидается.
Для unit можно задать:
[Service]
WorkingDirectory=/var/lib/myapp
А фактическое значение уже запущенного процесса проверить через
/proc.Это полезнее, чем тупо смотреть конфигурацию:
/proc/PID/cwd показывает состояние конкретного процесса прямо сейчас.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
return против exit: как не завершить весь скрипт внутри функцииВ Bash функция может вернуть управление вызывающему коду или полностью завершить текущий shell. Эти два случая часто путают.
exit завершает весь скриптcheck_config() {
if [ ! -f config.conf ]; then
echo "Config not found"
exit 1
fi
}
check_config
echo "Продолжаем работу"Если
config.conf нет, выполнение закончится прямо внутри функции. Строка Продолжаем работу никогда не выполнится.return завершает только функциюcheck_config() {
if [ ! -f config.conf ]; then
echo "Config not found"
return 1
fi
}
check_config
echo "Продолжаем работу"Теперь функция завершилась с кодом
1, но сам скрипт продолжил выполнение.Именно поэтому
return обычно используют внутри функций, а exit - на уровне самого скрипта.if check_config; then
echo "Config OK"
else
echo "Config error"
fi
Или сохранить результат:
check_config
status=$?
if [ "$status" -ne 0 ]; then
echo "Ошибка: $status"
fi
return можно использовать только внутри функции или в контексте, где Bash допускает возврат из текущего sourced-файла.Например:
return 1
в обычном запускаемом скрипте верхнего уровня приведёт к ошибке:
return: can only `return' from a function
deploy() {
check_config || exit 1
start_service
}Если
deploy вызывается из другого скрипта как функция, exit завершит уже весь shell.Чаще правильнее:
deploy() {
check_config || return 1
start_service
}А решение о завершении оставить вызывающему коду:
deploy || exit 1
Так функция сообщает об ошибке, а не решает за весь скрипт, что делать дальше.
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
$? после &&, || и if: почему код возврата Bash бывает неочевиднымВ Bash
$? содержит код завершения последней выполненной команды. Но конструкции &&, || и if меняют то, какие команды вообще выполняются.false && echo "OK"
echo $?
echo после && не запускается, потому что false вернула 1.Итоговый
$? - 1.А здесь:
true && echo "OK"
echo $?
последней командой становится
echo "OK", поэтому $? уже равен 0.|| ситуация аналогичнаяfalse || echo "fallback"
echo $?
false запускается, затем выполняется echo, потому что первая команда завершилась ошибкой.В итоге
$? будет 0 - код echo, а не false.Если нужен исходный результат:
false
status=$?
if [ "$status" -ne 0 ]; then
echo "Command failed: $status"
fi
if не создаёт обычный 1if false; then
echo "success"
fi
echo $?
Здесь
if возвращает статус последней выполненной команды в выбранной ветке. Если ни одна ветка не выполнялась, результатом становится код проверки условия.command || echo "failed"
status=$?
status здесь содержит результат echo, а не command.Если важно сохранить код ошибки:
if ! command; then
status=$?
fi
Но здесь есть ещё один нюанс:
! инвертирует статус, поэтому для получения исходного кода лучше сохранять его непосредственно после команды.Например:
command
status=$?
if [ "$status" -ne 0 ]; then
echo "failed: $status"
fi
В сложных Bash-скриптах
$? легко прочитать слишком поздно. Любая следующая команда - даже echo, printf или test - может заменить предыдущий код возврата.Поэтому если результат команды важен, сохраняйте его сразу.
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8
lastpipe: как Bash меняет поведение последней команды в pipelineОдна из особенностей Bash - команды в pipeline обычно выполняются в отдельных subshell. Из-за этого переменные, изменённые внутри
while, после цикла могут не сохраниться.Например:
count=0
printf '%s\n' a b c |
while read -r line; do
((count++))
done
echo "$count"
В некоторых случаях результатом будет
0.Но у Bash есть настройка
lastpipe, которая меняет это поведение.lastpipeshopt -s lastpipe
Теперь последняя команда pipeline может выполняться в текущем shell.
count=0
printf '%s\n' a b c |
while read -r line; do
((count++))
done
echo "$count"
При подходящих условиях получим:
3lastpipe работает только когда job control отключён:set +m
shopt -s lastpipe
Именно поэтому поведение может отличаться между интерактивным Bash и обычным скриптом.
shopt lastpipe
Получим:
lastpipe off
или:
lastpipe on
Вместо изменения поведения shell часто проще использовать редирект:
while read -r line; do
((count++))
done < <(printf '%s\n' a b c)
Так код не зависит от настройки
lastpipe.lastpipe действительно интересенЭто полезно знать при разборе чужих Bash-скриптов: одна и та же конструкция с pipeline может вести себя по-разному в зависимости от настроек shell и режима job control.
lastpipe - хороший пример того, как небольшая опция Bash меняет фундаментальное поведение pipeline и область видимости изменений.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
nullglob: когда *.log внезапно превращается в строкуBash умеет раскрывать glob-шаблоны:
*.log
Но если файлов с таким расширением нет, по умолчанию шаблон может остаться обычной строкой.
Например:
for file in /var/log/*.log; do
echo "$file"
done
Если
.log файлов нет, цикл может получить буквально:/var/log/*.log
Это легко превращается в ошибку в автоматизации.
nullglobshopt -s nullglob
Теперь шаблон без совпадений исчезает:
files=(/var/log/*.log)
echo "${#files[@]}"
Если файлов нет:
0А не
1 с буквальным *.log.Без
nullglob:files=(/tmp/*.tmp)
может создать массив из одного элемента:
/tmp/*.tmp
После:
shopt -s nullglob
получаем действительно пустой массив.
nullglob влияет только на текущий shellЕго можно включить локально внутри функции:
check_logs() {
local old_nullglob
shopt -q nullglob
old_nullglob=$?
shopt -s nullglob
files=(/var/log/*.log)
((${#files[@]})) && printf '%s\n' "${files[@]}"
((old_nullglob == 0)) || shopt -u nullglob
}Но для простых скриптов обычно достаточно включить настройку в начале.
Тогда существует противоположная настройка:
shopt -s failglob
При отсутствии совпадений Bash выдаст ошибку вместо передачи буквального шаблона дальше.
Glob кажется простой частью Bash, пока скрипт не запускается на сервере, где ожидаемых файлов просто нет.
nullglob позволяет отличить настоящий список файлов от строки с нераскрытым шаблоном - особенно полезно при работе с массивами, логами и автоматической обработкой каталогов.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4🔥2
noclobber: как запретить Bash случайно перезаписать файлОбычный оператор:
echo "new config" > config.txtбез предупреждения удалит старое содержимое
config.txt.Для скриптов, где случайная перезапись критичного файла недопустима, в Bash есть режим
noclobber.set -o noclobber
Теперь:
echo "new config" > config.txt
если файл уже существует, завершится ошибкой:
bash: config.txt: cannot overwrite existing file
set -o | grep noclobber
Или короткая форма:
set -C
Выключить обратно:
set +C
>> продолжает работатьnoclobber защищает именно от перезаписи через >:echo "new line" >> config.txt
добавит данные в конец файла.
Есть специальная конструкция:
echo "new config" >| config.txt
Оператор
>| игнорирует noclobber и разрешает перезапись.Это удобно, когда защита включена глобально, но конкретный участок скрипта действительно должен заменить файл.
Например, скрипт генерирует конфигурацию:
set -Cgenerate_config > /etc/myapp/config.conf
Если файл уже существует, операция остановится вместо того, чтобы молча уничтожить текущую конфигурацию.
noclobber не является полноценной защитой от всех способов изменения файла. Это именно настройка shell для оператора >.Её задача проще: превратить потенциально опасную случайную перезапись в явную ошибку.
В Bash
> выглядит безобидно, но фактически означает «открыть файл на запись и обнулить его». noclobber позволяет добавить дополнительный барьер там, где потеря существующего содержимого недопустима.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
mapfile: как загружать вывод команды в массив без while readЕсли результат команды нужно сохранить построчно, часто используют:
items=()
while IFS= read -r line; do
items+=("$line")
done < <(some_command)
В Bash для этого есть встроенный
mapfile.mapfile -t files < <(find /tmp -type f)
Теперь каждая строка - отдельный элемент массива:
printf '%s\n' "${files[@]}"-t убирает символ переноса строки из каждого элемента.Нет отдельного
while, нет проблемы с subshell и не нужно вручную добавлять элементы:mapfile -t users < <(cut -d: -f1 /etc/passwd)
echo "Users: ${#users[@]}"
Можно обращаться к конкретному элементу:
echo "${users[0]}"mapfile -t lines < config.txt
А затем:
for line in "${lines[@]}"; do
printf 'CONFIG: %s\n' "$line"
doneНапример:
mapfile -t -n 10 lines < access.log
В массив попадут только первые 10 строк.
mapfile ориентирован именно на разделённые строки данные. Если содержимое может содержать \n внутри логического элемента, обычный построчный массив уже не подходит.Для других разделителей можно использовать
-d:mapfile -d ',' -t items < data.txt
Теперь разделителем считается запятая.
Например, получить список systemd-сервисов:
mapfile -t services < <(
systemctl list-units --type=service --state=running --no-legend |
awk '{print $1}'
)
После этого массив можно безопасно передавать дальше:
for service in "${services[@]}"; do
systemctl is-active "$service"
donemapfile - встроенный механизм Bash для преобразования потоковых данных в массив. Он делает код короче и избавляет от целого класса проблем, связанных с while read, пайпами и сохранением переменных.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
BASH_SOURCE и FUNCNAME: как понять, откуда реально вызвали функциюВ больших Bash-скриптах функция может вызываться из другой функции, которая пришла из отдельного файла. Обычный
$0 в такой ситуации мало что объясняет.Для этого у Bash есть специальные массивы:
BASH_SOURCE
FUNCNAME
foo() {
echo "file: ${BASH_SOURCE[0]}"
echo "caller: ${BASH_SOURCE[1]}"
echo "function: ${FUNCNAME[0]}"
}
fooBASH_SOURCE[0] показывает файл, в котором выполняется текущая функция, а BASH_SOURCE[1] - файл, откуда она была вызвана.foo() {
printf '%s\n' "${FUNCNAME[@]}"
}
bar() {
foo
}
barМожно получить примерно:
foo
bar
main
То есть Bash хранит стек функций.
Например:
log() {
printf '[%s] %s: %s\n' \
"$(date '+%F %T')" \
"${FUNCNAME[1]}" \
"$*"
}Теперь:
deploy() {
log "starting deployment"
}
deployможет вывести:
[2026-08-24 12:30:01] deploy: starting deployment
Лог сразу показывает, какая функция создала сообщение.
Допустим:
main.sh
lib.sh
utils.sh
и функции вызывают друг друга через
source.Тогда:
printf '%s\n' "${BASH_SOURCE[@]}"покажет цепочку файлов, через которые прошёл вызов.
$0echo "$0"
обычно показывает имя запущенного скрипта или shell.
А:
echo "${BASH_SOURCE[0]}"показывает конкретный Bash-файл, где находится текущий код.
Это особенно заметно при использовании:
source lib.sh
При отладке больших shell-проектов сообщение вроде:
ERROR: failedпочти бесполезно.
А:
ERROR: deploy() in lib/deploy.shуже позволяет быстро найти источник проблемы.
BASH_SOURCE и FUNCNAME превращают Bash-функции из непрозрачного набора вызовов в трассируемый стек.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
/proc/PID/wchan: где именно завис процессЕсли процесс показывает высокий
D в ps, сам статус ещё не говорит, на чём именно он остановился.Для этого Linux предоставляет
/proc/<PID>/wchan.ps -eo pid,stat,wchan:30,cmd | grep '[D]'
Например:
2418 D io_schedule backup
Здесь
wchan показывает kernel wait channel - функцию ядра, в которой процесс ожидает продолжения.cat /proc/2418/wchan
Можно получить:
io_schedule
Или:
nfs_wait_bit_killable
Во втором случае уже появляется конкретная зацепка: процесс может ждать операции, связанной с NFS.
for p in /proc/[0-9]*; do
pid=${p##*/}
state=$(awk '{print $3}' "$p/stat" 2>/dev/null)
[ "$state" = D ] || continue
printf '%-8s %s\n' \
"$pid" \
"$(cat "$p/wchan" 2>/dev/null)"
done
Получится примерно:
2418 io_schedule
2421 io_schedule
2517 nfs_wait_bit_killable
Если десятки процессов одновременно находятся в одном
wchan, это уже может указывать на общий узкий участок.Для более глубокого разбора:
cat /proc/2418/stack
Можно увидеть цепочку вызовов ядра, в которой находится поток.
Например:
io_schedule
schedule
...
Доступ к
/proc/PID/stack зависит от прав и настроек безопасности.kill -9 иногда не помогаетПроцесс в состоянии
D находится в uninterruptible sleep. Если он ждёт завершения определённой операции ядра, SIGKILL не может мгновенно вывести его из этого состояния.Поэтому вместо бесконечного:
kill -9 2418
полезнее сначала посмотреть:
ps -o pid,stat,wchan,cmd -p 2418
cat /proc/2418/wchan
cat /proc/2418/stack
Так можно понять, почему процесс не реагирует.
ps показывает симптом - D. wchan помогает увидеть причину ожидания на уровне ядра.Для проблем с дисками, NFS, storage и зависшими I/O-операциями это один из самых быстрых способов перейти от «процесс завис» к пониманию, где именно он застрял.
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
read -d: как читать Bash-поток не по строкамread обычно ждёт перевод строки:while IFS= read -r line; do
printf '%s\n' "$line"
done < file.txt
Но у
read можно изменить разделитель. Это особенно полезно для данных, где обычный \n не является надёжной границей записи.Например,
find умеет выдавать имена файлов через \0:find /tmp -type f -print0 |
while IFS= read -r -d '' file; do
printf '%s\n' "$file"
done
-d '' означает: читать до NUL-символа.Это позволяет корректно обработать имя вроде:
report
2026.txt
где внутри имени действительно может быть перевод строки.
read здесь опасенТакой вариант:
find /tmp -type f -print0 |
while read -r file; do
...
done
не соответствует формату данных:
find разделяет записи \0, а read по умолчанию ищет \n.Например, прочитать значения через
::
while IFS= read -r -d ':' value; do
printf '<%s>\n' "$value"
done <<< 'one:two:three:'
Получим:
<one>
<two>
<three>
read умеет ещё и ограничивать количество символовIFS= read -r -n 1 char
Теперь Bash прочитает только один символ.
А:
IFS= read -r -N 8 chunk
будет ждать ровно 8 символов, не считая разделитель строки.
read -d особенно хорошо сочетается с командами, поддерживающими NUL-разделители:find ... -print0
git ... -z
sort -z
xargs -0
Получается безопасная цепочка, в которой пробелы, кавычки и даже переводы строк внутри имён файлов не ломают обработку.
Разделитель данных должен совпадать с тем, как эти данные реально генерируются.
read - это не только «прочитать строку»: с -d, -n и -N он может работать с потоками гораздо точнее.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
local с подстановкой команды: почему local result=$(cmd) скрывает ошибкуВ Bash есть тонкая ловушка внутри функций. На первый взгляд эти два варианта выглядят одинаково:
result=$(false)
echo "$?"
Здесь
$? будет 1.Но если написать:
local result=$(false)
echo "$?"
получим:
0
В конструкции:
local result=$(cmd)
выполняются сразу две вещи:
1. Bash выполняет
$(cmd) и получает результат.2.
local объявляет переменную.Проблема в том, что код возврата всей команды определяется
local, а не самой command substitution.local успешно создал переменную - поэтому функция видит 0.Например:
get_data() {
local result=$(curl -fsS https://example.com/api)
echo "status=$?"
}Если
curl завершится с ошибкой, проверка $? после local может показать успешное выполнение.Ошибка потеряна.
Разделите объявление и присваивание:
get_data() {
local result
result=$(curl -fsS https://example.com/api)
local status=$?
echo "status=$status"
}Теперь
$? относится непосредственно к curl.get_data() {
local result
if result=$(curl -fsS https://example.com/api); then
printf '%s\n' "$result"
else
echo "command failed"
return 1
fi
}Здесь условие
if проверяет именно exit code command substitution.localПохожая ситуация возникает с некоторыми встроенными командами, когда результат присваивания объединяется с другой операцией.
Поэтому правило простое:
local result
result=$(cmd)
а не:
local result=$(cmd)
В обычном скрипте такая мелочь может остаться незаметной. В функции, которая используется в цепочке:
deploy || exit 1
потерянный exit code уже превращается в реальную логическую ошибку: функция может сообщить об успехе, хотя команда внутри неё завершилась с ошибкой.
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
RuntimeMaxSec: как systemd убивает сервис, который работает слишком долгоИногда сервис не падает и не зависает в классическом смысле - он просто продолжает работать часами, хотя по логике приложения должен завершиться.
В systemd для этого есть
RuntimeMaxSec=.В unit-файле:
[Service]
ExecStart=/usr/local/bin/job.sh
RuntimeMaxSec=30min
После 30 минут с момента запуска systemd остановит сервис.
Для более точного ограничения:
RuntimeMaxSec=1h 30min
Это не просто
kill -9.По умолчанию systemd инициирует обычную остановку сервиса. Если процесс не завершится в течение
TimeoutStopSec=, systemd применит настроенное принудительное завершение.Например:
[Service]
RuntimeMaxSec=30min
TimeoutStopSec=20s
Получается цепочка:
30 мин → остановка
↓
20 сек на graceful shutdown
↓
принудительное завершение
После запуска:
systemctl show myjob.service \
-p RuntimeMaxUSec \
-p TimeoutStopUSec
А текущий статус:
systemctl status myjob.service
RuntimeMaxSec не запрещает сервису запускаться снова.Если unit настроен так:
[Service]
Restart=on-failure
RuntimeMaxSec=30min
важен результат завершения и дополнительные параметры restart policy.
Сервис может быть запущен systemd снова после остановки.
Поэтому
RuntimeMaxSec стоит рассматривать вместе с Restart=, а не как самостоятельный механизм watchdog.Допустим, есть batch-задача:
[Service]
Type=oneshot
ExecStart=/opt/scripts/import.sh
RuntimeMaxSec=2h
Если импорт по какой-то причине зависнет на бесконечной операции, systemd не оставит процесс работать сутки.
RuntimeMaxSec - это лимит общего времени работы, а не проверка того, отвечает ли приложение.Если сервис должен завершаться за 5 минут, но иногда работает 10 минут по штатной причине, такой лимит будет ошибкой конфигурации.
Для контроля «жив ли сервис и отвечает ли он» нужны другие механизмы.
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
tee + process substitution: один поток и несколько получателей
Иногда нужно одновременно: увидеть вывод команды в терминале, записать его в файл и передать на обработку другой программе Если делать это по очереди - потеряется поток данных. Здесь помогает связка tee + process substitution.
▪️ Что делает tee. tee дублирует поток:
command | tee file.log
вывод остается в терминале и одновременно пишется в
▪️ Несколько получателей. tee может писать сразу в несколько файлов:
command | tee out1.log out2.log
Но иногда нужно не просто файл, а другую команду.
▪️ Process substitution. Bash позволяет подставить вывод команды как файл:
>(command)
Это называется process substitution.
▪️ Комбинируем
command | tee >(grep ERROR > errors.log)
Одна копия идет в терминал, а другая в grep. Если найден ERROR, запись попадет в errors.log.
▪️ Более реальный пример. Допустим, идет сбор логов:
journalctl -f | tee >(grep ERROR >> errors.log)
Теперь: полный поток остается в терминале, ошибки автоматически сохраняются
▪️ Можно делать несколько обработчиков
journalctl -f | tee \
>(grep ERROR >> errors.log) \
>(grep WARN >> warn.log)
Один поток и сразу несколько фильтров.
BashTex📱 #bash #utils
Иногда нужно одновременно: увидеть вывод команды в терминале, записать его в файл и передать на обработку другой программе Если делать это по очереди - потеряется поток данных. Здесь помогает связка tee + process substitution.
command | tee file.log
вывод остается в терминале и одновременно пишется в
file.logcommand | tee out1.log out2.log
Но иногда нужно не просто файл, а другую команду.
>(command)
Это называется process substitution.
command | tee >(grep ERROR > errors.log)
command генерирует потокtee дублирует егоОдна копия идет в терминал, а другая в grep. Если найден ERROR, запись попадет в errors.log.
journalctl -f | tee >(grep ERROR >> errors.log)
Теперь: полный поток остается в терминале, ошибки автоматически сохраняются
journalctl -f | tee \
>(grep ERROR >> errors.log) \
>(grep WARN >> warn.log)
Один поток и сразу несколько фильтров.
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥6👍1
KillMode=: почему systemctl stop иногда оставляет процессыОстановить сервис через:
systemctl stop myapp.service
не всегда означает, что исчезнет только один процесс. У systemd есть отдельная настройка, определяющая, какие процессы будут остановлены вместе с unit.
Это
KillMode=.[Service]
ExecStart=/opt/myapp/start.sh
KillMode=control-group
По умолчанию systemd работает с control group сервиса. При остановке он может завершить все процессы внутри cgroup, а не только основной PID.
Проверить:
systemctl show myapp.service -p KillMode
process[Service]
KillMode=process
В этом режиме сигнал отправляется только основному процессу сервиса.
Если start.sh породил:
myapp
├─ worker1
└─ worker2
дочерние процессы могут продолжить работу после остановки unit.
control-group[Service]
KillMode=control-group
systemd отслеживает cgroup целиком:
myapp.service
├─ PID 1200
├─ PID 1201
└─ PID 1202
При остановке unit процессы внутри группы также попадут под завершение.
systemctl status myapp.service
или:
systemctl show myapp.service -p ControlGroup
Затем:
systemd-cgls
Можно увидеть дерево процессов по cgroup.
KillMode=mixed
При остановке
SIGTERM получает основной процесс, а при последующем принудительном завершении systemd применяет его ко всей control group.Это позволяет дать главному процессу шанс корректно завершиться, не оставляя дочерние процессы навсегда.
Особенно легко получить «призраков» при запуске приложения через shell-обёртку:
ExecStart=/bin/bash /opt/start.sh
Если скрипт запускает фоновые процессы, нужно понимать, в какой cgroup они окажутся и как
KillMode повлияет на их завершение.systemctl stop - это не просто отправка SIGTERM одному PID. В systemd процесс существует внутри cgroup, и политика KillMode определяет масштаб остановки.BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6