Troubleshooting — Linux & Bash для Junior Data Engineer
База знаний типичных ошибок курса Linux & Bash для Junior Data Engineer.
Категория
Симптомы
- При попытке запустить скрипт через `./script.sh` shell отвечает: `bash: ./script.sh: Permission denied` или `zsh: permission denied: ./script.sh`. При этом `cat script.sh` читает файл нормально, `ls -l` показывает файл как существующий.
Причина
У файла нет execute-бита: `-rw-r--r--` вместо `-rwxr-xr-x` Файл принадлежит другому пользователю, а у вас нет прав execute даже через group/other Файловая система примонтирована с опцией `noexec` (`/tmp` на ужесточённых серверах) Файл лежит на FAT/exFAT/NTFS — там нет UNIX-permissions, права назначаются опциями монтирования SELinux/AppArmor блокирует execute несмотря на правильные UNIX-перми Файл — symlink на несуществующий target Имя файла начинается с `-` и shell интерпретирует его как флаг
Решение
- Добавить execute-бит:
chmod +x script.sh(всем) илиchmod u+x script.sh(только owner) - Если фс смонтирована noexec — запустить через интерпретатор явно:
bash script.sh - Перенести скрипт в обычную фс (
/home,/opt) если изначально лежит на noexec /tmp - Сменить владельца если файл root-owned:
sudo chown $USER script.sh - Для SELinux deny — проверить
audit2allowилиsudo setenforce 0для теста (НЕ для prod) - Если имя начинается с
-: использовать./-script.shилиbash -- -script.sh
Симптомы
- Вы запускаете команду или скрипт и получаете `bash: ./script.sh: No such file or directory`, при этом `ls` показывает script.sh в текущей директории, файл точно есть. Или: `python script.py` — `python: can't open file '/path/script.py': [Errno 2] No such file or directory`, хотя путь корректный.
Причина
Опечатка в имени файла (lower/upper case, regular vs latin l/I/1/0/O) Не та рабочая директория — вы в `~/proj` а файл в `~/proj/src` Файл имеет CRLF line endings (Windows) и shebang выглядит как `#!/bin/bash\r` — ядро ищет интерпретатор `/bin/bash\r` Shebang указывает на несуществующий путь (`#!/usr/local/bin/python` когда python в `/usr/bin/python`) Файл — symlink на несуществующий target (broken symlink) Имя файла содержит невидимые Unicode-символы (например NBSP вместо обычного пробела) У directory выше нет permission `x` (search) — даже cd не пройдёт
Решение
- Проверить cwd через
pwd, перейти в правильную директорию - Убрать CRLF:
sed -i 's/\r$//' script.shилиdos2unix script.sh - Исправить shebang на портабельный:
#!/usr/bin/env bashвместо жёсткого пути - Если broken symlink — пересоздать:
rm broken-link && ln -s real-target broken-link - Удалить и перепечатать имя файла, если подозрение на Unicode-typos
- Проверить permissions на родительских директориях:
namei -m /full/path/to/script.sh
Симптомы
- Запускаете команду и получаете: `bash: jq: command not found` или `zsh: command not found: rg`. При этом вы уверены, что устанавливали этот пакет, или команда работает у коллеги.
Причина
Пакет не установлен в системе Пакет установлен, но бинарь не в `$PATH` (например, в `~/.local/bin` или `/usr/local/bin` для brew на M1 macOS) Сессия началась до установки — `$PATH` не обновился, нужно `source ~/.bashrc` или новый shell Бинарь установлен в venv/conda, но env не активирован В cron / systemd — наследуется минимальный $PATH без пользовательских директорий Имя команды поменялось между версиями (`ifconfig` -> `ip a`, `netstat` -> `ss`) Hash table bash закеширована старая отсутствующая запись (`hash -r` помогает)
Решение
- Установить пакет:
sudo apt install jq(Ubuntu/Debian) илиbrew install jq(macOS) - Добавить директорию в PATH:
export PATH="$HOME/.local/bin:$PATH"в~/.bashrc - После установки обновить cache bash:
hash -r - В cron — указывать абсолютный путь:
/usr/local/bin/jqили экспортить PATH в скрипте - Активировать venv:
source venv/bin/activateилиconda activate envname - Найти правильное имя в новой версии (apt search, brew search)
Симптомы
- Пытаетесь использовать `cmd "hello!"` или history expansion вроде `!$`, `!!`, `!-1`, и получаете: `bash: !$: event not found` или в zsh: `zsh: event not found: ...`. Восклицательный знак внутри двойных кавычек интерпретируется как history expansion.
Причина
Восклицательный знак в double-quoted строке триггерит history expansion в interactive shell В скриптах (non-interactive) этого нет, но в копировании команд из примеров — да zsh с EXTENDED_GLOB включает дополнительные history-операторы Bash 5.x по умолчанию имеет histexpand включённым в interactive mode
Решение
- Использовать одинарные кавычки:
echo 'hello!'вместоecho "hello!" - Эскейпить:
echo "hello\!"(но в bash 5.x не всегда работает корректно) - Отключить history expansion в interactive shell:
set +H(для bash) — в~/.bashrc - В zsh:
setopt NO_BANG_HIST - В скриптах проблемы нет — там shell non-interactive, histexpand отключён по умолчанию
Симптомы
- На своей машине (zsh, macOS) скрипт работает. На сервере (bash, Ubuntu) — синтаксические ошибки, неожиданные пустые переменные, regex не матчит, glob expansion даёт другие результаты.
Причина
Shebang `#!/bin/sh` — на Ubuntu это dash (минимальный POSIX), на macOS — bash в режиме POSIX. Различные расширения недоступны Использование bash-isms (`[[ ]]`, arrays, `$'...'`, `<<<`) в `#!/bin/sh` Zsh имеет глобальные различия: NULL-glob (пустой `*.csv` пропускается, не ошибка); читает массивы 1-indexed (`arr[1]` — первый!); split строк не делается по словам автоматически Bash и zsh имеют разный синтаксис для substring: `${var:0:5}` работает в обоих, но zsh поддерживает доп. формы Прямой запуск через `sh script.sh` игнорирует shebang
Решение
- Явный shebang:
#!/usr/bin/env bashдля bash-скриптов,#!/bin/shТОЛЬКО для POSIX-only - Запускать через нужный интерпретатор:
bash script.shа неsh script.sh - Использовать
shellcheck— он флагнет bash-isms в#!/bin/shскриптах - Для портабельности — писать в POSIX sh (без
[[ ]], без arrays); или явно требовать bash 4+ - Тестировать скрипты на целевой системе перед deploy
Симптомы
- Вы запустили `rm -rf` с неправильным аргументом — либо переменной, которая раскрылась в `/` или пустую строку, либо нажали Enter раньше чем закончили путь. Файлы пропали. На системных файлах — система разваливается (бинари не найдены, ssh падает после reconnect).
Причина
Опечатка: `rm -rf / tmp/cache` вместо `rm -rf /tmp/cache` (пробел!) Unset переменная: `rm -rf $DIR/*` где DIR пуст — стало `rm -rf /*` Symlink указывал в неожиданное место Скрипт без `set -u` и без проверок аргументов Запустили `find / -delete` с неправильным фильтром
Решение
- Прекратить ЛЮБУЮ запись на пострадавший диск — каждая запись может перезаписать освобождённые блоки
- Если файл всё ещё открыт процессом —
cp /proc/<pid>/fd/<n> recovered-file(для логов, БД) - Восстановить из backup: rsync snapshots, BTRFS/ZFS snapshots, восстановление из дампа
- Для ext4 с journaling —
extundeleteилиdebugfsмогут спасти recently deleted (без гарантий) - Для XFS —
xfs_undelete(тоже без гарантий) - Если удалены системные файлы — boot с live USB, mount раздел, восстановить из backup или reinstall
- ПРОФИЛАКТИКА: alias rm='rm -i', set -u в скриптах, проверка переменных перед rm, всегда echo сначала:
echo rm -rf "$DIR"/*
Симптомы
- Запускаете `mv * old/`, `rm *.log`, `cp -t dest/ *.csv` — получаете: `bash: /bin/mv: Argument list too long` или `-bash: /bin/rm: Argument list too long`. Файлов в директории очень много (десятки/сотни тысяч).
Причина
Glob `*` раскрывается shell ДО запуска команды в огромный список аргументов Лимит ядра ARG_MAX — максимальный размер argv+envp при `execve()`. Обычно ~2MB на Linux Большие имена файлов или много мелких файлов одинаково забивают лимит Команды-builtins (echo) не имеют этого лимита (выполняются shell-ом без execve)
Решение
- Использовать
find ... -exec ... {} +— батчит вызовы в пределах ARG_MAX:find . -name '*.log' -exec rm {} + - Через
xargsс делением на батчи:find . -name '*.log' -print0 | xargs -0 rm -print0/-0обязательны если в именах есть пробелы или newlines- Для mv в директорию:
find . -maxdepth 1 -name '*.csv' -exec mv -t dest/ {} + - Альтернатива:
rsync --remove-source-files src/ dest/— не имеет лимита - Если файлов миллионы — обрабатывать в Python/Go скрипте через os.scandir(), который streamит, не строит весь список в памяти
Симптомы
- Приложение или скрипт падает с ошибкой `OSError: [Errno 24] Too many open files`, `accept: too many open files`, или `socket: too many open files`. Часто проявляется у долгоживущих сервисов (Airflow worker, db connection pool, web-server) под нагрузкой.
Причина
Утечка file descriptors — программа открывает файлы/сокеты и не закрывает Лимит на процесс (`ulimit -n`) слишком низкий — default часто 1024 Лимит на всю систему (`fs.file-max`) исчерпан Открытые но не используемые connections (TIME_WAIT, CLOSE_WAIT в БД pool) Большое количество inotify watches (file watchers): IDE, syncthing, syslog
Решение
- Увеличить ulimit для текущей сессии:
ulimit -n 65535 - Постоянно —
/etc/security/limits.conf:* soft nofile 65535и* hard nofile 65535 - Для systemd-сервиса:
LimitNOFILE=65535в [Service] - Системный лимит:
sysctl -w fs.file-max=2097152+ в/etc/sysctl.conf - Найти и починить FD leak — проверять что код закрывает файлы (with в Python, try-with-resources в Java, defer в Go)
- Для inotify:
sysctl -w fs.inotify.max_user_watches=524288
Симптомы
- Пытаетесь размонтировать диск: `sudo umount /mnt/data` — получаете `umount: /mnt/data: target is busy`. Невозможно отмонтировать, хотя визуально кажется что никто не использует.
Причина
Процесс открыл файл на этом mount-е — даже один открытый FD держит mount busy У какого-то shell в этом mount cwd (включая sub-shell в screen/tmux) Файл из mount mapped в память процесса (mmap) Внутри mount есть submount (другая FS примонтирована во вложенную директорию) Открытый pipe/socket связан с файлом на этом FS
Решение
- Найти и убить процессы через lsof или fuser:
fuser -km /mnt/data(SIGKILL всем) - Перейти из cwd на этом mount:
cd /в каждом shell - Закрыть текстовый редактор, IDE, файловый менеджер с открытыми файлами оттуда
- Lazy umount:
umount -l /mnt/data— фактически отключит когда все FD закроются (опасно: данные не сразу flush) - Force umount:
umount -f /mnt/data(только для NFS, не для локальных FS) - Перезагрузка как последний resort
Симптомы
- `ps aux` показывает процессы в статусе `Z` или `<defunct>`. `kill -9` не помогает — процесс уже мёртв, но запись в process table остаётся. Накопление зомби исчерпывает PID space.
Причина
Parent-процесс не вызвал `wait()` для прочтения exit status дочернего — зомби висит Bug в parent: не обрабатывает SIGCHLD или не делает waitpid Parent сам завис (D-state, uninterruptible sleep) — не может прочитать status Старый/badly-written daemon без proper reaping
Решение
- Убить PARENT процесс (тот, который должен был сделать wait):
kill <PPID>— после этого init/systemd адоптирует зомби и пожнёт их - Если parent — критичный сервис, рестартовать через systemctl:
systemctl restart <service> - Зомби сами по себе не потребляют CPU/RAM — только PID в таблице
- Если зомби много и parent рабочий — это bug в parent, нужно исправить код (SIGCHLD handler, proper double-fork в daemon)
- init/systemd как PID 1 автоматически жнёт зомби; запускать долгоживущие services через systemd, а не через bash в nohup
Симптомы
- `crontab -l` показывает строку с расписанием, но job не запускается. В логах ничего, скрипт не выполняется. Запустив скрипт вручную в shell — всё работает.
Причина
Cron имеет МИНИМАЛЬНЫЙ environment: `PATH=/usr/bin:/bin`, нет HOME для не-root, нет $LANG. Скрипт не находит команды из `/usr/local/bin`, conda, venv Скрипт зависит от cwd — cron запускает с cwd = $HOME (или `/`) Stdout/stderr cron job уходит в почту root, которая никем не читается Неправильный синтаксис crontab (запятые, диапазоны, отсутствие пользователя в системном crontab) `%` в команде интерпретируется как newline в crontab (нужно эскейпить как `\%`) Cron-демон не запущен (на минимальных образах) SELinux/AppArmor блокирует cron-execve Timezone: cron по UTC, а вы думаете что server-local
Решение
- В crontab установить env явно:
PATH=/usr/local/bin:/usr/bin:/binв начале файла - В скрипте — абсолютные пути для всех команд (
/usr/bin/python3а неpython) - В первой строке скрипта
cd /path/to/workdirили использоватьcd $(dirname "$0") - Перенаправить вывод:
* * * * * /script.sh > /tmp/job.log 2>&1 - Эскейпить
%как\% - Альтернатива cron — systemd timers (наследуют env, лучшие логи через journalctl)
- Проверять TZ:
0 3 * * *это 3:00 UTC, для local timezone —CRON_TZ=Europe/Moscowв начале crontab (если поддерживается)
Симптомы
- `systemctl start myservice` падает с `Job for myservice.service failed`. `systemctl status myservice` показывает `failed (Result: exit-code)`. После рестарта продолжает падать.
Причина
Опечатка в `ExecStart` — путь к бинарю неверен Бинарь требует переменные окружения, которых нет (DATABASE_URL, API_KEY) Working directory не существует (`WorkingDirectory=/opt/app` без mkdir) Юзер `User=appuser` не существует или не имеет прав на нужные файлы Зависимость не запущена (`After=postgresql.service` но БД ещё не up) Port уже занят другим процессом SELinux/AppArmor блокирует операции Скрипт требует TTY (interactive prompt), которого нет в systemd Скрипт читает stdin, но `StandardInput=null` в systemd
Решение
- Прочитать journalctl до первой ошибки — обычно там exact error
- Запустить ExecStart вручную из shell, под тем же юзером (
sudo -u appuser /path/to/bin) - Установить env через
EnvironmentFile=/etc/myservice.envилиEnvironment=KEY=val - Создать WorkingDirectory:
mkdir /opt/app && chown appuser /opt/app - Добавить
Restart=on-failure+RestartSec=10sдля авторестарта - Если зависит от БД:
After=postgresql.service+Requires=postgresql.service - Для скриптов с
set -eобязательно правильныйType=simpleилиType=forking - После правки unit:
systemctl daemon-reload && systemctl restart myservice
Симптомы
- Сервер начинает падать с `No space left on device`. `df -h` показывает 100% на `/` или `/var`. Сервисы перестают писать логи, БД отказывает в записи, скрипты валятся.
Причина
Один сервис пишет в лог без ротации (например, `/var/log/myapp.log` растёт до 200GB) Core dumps в `/var/lib/systemd/coredump/` или `/var/crash/` Не работает logrotate (broken конфиг, нет write-прав) tmp-файлы накопились в `/tmp` или `/var/tmp` (если эти не tmpfs) Docker images/volumes/containers в `/var/lib/docker/` journald без ограничения размера в `/var/log/journal/` Backup-скрипт сохраняет копии не удаляя старые
Решение
- ОЧЕНЬ ВАЖНО: если файл удалён, но процесс держит FD, место не освобождается! Найти
lsof | grep deleted, рестартить процесс - НЕ удалять активно растущий лог через
rm— процесс продолжит писать в открытый FD, место не освободится. Использоватьtruncate -s 0 /var/log/big.logили: > /var/log/big.log - Очистить journald:
journalctl --vacuum-size=1Gили--vacuum-time=7d - Очистить /tmp если не tmpfs:
find /tmp -type f -atime +7 -delete - Настроить logrotate: создать
/etc/logrotate.d/myappс правилами - Для Docker:
docker system prune -af --volumes(ОПАСНО — удаляет все unused) - В скриптах: ограничение через
truncateили logrotate. Не использоватьtail -f log > newfileбез обрезки
Симптомы
- `touch file` или `mkdir dir` падает с `No space left on device`, но `df -h` показывает что место есть (например, 60% free). Любая операция создания файла фейлится.
Причина
Закончились inodes — каждый файл (и каталог) занимает один inode, количество фиксировано при mkfs Много мелких файлов в одной FS (миллионы лог-файлов, кэш веб-сервера, mail spool) Mail queue с миллионами сообщений (postfix /var/spool/mail/) PHP/Python session files в `/var/lib/php/sessions/` без cleanup Невидимый bug процесса, создающего файлы в loop
Решение
- Найти и удалить лишние файлы — обычно в одном каталоге их миллионы
- Если ext4 — количество inodes фиксировано при mkfs, увеличить нельзя без пересоздания FS
- На XFS — inodes выделяются динамически, проблема исключена (опция при mkfs.xfs)
- Перенести данные на новый раздел с большим bytes-per-inode (
mkfs.ext4 -i 16384) - Для PHP sessions: настроить session.gc_maxlifetime + cron на cleanup
- Для mail queue:
postsuper -d ALL(опасно, удаляет всё) или selective cleanup
Симптомы
- Скрипт запускается, что-то делает, заканчивается с exit 0, но половина работы не сделана. Логи не показывают ошибку. На самом деле где-то в середине одна команда упала, а скрипт пошёл дальше, как ни в чём не бывало.
Причина
Нет `set -e` — bash продолжает выполнение после команды с ненулевым exit code Pipeline без `pipefail`: `cmd1 | cmd2` — exit code только последней команды, ошибки cmd1 теряются Команды в `if`/`while`/`||`/`&&` — даже с `set -e` не вызывают exit, exit code лишь записывается Subshell — exit из subshell не убивает родителя Бэкграунд-команды (`cmd &`) — их exit code теряется без `wait $!`
Решение
- В первой строке после shebang:
set -euo pipefail(errexit + nounset + pipefail) - Каждую критичную команду оборачивать в check:
cmd || { echo "cmd failed"; exit 1; } - Для background:
wait $!илиwait(ждать всех) и проверять$? - Логирование:
exec > >(tee -a /var/log/script.log) 2>&1в начале скрипта - Trap для cleanup:
trap 'rm -f /tmp/lockfile' EXIT - В CI/cron: всегда логировать stdout+stderr в файл с timestamp
Симптомы
- Написали скрипт `goto-project.sh` который делает `cd ~/projects/etl`. Запускаете `./goto-project.sh` — скрипт «отработал», но в текущем shell вы остались в той же директории. cd как будто не сработал.
Причина
Скрипт запускается в SUBSHELL — отдельный процесс с собственным cwd. Все изменения cwd теряются при exit subshell cd работает внутри скрипта (для команд после cd), но НЕ влияет на parent shell Это фундаментальное свойство процессов в UNIX — child не может менять cwd parent Не зависит от bash/zsh — общее правило
Решение
- Использовать
source(или.) — выполнить скрипт в ТЕКУЩЕМ shell, не в subshell:source goto-project.shили. goto-project.sh - Создать функцию в
~/.bashrcвместо скрипта:gotoetl() { cd ~/projects/etl; } - Использовать alias:
alias gotoetl='cd ~/projects/etl' - Использовать
cdс command substitution в текущем shell:cd $(./find-project.sh) - Modern alternative —
zoxide(z) для smart-cd по частоте посещений
Симптомы
- Bash-скрипт с `set -u` падает: `./script.sh: line 5: VARNAME: unbound variable`. Без `set -u` — тихо использует пустую строку, что часто приводит к проблемам типа `rm -rf $UNSET/`.
Причина
Опечатка в имени переменной (`$USR` вместо `$USER`) Переменная зависит от окружения, которого нет (cron не имеет $HOME для не-root, нет $DISPLAY) Использование `$1`, `$2` в функции/скрипте без проверки `$#` Переменная установлена в branch `if`, который не выполнился Source файл не найден или не выполнился — переменные не установлены
Решение
- Использовать default value:
"${VAR:-default}"— если unset/empty, использовать 'default' - Падать с понятным сообщением:
"${VAR:?VAR must be set}" - Проверить количество аргументов в начале:
(( $# >= 2 )) || { echo "Usage: $0 <a> <b>"; exit 1; } - ВСЕГДА использовать
set -u(илиset -o nounset) в production-скриптах - Хранить env-переменные в
.envфайле и source-ить:set -a; source .env; set +a - Использовать
declare/localдля явного объявления переменных функции
Симптомы
- Запускаете `sudo rm file` или `sudo chmod 644 file` — получаете `Operation not permitted` несмотря на то, что вы root. UNIX-permissions показывают права root, но операция всё равно падает.
Причина
Файл имеет immutable атрибут: `chattr +i file` — нельзя удалить/изменить никому, даже root Файл имеет append-only атрибут: `chattr +a` — только append, никаких modifications SELinux в enforcing-режиме блокирует операцию (политика denies даже root) AppArmor profile применён к процессу Файл на read-only mounted FS (`mount -o remount,ro` или сам диск read-only) Файл — на FUSE-mount, требующий специальных прав В контейнере: capabilities ограничены, даже root не имеет CAP_DAC_OVERRIDE Файл — system file под защитой `/proc/sys/kernel/...`
Решение
- Снять immutable:
sudo chattr -i file, затем уже стандартные операции - Снять append-only:
sudo chattr -a file - Проверить SELinux:
getenforce— если Enforcing, посмотретьaudit2why -aдля конкретного denial - Временно переключить SELinux в Permissive:
sudo setenforce 0(НЕ для prod!) - Перемонтировать с rw:
sudo mount -o remount,rw /mountpoint - В контейнере: запустить с
--cap-addили--privileged(с пониманием рисков) - Для
/proc/sys/*— некоторые параметры readonly даже для root
Симптомы
- `ssh user@host` зависает на минуту, потом `ssh: connect to host X port 22: Connection timed out`. Или сразу: `ssh: connect to host X port 22: Connection refused`. Или: `ssh: Could not resolve hostname X: Name or service not known`.
Причина
Хост недоступен по сети (firewall, VPN не подключён, неправильный IP/hostname) Connection refused — порт 22 закрыт (sshd не запущен) или прослушивает на другом порту Connection timed out — пакеты не доходят (firewall дропает, routing проблема) DNS не резолвит hostname fail2ban или denyhosts заблокировал ваш IP после неудачных попыток SSH прослушивает на нестандартном порту (например, 2222) VPN/corporate firewall блокирует исходящий 22 На стороне сервера sshd упал/crashed
Решение
- Проверить, что вы в правильной сети (VPN, корпоративная сеть)
- Указать порт если нестандартный:
ssh -p 2222 user@host - Указать IP вместо hostname если DNS работает плохо:
ssh [email protected] - Если fail2ban заблокировал: попросить админа разбанить (
fail2ban-client set sshd unbanip <IP>) - На сервере:
sudo systemctl restart sshесли sshd упал - Проверить firewall на сервере:
sudo ufw statusилиsudo iptables -L - Через bastion/jump host если direct connection невозможен:
ssh -J bastion user@target
Симптомы
- При попытке `ssh user@host` появляется пугающее сообщение: `@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@` `@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @` `@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@` `IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!` SSH отказывается подключаться.
Причина
Сервер был переустановлен — новый SSH host key Hostname/IP был переиспользован для другого сервера (cloud-серверы часто переиспользуют IP) Действительно MITM-атака (редко, но возможно) DNS изменился и hostname теперь указывает на другой сервер Изменился порт SSH и записи в known_hosts конфликтуют
Решение
- Удалить старую запись:
ssh-keygen -R hostname(точное удаление) илиssh-keygen -R '[hostname]:2222'для нестандартного порта - После удаления при следующем
sshбудет вопрос про fingerprint — проверить с админом сервера и принять - Никогда не удалять запись если НЕ ожидали смены ключа — это может быть реальный MITM
- Для CI/automation —
StrictHostKeyChecking=accept-new(принимать новые, но защищаться от смены) - НЕ использовать
StrictHostKeyChecking=no— отключает защиту полностью - Получить fingerprint с сервера через out-of-band канал (admin, документация) и проверить вручную
Симптомы
- При `ssh user@host` после ввода команды сразу: `user@host: Permission denied (publickey).`. Парольная аутентификация отключена, ключ не принимается.
Причина
Приватный ключ не в `~/.ssh/id_rsa` / `id_ed25519` (нужен `-i path/to/key`) Permissions на `~/.ssh/` слишком открытые (должно быть 700, на ключ — 600). SSH отказывается использовать ключ с loose-permissions Не тот username (`ssh ubuntu@host` когда нужен `ec2-user`) Публичный ключ не в `~/.ssh/authorized_keys` на сервере, или там не тот ключ Серверная конфигурация запрещает этого юзера/группу (`AllowUsers`, `DenyUsers` в sshd_config) Ключ зашифрован паролем (passphrase), а вы не разблокировали ssh-agent На сервере SELinux мешает sshd читать `~/.ssh/` Файл `~/.ssh/authorized_keys` имеет неправильные perm (должно быть 600 и owned-by user)
Решение
- Чинить permissions локально:
chmod 700 ~/.ssh && chmod 600 ~/.ssh/id_* - На сервере:
chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys && chown -R user:user ~/.ssh - Указать ключ явно:
ssh -i ~/.ssh/specific-key user@host - Скопировать публичный ключ на сервер:
ssh-copy-id user@host(если ещё хоть какая-то auth работает) - Запустить ssh-agent и добавить ключ:
eval $(ssh-agent) && ssh-add ~/.ssh/id_ed25519 - В
~/.ssh/configпрописать Host/IdentityFile/User для удобства
Симптомы
- Запустили `rsync -av source dest/` ожидая что содержимое source попадёт в dest. Получили: `dest/source/file1, file2, ...` — содержимое попало в подкаталог dest/source/, а не прямо в dest/. Или наоборот: `rsync -av source/ dest` без trailing slash на source — получили правильно содержимое в dest, но рассчитывали увидеть подкаталог.
Причина
rsync: trailing slash на source имеет принципиальное значение `rsync src/ dest/` — копировать СОДЕРЖИМОЕ src внутрь dest `rsync src dest/` — копировать ПАПКУ src внутрь dest (получаем dest/src/...) trailing slash на dest почти не важен В cp поведение похожее, но менее заметное
Решение
- Запомнить мантру: 'trailing slash on source = copy contents, no slash = copy folder'
- Всегда использовать
-n/--dry-runдля проверки перед реальной синхронизацией - Для копирования содержимого:
rsync -av src/ dest/ - Для копирования папки целиком:
rsync -av src dest/ - Использовать
--itemize-changes(-i) для подробного списка изменений - После реальной синхронизации проверить
ls -la dest/
Симптомы
- Пытаетесь `tar -xf archive.tar.gz` и получаете: `tar: This does not look like a tar archive` или `gzip: stdin: not in gzip format / tar: Child returned status 1`.
Причина
Файл не tar-архив (например, zip или 7z с расширением .tar.gz) Файл повреждён (incomplete download) Файл сжат другим алгоритмом, чем подразумевает расширение (xz назван .gz) Двойное сжатие (`tar.gz.gz` или `tar.tar.gz`) Файл — base64-encoded (`base64 -d file | tar xz`) На macOS — BSD tar поддерживает auto-detect, на старых Linux нужно явно указывать
Решение
tar -xf(без -z/-j/-J) — современный GNU tar auto-detect сжатие. Использовать просто-xf- Явный формат:
tar -xzfдля gz,-xjfдля bz2,-xJfдля xz,--zstdдля zstd - Если повреждён — скачать заново; проверить checksum через sha256sum
- Если zip с расширением .tar.gz:
unzip archive.tar.gz - Двойное сжатие:
gunzip file.tar.gz.gzпотомtar -xf file.tar.gzи снова - Если уверены в формате, но
tarошибается — попробовать другую реализацию (bsdtar, libarchive)
Симптомы
- Вы видите строку в файле (`cat file | grep -i hello` ничего не выводит), но `vim file` показывает её. Или `grep error` на logs молчит, хотя `tail` показывает множество строк с error.
Причина
CRLF line endings — `\r\n` вместо `\n`. grep матчит регекс, но в конце строки есть `\r` и pattern не совпадает Невидимые Unicode-символы (BOM в начале файла, NBSP вместо обычного пробела, fancy quotes) Кодировка файла не UTF-8 (например UTF-16 или CP1251) Опечатка с visual-similar символами (latin a vs кириллица а) LC_ALL / LANG влияет на regex behavior с non-ASCII Файл с binary-content — `grep` пропускает binary files с -I (default `--exclude-binary`) Tabs vs пробелы — `grep ' error'` (с пробелом) не найдёт `\terror`
Решение
- Конвертировать CRLF в LF:
dos2unix fileилиsed -i 's/\r$//' file - Перекодировать в UTF-8:
iconv -f CP1251 -t UTF-8 file > file.utf8 - Использовать
LC_ALL=C grepдля байтового поиска (быстрее и без locale-surprises) - Для binary файлов:
grep -a(treat as text) илиstrings file | grep - Использовать
ripgrep(rg) — современная альтернатива, лучше обрабатывает encoding и binary - Для regex с non-ASCII — устанавливать
LANG=en_US.UTF-8и использовать-P(Perl regex)
Симптомы
- Запускаете `awk` команду и получаете: `awk: cmd. line:1: ... syntax error` или `awk: cmd. line:1: ... bailing out`. Особенно часто при копипасте из stackoverflow или построении awk-команд в bash-скрипте.
Причина
Использование double quotes для всего awk-выражения и `$1` в нём — bash сначала разворачивает `$1` (аргумент скрипта), пустота, awk получает кривое выражение Двойные vs одинарные кавычки — `awk "{print $1}"` неправильно, должно быть `awk '{print $1}'` Не закрытая скобка/строка/блок BSD awk не поддерживает gawk-расширения (length(arr), asort, gensub) Пропущенная точка с запятой между statements в одном action блоке Не-ASCII символы (умные кавычки от Word) вместо обычных Использование `\n` внутри single-quoted строки awk-программы без понимания контекста
Решение
- Использовать одинарные кавычки для awk-программы:
awk '{print $1}' file - Если нужны bash-переменные внутри awk:
awk -v var="$bash_var" '{print var, $1}' - Для длинных awk — выносить в файл:
awk -f script.awk data.txt - Проверить версию awk: на macOS установить gawk через
brew install gawk - Использовать
gawkявно если нужны его расширения - Для сложных задач — Python с pandas, или miller (mlr) — более удобный CLI инструмент
Симптомы
- Запускаете на macOS команду из tutorial: `sed -i 's/foo/bar/g' file` — получаете: `sed: 1: "file": invalid command code f`. Или создаётся файл с именем `file-extension`. На Linux та же команда работает корректно.
Причина
BSD sed (macOS) и GNU sed (Linux) отличаются синтаксисом `-i` GNU sed: `-i` принимает опциональный аргумент-extension для backup, БЕЗ пробела или вообще без аргумента BSD sed: `-i` ТРЕБУЕТ extension-аргумент (даже пустую строку): `sed -i '' 's/...'` Это известная переносимость-проблема со скриптами macOS↔Linux
Решение
- На macOS использовать пустой extension:
sed -i '' 's/foo/bar/g' file - Кроссплатформенно:
sed -i.bak 's/foo/bar/g' file(создаст .bak — работает в обеих версиях) - Установить GNU sed на macOS:
brew install gnu-sed(доступен какgsed) - В скриптах проверять платформу:
if [[ $(uname) == Darwin ]]; then sed -i '' ...; else sed -i ...; fi - Альтернатива: использовать
perl -i -pe 's/foo/bar/g' file— работает одинаково везде - Современные альтернативы:
sd(Rust, кроссплатф.) —sd 'foo' 'bar' file
Симптомы
- При SSH-подключении или в скрипте: `bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8): No such file or directory` или `perl: warning: Setting locale failed.`. После этого многие команды (sort, awk, date) ведут себя странно — например, неправильно сортируют не-ASCII или показывают дату не на нужном языке.
Причина
На сервере не сгенерированы запрошенные локали (только `en_US.UTF-8` minimal) Клиент пересылает свои локали через SSH (опция `SendEnv LANG LC_*` в ssh_config), а сервер их не понимает Минимальные Docker-образы не содержат locale-data (`locales` пакет не установлен) Установлена локаль, но `update-locale` не запущена Системный default `/etc/default/locale` устанавливает что-то странное
Решение
- Сгенерировать локаль:
sudo locale-gen en_US.UTF-8(Ubuntu/Debian), затемsudo update-locale LANG=en_US.UTF-8 - Для русской:
sudo locale-gen ru_RU.UTF-8 - В Docker: добавить
RUN apt-get install -y locales && locale-gen en_US.UTF-8в Dockerfile - На клиенте: убрать пересылку LC_*: закомментировать
SendEnv LANG LC_*в/etc/ssh/ssh_config - Или на сервере:
AcceptEnvубрать из/etc/ssh/sshd_config - В скрипте — явно:
export LC_ALL=Cдля byte-level операций (sort, grep быстрее без locale)
Симптомы
- `find / -name '*.csv'` бежит минутами, при этом CPU и disk загружены. На production-серверах с миллионами файлов поиск может занимать часы.
Причина
find делает stat() на каждый файл — нагрузка на FS и cache Поиск в `/` включает виртуальные FS (proc, sys), сетевые mounts (NFS), backup-снапшоты Нет сортировки/индексации — линейный обход дерева Многие предикаты (`-mtime`, `-perm`) требуют stat По умолчанию find следует symlinks с `-L` (но они и так на каждом шагу)
Решение
- Использовать
locate/plocateесли есть updatedb-индекс:locate '*.csv'— миллисекунды - Ограничить scope:
find /home /dataвместо/ - Исключить виртуальные FS:
find / -xdev(не пересекать mount boundaries) - Использовать
-pruneдля исключения каталогов:find / -path /proc -prune -o -name '*.csv' -print - Modern alternative —
fd:fd csv— в разы быстрее, дружелюбнее синтаксис, цветной вывод - Если ищете содержимое —
ripgrep(rg) вместоfind ... -exec grep - Для регулярных запросов настроить
updatedbчерез cron —plocateработает мгновенно
Симптомы
- Запускаете `cat log.json | jq '.message'` — получаете `jq: error (at <stdin>:1): Cannot index ...` или `parse error: Invalid numeric literal at line ...`. При этом если открыть файл в редакторе — JSON выглядит валидно.
Причина
Файл содержит JSON Lines / NDJSON — несколько JSON-объектов по одному на строку, не массив В файле много пустых строк или комментариев Файл — concatenation нескольких JSON без разделителей Файл частично записан (стрим прервался) BOM в начале файла (UTF-8 with BOM) Лишние trailing characters после JSON В JSON есть JS-specific конструкции — single quotes, trailing commas, comments
Решение
- Для NDJSON / JSON Lines: использовать
jq -c .БЕЗ дополнительных опций, илиjq --slurp '. | map(...)'чтобы собрать в массив - По-строчно:
while read -r line; do echo "$line" | jq '.msg'; done < file.ndjson - Удалить BOM:
sed -i '1s/^\xEF\xBB\xBF//' file.json - Удалить пустые строки:
grep -v '^$' file.json | jq . - Проверить целостность JSON:
python -m json.tool file.json(Python даёт более внятные ошибки) - Использовать
gron/fx— альтернативы jq с лучшими ошибками - Для большого файла:
head -100 file.json | jq .чтобы найти первую плохую строку
Симптомы
- У вас была долгая tmux-сессия с открытыми окнами. После случайной перезагрузки сервера (или просто `pkill tmux`) сессия потерялась. `tmux ls` показывает: `no server running on /tmp/tmux-1000/default`.
Причина
tmux/screen хранят сессии в памяти процесса — после kill процесса всё теряется Reboot убивает все процессы (включая tmux server) — сессии не персистентны `/tmp` очистился при boot и socket-файлы пропали На systemd с user services — `loginctl enable-linger` нужен для сохранения процессов после logout Tmux НЕ восстанавливает содержимое — только окна и их layout
Решение
- Установить tmux-resurrect / tmux-continuum (плагины через TPM) — сохраняет/восстанавливает layout и опционально содержимое pane
- После reboot:
tmux new -s work+prefix + Ctrl+rчтобы restore через resurrect - Запускать tmux через systemd-user-service для авто-restart
- linger для user (чтобы процессы жили без активного login):
sudo loginctl enable-linger $USER - Альтернатива: zellij — modern terminal multiplexer с встроенным session persistence
- Для критичных long-running задач — запускать через systemd-сервис, не через tmux
Симптомы
- `curl https://example.com` падает: `curl: (60) SSL certificate problem: unable to get local issuer certificate` или `wget`: `ERROR: cannot verify ... certificate`. Иногда: `certificate has expired`.
Причина
Системные CA-сертификаты устарели или повреждены (`/etc/ssl/certs/ca-certificates.crt`) Self-signed сертификат на сервере (внутренний CA, dev-среда) Время на системе сдвинуто (срок действия сертификата проверяется) Сертификат сервера действительно истёк MITM proxy (corporate firewall) подменяет сертификаты, его CA не установлен В Docker-image нет ca-certificates пакета (minimal alpine/scratch)
Решение
- Обновить CA-сертификаты:
sudo apt update && sudo apt install --reinstall ca-certificates && sudo update-ca-certificates - Синхронизировать время:
sudo systemctl restart systemd-timesyncdилиsudo ntpdate pool.ntp.org - Для corporate CA: положить свой CA в
/usr/local/share/ca-certificates/corp.crtиsudo update-ca-certificates - В скриптах для self-signed:
curl --cacert /path/to/ca.pem https://host - НЕ использовать
curl -k/wget --no-check-certificateв production — отключает защиту - В Docker:
RUN apt-get install -y ca-certificatesилиapk add ca-certificates - Если истёк сертификат сервера — связаться с админом, проблема на их стороне
Симптомы
- `sudo apt install something`: `E: Could not get lock /var/lib/dpkg/lock-frontend - open (11: Resource temporarily unavailable)` или `E: Unable to acquire the dpkg frontend lock`. apt отказывается работать.
Причина
Параллельно работает другой apt/dpkg (unattended-upgrades, apt-daily.service, ручной apt в другом терминале) Предыдущий apt-процесс упал и оставил lock-файл Графический Software Updater (GNOME, KDE) держит lock Snap/snapd refresh в фоне Запустили `apt install` через скрипт в cron одновременно с автоматическим обновлением
Решение
- Подождать — если активны
apt-daily/unattended-upgrades, дать им закончить (10-30 минут) - Если уверены, что никто не работает — удалить stale locks:
sudo rm /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock /var/lib/dpkg/lock - Затем:
sudo dpkg --configure -aчтобы починить состояние - Если процесс висит и не закрывает:
sudo kill <PID>процесса apt - Отключить автообновления (для серверов с manual control):
sudo systemctl disable --now unattended-upgrades - Использовать
sudo lsof /var/lib/dpkg/lockчтобы увидеть имя процесса перед kill
Симптомы
- В скрипте `for f in *.csv; do echo "$f"; done` — если CSV-файлов нет, печатает буквально `*.csv` вместо ничего. Дальнейшая обработка ломается. Или `mv *.bak archive/` падает `mv: cannot stat '*.bak': No such file or directory`.
Причина
Default-поведение bash: если glob не матчит, остаётся как литеральная строка (для совместимости с UNIX-историей) В zsh default — ошибка `no matches found`, что тоже сюрприз для bash-привыкших Это касается всех команд, которым shell передаёт развёрнутые аргументы Hidden files (`.git`, `.env`) НЕ матчатся обычным `*` — нужно `* .*` или `shopt -s dotglob`
Решение
- В скриптах:
shopt -s nullglob— пустой glob становится пустым массивом, цикл не выполнится - Альтернатива
shopt -s failglob— упасть с error если glob не матчит (для критичных операций) - Проверять руками:
if compgen -G '*.csv' > /dev/null; then for f in *.csv; do ...; done; fi - Использовать
findвместо glob:find . -maxdepth 1 -name '*.csv' -exec ... {} + - Для расширенных glob (
**рекурсивно):shopt -s globstar - В zsh: использовать
setopt NULL_GLOBили(N)qualifier:for f in *.csv(N); do
Симптомы
- Запускаете `command | head` и получаете в stderr: `<command>: write error: Broken pipe` или `yes: standard output: Broken pipe`. Скрипт может завершиться с ненулевым exit code, хотя данные обработались.
Причина
Receiver (например, head) закрыл pipe раньше, чем sender закончил писать Sender получает SIGPIPE при попытке записи в закрытый pipe — по default это завершение процесса с exit 141 (128+13) Не настоящая ошибка — это нормальное поведение `head` (читает N строк и закрывается) С `set -o pipefail` exit code попадает в exit pipeline -> весь скрипт падает
Решение
- Игнорировать SIGPIPE — это нормально для head/tail/grep -m
- Если
set -o pipefailмешает: проверять конкретные exit codes через${PIPESTATUS[@]} - Альтернатива: использовать
readдля построчной обработки вместо передачи большого потока - Подавить ошибку в stderr:
command 2>/dev/null | head(не лучшая практика) - Для yes-like команд:
yes | head -100—yesпытается писать вечно, получает SIGPIPE на 101 итерации — это by design - В Python: ignore SIGPIPE через
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
Симптомы
- В вашем shell `./etl.sh` отрабатывает идеально. Та же команда в cron (`* * * * * /home/user/etl.sh`) или systemd-сервисе падает с `command not found`, `permission denied`, или просто молчит. В логах cron / journalctl — невнятная ошибка.
Причина
Cron environment minimal: только `PATH=/usr/bin:/bin`, без $HOME для не-root, без $LANG, без $USER systemd-сервис: только базовый PATH, нет TERM, нет $DISPLAY Активные venv / conda НЕ наследуются — `python` указывает на system python AWS / GCP credentials в `~/.aws/credentials` не читаются, потому что HOME другое Скрипт делает `cd $(dirname "$0")` — но `$0` в cron — полный путь, надо `dirname "$(readlink -f "$0")"` Locale issues: cron без LANG ломает `sort`, `awk` с не-ASCII
Решение
- В первой строке crontab указать env:
PATH=/usr/local/bin:/usr/bin:/bin\nHOME=/home/user - В скрипте — load env явно:
source /home/user/.profileилиsource venv/bin/activate - Для systemd:
EnvironmentFile=/etc/myservice.env+ явныйEnvironment=PATH=... - Использовать абсолютные пути:
/usr/bin/python3а неpython - Обернуть в bash login shell:
/bin/bash -lc './script.sh'— наследует env как при login - Для conda:
eval "$(/opt/conda/bin/conda shell.bash hook)"; conda activate envnameв скрипте - Хранить путь к проекту в env-файле и читать:
source /etc/myapp/env