Команды Linux

104 команд, которые реально набирают; каждый ключ разобран — по задачам

📁 Файлы и каталоги (8)

Поиск, пакетная обработка, архивы — ошибка в параметре здесь обычно означает удалён не тот файл или пропущены скрытые каталоги

ls: права, время и размеры

-l длинный формат: права, владелец, размер, время
-a включить . и .. и все скрытые файлы
-h размеры с единицами (1.2K вместо 1234), только вместе с -l
-t сортировка по времени изменения, новые сверху
-r обратный порядок: с -t самые старые сверху
-S сортировка по размеру, крупные сверху — быстрее найти «кто занял место», чем с -t
--time-style=long-iso время в виде 2026-10-29 09:18 — читается лучше дефолта

# самый свежий файл сверху, с размером и временем
ls -laht --time-style=long-iso

# найти самые тяжёлые по размеру (-S)
ls -lhS /var/log | head

find: поиск файлов по условиям и действия

-name совпадение по имени, с учётом регистра; -iname без учёта
-type f только обычные файлы, -type d только каталоги, -type l симлинки
-mtime +7 изменён больше 7 дней назад; -mmin -30 изменён за последние 30 минут
-size +50M больше 50M; -size -1k меньше 1k
-maxdepth 1 без рекурсии в подкаталоги — при разборе добавлять первым, ускоряет на порядок
-ls выполняет ls -dils по результату: размер, inode и время сразу
-delete удаляет сразу, только пустые каталоги; -exec ... \; команда на каждый результат
-prune пропустить каталог, обязателен вместе с -path
-o это «или» при связке условий; -print явный вывод (обязателен с -prune)

# крупные файлы, изменённые за 30 минут (поиск причины роста диска)
find /var -type f -mmin -30 -size +50M -ls

# массово удалить логи старше 7 дней
find /var/log -name '*.log.*' -mtime +7 -delete

# пропустить .git и node_modules и искать
find . -path ./node_modules -prune -o -name '*.js' -print

grep: рекурсивный поиск с контекстом

-r рекурсия по каталогам; -R рекурсия с переходом по симлинкам
-i без учёта регистра; -n номера строк; -v инверсия, только несовпавшие строки
-E расширенное регулярное выражение (| обязательно через -E, иначе это литерал)
-l только имена файлов — удобно сначала локализовать, потом читать
-A3 / -B3 / -C3 показать 3 строки после / до / вокруг совпадения
--include='*.conf' только по указанному суффиксу; --exclude-dir пропустить каталог
-w совпадение по целому слову, чтобы cat не находил category
-rn самая частая связка: рекурсия плюс номера строк, иначе неясно, куда смотреть

# найти upstream в конфигах nginx, с 3 строками до и после
grep -rn -C3 'upstream' /etc/nginx --include='*.conf'

# любое совпадение из нескольких ключевых слов (обязательно -E)
grep -rnE 'timeout|refused|reset' /var/log/app/error.log

# вывести только имена файлов с конфигом, потом смотреть по одному
grep -rli 'proxy_pass' /etc/nginx

tar: упаковка и распаковка

-c создать; -x распаковать; -t только список; -z через gzip; -j через bzip2
-f принимает имя файла, поэтому -f всегда последним
-v печатать ход работы; на больших архивах сильно тормозит — перенаправлять
-C распаковать в указанный каталог, он должен существовать
--exclude исключить при упаковке; относительные пути должны совпадать
-czf / -xzf / -tzf три ходовые связки, этих достаточно
перед распаковкой прогнать -t: tar-бомба иначе зальёт текущий каталог

# упаковать, исключив каталоги кэша
tar -czf app.tar.gz --exclude='node_modules' --exclude='.git' -C /srv app

# сначала посмотреть содержимое, затем распаковать в указанный каталог
tar -tzf app.tar.gz | head -20
tar -xzf app.tar.gz -C /tmp/restore

cp / mv / rm: три опасных параметра

rm -rf рекурсивное удаление без подтверждения — при пустой переменной rm -rf "$DIR/" станет rm -rf /
-f без запросов; -r рекурсия по каталогам; вместе это самая опасная связка
cp -a режим архива: права, время, симлинки — первый выбор для бэкапа
cp -n не перезаписывать существующие; mv -n так же (-n это no-clobber)
mv между ФС это «копирование + удаление», не атомарно: обрыв оставит обе стороны неполными
файлы, удалённые при открытом процессе, занимают место до его завершения (см. разбор диска)
test -n "$DIR" проверяет, что переменная не пуста — обязательная защита перед rm
-- прекращает разбор опций, чтобы имя файла с - в начале не стало параметром

# безопасное удаление: сначала убедиться, что переменная не пуста, потом удалять
[ -n "$DIR" ] && rm -rf -- "$DIR"

# полный бэкап с сохранением атрибутов
cp -a /srv/app /backup/app-$(date +%F)

du / df: кто съел диск

df -h занятость точек монтирования; -i занятость inode (при переполнении inode запись тоже невозможна)
du -sh суммарный размер каталога; -s только итог без подкаталогов, -h человекочитаемо
du -sh * | sort -h кто больше в текущем каталоге; sort -h правильно сортирует 1K/2M/3G
--max-depth=1 только один уровень, заметно быстрее полной рекурсии
большая разница между du и df почти всегда означает удалённые, но открытые файлы
2>/dev/null убирает ошибки доступа, иначе ими забит весь вывод

# идти от корня вниз по уровням: где резко выросло — заходить внутрь
du -h --max-depth=1 / 2>/dev/null | sort -h | tail -10

# удалённые, но занятые процессом файлы (когда df полон, а du мал)
lsof +L1 2>/dev/null | head

ln: символические и жёсткие ссылки

ln -s ЦЕЛЬ ИМЯ — цель идёт первой; в обратном порядке получится битая ссылка
-s симлинк; без -s жёсткая ссылка (только в одной ФС, не на каталог)
-f перезаписать существующую ссылку
-n если ссылка указывает на каталог: только с -f заменит её, а не положит внутрь
-sfn стандартная связка для переключения релиза (current на новый release)
симлинк хранит путь и ломается при переносе цели; жёсткая ссылка делит inode, удаление одной не влияет на другую

ln -sfn /srv/app/releases/20261029 /srv/app/current

stat / file: что это за файл на самом деле

stat сразу даёт размер, inode, права и три времени — прямой ответ на «файл правда менялся»
-c свой формат: %y время изменения, %s байты, %U:%G владелец, %a восьмеричные права, %n имя
-L идти по симлинку и описывать цель (без него описывается сама ссылка)
три времени: Access чтение, Modify изменение содержимого, Change изменение метаданных (права/владелец)
-f данные уровня ФС (размер блока, всего и свободно inode), дополняет df
touch -d '2026-10-01 08:00' ФАЙЛ меняет mtime; вместе с find -newer очерчивает диапазон изменений
file показывает настоящий тип, надёжнее расширения — его можно поставить любое
stat -c '%a' самый быстрый способ увидеть фактические биты прав

stat -c '%y %s %U:%G %a %n' /etc/nginx/nginx.conf
stat -L -c '%y %s' /srv/app/current        # посмотреть реальный файл, на который указывает симлинк
file /usr/local/bin/app

✂️ Текст и конвейеры (8)

Правка конфигов, статистика по колонкам, сборка командных строк — «руками 200 строк конфига» отдавать sed/awk

sed: правка содержимого файла, в том числе на месте

-i правит файл на месте — на проде сначала прогнать без -i и посмотреть вывод
-i.bak оставляет копию .bak при правке, откат мгновенный (стоит взять в привычку)
-n без автопечати, с p выводит только заданные строки — то есть «посмотреть несколько строк»
-E расширенное регулярное выражение (+ ? | ( ) без экранирования); в старых системах -r, смысл тот же
-e сцепляет несколько выражений; можно и через точку с запятой в одном
g в s/старое/новое/g заменяет все вхождения в строке; без g только первое
адреса-диапазоны: '/^server/,/}/ s/a/b/' правит только внутри одного блока конфига
\1 \2 ссылки на группы захвата, & обозначает всё совпадение
порядок важен: sed -E -i.bak 's/x/y/' ФАЙЛ

sed -n '1,20p' /etc/nginx/nginx.conf              # посмотреть только первые 20 строк
sed -i.bak 's/listen 80;/listen 8080;/' /etc/nginx/sites-enabled/app
sed -E -i.bak 's/^server_name[[:space:]]+([^;]+);/server_name \1.bak;/' app.conf
sed '/^[[:space:]]*#/d;/^[[:space:]]*$/d' app.conf | head        # убрать комментарии и пустые строки

awk: колонки, фильтры и суммы

-F задаёт разделитель: -F: по двоеточию, -F'\t' по табуляции, по умолчанию серии пробелов
'{print $1,$3}' берёт 1-ю и 3-ю колонки; $0 вся строка; NF число полей; NR номер строки
условие пишется перед программой: '$9==500' означает 9-я колонка равна 500
для числового сравнения приводить к числу: $9+0 >= 500, иначе строковое сравнение даст сюрприз
'{s+=$7} END {print s}' шаблон суммы; счётчик через '{c++} END {print c}'
статистика по массиву: {c[$1]++} END {for (k in c) print k, c[k]}
-v передаёт внешнюю переменную: awk -v lim=500 '$3>lim'; -v ставится до программы
-F и -v разбирает сам awk, поэтому они идут перед программой в кавычках
в примерах sort -rn сортирует числом по убыванию, uniq -c считает повторы, ss -tn показывает TCP-соединения

awk -F: '$3>=1000 {print $1, $3}' /etc/passwd
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
awk -v lim=500 '{ if ($9+0 >= lim) print NR": "$7 }' access.log
ss -tn | awk 'NR>1 {c[$1]++} END {for (k in c) print k, c[k]}'

sort / uniq: сортировка, дедупликация, Top N

sort -n числовая сортировка (по умолчанию 10 окажется перед 9)
sort -h понимает 1K/2M/3G — обязателен для вывода du и df
sort -r обратный порядок; -u сортировка с удалением дублей
-k2 сортировка по 2-й колонке, -k3 аналогично; несколько ключей как -k1,1 -k2,2
-t: разделитель двоеточие (по умолчанию пробелы)
uniq -c считает подряд идущие повторы — поэтому нужен sort, иначе равные значения распадутся на блоки
uniq -d только дубликаты, uniq -u только уникальные строки
типовой Top N: sort | uniq -c | sort -rn | head
в примере du -sh: -s даёт только итог вместо строк по элементам (см. du / df)

