Перейти к содержимому

Shell и утилиты

Shell — это не «чёрное окно», а программа-интерпретатор (bash, zsh, dash, sh), которая читает строку, раскрывает её (подстановка переменных, ~, глоббинг *, подстановка команд $(...), арифметика $((...)), разбиение на слова), затем через fork/execve запускает процессы и связывает их файловыми дескрипторами. Ключевая модель в голове: у каждого процесса есть три стандартных дескриптора — 0 (stdin), 1 (stdout), 2 (stderr); пайп | соединяет stdout левой команды с stdin правой через анонимный pipe в ядре; редиректы >, 2>, &>, <, >> переназначают эти дескрипторы. Отсюда сразу следуют правильные ответы на кучу вопросов: почему cmd > f 2>&1 и cmd 2>&1 > f работают по-разному (порядок применения слева направо), почему grep x file | wc -l быстрее, чем цикл на bash (данные не проходят через интерпретатор), почему sudo echo x > /root/f не работает (редирект выполняет shell под вашим uid, а не sudo).

Второй столп — код возврата. Каждый процесс завершается числом 0..255, 0 — успех, всё остальное — ошибка; в shell он доступен как $?. На нём строятся &&, ||, if, set -e. Отдельно стоит помнить, что в пайплайне по умолчанию $? — это код последней команды, поэтому в скриптах пишут set -euo pipefail, а для разбора кодов всех звеньев есть массив PIPESTATUS. Процесс, убитый сигналом N, отдаёт shell код 128+N — отсюда знаменитые 137 (128+9, SIGKILL, чаще всего OOM-killer или docker kill) и 143 (128+15, SIGTERM). Это ровно то, что спрашивают под видом «а что означает exit code 137 у пода».

Третий столп — набор утилит и то, что они все говорят на общем языке «текст, разделённый на строки». Классический инструментарий делится на: просмотр и фильтрацию (cat, less, grep, head, tail -f), трансформацию (sed, awk, cut, tr, sort, uniq, jq для JSON), диагностику процессов и ресурсов (ps, top/htop, uptime, vmstat, iostat, pidstat, free, df, du), сеть (ss, ip, dig, curl, tcpdump) и трассировку (strace, ltrace, perf, lsof). Для Go-инженера это язык, на котором ведётся разбор инцидента на проде до того, как вы дойдёте до pprof: сначала «кто ест CPU/диск/память», потом уже профиль процесса.

Отдельно про интерпретацию нагрузки, потому что именно на этом чаще всего валятся. Load average в Linux — это не «загрузка CPU в процентах», а усреднённое число задач, которые либо исполняются, либо готовы исполняться, либо застряли в непрерываемом ожидании (обычно диск). Поэтому диагностика всегда двухшаговая: сначала смотрим LA относительно числа ядер, затем выясняем, чем именно она набрана — CPU или I/O.

Load Average в top показывает три значения (1, 5, 15 минут). Что они означают и как интерпретировать?

Заголовок раздела «Load Average в top показывает три значения (1, 5, 15 минут). Что они означают и как интерпретировать?»

Коротко. Это среднее число задач в очереди на выполнение, экспоненциально сглаженное с постоянными времени 1, 5 и 15 минут. В Linux сюда попадают не только процессы в состоянии R (исполняются или готовы исполняться), но и процессы в непрерываемом сне D (обычно ждут диск или сеть в NFS), поэтому LA — это метрика «нагрузки на систему в целом», а не «загрузки CPU». Интерпретируют её всегда делением на число логических ядер (nproc): LA ≈ числу ядер — система работает «под завязку», LA заметно больше — задачи стоят в очереди. Сравнение трёх чисел даёт тренд: если 1-минутное больше 15-минутного, нагрузка растёт, если меньше — пик уже прошёл.

Глубже. Считает это ядро в kernel/sched/loadavg.c: раз в LOAD_FREQ (5 секунд) берётся мгновенный снимок числа задач в состояниях TASK_RUNNING и TASK_UNINTERRUPTIBLE, и им обновляются три экспоненциальных скользящих средних (EWMA) по формуле load = load * exp(-5/60/T) + n * (1 - exp(-5/60/T)). Значения хранятся в fixed-point (FSHIFT = 11), константы затухания — EXP_1 = 1884, EXP_5 = 2014, EXP_15 = 2037. Отсюда два практических следствия: во-первых, это выборка раз в 5 секунд, поэтому короткие всплески LA может вообще не заметить; во-вторых, из-за экспоненциального сглаживания «1 минута» означает не «среднее ровно за последнюю минуту», а «вес последней минуты около 63%». Прочитать сырые значения можно из /proc/loadavg: там же 4-е поле вида 3/512 — число runnable-задач к общему числу задач, и 5-е — последний выданный PID.

