Перейти к содержанию
Learning Platform
Глоссарий
Troubleshooting

Troubleshooting — Linux & Bash для Junior Data Engineer

База знаний типичных ошибок курса Linux & Bash для Junior Data Engineer.

Категория

Показано 35 из 35 ошибок

Симптомы

  • При попытке запустить скрипт через `./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 интерпретирует его как флаг

Решение

  1. Добавить execute-бит: chmod +x script.sh (всем) или chmod u+x script.sh (только owner)
  2. Если фс смонтирована noexec — запустить через интерпретатор явно: bash script.sh
  3. Перенести скрипт в обычную фс (/home, /opt) если изначально лежит на noexec /tmp
  4. Сменить владельца если файл root-owned: sudo chown $USER script.sh
  5. Для SELinux deny — проверить audit2allow или sudo setenforce 0 для теста (НЕ для prod)
  6. Если имя начинается с -: использовать ./-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 не пройдёт

Решение

  1. Проверить cwd через pwd, перейти в правильную директорию
  2. Убрать CRLF: sed -i 's/\r$//' script.sh или dos2unix script.sh
  3. Исправить shebang на портабельный: #!/usr/bin/env bash вместо жёсткого пути
  4. Если broken symlink — пересоздать: rm broken-link && ln -s real-target broken-link
  5. Удалить и перепечатать имя файла, если подозрение на Unicode-typos
  6. Проверить 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` помогает)