du -sh /var/log/* | sort -h | tail -5
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
sort -t: -k3 -n /etc/passwd | head

cut / tr: выбор колонок и замена символов

cut -d: -f1,7 берёт поля по разделителю: -d задаёт его, -f номера полей (можно несколько)
cut -c1-19 режет по позициям символов — удобно для метки времени в логах фиксированной ширины
cut понимает только один разделитель; вывод с выравниванием пробелами он режет неверно, брать awk
tr 'a-z' 'A-Z' посимвольное отображение; tr меняет только символы, для строк нужен sed
tr -d удаляет символы, чаще всего tr -d '\r' для снятия виндовых переводов строки
tr -s сжимает повторяющиеся символы в один — обычно чтобы свернуть серии пробелов
tr читает только stdin, имя файла не принимает — нужен редирект <
в примерах: df -h человекочитаемо, cut -f5 берёт 5-е поле (Use%), sort -u убирает дубли

cut -d: -f1,7 /etc/passwd | head
cut -c1-19 /var/log/nginx/access.log | head -3
tr -d '\r' < crlf.txt > lf.txt
df -h | tr -s ' ' | cut -d' ' -f5 | sort -u

xargs: вывод в аргументы команды

xargs превращает каждую строку stdin в аргумент команды — так и работает find | xargs rm
-0 вместе с find -print0: имена с пробелами и переводами строки не разрываются (обязательно для пользовательских файлов)
find -print0 завершает элементы через \0, ровно под xargs -0
-n1 по одному аргументу за вызов: безопаснее и видно, на каком элементе упало
-I {} подстановка, аргумент можно ставить в середину: xargs -I {} cp {} /backup
-p спрашивает перед запуском, -t сначала печатает — при сомнениях начинать с -t
-r не запускать при пустом вводе (расширение GNU), иначе пустой список тоже дёрнет rm
-P4 четыре процесса параллельно: пакетное сжатие и загрузка ускоряются в разы
cp --parents копирует вместе со структурой каталогов — полезно при восстановлении бэкапа
в примерах find -name / -mtime / -type задают отбор, а -f у rm -f отключает подтверждение

find /var/log -name '*.log.*' -mtime +7 -print0 | xargs -0 -r rm -f
find /srv/uploads -type f -print0 | xargs -0 -I {} cp --parents {} /backup
printf '%s\n' a b c | xargs -n1 -t echo

tee: смотреть и писать одновременно

tee пишет вход одновременно в файл и в stdout — лучший способ оставить след перед правкой конфига
-a дописывать, а не перезаписывать; для логов всегда он
зачем sudo tee: в sudo echo x > /etc/f редирект делает текущий shell и падает по правам
-i игнорирует сигналы прерывания, длинный захват не оборвётся по Ctrl-C
перед конвейером свести stderr через 2>&1, иначе ошибки в tee вообще не попадут
отметки времени: ... | ts или | awk '{print strftime("%F %T"), $0}'

./deploy.sh 2>&1 | tee -a /var/log/deploy-$(date +%F).log
echo 'net.ipv4.ip_forward=1' | sudo tee -a /etc/sysctl.conf

jq: разбор JSON-ответов

jq разбирает JSON по структуре, а не построчным совпадением — grep так надёжно не умеет
. читает поля: .data .items[0].name; .[] перебирает массив
-r выводит сырые строки без кавычек — обязателен для переменных и дальнейших конвейеров
-c укладывает каждый результат в одну строку, удобно для sort / uniq
'keys' список полей объекта; 'length' длина массива; '..' все пути, когда структура неизвестна
select() фильтрует: '.[] | select(.status != "ok")'
-C цветной вывод для человека, -M без цвета для файлов и скриптов
по умолчанию jq форматирует вывод; -e превращает результат в код возврата для скриптов
в примерах curl -s глушит прогресс, иначе он попадёт в разбор JSON (см. curl)

curl -s https://api.example.com/v1/tools | jq -r '.data[].name'
curl -s 'https://api.example.com/v1/list?page=1' | jq -c '.items[] | {id, name}'
jq -r '.[] | select(.status != "ok") | .id' resp.json

diff / wc: сравнение и подсчёт

diff -u унифицированный формат, он же формат патча — patch применяет его напрямую
diff -r рекурсивно сравнивает два дерева; -q только сообщает, какие файлы различаются
diff -y вывод бок о бок, -W 200 задаёт ширину — удобно глазами сверять конфиги
wc -l число строк (обычная метрика логов); -c байты; -w слова
wc печатает ещё и имя файла, в скрипте берите awk '{print $1}'
comm лучше для разницы двух отсортированных списков, но обе стороны надо сначала sort
comm -13 a b печатает строки только из b; логику колонок стоит проверить, прежде чем верить
перед сравнением отсортировать и убрать дубли, иначе «всё разное» это просто разный порядок

diff -u /etc/nginx/nginx.conf.bak /etc/nginx/nginx.conf | head -40
diff -rq /srv/app/releases/20261028 /srv/app/releases/20261029
wc -l /var/log/nginx/access.log
sort a.txt > a.s; sort b.txt > b.s; comm -13 a.s b.s   # строки только в b

🖥️ Удалённый доступ и передача файлов (8)

Вход, ключи, передача, докачка — больше всего времени обычно уходит на «как перекинуть файл»

ssh: вход на удалённый сервер

ssh пользователь@хост базовый вход; порт 22 по умолчанию
-p 2222 задаёт порт (внимание: scp и sftp используют -P в верхнем регистре, не путать)
-i указывает файл закрытого ключа; иначе берутся ~/.ssh/id_ed25519 и id_rsa
-t принудительно выделяет псевдотерминал — обязателен для удалённых sudo, top и подобных интерактивных команд
-v печатает рукопожатие и аутентификацию: видно, встал он на сети или на аутентификации; -vvv подробнее
-o передаёт опции: -o ConnectTimeout=5 против зависаний, -o BatchMode=yes запрещает любые запросы (обязателен в скриптах)
одна удалённая команда без терминала: ssh ХОСТ 'команда' — основа пакетных проверок
-J прыжок через бастион: ssh -J bastion@1.2.3.4 deploy@10.0.0.5 (старый вид: -o ProxyCommand)
в примерах df -h: -h означает человекочитаемость (см. du / df)

ssh -p 2222 -i ~/.ssh/id_ed25519 deploy@10.0.0.5
ssh -o ConnectTimeout=5 -o BatchMode=yes 10.0.0.5 'uptime; df -h /'
ssh -v -t 10.0.0.5 'sudo systemctl restart app'
ssh -J bastion@1.2.3.4 deploy@10.0.0.5

ssh-keygen / ssh-copy-id: вход без пароля

ssh-keygen -t ed25519 создаёт пару ключей (короче и быстрее rsa, поддержка везде)
-t тип: ed25519 предпочтителен, rsa только для старых систем вместе с -b 4096
-C комментарий с назначением или почтой — по нему различать несколько ключей
-f имя выходного файла, чтобы не затереть ключ по умолчанию (затрёшь — потеряешь вход)
-N '' пустая парольная фраза для автоматизации; в интерактиве можно просто Enter
ssh-copy-id дописывает публичный ключ в ~/.ssh/authorized_keys на той стороне и правит права каталога и файла
-i (у ssh-copy-id) выбирает, какой ключ копировать; по умолчанию уходит только id_*.pub
-R удаляет из known_hosts старый отпечаток хоста — после переустановки машины обязательно, иначе REMOTE HOST IDENTIFICATION HAS CHANGED
-lf печатает отпечаток и тип ключа, чтобы сверить, что на двух машинах он один
у закрытого ключа права 600, у каталога 700, иначе ssh просто откажется его использовать

ssh-keygen -t ed25519 -C 'deploy@icetools' -f ~/.ssh/id_deploy -N ''
ssh-copy-id -i ~/.ssh/id_deploy.pub deploy@10.0.0.5
ssh-keygen -R 10.0.0.5
ssh-keygen -lf ~/.ssh/id_deploy.pub

~/.ssh/config: псевдонимы и бастионы

после настройки ssh app равно ssh -p 2222 deploy@10.0.0.5 — не надо каждый раз набирать строку
Host это псевдоним и область действия всех следующих параметров
HostName реальный адрес; User логин; Port порт; IdentityFile путь к ключу
IdentitiesOnly yes предлагает только этот ключ, а не перебирает десяток из агента (лишние попытки блокируются на сервере)
ProxyJump бастион, эквивалент -J в командной строке, причём scp / rsync наследуют его сами
ServerAliveInterval 30 шлёт heartbeat каждые 30 секунд: NAT и облачные балансировщики рвут простаивающие соединения
ServerAliveCountMax 3 рвёт после трёх неудачных heartbeat, в паре с интервалом выше
Host * в конце файла задаёт глобальные значения по умолчанию (heartbeat, таймауты)
права: config 600 и ~/.ssh 700, иначе Bad owner or permissions

# ~/.ssh/config  (chmod 600)
Host app
    HostName 10.0.0.5
    User deploy
    Port 2222
    IdentityFile ~/.ssh/id_deploy
    IdentitiesOnly yes
    ServerAliveInterval 30
    ServerAliveCountMax 3

Host db
    HostName 10.0.0.6
    ProxyJump app

Host *
    ServerAliveInterval 30

Туннели SSH: проброс портов

-L локальный форвард: ssh -L 8080:127.0.0.1:80 ХОСТ — порт 80 на удалённой стороне становится локальным 8080
-R обратный форвард: локальный порт публикуется на удалённой стороне, внешние видят внутренний сервис
-D динамический форвард, поднимает SOCKS-прокси (в паре с curl --socks5 или браузером)
-N только форвардинг, без shell; -f уводит в фон — почти всегда идут парой
-g разрешает другим машинам подключаться к локальному форвард-порту (по умолчанию слушается только 127.0.0.1)
средний адрес в -L резолвится с удалённой стороны: 127.0.0.1 там означает сам удалённый хост
сценарий «к базе напрямую нельзя»: сначала -L вытягивает порт локально, затем подключаемся к 127.0.0.1
если локальный порт занят, взять другой; lsof -i :8080 покажет, кто держит

ssh -N -f -L 15432:127.0.0.1:5432 deploy@10.0.0.5
ssh -N -L 8080:10.0.0.6:80 app
ssh -D 1080 -N -f app
ssh -g -L 8080:127.0.0.1:80 app

scp: выгрузка и загрузка файлов

scp ИСТОЧНИК ЦЕЛЬ — направление решает: локальный первым это выгрузка, удалённый первым это загрузка
-P заглавная задаёт порт (в ssh строчная -p, самое частое место путаницы)
-r рекурсия по каталогу; без -r копирование каталога сразу падает
-i выбирает ключ; -o передаёт опции ssh, например -o ConnectTimeout=5
-C сжатие при передаче — заметно на текстах и логах, бесполезно для уже сжатых архивов
-l 8000 ограничивает скорость в Kbit/s (8000 ≈ 1MB/s), чтобы не забить прод-канал
-p строчная сохраняет время и права — к -P отношения не имеет, не путать
scp не умеет докачку: оборванный файл начинается заново, для гигабайтов брать rsync -P

scp -P 2222 app.tar.gz deploy@10.0.0.5:/srv/
scp -r -i ~/.ssh/id_deploy ./dist deploy@10.0.0.5:/srv/app/
scp -C -l 8000 backup.tar.gz deploy@10.0.0.5:/backup/
scp -p deploy@10.0.0.5:/var/log/app/error.log ./

sftp: интерактивная передача файлов

sftp пользователь@хост открывает интерактивную сессию — «ls по ходу передачи» управляемее слепого scp
-P заглавная задаёт порт, -i ключ — как в scp
get УДАЛЁННЫЙ загружает; put ЛОКАЛЬНЫЙ выгружает; get -r тянет каталог целиком
reget продолжает загрузку, reput выгрузку — главное преимущество sftp перед scp
-p (у get/put) сохраняет права и метки времени
mget / mput принимают маски, но по умолчанию спрашивают по каждому файлу — сначала prompt off
ls / lls смотрят удалённую и локальную сторону; cd / lcd меняют каталог там и тут (с l это локально)
!df -h выполняет локальную команду shell — перед большой загрузкой проверить свободное место
bye / exit выход; heredoc-скрипт делает пакетную передачу неинтерактивной

sftp -P 2222 -i ~/.ssh/id_deploy deploy@10.0.0.5 <<'EOF'
get -r /srv/app/logs ./
reget backup.tar.gz
put ./dist/app.tar.gz /srv/
EOF

rsync: инкрементальная синхронизация и докачка

-a режим архива (= -rlptgoD): сохраняет права, время, симлинки, группу — повседневный выбор
-v показывает ход; -z сжатие при передаче, обязательно через интернет
-P это --partial --progress: сохранить недокачанное и показывать прогресс — вот и вся «докачка»
--partial оставляет принятое, следующий запуск продолжит; без него каждый раз с нуля
--append только дописывает — годится для дописываемых логов, не для изменяемых файлов
--delete приводит приёмник к источнику (лишнее удаляется) — сначала прогон с -n
-n и --dry-run одно и то же: напечатать план, файлы не трогать
-e 'ssh -p 2222' задаёт удалённый shell и порт; своего параметра порта у rsync нет
--exclude исключает одно правило, --exclude-from читает список из файла
--bwlimit=2000 ограничивает в KB/s; --checksum сравнивает содержимое, а не размер и время
--info=progress2 одна строка общего прогресса, для больших деревьев лучше попроцессного -P
главная ловушка: слеш в конце источника копирует СОДЕРЖИМОЕ каталога, без слеша сам каталог

rsync -avzP ./dist/ deploy@10.0.0.5:/srv/app/dist/
rsync -avzP -e 'ssh -p 2222' deploy@10.0.0.5:/var/log/app/ ./logs/
rsync -avn --delete /srv/data/ /backup/data/          # прогон без изменений: сначала разобраться, потом запускать по-настоящему
rsync -avz --bwlimit=2000 --exclude='*.tmp' ./ /mnt/nas/app/

wget: загрузка и докачка

wget URL качает в текущий каталог; -O задаёт имя файла, -O - пишет в stdout
-c докачка (в curl это -C -): после обрыва та же команда продолжит
-q тихий режим; --progress=dot:giga даёт компактный прогресс
--limit-rate=1m ограничивает скорость, суффиксы k/m; --tries=3 число попыток
-T 30 таймаут соединения в секундах, чтобы не висло; --timeout его длинная форма
-r зеркалит сайт, -np не поднимается в родительские каталоги (иначе утянет весь домен)
--no-check-certificate пропускает проверку сертификата — только для диагностики внутренних ресурсов, не в скрипт
для своих заголовков, Cookie и сессии wget гибкостью уступает curl

wget -c https://example.com/big.iso
wget -O - https://example.com/install.sh | head -40   # сначала посмотреть содержимое, потом решать, запускать ли
wget --limit-rate=1m --tries=3 -c -O app.tar.gz https://example.com/app.tar.gz

🔐 Права и пользователи (6)

Цифровые режимы, особые биты, sudo — типичные симптомы ошибки прав: «пароль верный, а доступа нет» и «сервис не стартует»

chmod: цифровой и символьный режимы

цифры: 4 чтение / 2 запись / 1 выполнение, три разряда — владелец / группа / остальные
755 обычен для каталогов (rwxr-xr-x); 644 для файлов (rw-r--r--)
600 только владелец читает и пишет — обязательный режим для закрытого ключа
символьная запись: u/g/o/a это кто, +/-/= действие, X добавляет выполнение только каталогам
-R рекурсия; для каталогов безопаснее a+X, обычные файлы не станут исполняемыми
go-w снимает запись у группы и остальных, в один проход с a+X
после рекурсивной правки проверить ls -l один файл и один каталог: ошибки обычно от лишней рекурсии

chmod 600 ~/.ssh/id_rsa
chmod -R a+X,go-w /srv/app/public
ls -l /srv/app/public | head    # после рекурсивного изменения выборочно проверить

chown: смена владельца и группы

chown пользователь:группа путь; вместо двоеточия допустима точка
только группу менять через chown :группа или chgrp
-R рекурсия; с -h меняется сама ссылка, а не её цель
--reference=ФАЙЛ копирует владельца и права с готового файла — удобно при массовой синхронизации
владельца меняет только root; обычный пользователь может сменить группу своих файлов и только на свою
при Permission denied на томе контейнера сверить uid каталога на хосте с uid пользователя внутри

chown -R www-data:www-data /var/www/app
chown --reference=/srv/app/a /srv/app/b

SUID / SGID / sticky: три особых бита

4000 SUID: выполняется от владельца файла (так работает /usr/bin/passwd)
2000 SGID: выполняется от группы; на каталоге новые файлы наследуют его группу
1000 sticky: в каталоге удалять может только владелец файла (так устроен /tmp)
буква s или t в ls означает установленный бит; заглавные S/T — бит стоит, а x нет
-perm -4000 значит «в правах есть SUID»; -perm это проверка прав у find
-xdev не выходить за файловую систему — иначе find ползёт по NFS и точкам монтирования контейнеров
-ls печатает найденное как ls -dils: размер и время сразу
-type f только обычные файлы, без каталогов и симлинков
при аудите повышения привилегий сначала искать SUID: find / -xdev -perm -4000 -type f

# аудит: вывести все SUID-программы
find / -xdev -perm -4000 -type f -ls 2>/dev/null

# общий каталог: работа в группе + удалять может только владелец
chmod 2770 /srv/share

umask: права по умолчанию для новых файлов

umask это набор битов, которые СНИМАЮТСЯ, а не сами права
022 → каталоги 755, файлы 644 (файлы и так без бита выполнения)
077 → каталоги 700, файлы 600 — подходит для каталогов с ключами
-S печатает итоговый режим символьно (rwxr-xr-x), читается легче цифр
временно: umask 077 действует только в текущем shell
постоянно: прописать в /etc/profile или в ~/.bashrc пользователя
если новые ключи читают посторонние — сначала проверить umask

umask            # посмотреть текущее значение
umask 077        # действует в текущей сессии
umask -S         # показать фактические права по умолчанию в символьном виде

sudo / su: как правильно повышать права

sudo -i даёт свежий login-shell root (с его переменными окружения)
sudo -s root-shell с окружением текущего пользователя, PATH не меняется
sudo -u ПОЛЬЗОВАТЕЛЬ выполнить от другого пользователя — часто при проверке сервисов
sudo -l показывает, что разрешено — первое действие, когда «почему нельзя»
su - переключает на root с его окружением; без дефиса остаётся своё и PATH может быть неверным
sudoers править через visudo, там проверка синтаксиса; -c только проверяет без записи — прогонять после каждой правки
правка /etc/sudoers руками запирает доступ — классический самострел в эксплуатации

sudo -l
sudo -u www-data /usr/bin/env      # посмотреть окружение сервисной учётной записи
visudo -c                          # только проверить синтаксис

useradd / usermod / id

useradd -m создаёт домашний каталог; -s задаёт shell; -G добавляет в дополнительные группы
без -m домашнего каталога нет, и многие инструменты после входа странно падают
usermod -aG ГРУППА ПОЛЬЗОВАТЕЛЬ — без -a пользователь вылетит из остальных групп (-a это append)
-G список дополнительных групп, пишется вместе с -a как -aG
id ПОЛЬЗОВАТЕЛЬ показывает uid/gid и все группы; groups ПОЛЬЗОВАТЕЛЬ только группы
userdel -r удаляет и домашний каталог; без -r останутся осиротевшие файлы
смена групп не действует на открытые сессии — нужен повторный вход

useradd -m -s /bin/bash deploy
usermod -aG docker deploy
id deploy

⚙️ Процессы и ресурсы (8)

Кто ест CPU, кто держит файл, как остановить аккуратно — типичная ошибка здесь это kill -9 слишком рано

ps: два стиля, не смешивать

ps aux это стиль BSD: %CPU, %MEM, STAT, COMMAND — повседневный вариант
ps -ef это System V: UID, PID, PPID, C, STIME, CMD — видно родительский процесс
-e все процессы (стиль System V), -o задаёт колонки вывода
ps -aux тоже запустится, но смысл другой — не брать в привычку
--sort=-%cpu сортировка по CPU по убыванию (минус и есть убывание), запятая разделяет ключи
--sort=-rss по реальной памяти; в STAT Z это зомби, D непрерываемое ожидание, R выполняется
RSS реальное потребление, VSZ виртуальное адресное пространство и почти бесполезно

ps aux --sort=-%cpu | head
ps -eo pid,ppid,rss,pcpu,cmd --sort=-rss | head

top / htop: клавиши полезнее параметров

в top клавиша P сортирует по CPU, M по памяти, T по накопленному времени
1 разворачивает ядра — видно, упёрлось одно ядро или нагрузка общая
c показывает полную командную строку вместо обрезки
H переключает потоки; u фильтр по пользователю; k убивает процесс
-b пакетный режим, -n1 один снимок (-n это число итераций) — годится для скриптов и логов
-p ограничивает указанными PID через запятую; -d, у pgrep как раз склеивает PID через запятую
load average это средние за 1/5/15 минут; выше числа ядер означает очередь

top -b -n1 | head -15          # снять один снимок
top -p $(pgrep -d, nginx)      # смотреть только указанные процессы

kill: сначала по-хорошему

по умолчанию kill шлёт TERM(15): процесс успевает закрыться и дописать файлы
-15 то же, что -TERM; именованная форма читается лучше — с неё и начинать
-9 это KILL: ядро забирает сразу, без уборки — базы и очереди могут остаться с битыми данными
-1 или -HUP обычно означает «перечитать конфиг» (nginx и sshd это умеют)
-0 ничего не шлёт, только проверяет существование; код возврата и есть ответ
kill принимает PID, а не имя; для имён есть pkill и killall
pkill -HUP nginx шлёт HUP всем процессам nginx по имени
сначала TERM, подождать пару секунд, потом KILL — этот порядок пишут почти в каждом регламенте

kill -TERM 1234 && sleep 3
kill -0 1234 && kill -9 1234
pkill -HUP nginx               # перечитать конфигурацию

nohup / setsid: пережить закрытие терминала

& только уводит в фон: закрыл терминал — пришёл SIGHUP и процесс умер
nohup игнорирует SIGHUP и по умолчанию пишет в nohup.out (лучше перенаправить явно)
2>&1 сводит stderr в stdout, иначе ошибки уходят неизвестно куда
< /dev/null подключает stdin к пустому устройству, чтобы фон не вставал на чтении терминала
setsid отрывает от текущей сессии — надёжнее nohup
disown убирает уже запущенную фоновую задачу из таблицы задач shell
для долгой службы нужен systemd; nohup годится только для быстрой проверки

nohup ./run.sh > /var/log/run.log 2>&1 &
setsid ./worker.sh < /dev/null > /var/log/worker.log 2>&1 &

lsof: кто держит файл или порт

lsof -i :8080 показывает, кто держит порт 8080 (-i фильтр по сетевому адресу)
-p PID все файлы одного процесса
-u ПОЛЬЗОВАТЕЛЬ фильтр по пользователю; -n без резолва имён — быстрее и не встаёт на DNS
+D /path рекурсивно по каталогу (медленно, на больших деревьях осторожно)
+L1 только файлы с числом ссылок меньше 1 — удалённые, но открытые, виновники занятого места
дальше sort -k7 -n: -k7 берёт 7-ю колонку (SIZE/OFF), -n сравнивает как числа
если lsof нет, подойдут fuser -v или /proc/PID/fd

lsof -i :8080
lsof -n +L1 | sort -k7 -n | tail    # занятые удалённые файлы с сортировкой по размеру

nice / renice / taskset: уступить CPU

nice от -20 (высший приоритет) до 19 (низший); обычный пользователь может только повышать число
nice -n 19 КОМАНДА стартует с низким приоритетом — подходит бэкапам и сжатию
-n несёт само значение nice, а не количество
renice -n 10 -p PID меняет работающий процесс; -p задаёт PID, -u работает по пользователю
taskset -c 0-3 привязка к ядрам: -c задаёт список CPU, снижает борьбу инстансов за одно ядро
в taskset -cp 0-3 1234 флаг -p означает «работать с существующим процессом», дальше идёт PID
понижение приоритета не добавляет ресурсов — оно лишь ограждает важную работу
в примерах tar -czf: -c создаёт, -z сжимает gzip, -f задаёт имя архива (см. tar)

nice -n 19 tar -czf backup.tar.gz /srv/data
renice -n 10 -p 1234
taskset -cp 0-3 1234

watch: следить за изменениями, а не за снимком

watch по умолчанию повторяет команду каждые 2 секунды с очисткой экрана — видно изменение, а не снимок
-n 1 обновление раз в секунду; -n принимает секунды (можно 0.5)
-d подсвечивает различия между двумя запусками — сразу видно, что шевелится
-d=cumulative накапливает подсветку на всём, что менялось — удобно для плавающих всплесков
-t убирает строку заголовка (команда и интервал), экран чище
команду обязательно в кавычки, иначе shell разберёт конвейер и watch получит только начало
не годится для высокочастотных метрик (соединения, QPS) — нужен vmstat 1 / sar, watch пропустит середину
в примерах ss -s это сводка соединений, wc -l считает строки (l и есть lines)

watch -n 1 -d 'ss -tn state established | wc -l'
watch -d=cumulative -t 'ss -s'

screen / tmux: пережить обрыв связи

screen -S deploy открывает именованную сессию: терминал закрыт, сеть упала — задача живёт
-list список сессий (или -ls) — сначала проверить старую в состоянии Attached
-r deploy переподключает; если пишет Attached, сессия занята — забрать через -d -r
Ctrl-a d отключает в screen; Ctrl-a " список окон; Ctrl-a k убивает текущее окно
tmux new -s deploy создаёт, tmux attach -t deploy переподключает (-t задаёт имя)
в tmux Ctrl-b d отключает, Ctrl-b [ включает режим прокрутки по истории вывода
перед миграцией, бэкапом или большой передачей поднять сессию: сеть не останавливает задачу, но скрывает вывод

screen -S deploy
# затем отцепиться по Ctrl-a d
screen -list
screen -r deploy
tmux new -s deploy

🌐 Сеть и порты (8)

Сервис не отвечает — сначала различить «не слушает», «не пропускают» и «не достучаться»; неверный порядок проверок стоит часов

ss: кто и где слушает порт

-t TCP; -u UDP; -l только слушающие; -n без имён сервисов (числа, быстрее); -p показать процесс
-tulnp самая ходовая связка: пять букв, пять задач
-tnp без l включает и процессы установленных соединений — этим смотрят их количество
-a включает установленные соединения; -s сводка — первым делом при скачке числа соединений
state established фильтр по состоянию, точнее чем grep ESTAB (ESTAB только сокращение колонки)
netstat устарел и на новых системах обычно не установлен — сразу ss
слушать на 127.0.0.1 и на 0.0.0.0 разные вещи: первое доступно только локально, и это причина номер один «порт открыт, а снаружи не достучаться»

ss -tulnp
ss -tnp state established | head
ss -s

curl: швейцарский нож для проверки API

-s тихий режим: без прогресса и текста ошибок — в скриптах почти всегда
-S работает с -s и возвращает сообщения об ошибках: -sS значит «тихо, но при сбое сказать»
-i выводит и заголовки ответа; -I только заголовки (шлёт HEAD, без body)
-v полная трассировка рукопожатия и запроса — видно, на каком шаге встало
-L идти по редиректам; обязательно при разборе цепочки 301/302
-X задаёт метод; -d отправляет body (по умолчанию POST); -H добавляет заголовок
-o /dev/null выбрасывает body, вместе с -w остаются только код и тайминги
-w '%{http_code} %{time_total}' свой вывод, быстрее чем глазами
-m 10 весь запрос не дольше 10 секунд; --connect-timeout 3 только фаза соединения
--resolve хост:443:IP прибивает IP и обходит DNS — так видно, DNS ли виноват
-x http://proxy:8080 через прокси; --noproxy '*' принудительно напрямую (при локальном разборе обычно нужен)
-k пропускает проверку сертификата — только для диагностики, не в скрипт
в -w последовательность \n даёт перевод строки, без неё всё слипнется в одну строку

curl -sS -o /dev/null -w '%{http_code} %{time_total}s\n' https://example.com/
curl -m 10 --connect-timeout 3 -sS -o /dev/null \
  -w 'dns:%{time_namelookup} conn:%{time_connect} ttfb:%{time_starttransfer} total:%{time_total}\n' \
  https://example.com/api
curl -iL --resolve example.com:443:1.2.3.4 https://example.com/
curl -v -X POST -H 'Content-Type: application/json' -d '{"a":1}' http://127.0.0.1:8080/api

Проверка портов: nc / telnet / /dev/tcp

nc -zv ХОСТ ПОРТ — -z только проба без данных, -v печатает вердикт, самый быстрый вариант
-w3 таймаут соединения 3 секунды, иначе команда может висеть долго
telnet ХОСТ ПОРТ есть в старых системах; Connected означает доступность
встроенный в bash /dev/tcp не требует установки: echo > /dev/tcp/ХОСТ/ПОРТ
refused значит хост достижим, но никто не слушает; таймаут значит пакеты режет фаервол или security group — смысл совершенно разный
разбор всегда послойно: локальный ss, затем curl 127.0.0.1, затем nc из той же подсети, затем nc снаружи

nc -zv -w3 10.0.0.5 5432
echo > /dev/tcp/10.0.0.5/5432 && echo open || echo closed

ip: адреса, маршруты, соседи

ip a показывает адреса всех интерфейсов (старый ifconfig); -4 только IPv4, -6 только IPv6
ip r печатает таблицу маршрутов; строка default и есть шлюз по умолчанию
ip neigh показывает таблицу ARP — смотреть, когда не идёт трафик внутри подсети
ip -s link даёт счётчики пакетов и ошибок (-s это statistics); ненулевые errors/dropped означают проблему канального уровня
show eth0 ограничивает вывод одним интерфейсом, виртуальные его не топят
add / del меняют адрес временно, до перезагрузки
постоянно — по дистрибутиву: netplan или /etc/network/interfaces в Debian, nmcli в RHEL

ip -4 a
ip r
ip -s link show eth0

dig: что на самом деле вернул DNS

+short печатает только ответ — удобно для скриптов
+A / +AAAA / +MX задают тип записи (по умолчанию A)
@8.8.8.8 запрос к конкретному резолверу — сравнить локальный и публичный ответ
+trace идёт от корня и показывает, какая делегация сломана
при отказе смотреть status: NXDOMAIN записи нет, SERVFAIL сервер неисправен
TTL определяет, сколько ждать после правки записи; остаток TTL в ANSWER SECTION показывает, насколько свеж кэш
dig видит только DNS. Если резолв идёт, а трафик нет — смотреть маршрутизацию и порты

dig +short example.com
dig @8.8.8.8 example.com A
dig +trace example.com

tcpdump: увидеть, кто перестал отвечать ACK

-i any все интерфейсы (можно указать конкретный); -n без резолва имён и портов — быстро и не встаёт на DNS
-nn значит «IP и порты числами», при разборе понятнее, чем -n
port 443 только этот порт; host 1.2.3.4 только этот хост; комбинируются через and / or
-c 100 остановиться после 100 пакетов, иначе терминал зальёт
-w file.pcap сохранить и открыть в Wireshark — читать там гораздо удобнее
-A содержимое пакета как ASCII, -X как hex плюс ASCII (имеет смысл только для открытых протоколов)
tcp[tcpflags] & tcp-rst != 0 ловит только RST — так видно, кто закрыл первым
нужен root; на проде обязательно -c и фильтр

tcpdump -i any -nn -c 50 'port 443 and host 1.2.3.4'
tcpdump -i eth0 -nn -w /tmp/cap.pcap 'tcp[tcpflags] & tcp-rst != 0'

openssl s_client: срок и цепочка сертификатов

-connect хост:443 смотрит сертификат собеседника на уровне TLS, HTTP не трогает
-servername хост обязателен (SNI), иначе на общем IP можно получить чужой сертификат
-showcerts печатает всю цепочку — видно, не хватает ли промежуточного
-noout не выводит сам сертификат, только запрошенные поля
-dates показывает notBefore / notAfter — первый шаг при разборе истечения
-subject показывает, кому выдан сертификат; -ext subjectAltName перечисляет покрытые имена
-checkend 2592000 проверяет, не истечёт ли за 30 дней (в секундах); код возврата годится для мониторинга
</dev/null заставляет s_client выйти после чтения, иначе он висит в ожидании ввода

openssl s_client -connect www.icetools.cn:443 -servername www.icetools.cn </dev/null 2>/dev/null \
  | openssl x509 -noout -dates -subject
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null | head -30
openssl s_client -connect example.com:443 -servername example.com -checkend 2592000 </dev/null >/dev/null \
  && echo '30 天内不过期'

Фаервол: сначала убедиться, что режет он

firewall-cmd --list-all показывает, что разрешено в текущей зоне (семейство RHEL)
--add-port=8080/tcp добавляет правило; только --permanent переживёт перезагрузку
--reload применяет постоянные правила — обязателен после добавления, иначе они не попадут в рабочее состояние
ufw status verbose печатает правила (Ubuntu)
iptables -L выводит таблицу filter; -n оставляет имена числами, -v показывает счётчики пакетов — доказательство, что правило сработало
--line-numbers печатает номера правил — по ним и пишут iptables -D INPUT 3
-I INPUT 1 вставляет в начало, -A дописывает в конец — порядок решает исход
если после правила всё равно не идёт, подозревать security group облака: он вне этой машины

firewall-cmd --list-all
firewall-cmd --add-port=8080/tcp --permanent && firewall-cmd --reload
iptables -L INPUT -n -v --line-numbers

💾 Диски и файловые системы (5)

«Диск полон» имеет четыре разные причины: много файлов, один огромный файл, исчерпанные inode, удалённые но открытые файлы

Диск полон: разбор в четыре шага

шаг 1: df -h показывает, какая точка монтирования полна — сервис пока не перезапускать
шаг 2: df -i по inode. При исчерпании inode пишет «no space left on device», а df -h показывает запас
шаг 3: du -h --max-depth=1 спускается по уровням к большому каталогу (--max-depth это глубина)
шаг 4: если цифры расходятся, lsof +L1 находит удалённые, но открытые файлы; освободит только перезапуск или kill этого процесса
строка с awk складывает 7-ю колонку (байты) и переводит в MB — быстрее, чем прикидывать глазами
рост логов обычно виден уже на шаге 1 — не удалять сразу, а посмотреть, не сыплет ли ошибками

df -h; df -i
du -h --max-depth=1 / | sort -h | tail
lsof +L1 2>/dev/null | awk '{s+=$7} END {print s/1024/1024" MB held"}'

lsblk: сначала понять структуру дисков

по умолчанию выводит дерево: видно вложенность LVM, разделов и точек монтирования
-f показывает тип ФС, UUID и точку монтирования — смотреть перед правкой fstab
-p печатает полные пути устройств, чтобы не путать /dev/sda и /dev/sda1
-d не разворачивает дочерние устройства, только диски целиком
-o задаёт колонки; NAME,SIZE,FSTYPE,MOUNTPOINT,UUID самый полезный набор
если после расширения lsblk показывает новый размер, а df нет — файловая система ещё не расширена

lsblk -f
lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT,UUID

Расширение: раздел и файловая система

расширение облачного диска в два шага: раздел, потом файловая система; пропустишь — df не изменится
growpart /dev/vda 1 расширяет раздел (пакет cloud-utils-growpart), второй аргумент номер раздела
xfs_growfs / расширяет XFS (только рост, сжатие невозможно)
resize2fs /dev/vda1 расширяет ext4 (сжимать тоже можно, но только после umount и проверки)
для LVM добавляется lvextend, и уже потом расширение ФС
в конце df -h подтверждает, что ёмкость действительно изменилась
перед работой снять снапшот: отключение питания посреди resize может стоить данных

growpart /dev/vda 1
xfs_growfs /            # XFS
resize2fs /dev/vda1     # ext4
df -h /

mount / umount / fstab

частые опции -o: ro только чтение, remount перемонтировать, noatime меньше записи
remount самый нужный: параметры меняются без размонтирования, сервис не встаёт
если umount пишет device is busy, искать держателя через lsof +D или fuser -vm
в fstab лучше UUID, а не /dev/sdb: имена устройств меняются
4-е поле обычно defaults,noatime; 5-е и 6-е это dump и порядок fsck, для диска данных 0 0
после правки fstab обязателен mount -a; ошибка оставит систему без загрузки
mount -a монтирует всё из fstab, что не смонтировано, и прямо укажет проблемную строку

mount -o remount,rw /
fuser -vm /mnt/data
mount -a                      # проверить запись в fstab

Исчерпание inode: места нет при мелких файлах

каждый файл занимает один inode, общее число задаётся при форматировании
симптом: df -h показывает место, а запись падает с No space left on device
df -i показывает IUse%; близко к 100% — это оно
-xdev не выходить за ФС, чтобы подсчёт не ушёл на другие точки монтирования
-type f считает только файлы, без каталогов (они тоже едят inode, но распределение файлов показательнее)
sed отрезает всё после последнего слеша, оставляя каталог; sort | uniq -c считает файлы по каталогам
обычные виновники: session-файлы, временные файлы загрузок, mail spool, кэш-каталоги с сотнями тысяч мелочи
радикально — переформатировать с большим -i; в короткой перспективе только чистить мелкие файлы
завершающий sort -rn означает числовой обратный порядок: -r обратно, -n как числа

df -i
find /var -xdev -type f | sed 's#/[^/]*$##' | sort | uniq -c | sort -rn | head

⏰ Планировщик и время (4)

cron не срабатывает в восьми случаях из десяти из-за PATH и перенаправления вывода; неверное время ломает сертификаты, JWT и логи разом

crontab: пять полей времени

crontab -l список задач пользователя; -e правка; -r удаляет всё без подтверждения — рука не должна дрогнуть
-u ПОЛЬЗОВАТЕЛЬ работа с чужим crontab (запускать от root)
пять полей это минута час день месяц день недели: 0 3 * * * значит каждый день в 3:00
*/5 каждые 5 минут; 1-5 с понедельника по пятницу; 0,12 только в 0 и 12 часов
окружение cron совсем не как у login-shell, PATH крайне короткий — в скриптах только абсолютные пути
cron не читает ~/.bashrc: переменные писать в начале crontab или source в скрипте
вывод обязательно перенаправлять: >> /var/log/x.log 2>&1, иначе ошибки уйдут в почту и останутся невидимыми
если не запускалось: journalctl -u cron в Debian, /var/log/cron в RHEL
в примерах curl -sS тихий, но сообщает об ошибках (-s тишина, -S показать ошибки), -o /dev/null выбрасывает body