Включение TASK_UNINTERRUPTIBLE — это специфика именно Linux (в классических UNIX учитывались только runnable-задачи), сделано ещё в начале 1990-х, чтобы «нагрузка» отражала и I/O-затык, а не только CPU. Практический вывод: LA = 30 на 8-ядерной машине может означать и «30 потоков жгут CPU», и «один медленный диск, и 30 процессов висят в D на read()». Различать надо по другим метрикам: %wa (iowait) в top, iostat -xz 1 (%util, await), список процессов в D-состоянии (ps -eo state,pid,comm | awk '$1 ~ /^D/'). Более точная современная замена LA — PSI (Pressure Stall Information, ядро 4.20+): /proc/pressure/cpu, /proc/pressure/io, /proc/pressure/memory показывают в процентах, сколько времени задачи реально простаивали из-за нехватки конкретного ресурса, и, в отличие от LA, нормированы и не требуют деления на число ядер.

Отдельная ловушка, которую любят на собеседованиях про Kubernetes: /proc/loadavg внутри контейнера по умолчанию не изолирован cgroup’ой и показывает нагрузку всей хост-машины. Поэтому Java/Go-рантаймы и мониторинг, ориентирующиеся на LA внутри пода, видят чужую нагрузку; корректно смотреть на cgroup-метрики (cpu.stat, поле throttled_usec в cgroup v2) и на PSI. По той же причине бессмысленно алертить на абсолютное значение LA — алертить надо на LA / nproc и на длительное превышение, а не на мгновенный всплеск.

Окно терминала
uptime
# 15:04:12 up 41 days, 2:11, 3 users, load average: 12.31, 8.02, 4.55
nproc # 8 -> LA1/nproc ≈ 1.54, очередь в полтора раза длиннее числа ядер
cat /proc/loadavg # 12.31 8.02 4.55 5/1024 31337
top -b -n1 | head -5 # смотрим %Cpu(s): us / sy / wa
ps -eo state,pid,comm | awk '$1 ~ /^D/' # кто висит в непрерываемом сне
cat /proc/pressure/io # some avg10=... — реальное давление по I/O

Коротко. scp file user@host:/path/ для разового копирования, scp -r dir user@host:/path/ для каталога, и rsync -avzP -e ssh src/ user@host:/dst/ — если файлов много, копирование надо возобновлять или синхронизировать инкрементально. Всё это работает поверх обычного SSH-соединения, то есть подхватывает ваш ~/.ssh/config, ключи и агента.

Глубже. Практический набор:

Окно терминала
# локальный -> удалённый
scp ./app.tar.gz deploy@10.0.0.5:/srv/releases/
scp -P 2222 ./app.tar.gz deploy@host:/srv/ # ВНИМАНИЕ: у scp порт это -P, у ssh -p
scp -r ./configs deploy@host:/etc/myapp/ # каталог рекурсивно
scp -C -p ./big.bin host:/data/ # -C сжатие, -p сохранить mtime/права
# удалённый -> локальный
scp deploy@host:/var/log/app/app.log ./
scp 'host:/var/log/app/*.log' ./logs/ # кавычки: глоббинг раскрывает удалённый shell
# синхронизация — почти всегда лучше scp
rsync -avzP --delete -e 'ssh -p 2222' ./build/ deploy@host:/srv/app/
# -a архивный режим, -v verbose, -z сжатие, -P = --partial --progress (докачка)
# завершающий '/' у src означает «содержимое каталога», без него — «сам каталог»
# интерактивно, с докачкой отдельных файлов
sftp deploy@host # затем get/put/reget/reput
# много мелких файлов: один поток вместо тысяч round-trip'ов
tar czf - ./dir | ssh host 'tar xzf - -C /dst'
ssh host 'tar czf - /var/log/app' | tar xzf - -C ./backup
# просто прокачать поток
ssh host 'cat /etc/hosts' > hosts.local
cat dump.sql | ssh host 'mysql mydb'

Что стоит знать про версии и подводные камни. Начиная с OpenSSH 9.0 (2022) scp по умолчанию использует протокол SFTP вместо старого протокола rcp; вернуть легаси-поведение можно флагом -O. Это ломает редкие сценарии, где на удалённой стороне полагались на раскрытие путей удалённым shell’ом, зато закрывает класс уязвимостей вида CVE-2020-15778 и «злой сервер подсовывает лишние файлы» (CVE-2019-6111). Там же копирование между двумя удалёнными хостами (scp host1:f host2:f) стало по умолчанию идти транзитом через локальную машину, как раньше делал флаг -3. Сам по себе scp — «устаревший и негибкий» по формулировке разработчиков OpenSSH, поэтому в скриптах лучше rsync или sftp.