Решение

  1. Установить пакет: sudo apt install jq (Ubuntu/Debian) или brew install jq (macOS)
  2. Добавить директорию в PATH: export PATH="$HOME/.local/bin:$PATH" в ~/.bashrc
  3. После установки обновить cache bash: hash -r
  4. В cron — указывать абсолютный путь: /usr/local/bin/jq или экспортить PATH в скрипте
  5. Активировать venv: source venv/bin/activate или conda activate envname
  6. Найти правильное имя в новой версии (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

Решение

  1. Использовать одинарные кавычки: echo 'hello!' вместо echo "hello!"
  2. Эскейпить: echo "hello\!" (но в bash 5.x не всегда работает корректно)
  3. Отключить history expansion в interactive shell: set +H (для bash) — в ~/.bashrc
  4. В zsh: setopt NO_BANG_HIST
  5. В скриптах проблемы нет — там 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

Решение

  1. Явный shebang: #!/usr/bin/env bash для bash-скриптов, #!/bin/sh ТОЛЬКО для POSIX-only
  2. Запускать через нужный интерпретатор: bash script.sh а не sh script.sh
  3. Использовать shellcheck — он флагнет bash-isms в #!/bin/sh скриптах
  4. Для портабельности — писать в POSIX sh (без [[ ]], без arrays); или явно требовать bash 4+
  5. Тестировать скрипты на целевой системе перед 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` с неправильным фильтром

Решение

  1. Прекратить ЛЮБУЮ запись на пострадавший диск — каждая запись может перезаписать освобождённые блоки
  2. Если файл всё ещё открыт процессом — cp /proc/<pid>/fd/<n> recovered-file (для логов, БД)
  3. Восстановить из backup: rsync snapshots, BTRFS/ZFS snapshots, восстановление из дампа
  4. Для ext4 с journaling — extundelete или debugfs могут спасти recently deleted (без гарантий)
  5. Для XFS — xfs_undelete (тоже без гарантий)
  6. Если удалены системные файлы — boot с live USB, mount раздел, восстановить из backup или reinstall
  7. ПРОФИЛАКТИКА: 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)

Решение

  1. Использовать find ... -exec ... {} + — батчит вызовы в пределах ARG_MAX: find . -name '*.log' -exec rm {} +
  2. Через xargs с делением на батчи: find . -name '*.log' -print0 | xargs -0 rm
  3. -print0/-0 обязательны если в именах есть пробелы или newlines
  4. Для mv в директорию: find . -maxdepth 1 -name '*.csv' -exec mv -t dest/ {} +
  5. Альтернатива: rsync --remove-source-files src/ dest/ — не имеет лимита
  6. Если файлов миллионы — обрабатывать в 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

Решение

  1. Увеличить ulimit для текущей сессии: ulimit -n 65535
  2. Постоянно — /etc/security/limits.conf: * soft nofile 65535 и * hard nofile 65535
  3. Для systemd-сервиса: LimitNOFILE=65535 в [Service]
  4. Системный лимит: sysctl -w fs.file-max=2097152 + в /etc/sysctl.conf
  5. Найти и починить FD leak — проверять что код закрывает файлы (with в Python, try-with-resources в Java, defer в Go)
  6. Для 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

Решение

  1. Найти и убить процессы через lsof или fuser: fuser -km /mnt/data (SIGKILL всем)
  2. Перейти из cwd на этом mount: cd / в каждом shell
  3. Закрыть текстовый редактор, IDE, файловый менеджер с открытыми файлами оттуда
  4. Lazy umount: umount -l /mnt/data — фактически отключит когда все FD закроются (опасно: данные не сразу flush)
  5. Force umount: umount -f /mnt/data (только для NFS, не для локальных FS)
  6. Перезагрузка как последний 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

Решение

  1. Убить PARENT процесс (тот, который должен был сделать wait): kill <PPID> — после этого init/systemd адоптирует зомби и пожнёт их
  2. Если parent — критичный сервис, рестартовать через systemctl: systemctl restart <service>
  3. Зомби сами по себе не потребляют CPU/RAM — только PID в таблице
  4. Если зомби много и parent рабочий — это bug в parent, нужно исправить код (SIGCHLD handler, proper double-fork в daemon)
  5. 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

Решение

  1. В crontab установить env явно: PATH=/usr/local/bin:/usr/bin:/bin в начале файла
  2. В скрипте — абсолютные пути для всех команд (/usr/bin/python3 а не python)
  3. В первой строке скрипта cd /path/to/workdir или использовать cd $(dirname "$0")
  4. Перенаправить вывод: * * * * * /script.sh > /tmp/job.log 2>&1
  5. Эскейпить % как \%
  6. Альтернатива cron — systemd timers (наследуют env, лучшие логи через journalctl)
  7. Проверять 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

Решение

  1. Прочитать journalctl до первой ошибки — обычно там exact error
  2. Запустить ExecStart вручную из shell, под тем же юзером (sudo -u appuser /path/to/bin)
  3. Установить env через EnvironmentFile=/etc/myservice.env или Environment=KEY=val
  4. Создать WorkingDirectory: mkdir /opt/app && chown appuser /opt/app
  5. Добавить Restart=on-failure + RestartSec=10s для авторестарта
  6. Если зависит от БД: After=postgresql.service + Requires=postgresql.service
  7. Для скриптов с set -e обязательно правильный Type=simple или Type=forking
  8. После правки 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-скрипт сохраняет копии не удаляя старые

Решение

  1. ОЧЕНЬ ВАЖНО: если файл удалён, но процесс держит FD, место не освобождается! Найти lsof | grep deleted, рестартить процесс
  2. НЕ удалять активно растущий лог через rm — процесс продолжит писать в открытый FD, место не освободится. Использовать truncate -s 0 /var/log/big.log или : > /var/log/big.log
  3. Очистить journald: journalctl --vacuum-size=1G или --vacuum-time=7d
  4. Очистить /tmp если не tmpfs: find /tmp -type f -atime +7 -delete
  5. Настроить logrotate: создать /etc/logrotate.d/myapp с правилами
  6. Для Docker: docker system prune -af --volumes (ОПАСНО — удаляет все unused)
  7. В скриптах: ограничение через 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

Решение

  1. Найти и удалить лишние файлы — обычно в одном каталоге их миллионы
  2. Если ext4 — количество inodes фиксировано при mkfs, увеличить нельзя без пересоздания FS
  3. На XFS — inodes выделяются динамически, проблема исключена (опция при mkfs.xfs)
  4. Перенести данные на новый раздел с большим bytes-per-inode (mkfs.ext4 -i 16384)
  5. Для PHP sessions: настроить session.gc_maxlifetime + cron на cleanup
  6. Для 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 $!`

Решение

  1. В первой строке после shebang: set -euo pipefail (errexit + nounset + pipefail)
  2. Каждую критичную команду оборачивать в check: cmd || { echo "cmd failed"; exit 1; }
  3. Для background: wait $! или wait (ждать всех) и проверять $?
  4. Логирование: exec > >(tee -a /var/log/script.log) 2>&1 в начале скрипта
  5. Trap для cleanup: trap 'rm -f /tmp/lockfile' EXIT
  6. В 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 — общее правило

Решение

  1. Использовать source (или .) — выполнить скрипт в ТЕКУЩЕМ shell, не в subshell: source goto-project.sh или . goto-project.sh
  2. Создать функцию в ~/.bashrc вместо скрипта: gotoetl() { cd ~/projects/etl; }
  3. Использовать alias: alias gotoetl='cd ~/projects/etl'
  4. Использовать cd с command substitution в текущем shell: cd $(./find-project.sh)
  5. 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 файл не найден или не выполнился — переменные не установлены

Решение

  1. Использовать default value: "${VAR:-default}" — если unset/empty, использовать 'default'
  2. Падать с понятным сообщением: "${VAR:?VAR must be set}"
  3. Проверить количество аргументов в начале: (( $# >= 2 )) || { echo "Usage: $0 <a> <b>"; exit 1; }
  4. ВСЕГДА использовать set -u (или set -o nounset) в production-скриптах
  5. Хранить env-переменные в .env файле и source-ить: set -a; source .env; set +a
  6. Использовать 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/...`

Решение

  1. Снять immutable: sudo chattr -i file, затем уже стандартные операции
  2. Снять append-only: sudo chattr -a file
  3. Проверить SELinux: getenforce — если Enforcing, посмотреть audit2why -a для конкретного denial
  4. Временно переключить SELinux в Permissive: sudo setenforce 0 (НЕ для prod!)
  5. Перемонтировать с rw: sudo mount -o remount,rw /mountpoint
  6. В контейнере: запустить с --cap-add или --privileged (с пониманием рисков)
  7. Для /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

Решение

  1. Проверить, что вы в правильной сети (VPN, корпоративная сеть)
  2. Указать порт если нестандартный: ssh -p 2222 user@host
  3. Указать IP вместо hostname если DNS работает плохо: ssh [email protected]
  4. Если fail2ban заблокировал: попросить админа разбанить (fail2ban-client set sshd unbanip <IP>)
  5. На сервере: sudo systemctl restart ssh если sshd упал
  6. Проверить firewall на сервере: sudo ufw status или sudo iptables -L
  7. Через 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 конфликтуют

Решение

  1. Удалить старую запись: ssh-keygen -R hostname (точное удаление) или ssh-keygen -R '[hostname]:2222' для нестандартного порта
  2. После удаления при следующем ssh будет вопрос про fingerprint — проверить с админом сервера и принять
  3. Никогда не удалять запись если НЕ ожидали смены ключа — это может быть реальный MITM
  4. Для CI/automation — StrictHostKeyChecking=accept-new (принимать новые, но защищаться от смены)
  5. НЕ использовать StrictHostKeyChecking=no — отключает защиту полностью
  6. Получить 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)

Решение

  1. Чинить permissions локально: chmod 700 ~/.ssh && chmod 600 ~/.ssh/id_*
  2. На сервере: chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys && chown -R user:user ~/.ssh
  3. Указать ключ явно: ssh -i ~/.ssh/specific-key user@host
  4. Скопировать публичный ключ на сервер: ssh-copy-id user@host (если ещё хоть какая-то auth работает)
  5. Запустить ssh-agent и добавить ключ: eval $(ssh-agent) && ssh-add ~/.ssh/id_ed25519
  6. В ~/.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 поведение похожее, но менее заметное

Решение

  1. Запомнить мантру: 'trailing slash on source = copy contents, no slash = copy folder'
  2. Всегда использовать -n / --dry-run для проверки перед реальной синхронизацией
  3. Для копирования содержимого: rsync -av src/ dest/
  4. Для копирования папки целиком: rsync -av src dest/
  5. Использовать --itemize-changes (-i) для подробного списка изменений
  6. После реальной синхронизации проверить 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 нужно явно указывать

Решение

  1. tar -xf (без -z/-j/-J) — современный GNU tar auto-detect сжатие. Использовать просто -xf
  2. Явный формат: tar -xzf для gz, -xjf для bz2, -xJf для xz, --zstd для zstd
  3. Если повреждён — скачать заново; проверить checksum через sha256sum
  4. Если zip с расширением .tar.gz: unzip archive.tar.gz
  5. Двойное сжатие: gunzip file.tar.gz.gz потом tar -xf file.tar.gz и снова
  6. Если уверены в формате, но 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`

Решение

  1. Конвертировать CRLF в LF: dos2unix file или sed -i 's/\r$//' file
  2. Перекодировать в UTF-8: iconv -f CP1251 -t UTF-8 file > file.utf8
  3. Использовать LC_ALL=C grep для байтового поиска (быстрее и без locale-surprises)
  4. Для binary файлов: grep -a (treat as text) или strings file | grep
  5. Использовать ripgrep (rg) — современная альтернатива, лучше обрабатывает encoding и binary
  6. Для 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-программы без понимания контекста

Решение

  1. Использовать одинарные кавычки для awk-программы: awk '{print $1}' file
  2. Если нужны bash-переменные внутри awk: awk -v var="$bash_var" '{print var, $1}'
  3. Для длинных awk — выносить в файл: awk -f script.awk data.txt
  4. Проверить версию awk: на macOS установить gawk через brew install gawk
  5. Использовать gawk явно если нужны его расширения
  6. Для сложных задач — 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

Решение

  1. На macOS использовать пустой extension: sed -i '' 's/foo/bar/g' file
  2. Кроссплатформенно: sed -i.bak 's/foo/bar/g' file (создаст .bak — работает в обеих версиях)
  3. Установить GNU sed на macOS: brew install gnu-sed (доступен как gsed)
  4. В скриптах проверять платформу: if [[ $(uname) == Darwin ]]; then sed -i '' ...; else sed -i ...; fi
  5. Альтернатива: использовать perl -i -pe 's/foo/bar/g' file — работает одинаково везде
  6. Современные альтернативы: 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` устанавливает что-то странное

Решение

  1. Сгенерировать локаль: sudo locale-gen en_US.UTF-8 (Ubuntu/Debian), затем sudo update-locale LANG=en_US.UTF-8
  2. Для русской: sudo locale-gen ru_RU.UTF-8
  3. В Docker: добавить RUN apt-get install -y locales && locale-gen en_US.UTF-8 в Dockerfile
  4. На клиенте: убрать пересылку LC_*: закомментировать SendEnv LANG LC_* в /etc/ssh/ssh_config
  5. Или на сервере: AcceptEnv убрать из /etc/ssh/sshd_config
  6. В скрипте — явно: 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` (но они и так на каждом шагу)

Решение

  1. Использовать locate / plocate если есть updatedb-индекс: locate '*.csv' — миллисекунды
  2. Ограничить scope: find /home /data вместо /
  3. Исключить виртуальные FS: find / -xdev (не пересекать mount boundaries)
  4. Использовать -prune для исключения каталогов: find / -path /proc -prune -o -name '*.csv' -print
  5. Modern alternative — fd: fd csv — в разы быстрее, дружелюбнее синтаксис, цветной вывод
  6. Если ищете содержимое — ripgrep (rg) вместо find ... -exec grep
  7. Для регулярных запросов настроить 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

Решение

  1. Для NDJSON / JSON Lines: использовать jq -c . БЕЗ дополнительных опций, или jq --slurp '. | map(...)' чтобы собрать в массив
  2. По-строчно: while read -r line; do echo "$line" | jq '.msg'; done < file.ndjson
  3. Удалить BOM: sed -i '1s/^\xEF\xBB\xBF//' file.json
  4. Удалить пустые строки: grep -v '^$' file.json | jq .
  5. Проверить целостность JSON: python -m json.tool file.json (Python даёт более внятные ошибки)
  6. Использовать gron / fx — альтернативы jq с лучшими ошибками
  7. Для большого файла: 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

Решение

  1. Установить tmux-resurrect / tmux-continuum (плагины через TPM) — сохраняет/восстанавливает layout и опционально содержимое pane
  2. После reboot: tmux new -s work + prefix + Ctrl+r чтобы restore через resurrect
  3. Запускать tmux через systemd-user-service для авто-restart
  4. linger для user (чтобы процессы жили без активного login): sudo loginctl enable-linger $USER
  5. Альтернатива: zellij — modern terminal multiplexer с встроенным session persistence
  6. Для критичных 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)

Решение

  1. Обновить CA-сертификаты: sudo apt update && sudo apt install --reinstall ca-certificates && sudo update-ca-certificates
  2. Синхронизировать время: sudo systemctl restart systemd-timesyncd или sudo ntpdate pool.ntp.org
  3. Для corporate CA: положить свой CA в /usr/local/share/ca-certificates/corp.crt и sudo update-ca-certificates
  4. В скриптах для self-signed: curl --cacert /path/to/ca.pem https://host
  5. НЕ использовать curl -k / wget --no-check-certificate в production — отключает защиту
  6. В Docker: RUN apt-get install -y ca-certificates или apk add ca-certificates
  7. Если истёк сертификат сервера — связаться с админом, проблема на их стороне

Симптомы

  • `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 одновременно с автоматическим обновлением

Решение

  1. Подождать — если активны apt-daily / unattended-upgrades, дать им закончить (10-30 минут)
  2. Если уверены, что никто не работает — удалить stale locks: sudo rm /var/lib/dpkg/lock-frontend /var/lib/apt/lists/lock /var/lib/dpkg/lock
  3. Затем: sudo dpkg --configure -a чтобы починить состояние
  4. Если процесс висит и не закрывает: sudo kill <PID> процесса apt
  5. Отключить автообновления (для серверов с manual control): sudo systemctl disable --now unattended-upgrades
  6. Использовать 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`

Решение

  1. В скриптах: shopt -s nullglob — пустой glob становится пустым массивом, цикл не выполнится
  2. Альтернатива shopt -s failglob — упасть с error если glob не матчит (для критичных операций)
  3. Проверять руками: if compgen -G '*.csv' > /dev/null; then for f in *.csv; do ...; done; fi
  4. Использовать find вместо glob: find . -maxdepth 1 -name '*.csv' -exec ... {} +
  5. Для расширенных glob (** рекурсивно): shopt -s globstar
  6. В 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 -> весь скрипт падает

Решение

  1. Игнорировать SIGPIPE — это нормально для head/tail/grep -m
  2. Если set -o pipefail мешает: проверять конкретные exit codes через ${PIPESTATUS[@]}
  3. Альтернатива: использовать read для построчной обработки вместо передачи большого потока
  4. Подавить ошибку в stderr: command 2>/dev/null | head (не лучшая практика)
  5. Для yes-like команд: yes | head -100yes пытается писать вечно, получает SIGPIPE на 101 итерации — это by design
  6. В 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

Решение

  1. В первой строке crontab указать env: PATH=/usr/local/bin:/usr/bin:/bin\nHOME=/home/user
  2. В скрипте — load env явно: source /home/user/.profile или source venv/bin/activate
  3. Для systemd: EnvironmentFile=/etc/myservice.env + явный Environment=PATH=...
  4. Использовать абсолютные пути: /usr/bin/python3 а не python
  5. Обернуть в bash login shell: /bin/bash -lc './script.sh' — наследует env как при login
  6. Для conda: eval "$(/opt/conda/bin/conda shell.bash hook)"; conda activate envname в скрипте
  7. Хранить путь к проекту в env-файле и читать: source /etc/myapp/env