crontab -e
crontab -l
# бэкап каждый день в 3:30, вывод в лог
30 3 * * * /srv/app/backup.sh >> /var/log/backup.log 2>&1
# проверка здоровья каждые 5 минут
*/5 * * * * /usr/bin/curl -sS -o /dev/null http://127.0.0.1:8080/health

systemd timer: отлаживать проще, чем cron

преимущества реальные: логи в journalctl, видно время следующего запуска, пропуски догоняются
OnCalendar задаёт календарное время: *-*-* 03:30:00 ежедневно в 3:30; OnUnitActiveSec=5min каждые 5 минут
Persistent=true догоняет пропущенный за время простоя запуск — cron так не умеет
Unit= указывает service; если опустить, берётся одноимённый .service
systemctl list-timers показывает все таймеры и время следующего срабатывания
enable --now это «автостарт при загрузке плюс запуск сейчас»; один enable ничего не запустит
для проверки start делают .service, а не .timer — запуск таймера задачу не выполнит
последняя строка: journalctl -u задаёт unit, -n 30 берёт последние 30 строк, --no-pager без постраничного вывода

# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup

[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true

[Install]
WantedBy=timers.target

# включить и проверить
systemctl list-timers
systemctl enable --now backup.timer
journalctl -u backup -n 30 --no-pager

date / timedatectl: время и часовой пояс

date показывает время и зону; +%F даёт 2026-10-29, +%T время, '+%F %T' самая частая связка
-d '-1 hour' считает час назад, -d 'next monday' тоже работает — все окна в скриптах считаются через -d
-s меняет системные часы напрямую; на проде не трогать, это ломает порядок логов и проверку сертификатов
timedatectl показывает зону и состояние синхронизации NTP, информации больше, чем у date
set-timezone Asia/Shanghai меняет зону (частая проблема локальных машин — остались в UTC)
set-ntp true включает автоматическую сверку; в контейнере время наследуется от хоста
последствия неверного времени конкретны: сертификат not yet valid, мгновенно истёкший JWT, несовпадающее время в логах, задачи в неверный момент

date '+%F %T'
date -d '-1 hour' '+%F %T'
timedatectl
timedatectl set-timezone Asia/Shanghai

chronyc / ntpq: идёт ли синхронизация

chronyc tracking показывает смещение и stratum; строка System time буквально говорит, насколько часы врут
смещение больше нескольких сотен миллисекунд требует разбора: источник недоступен, UDP 123 закрыт, часы ВМ плывут
chronyc sources -v перечисляет источники, -v печатает детали (задержка, джиттер, выбран ли)
в начале строки ^* стоит используемый источник, ^? означает недоступность
chronyc makestep подводит часы сразу — вручную только при большом смещении, событие попадёт в лог
ntpq -p эквивалент для старых систем с ntpd, -p печатает список пиров
на облачных серверах лучше внутренний NTP провайдера: публичный часто ограничен по rate или закрыт

chronyc tracking
chronyc sources -v
chronyc makestep
ntpq -p          # старые системы (ntpd)

📋 Логи и поиск причины (5)

Порядок важнее команды: сначала очертить окно по времени, потом сузить по ключевому слову, затем читать контекст

journalctl: единый журнал systemd

-u UNIT фильтр по одному сервису
-f следить дальше; --since '10 min ago' или --since '2026-10-29 09:00' очерчивает окно
-p err показывает error и выше; приоритеты идут от emerg до debug
-n 100 последние 100 строк; -r обратный порядок (новые сверху)
--no-pager без пейджера — удобно при редиректе в файл и в конвейере
-o short-iso метка времени со смещением зоны, обязательна при разборе между регионами
--disk-usage сколько места занимает журнал; --vacuum-time=7d оставить только 7 дней
--vacuum-size=500M чистит по объёму — выбирать одно из двух с --vacuum-time

journalctl -u nginx -p err --since '30 min ago' --no-pager
journalctl -u app -f -o short-iso
journalctl --disk-usage

tail: чем -f отличается от -F

-f следит по дескриптору: после ротации (rename) продолжает читать старый файл и «лог будто замер»
-F следит по имени и после ротации открывает новый — для долгого наблюдения только он
-n 100 сначала показать последние 100 строк, потом следить; -n0 без истории, только новое
-Fn0 это просто -F вместе с -n0, самая частая связка для живого потока
--pid=PID прекращает слежение при выходе процесса
grep --line-buffered заставляет grep сбрасывать каждую строку; без него в конвейере ничего не видно
вместе: tail -Fn0 app.log | grep --line-buffered -i error
несколько файлов: tail -F a.log b.log печатает разделители ==>; не следить только за одним

tail -F /var/log/app/error.log
tail -Fn0 /var/log/nginx/access.log | grep --line-buffered -i 500

dmesg: что сказало ядро

OOM, ошибки IO диска, link down на сетевой карте и сбои драйверов видны здесь
-T человекочитаемое время; -w следить дальше
-l err,warn только указанные уровни (-l принимает список через запятую)
кольцевой буфер dmesg перезаписывается — важное забирать сразу; journalctl -k это та же информация в постоянном виде
строка «Out of memory: Killed process» означает, что процесс убил OOM killer, а не он сам упал
с grep -iE: -i игнорирует регистр, -E включает расширенное регулярное выражение (тогда работает |, см. grep)

dmesg -T -l err,warn | tail -30
dmesg -T | grep -iE 'oom|killed process'

OOM: почему процесс внезапно исчез

проверка: dmesg -T | grep -i -A2 'killed process' — -A2 добавляет две строки после совпадения, там объём памяти жертвы
в journalctl -k запись полнее и содержит состояние памяти на тот момент
если systemctl status показывает status=9/KILL или signal=KILL, почти наверняка это OOM killer
--no-pager не даёт пейджеру перехватить вывод — нужно и в скриптах, и на скриншотах
это не падение: лог приложения просто обрывается, без стека исключения
искать причину: free -h по доступной памяти, затем сортировка процессов по RSS
в контейнере смотреть ещё лимит cgroup: /sys/fs/cgroup/memory.max

dmesg -T | grep -i -A2 'killed process'
systemctl status app --no-pager | head -20
free -h

logrotate: чтобы логи не съели диск

конфиги кладутся в /etc/logrotate.d/, по файлу на сервис
daily / weekly задаёт период; rotate 7 хранит 7 копий
compress сжимает старые логи, delaycompress оставляет свежий несжатым (чтобы не сжать ещё пишущийся файл)
copytruncate копирует и обнуляет — для программ, не умеющих переоткрыть файл, но есть окно потери строк
postrotate посылает сигнал на переоткрытие — чище, чем copytruncate
-d только прогон без действий; после правки обязательно посмотреть, что он собирается делать
-f принудительно ротирует один раз, чтобы сразу проверить конфиг, не дожидаясь периода
missingok не ругается на отсутствующий файл, notifempty пропускает пустой — эти две нужны почти везде

# /etc/logrotate.d/app
/var/log/app/*.log {
    daily
    rotate 7
    compress
    delaycompress
    missingok
    notifempty
    postrotate
        systemctl reload app > /dev/null 2>&1 || true
    endscript
}

# сначала прогон, затем принудительный запуск для проверки
logrotate -d /etc/logrotate.d/app
logrotate -f /etc/logrotate.d/app

🚀 Сервисы systemd (5)

Почти все причины нестартующего сервиса — три: неверный путь, нет прав у пользователя, не поднята зависимость; всё это в status

systemctl: то немногое, что нужно ежедневно

start / stop / restart / reload — reload перечитывает конфиг без перезапуска, только если программа умеет
status показывает последние строки журнала и код выхода — полезнее догадок; --no-pager отключает пейджер
enable автозапуск при загрузке; --now запускает сразу
disable убирает автозапуск, но не останавливает; останавливать отдельно через stop
list-units --failed сразу показывает упавшие; --all включает и неактивные
коды возврата is-active / is-enabled напрямую годятся для скриптов
cat печатает unit так, как его видит systemd (с учётом drop-in) — смотреть после правки

systemctl status app --no-pager
systemctl enable --now app
systemctl list-units --failed

Как написать работающий unit

кладётся в /etc/systemd/system/ИМЯ.service, после записи обязателен daemon-reload
ExecStart требует абсолютного пути, и программа не должна сама форкаться (иначе Type=forking)
Restart=on-failure вместе с RestartSec=5 не даёт устроить шторм перезапусков
User / Group задают права; при уходе от root надо поправить и домашний каталог, и владельца файлов
EnvironmentFile держит переменные в отдельном файле, а не внутри ExecStart
WorkingDirectory влияет на относительные пути; без него многие программы не находят свой конфиг
StandardOutput=journal направляет логи в journalctl
After только упорядочивает запуск и ничего не гарантирует; для сети нужен Wants=network-online.target

# /etc/systemd/system/app.service
[Unit]
Description=App
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/srv/app
EnvironmentFile=/etc/app/env
ExecStart=/srv/app/bin/app
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Сервис не стартует: порядок проверок

1. systemctl status: Active и последние строки ошибок — большинство случаев решается здесь
2. journalctl -u СЕРВИС -n 50 --no-pager для полной трассировки (-n это число строк)
3. проверить, что путь из ExecStart существует и исполним: ls -l по этому пути
4. запустить руками от служебного пользователя через sudo -u — настоящая причина обычно видна сразу
5. проверить политику: ausearch -m avc -ts recent (-m задаёт тип записи avc, -ts начало периода)
отказы AppArmor попадают в dmesg; в контейнере смотреть ещё docker logs
status=203/EXEC значит путь или права; status=1 это собственный код возврата программы

systemctl status app --no-pager
journalctl -u app -n 50 --no-pager
sudo -u deploy /srv/app/bin/app     # запустить вручную и увидеть настоящую ошибку
ausearch -m avc -ts recent           # записи отказов SELinux

Когда daemon-reload обязателен

правил .service — daemon-reload обязателен, иначе systemd держит старое определение
менял только значения в EnvironmentFile — reload не нужен, но нужен restart
новый unit — enable возможен только после reload
работающий сервис не подхватывает конфиг по reload — его надо restart
типичный симптом забытого reload: ExecStart поправлен, а status показывает старую команду
cat показывает файл глазами systemd, включая перекрытия drop-in

systemctl daemon-reload && systemctl restart app
systemctl cat app            # убедиться, как этот файл видит systemd

Порядок: After не равно Requires

After задаёт только порядок, но не гарантирует успех другого юнита
Requires жёсткая зависимость: упал он — упал и этот
Wants мягкая: этот запустится в любом случае — в большинстве случаев нужна она
по сети: network.target означает лишь инициализацию стека, адреса может ещё не быть
After=network-online.target действительно ждёт только вместе с Wants=network-online.target
«не удалось разрешить имя» сразу после загрузки — почти всегда именно это
проверка зависимостей: systemctl list-dependencies СЕРВИС покажет дерево

[Unit]
Wants=network-online.target
After=network-online.target

# проверить
systemctl list-dependencies app

📦 Пакеты (4)

Перед установкой смотреть источник, перед удалением — что утащит с собой; на проде особенно

apt (Debian / Ubuntu)

apt update обновляет только индексы и ничего не ставит — самая частая путаница
apt upgrade поднимает установленные пакеты; full-upgrade может удалять пакеты ради зависимостей — осторожно
install -y без вопроса подтверждения, для скриптов; без -y apt встанет на вводе
apt purge удаляет и конфиги; обычный remove их оставляет
apt list --installed показывает установленное; apt list --upgradable (--upgradable как фильтр) обновляемое
apt-cache show ПАКЕТ даёт версию и описание
apt-mark hold фиксирует версию, чтобы массовое обновление её не унесло

apt update && apt install -y nginx
apt list --upgradable
apt purge nginx-common

dnf / yum (RHEL / CentOS)

dnf пришёл на смену yum; на новых системах yum обычно симлинк
install / remove / update не совпадают с apt — не путать глаголы
install -y без подтверждений, как в apt
provides ищет пакет по файлу или команде — «каким пакетом поставлена эта команда»
history показывает транзакции, history undo last откатывает последнюю — спасает после неудачного обновления
--enablerepo включает репозиторий на один запуск; --nogpgcheck только когда иначе никак
модули (dnf module list) нужны, когда сосуществуют несколько версий одного пакета

dnf install -y nginx
dnf provides /usr/sbin/ss
dnf history undo last

Какому пакету принадлежит файл или команда

RHEL: rpm -qf /usr/bin/ss — -q включает режим запроса, -f ищет по файлу
Debian: dpkg -S /usr/bin/ss; если пусто, сначала apt-file update, затем apt-file search
если известно только имя команды: dnf provides '*/ss' или apt-file search bin/ss
dpkg -L ПАКЕТ перечисляет все файлы пакета — полезно перед правкой конфигов
rpm -qc ПАКЕТ перечисляет только конфиги (-qc это -q плюс -c для config)
когда команды нет, сначала проверить, установлен ли пакет, а не винить PATH

rpm -qf /usr/bin/ss
dpkg -S /usr/bin/ss
dpkg -L nginx-common | head -20

Чистка кэша пакетов

apt clean чистит весь кэш .deb; apt autoclean только просроченное
dnf clean all чистит кэш и метаданные
apt autoremove удаляет пакеты, поставленные как зависимости и больше не нужные; с --purge и конфиги
-y для autoremove всё равно нужен, иначе он встанет на подтверждении
перед чисткой du -sh подтверждает, что место реально занято (-s итог, -h человекочитаемо)
если в образе после apt install не сделать clean, слои раздуваются на десятки MB

du -sh /var/cache/apt /var/cache/dnf 2>/dev/null
apt clean && apt autoremove --purge -y
dnf clean all

🐳 Docker и контейнеры (8)

Девять из десяти проблем контейнера — расхождение между «как настроено по ожиданию» и «как реально работает»; сначала inspect, потом логи

docker ps: работает ли и почему перезапускается

-a включает и завершённые контейнеры — по списку только работающих не видно только что упавший
-q печатает только ID, удобно передавать в docker rm или docker logs
-s показывает размер записываемого слоя — для поиска контейнера, забившего диск
--format свои колонки; '{{.Names}} {{.Status}}' читается лучше таблицы и удобен для скриптов
--filter отбор по условию: status=exited упавшие, name=web по имени
--no-trunc не обрезает длинные ID и полные команды
-n 5 только 5 последних созданных (сокращение для --last)
Up 3 seconds (restarting) в Status означает бесконечный цикл рестартов — смотреть docker logs --tail

# все контейнеры (включая остановленные)
docker ps -a --format '{{.Names}}  {{.Status}}'

# найти остановленные и перезапускающиеся
docker ps -a --filter status=exited -n 5

# кто съел диск
docker ps -s --format '{{.Names}}  {{.Size}}'

docker logs: где лежат следы падения

-f следить за потоком, то же что tail -f
--tail 200 только последние 200 строк — у долгоживущих контейнеров их десятки тысяч, иначе нужное уедет
-t добавляет метку времени к каждой строке, обязательно при сверке с логами хоста
--since 10m только последние 10 минут; абсолютное время вида 2026-10-29T10:00:00 тоже принимается
--until вместе с --since вырезает окно
-i у grep означает игнорирование регистра: error / ERROR / Error ловятся разом
логи лежат в /var/lib/docker/containers/<ID>/<ID>-json.log и исчезают вместе с контейнером —
важные сервисы должны писать в journald или внешний сборщик, а не только внутрь docker

docker logs -f --tail 200 -t myapp
docker logs --since 10m myapp | grep -i error

docker exec: зайти внутрь и посмотреть

-it интерактивный терминал; без -t нет приглашения, без -i нельзя вводить — нужны оба
-u root зайти под root — когда пользователь по умолчанию не читает конфиг
-w /app задаёт рабочий каталог, чтобы не делать cd внутри
-e KEY=val временно подставляет переменную, чтобы проверить «не передалась ли она неверно»
-- прекращает разбор аргументов docker; обязателен, когда сама команда начинается с -
-c у bash выполняет остаток как командную строку: bash -c 'a && b' делает несколько шагов сразу
-l у ls даёт длинный формат с правами и владельцем
если exec не пускает, сначала проверить, что контейнер running: docker ps -a покажет Exited, а exec в такой всегда падает,
и проблема не в exec, а в том, что основной процесс уже завершился
нет bash в образе — пробовать sh, в alpine-образах обычно только он
в distroless нет и sh: остаётся вытащить файлы через docker cp или поднять временный контейнер для отладки

docker exec -it -u root myapp sh
docker exec myapp env | grep -i database
docker exec -it myapp bash -c 'ls -l /app'

docker inspect: конфиг, который реально действует

--format вычисляет шаблон Go, -f его сокращение
частые пути: .State.Status состояние, .State.ExitCode код выхода,
.NetworkSettings.Networks IP контейнера, .Mounts монтирования, .Config.Env переменные окружения,
.HostConfig.RestartPolicy политика перезапуска
{{json .}} выдаёт сырой JSON, дальше удобнее через jq
--type image смотрит образ, а не контейнер
значения из compose могут быть перекрыты -e в командной строке; реально действует только то, что видно в inspect
в .Config.Env часто лежат пароли и токены — фильтровать, прежде чем кому-то показывать

docker inspect -f '{{.State.Status}} exit={{.State.ExitCode}}' myapp
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' myapp
docker inspect --format '{{json .Mounts}}' myapp | jq

docker stats: потребление в реальном времени

--no-stream снимает один срез и выходит — для скриптов и периодической записи
--format свои колонки, удобно грепать и складывать в базу
-a считает и остановленные контейнеры (у них нули, но они в списке)
MEM LIMIT это предел контейнера; MEM % у сотни означает скорый OOM
CPU % считается от числа ядер, на многоядерном хосте один контейнер выше 100% — норма
если контейнер убит по OOM, stats ничего не покажет (процесса уже нет) — смотреть ExitCode 137 в docker inspect
и записи oom-kill в dmesg

docker stats --no-stream --format '{{.Name}} {{.CPUPerc}} {{.MemUsage}}'
docker stats --no-stream -a

docker build: сборка и кэш

-t ставит тег; писать ИМЯ:ВЕРСИЯ, а не только latest
-f указывает путь к Dockerfile, если он не в каталоге сборки
--no-cache полностью игнорирует кэш — первый шаг при «код поправил, а образ тот же»
--build-arg передаёт аргумент сборки; работает только если в Dockerfile есть ARG
--target останавливает сборку на нужной стадии — отладка промежуточных слоёв многостадийной сборки
--progress=plain печатает полный журнал; режим TTY по умолчанию сворачивает параллельный вывод в строку и прячет ошибки
точка в конце это контекст сборки, а не путь к Dockerfile — большой контекст заметно замедляет, лишнее в .dockerignore (node_modules, .git)

docker build -t myapp:1.2.3 .
docker build -f docker/Dockerfile.prod --build-arg NODE_ENV=prod --progress=plain -t myapp:prod .

docker compose: поднять стек и посмотреть логи

-f выбирает compose-файл; для нескольких окружений два -f, поздний перекрывает ранний
-p задаёт имя проекта — по умолчанию берётся имя каталога, и при нескольких стеках на хосте его нужно задавать явно, иначе контейнеры и сети пересекутся
-d в фоне; без него compose привязывается к терминалу и Ctrl+C останавливает стек
--build пересобрать образы перед запуском
up создаёт и запускает; down останавливает и удаляет контейнеры с сетями
logs -f --tail 100 следит за логами; имя сервиса в конце ограничивает одним
config проверяет синтаксис и печатает итоговый слитый конфиг — после правки compose сначала он, это безопаснее up
compose v2 это docker compose (пробел), v1 это docker-compose (дефис); флаги немного различаются, в скрипте сначала уточнить версию

docker compose -f docker-compose.yml -f docker-compose.prod.yml -p shop up -d --build
docker compose -p shop logs -f --tail 100 api
docker compose -p shop config

Чистка: образы и логи контейнеров съели диск

system df сводит занятое по образам / контейнерам / томам / кэшу сборки, -v разворачивает до каждого объекта
prune удаляет неиспользуемое; по умолчанию только висячие образы (<none>), с -a удаляются все, на которые не ссылается контейнер
--volumes чистит и тома — в томе может лежать база, восстановить не выйдет
--filter until=24h удаляет только старше 24 часов, чтобы не задеть используемое
порядок: сначала system df -v и понять, кто занял, потом точечный prune; не начинать с system prune -a
логи контейнеров тоже забивают диск: /var/lib/docker/containers/*/*-json.log,
лечится настройкой log-opts с max-size и max-file в daemon.json

docker system df -v
docker image prune -a --filter until=24h
docker system prune --volumes

☸️ Kubernetes (8)

Порядок разбора в K8s почти фиксирован: статус → события → логи → shell в контейнер. Пропуск шага означает догадки

kubectl get: что есть, где и в каком состоянии

-A все пространства имён — добавлять первым, когда ресурс «не существует»; обычно просто не то ns
-n задаёт пространство имён; без него берётся из текущего context
-o wide добавляет IP и узел — обязательно при разборе сети и планирования
-o yaml полное определение; экспортировать перед правкой конфига
--show-labels показывает метки — когда Service не находит Pod, почти всегда дело в метках
-l app=api отбор по метке, надёжнее грепа по именам
--sort-by=.status.startTime сортировка по времени старта, новые внизу
у ресурсов есть сокращения: pod/po, svc, deploy, rs, no, cm, secret

kubectl get pod -A -o wide
kubectl get pod -n shop -l app=api --show-labels
kubectl get pod -A --sort-by=.status.startTime

kubectl describe: первая команда при проблеме

блок Events в конце — самая информативная часть при разборе K8s:
причины ImagePullBackOff, FailedScheduling, Unhealthy и OOMKilled написаны именно там
если не указать -n и попасть не в то пространство имён, будет голый NotFound без подсказок
Events по умолчанию живут около часа, поэтому их надо забирать; не рассчитывать найти позже
в конце describe node блок Allocated resources показывает долю занятых request / limit —
если Pod висит в Pending, сначала проверить, не выбраны ли requests (а не реальную загрузку CPU)
describe показывает только конфиг и события, логи приложения это kubectl logs

kubectl describe pod -n shop api-7d9f-abc12
kubectl describe node k8s-node-2 | tail -30

kubectl logs: текущие и предсмертные

-f следить за потоком
-n выбор пространства имён; без него берётся из текущего context
--tail 200 последние 200 строк; у долгоживущего Pod без этого терминал зальёт
-c выбор контейнера — обязателен при sidecar в Pod, иначе будет ambiguous со списком контейнеров
--previous читает лог предыдущего контейнера: CrashLoopBackOff перезапускает его снова и снова,
в текущем виден только фрагмент «упал сразу после старта», а настоящая причина в предыдущем
--since 10m только последние 10 минут
--timestamps добавляет метку времени к каждой строке
ресурс можно писать как тип/имя, например deploy/api — сразу логи Pod этого Deployment

kubectl logs -f --tail 200 -n shop deploy/api
kubectl logs -n shop api-7d9f-abc12 --previous
kubectl logs -n shop api-7d9f-abc12 -c istio-proxy --since 10m

kubectl exec / cp: shell внутри и передача файлов

-it интерактивный терминал, оба флага вместе
-n задаёт пространство имён
-c выбор контейнера, обязателен для Pod с несколькими контейнерами
-- прекращает разбор аргументов kubectl; обязателен, когда команда начинается с -
cp принимает источник и цель, со стороны Pod путь пишется как pod:путь
cp требует tar внутри контейнера, на distroless сразу падает
kubectl debug временно поднимает отладочный контейнер с общим сетевым пространством имён:
--image задаёт образ для отладки (busybox достаточно), --target указывает отлаживаемый контейнер —
когда в прод-образе нет shell, это единственный верный путь и куда быстрее пересборки

kubectl exec -it -n shop api-7d9f-abc12 -- sh
kubectl cp -n shop api-7d9f-abc12:/app/logs/app.log ./app.log
kubectl debug -it -n shop api-7d9f-abc12 --image=busybox --target=api

kubectl get events: события уровня кластера

тот же источник, что блок Events в describe, но сразу по всем ресурсам — когда неизвестно, какой объект сломан
-n задаёт пространство имён
--sort-by=.lastTimestamp сортировка по времени, новые внизу
--field-selector type=Warning только предупреждения — шума сразу на порядок меньше
--field-selector involvedObject.name=api-7d9f-abc12 сужает до конкретного объекта
-A все пространства имён; проблемы уровня кластера (NotReady у узла, вытеснения) видны только с ним
в Warning от FailedScheduling приходят слова самого планировщика, например
insufficient cpu или node(s) had taint — они прямо объясняют, почему Pod не размещается,
и гадать про ресурсы или taint не нужно
TTL событий около часа, при цикле рестартов их надо сразу сохранять

kubectl get events -A --sort-by=.lastTimestamp | tail -20
kubectl get events -n shop --field-selector type=Warning
kubectl get events -n shop --field-selector involvedObject.name=api-7d9f-abc12

kubectl apply: деплой, откат, перезапуск

-f указывает файл или каталог манифеста; -f - читает из stdin
--dry-run=client не отправляет, только печатает итоговый YAML — так удобно генерировать шаблоны
--dry-run=server проверяет на сервере (с admission webhook), заметно строже client
-o yaml только и делает вывод dry-run осмысленным
rollout status ждёт завершения обновления и прямо говорит, где застряло
rollout undo откатывает на предыдущую ревизию; --to-revision=3 на конкретную, номера смотреть в history
rollout restart перезапускает Pod без смены конфига — обычный шаг после правки ConfigMap или Secret
-n задаёт пространство имён
apply декларативный и идемпотентный; create императивный и на второй раз даст AlreadyExists

kubectl apply -f deploy/api.yaml --dry-run=client -o yaml > api.gen.yaml
kubectl rollout status deploy/api -n shop
kubectl rollout undo deploy/api -n shop --to-revision=3
kubectl rollout restart deploy/api -n shop

kubectl port-forward: достучаться до сервиса кластера

формат ресурс/имя локальный_порт:удалённый_порт; NodePort или Ingress не нужны
-n задаёт пространство имён
--address 0.0.0.0 даёт доступ и другим машинам подсети; по умолчанию слушается только 127.0.0.1
трафик идёт туннелем через kube-apiserver, минуя Service —
поэтому «жив ли сам сервис» и «не режет ли NetworkPolicy» это разные вопросы, и здесь решается первый
закрыл терминал — туннель умер; для долгого использования держать в tmux или screen
порядок: port-forward работает → приложение цело, дело в Ingress / Service / NetworkPolicy;
port-forward не работает → проблема в Pod или самом порту

kubectl port-forward -n shop svc/api 8080:80
kubectl port-forward -n shop --address 0.0.0.0 svc/api 8080:80
kubectl port-forward -n shop pod/api-7d9f-abc12 5432:5432

kubectl top / drain: ресурсы узла и обслуживание

top node и top pod требуют metrics-server; без него будет Metrics API not available
--sort-by=cpu или --sort-by=memory сортирует список и выносит наверх тяжёлых
--containers разбивает Pod по контейнерам — видно, какой sidecar ест память
-A все пространства имён
Allocated resources в describe node это доля распределённых request / limit —
при отказе планирования смотреть сюда: часто завышенные requests занимают узел «на бумаге», а CPU простаивает
cordon помечает узел невыделяемым, работающие Pod не трогает
drain вытесняет Pod с узла для обслуживания; --ignore-daemonsets пропускает DaemonSet
(их вытеснять нельзя) и --delete-emptydir-data разрешает удалить Pod с emptyDir (временные данные пропадут)
после обслуживания uncordon возвращает планирование

kubectl top node
kubectl top pod -A --sort-by=memory --containers
kubectl drain k8s-node-2 --ignore-daemonsets --delete-emptydir-data

🔬 Трассировка процессов (strace / perf) (7)

top говорит «этот процесс ест CPU», но не «чего он ждёт». Эти инструменты отвечают на второй вопрос

strace -p: на каком системном вызове встал процесс

-p подключается к работающему процессу; можно и префиксом к команде, тогда трассировка с запуска
-f следит за форкнутыми детьми — без него виден только родитель в ожидании, а работа идёт в детях
-T печатает длительность каждого вызова (<0.000012>) — так и находится медленный шаг
-tt метки времени с точностью до микросекунды
-s 200 до 200 символов на строку; по умолчанию 32 и ключевые пути обрезаются в ...
-o out.txt вывод в файл: десятки тысяч строк в терминале не читаются
-c по завершении сводит число вызовов и суммарное время по типам — сначала им найти дорогой класс,
затем -T точечно показывает конкретный медленный вызов
подключился через -p и не видно вообще ничего — процесс стоит в непрерываемом IO или на блокировке ядра,
дальше смотреть /proc/PID/stack и /proc/PID/wchan
на проде осторожно: -f по всем вызовам замедляет процесс в разы, помогают -c или фильтры

strace -p 1234 -T -tt -s 200
strace -f -p 1234 -o /tmp/st.txt
strace -c -p 1234

strace -e trace: оставить только нужные вызовы

-e trace= самый полезный параметр strace, без вариантов
-e trace=file только файловые вызовы (openat, stat, unlink) — для «файл не найден» и прав
-e trace=network только сетевые (connect, sendto, recvfrom) — для «не соединяется» и таймаутов
-e trace=openat,connect через запятую, когда нужны конкретные вызовы
-e trace=desc только операции с дескрипторами — видно, на каком fd встало
-e signal=SIGSEGV только по сигналу
фильтр сжимает десятки тысяч строк до десятков — правильный порядок: сначала -c и найти дорогой класс,
затем -e trace= только по этому классу
openat с ENOENT значит пути нет, EACCES не хватает прав, EAGAIN неблокирующий вызов без данных,
и лечатся эти три совершенно по-разному — не всякую error списывать на права
остальные флаги как у strace -p: -f следит за детьми, -p задаёт процесс, -T показывает время вызова, -tt даёт микросекунды, -o пишет в файл

strace -f -e trace=file -p 1234 -T
strace -f -e trace=network -p 1234 -tt
strace -o net.txt -e trace=openat,connect -f ./app

perf top: на какую функцию уходит CPU

perf top это живой рейтинг горячих функций: PID не нужен, смотрится вся машина
-p ограничивает одним процессом
-F 99 частота 99Hz — уходить от круглых 100Hz, чтобы не поймать ложные хиты от таймерных тиков
-g показывает граф вызовов; здесь обзор ограничен, настоящие цепочки даёт record
unknown вместо имени функции значит нет debuginfo или символы срезаны strip —
perf не сломан, просто бинарник без таблицы символов
нужен root или ослабленный kernel.perf_event_paranoid; в контейнере обычно --privileged или SYS_ADMIN
отличие от top: top отвечает «какой процесс», perf top «какая функция», и оптимизация начинается со второго

perf top
perf top -p 1234 -F 99
perf top -g

perf record: сэмплы для флеймграфа

record пишет сэмплы, report смотрит их интерактивно, script выгружает текст — трио
-F 99 сэмплировать 99 раз в секунду
-g записывать цепочки вызовов; для флеймграфа обязателен, иначе будут только разрозненные функции
-p задаёт процесс; -a собирает со всех CPU (вся система)
--call-graph dwarf разворачивает стек по DWARF, точнее дефолтного fp,
ценой файла данных на порядок больше; если бинарник собран с -fomit-frame-pointer, годится только этот вариант
-o файл вывода, по умолчанию perf.data в текущем каталоге
-i говорит report / script, какой файл читать
-- ставится перед командой: perf сам запускает программу и снимает профиль с самого старта
для флеймграфа нужен свёрнутый вывод script, дальше его обрабатывает stackcollapse из FlameGraph
в контейнере perf_event_paranoid обычно равен 2 и символы ядра не видны,
полная картина требует sysctl kernel.perf_event_paranoid=-1

perf record -F 99 -g -p 1234 -o /tmp/perf.data
perf report -i /tmp/perf.data
perf script -i /tmp/perf.data > /tmp/perf.txt

pidstat: CPU, IO и память по процессу

pidstat 1 печатает раз в секунду и обновляется; второе число ограничивает число итераций
-p ALL все процессы; -p 1234 только один
-d дисковый IO (kB_rd/s чтение, kB_wr/s запись, iodelay в миллисекундах)
-r память (страничные промахи, RSS)
-u загрузка CPU (режим по умолчанию)
-t разбивка по потокам — видно, какой поток ест CPU, обязательно для многопоточных программ
-w переключения контекста: cswch/s добровольные (ожидание ресурса), nvcswch/s вынужденные (вытеснение)
входит в пакет sysstat, на минимальных установках его надо доставить
лучше top тем, что умеет долго снимать с фиксированным интервалом в файл, а потом считать средние и строить графики

pidstat -p 1234 1 10
pidstat -d -p ALL 1 5
pidstat -t -p 1234 1

iostat / vmstat: узкие места диска и памяти

iostat -x 1 расширенная статистика с обновлением раз в секунду
-x включает расширенные метрики; без него таблица куда беднее
%util у 100% означает, что диск загружен полностью; await это среднее ожидание одного IO в миллисекундах
rkB/s и wkB/s это объём в секунду — большой await при малом throughput говорит о медленном диске,
большой await при большом throughput о реальной нагрузке; направления масштабирования разные
vmstat 1 строка в секунду: r длина очереди на CPU (устойчиво выше числа ядер — не хватает CPU),
b число заблокированных процессов, si / so подкачка (не ноль — память кончилась и пошёл swap)
wa доля времени CPU в ожидании IO; высокое значение указывает на диск, а не на процессор
-d в vmstat это статистика диска, выводятся только дисковые колонки
первая строка это средние с момента загрузки, живые значения начинаются со второй — по первой не делать выводов

iostat -x 1 5
vmstat 1 5
vmstat -d 1

gdb / gcore: снять стек при зависании

когда процесс завис, а strace молчит (непрерываемое состояние), нужен стек в user space
gdb -p приостанавливает процесс на время подключения — обязательно detach перед выходом, иначе он останется стоять, то есть сервис ляжет
-ex выполняет одну команду gdb и идёт дальше; их можно цепочкой: подключиться, снять стеки, отцепиться
thread apply all bt снимает полный стек всех потоков; для однопоточных хватает bt
gcore создаёт core-файл для спокойного разбора потом — безопаснее живой отладки
-o задаёт путь для core (суффикс .PID не добавляется)
на JVM gdb не применять — Java-стеки он не понимает, нужен jstack
--call-graph у perf снимает точки во времени, а для зависания нужен полный стек — одно другое не заменяет

gdb -p 1234 -ex 'thread apply all bt' -ex detach
gcore -o /tmp/core.1234 1234

⌨️ Vim и эффективность терминала (6)

На серверах часто только vi, правку конфига им не обойти. Хватит 20 клавиш, учить всё не нужно

Открыть, сохранить, выйти

vim +120 файл сразу открывает 120-ю строку — самый быстрый способ перейти по номеру из лога ошибки
-u NONE не грузит ни конфиг, ни плагины — чтобы исключить сломанный vimrc
-w /tmp/keys.log пишет все нажатия — разобраться, что именно было нажато
:w сохранить; :w! записать принудительно (файл только для чтения или нет прав)
:q выход, при несохранённых изменениях откажет; :q! выход с отбрасыванием правок
:wq или :x сохранить и выйти; :x не пишет файл, если изменений не было
:e! отбрасывает правки и перечитывает файл — когда всё испорчено и нужен чистый старт
:saveas /tmp/a.conf сохранить как — стандартный шаг бэкапа перед правкой конфига
случайно правил файл только для чтения: :q! отменяет быстрее всего, кнопку сохранения искать не надо

vim +120 /etc/nginx/nginx.conf
vim -u NONE /etc/nginx/nginx.conf

:w      保存        :q!     выйти без сохранения изменений
:wq     保存退出    :x      сохранить, только если были изменения
:e!     重新加载    :saveas /tmp/a.conf  сохранить как

Навигация: читать, не меняя

gg первая строка; G последняя; 42G переход на 42-ю
0 начало строки; ^ первый непробельный символ (нужен при выравнивании отступов); $ конец строки
w начало следующего слова; b начало предыдущего; e конец текущего
Ctrl+f страница вниз; Ctrl+b вверх; Ctrl+d полстраницы вниз; Ctrl+u вверх
% переход к парной скобке — быстрый способ найти конец блока конфига
'' возврат на прошлую позицию (два апострофа); Enter тоже работает
на больших файлах не пользоваться стрелками: hjkl и эти переходы куда быстрее
перед правкой :set nu, иначе номер строки из ошибки ничего не даёт

gg        文件首行            G        последняя строка файла
42G       第 42 行            0        начало строки
^         行首首个非空白      $        конец строки
w / b / e 下词首 / 上词首 / 词尾
Ctrl+f    下一页              Ctrl+b   страница назад
%         跳到匹配的括号      ''       вернуться к прошлой позиции

Правка: удалить, копировать, вставить, отменить

dd удаляет строку — фактически это вырезание, p вставит обратно
yy копирует строку; 5yy копирует пять строк
p вставка под текущей строкой; P над ней
u отмена; Ctrl+r повтор (когда отменил лишнего)
x удалить символ; r заменить символ под курсором, не входя в режим вставки
cc переписать строку целиком: содержимое стирается, отступ сохраняется
d$ удалить до конца строки; d0 до начала; dw одно слово
. повтор последнего изменения — самая ценная клавиша Vim: 20 строк это dd и 19 нажатий .
i вставка перед курсором; a после; o новая строка ниже; O выше
Esc возвращает в обычный режим; забыть его — и в текст уйдёт мусор

dd / yy / p    удалить строку / скопировать строку / вставить
5yy            скопировать 5 строк
u / Ctrl+r     отменить / повторить
x / r          удалить символ / заменить один символ
cc             переписать всю строку (с сохранением отступа)
.              повторить последнее изменение (самая ценная клавиша)
i / a / o / O  вставка перед курсором / после курсора / новая строка ниже / новая строка выше

Блочные операции и массовые правки

Ctrl+v включает блочное выделение: двигая курсор, охватываем колонку — так делают массовые комментарии и выравнивание
с выделением I вставляет сразу в начало каждой строки, и применяется только по Esc (забыл Esc — кажется, что ничего не произошло)
A дописывает в конец каждой выделенной строки
c заменяет весь блок, d удаляет его
:g/pattern/d удаляет все совпадающие строки
:g/pattern/s/old/new/g заменяет только внутри совпадающих строк — безопаснее глобальной замены
:sort сортирует выделенные строки; :sort u сортирует и убирает дубли
:normal @a выполняет макрос из регистра a по разу на каждую выделенную строку — для однотипной работы
сначала :set nu, чтобы в блочных операциях не промахнуться с диапазоном

Ctrl+v  I  Esc      после выделения блока вставить в начало каждой строки (массовый комментарий)
Ctrl+v  A  Esc      после выделения блока дописать в конец каждой строки
:g/^#/d             удалить все строки, начинающиеся с #
:g/ERROR/s/old/new/g  заменять только в строках с ERROR
:sort u             отсортировать и убрать дубли в выделенных строках

Кривая вставка, переводы строк, кодировка

вставка кода из браузера или Windows и отступы уезжают вправо: сначала :set paste, после вставки :set nopaste
(или :set pastetoggle=<F2> и переключать по F2)
файл правился в Windows и в концах строк ^M: :set ff=unix, затем :w
один :set ff показывает, dos сейчас или unix
кракозябры в тексте: :set fileencoding=utf-8 и сохранить; если нужно только прочитать, не меняя файл,
использовать :e ++enc=gbk и перечитать как GBK
:w !sudo tee % решает «открыл без sudo, теперь не сохранить» —
содержимое пишется через sudo под текущим именем, быстрее чем открывать заново и править снова
все эти настройки на сессию; чтобы закрепить — в ~/.vimrc

:set paste           перед вставкой отключить автоотступ
:set nopaste         после вставки вернуть обратно
:set ff=unix         убрать ^M из Windows
:set fileencoding=utf-8   исправить кодировку и сохранить
:w !sudo tee %      принудительно записать, если забыли sudo

🔥 Методики разбора (6)

«Сервер тормозит» — не одна проблема, а четыре: CPU, память, IO, сеть. Одна команда покажет, какая

Первый взгляд: какой слой тормозит

uptime показывает три значения load average: за 1/5/15 минут; выше числа ядер означает очередь
если load высокий, а CPU свободен (высокий %id в top), это почти всегда ожидание IO — смотреть колонку wa
в free -h читать колонку available, а не free
df -h и df -i смотреть вместе: -h это ёмкость, -i это inode; полный диск даёт самые странные отказы
vmstat 1 5 снимает раз в секунду пять раз: r (очередь), b (заблокированные), wa (ожидание IO), si/so (подкачка)
сначала определить слой, потом действовать — куда эффективнее, чем сразу перезапускать сервис

uptime
free -h
df -h; df -i
vmstat 1 5

Высокий load при низком CPU

сначала колонка wa в vmstat: устойчиво выше 10 означает, что тянет IO
затем b (заблокированные процессы): выше числа ядер — многие стоят в ожидании
найти виновника: pidstat -d 1 5 (-d только статистика IO, раз в секунду, пять раз)
в ps -eo флаг -e берёт все процессы, -o задаёт колонки; в state видно D (непрерываемый сон)
awk '$1=="D"' отбирает процессы в состоянии D — они и стоят на IO
типичные причины: полный скан таблицы, синхронная запись логов, идущий бэкап, капризный NFS или облачный диск
процесс в D не убивается — ждать возврата IO или перезагружать машину

vmstat 1 5
pidstat -d 1 5
ps -eo state,pid,cmd | awk '$1=="D"'

Хватает ли памяти на самом деле

в free -h именно available показывает, сколько ещё дадут новым процессам; низкий free это норма — Linux держит свободную память как кэш
высокий buff/cache не проблема: освобождается по требованию
активный swap означает реальное давление; устойчивые ненулевые si/so в vmstat это уже трешинг
сортировка по памяти: ps -eo pid,rss,comm --sort=-rss (-e все, -o колонки, минус перед ключом даёт убывание)
RSS реальное потребление, VSZ виртуальное адресное пространство и почти бесполезно
в контейнере важен лимит cgroup (/sys/fs/cgroup/memory.max), а не total хоста
при подозрении на утечку сравнить два среза RSS с интервалом в десять минут: у кого число только растёт

free -h
vmstat 1 5    # смотреть si / so
ps -eo pid,rss,comm --sort=-rss | head

Медленный эндпоинт: замерить каждый шаг

слой 1: curl -w разбивает на DNS, соединение, первый байт и общее время
curl -o /dev/null выбрасывает body, -s глушит прогресс, -w печатает свой формат — вместе остаются только цифры
слой 2: curl 127.0.0.1 на самом сервере, чтобы исключить сеть и шлюз
слой 3: сравнить время запроса в логах приложения с $request_time и $upstream_response_time в nginx
большой разрыв означает задержку в промежуточном слое (шлюз, прокси, пул соединений)
не забыть медленный журнал базы: многие «медленные API» на деле медленный SQL
высокое общее время при низком upstream обычно означает лимитирование или очередь

curl -o /dev/null -s -w 'dns:%{time_namelookup} conn:%{time_connect} ttfb:%{time_starttransfer} total:%{time_total}\n' https://example.com/api
# добавить в nginx log_format:
# $request_time $upstream_response_time

Потери пакетов и джиттер

ping -c 50 отправляет 50 пакетов и показывает loss% и mdev (джиттер); один ping ничего не доказывает
ping -M do -s 1472 проверяет MTU пути: -M do запрещает фрагментацию, -s задаёт размер, отказ означает проблему MTU
mtr -r -c 50: -r печатает отчёт вместо живого экрана, -c задаёт число пакетов, и в отличие от traceroute видно потери на каждом хопе
ICMP часто лимитируют, и это выглядит как потери; TCP-проба (nc -z или curl) точнее
ethtool -S СЕТЕВАЯ_КАРТА показывает счётчики ошибок и потерь rx/tx (-S это statistics)
grep -iE означает игнорирование регистра плюс расширенное регулярное выражение
потери внутри одного зала — сначала сетевой адаптер и кабель; между регионами — канал оператора

ping -c 50 10.0.0.5
mtr -r -c 50 10.0.0.5
ethtool -S eth0 | grep -iE 'err|drop'

Экспресс-проверка одной строкой

на незнакомой машине сначала прогнать эту серию и получить общую картину, а потом решать, что копать
каждая строка подписана вопросом, на который отвечает — не копировать вслепую
шпаргалка по флагам (всё разобрано выше): --sort=-%cpu по убыванию CPU, -h человекочитаемо, -i inode
grep -vE это «исключить плюс расширенное регулярное выражение» — здесь отсекает виртуальные ФС вроде tmpfs
ss -tulnp раскладывается как t/TCP, u/UDP, l/listening, n/числа, p/процесс
у systemctl --failed оставляет только упавшие юниты, --no-pager отключает пейджинг
у dmesg -T даёт читаемое время, -l фильтрует по уровню
ничего пишущего на проде: весь набор только читает, вывод можно сразу вставлять в тикет

echo "== 负载 =="; uptime
echo "== CPU 前 5 =="; ps aux --sort=-%cpu | head -6
echo "== 内存前 5 =="; ps aux --sort=-%mem | head -6
echo "== 内存 =="; free -h
echo "== 磁盘 =="; df -h | grep -vE 'tmpfs|overlay'
echo "== inode =="; df -i | grep -vE 'tmpfs|overlay'
echo "== 监听端口 =="; ss -tulnp
echo "== 失败服务 =="; systemctl list-units --failed --no-pager
echo "== 内核错误 =="; dmesg -T -l err | tail -10