Дальше — вещи, которые отличают уверенный ответ. scp не умеет докачку: оборвался — начинай сначала; rsync --partial и sftp reget умеют. Копирование через бастион делается не «сначала на бастион, потом дальше», а через ProxyJump: scp -J bastion@edge file host:/path или Host prod\n ProxyJump bastion в ~/.ssh/config. Для монтирования удалённого каталога есть sshfs. Для многогигабайтных файлов на быстром канале -C может замедлить (упрётесь в CPU на сжатии), а rsync -z полезен на медленном. Права и владелец: scp сохраняет режим только с -p, владельца не сохраняет вообще, rsync -a сохраняет владельца только под root (--numeric-ids при разных базах пользователей). И самая частая бытовая ошибка — путать -P (порт у scp) с -p (порт у ssh, «сохранить времена» у scp).

Коротко. cat, cut, awk, sed, tar, top, tee, who, env, dig — и дальше по вкусу: ssh, scp, pwd, man, seq, tac, cmp, ldd, xxd, git. Вопрос проверяет не память, а бытовую беглость в shell: называйте команды группами по назначению и будьте готовы к уточняющему «а что делает tee?» или «чем tac отличается от cat?».

Глубже. Разумно отвечать так, чтобы каждый пункт сразу нёс смысл:

КомандаЧто делаетТипичное применение
catсклеивает файлы в stdoutcat /proc/loadavg, cat a b > c
cutвырезает колонки по разделителю/позициямcut -d: -f1 /etc/passwd
awkпострочный язык обработки полейawk '{sum+=$3} END{print sum}'
sedпотоковый редакторsed -i 's/old/new/g' conf.yaml
tarархивирование (само по себе без сжатия)tar czf app.tgz ./app
topинтерактивный монитор процессовсмотрим LA, %wa, топ по CPU
teeпишет stdin и в файл, и в stdoutcmd | sudo tee /etc/f (обход проблемы sudo >)
whoкто залогинен (из utmp)who -b — время загрузки системы
envпечатает/подменяет окружениеenv -i cmd, env FOO=1 ./app
digDNS-запросыdig +short api.example.com A

Ещё десяток на добавку, если попросят продолжить: ssh и scp (удалённый доступ и копирование), pwd (текущий каталог), man (документация), seq (генерация чисел), tac (cat наоборот, построчно с конца), cmp (побайтовое сравнение файлов), ldd (список динамических зависимостей ELF — сразу видно, статический ли бинарь Go), xxd (hex-дамп), nft/git/jq-подобные — уже инструментальные.

Пара уточнений, которые отделяют «зазубрил список» от «работаю в терминале каждый день». Часть трёхбуквенных имён — это встроенные команды shell (builtins), а не программы в /usr/bin: set, pwd, fg, bg, cd — их не найдёт which, зато покажет type -a pwd. awk и sed в большинстве дистрибутивов — это симлинки на конкретные реализации (gawk/mawk, GNU sed), и поведение отличается: sed -i без аргумента работает в GNU, но требует суффикса в BSD/macOS. dig живёт в отдельном пакете (dnsutils/bind-utils) и в минимальных контейнерах его нет — там останется только getent hosts или busybox nslookup. Полезно уметь и самому построить такой список: compgen -c | grep -x '...' | sort -u в bash перечислит все доступные команды ровно из трёх символов.

  • Называют load average «загрузкой CPU в процентах» и не знают, что в Linux в неё входят процессы в непрерываемом сне (D), то есть I/O-затык неотличим от CPU-затыка по одному LA.
  • Оценивают LA в абсолютных числах, не деля на nproc: «LA=8 — катастрофа» на 32-ядерной машине это половина запаса, а на одноядерной — восьмикратная перегрузка.
  • Не читают тренд по трём числам и не понимают, что это экспоненциальные средние с выборкой раз в 5 секунд, а не «ровно среднее за минуту» — из-за чего ждут мгновенной реакции метрики на всплеск.
  • Забывают, что /proc/loadavg внутри контейнера показывает нагрузку хоста, и строят на этом алерты для подов вместо cgroup-метрик и PSI.
  • Путают -P и -p у scp/ssh, а на вопрос про большой каталог не вспоминают rsync (докачка, инкрементальность) и tar | ssh (много мелких файлов).
  • Считают, что scp сохраняет права и владельца по умолчанию, и что оборванную передачу можно продолжить.
  • На «10 трёхбуквенных команд» перечисляют двухбуквенные (ls, cp, mv, rm, ps, df, du, wc, ip, ss) — стоит хотя бы отследить длину, а лучше сгруппировать ответ по назначению и коротко пояснить каждую.
  • Не различают builtins и внешние бинари (pwd, set, fg — встроенные), из-за чего не могут объяснить, почему sudo cd /root не работает и почему time ведёт себя по-разному в bash и как /usr/bin/time.