Содержание<nav class="toc-nav"> <ul> <li><a href="#1-твой-тестовый-инструмент-пошёл-в-ipv6-а-сервис-живёт-в-ipv4">1. Твой тестовый инструмент пошёл в IPv6, а сервис живёт в IPv4</a></li> <li><a href="#2-systemctl-list-timers-без-sudo-врёт-в-лицо">2. <code>systemctl list-timers</code> без sudo врёт в лицо</a></li> <li><a href="#3-репозиторий-отдаёт-200-на-пустоту">3. Репозиторий отдаёт 200 на пустоту</a></li> <li><a href="#4-configtest-сказал-syntax-ok-и-apache-лёг">4. <code>configtest</code> сказал Syntax OK, и Apache лёг</a></li> <li><a href="#5-локаль-молча-ломает-разбор-вывода">5. Локаль молча ломает разбор вывода</a></li> <li><a href="#6-регулярка-которая-ничего-не-нашла-ничего-не-доказывает">6. Регулярка, которая ничего не нашла, ничего не доказывает</a></li> <li><a href="#7-pgrep--f-находит-сам-себя">7. <code>pgrep -f</code> находит сам себя</a></li> <li><a href="#общий-знаменатель">Общий знаменатель</a></li> </ul> </nav>

Самые дорогие по времени поломки - не те, которые трудно починить. А те, где инструмент диагностики дал уверенный неправильный ответ, вы ему поверили и пошли копать не туда.

Ниже семь таких случаев. Все мои, все реальные, каждый стоил мне от получаса до вечера. Общее у них одно: ни в одном не было ошибки в коде или в конфиге. Ошибка была в проверке.

1. Твой тестовый инструмент пошёл в IPv6, а сервис живёт в IPv4

Самый свежий и самый обидный.

Проверял, открыт ли на сервере исходящий SMTP. Написал пяток строк на Python:

import socket
socket.create_connection(("mail.example.net", 25), timeout=10)

Получил ENETUNREACH. Сделал вывод: порт закрыт, надо идти к провайдеру.

Вывод был неверный. На хосте нет глобального IPv6, а резолвер отдаёт AAAA-запись. socket.create_connection честно берёт первый адрес из getaddrinfo, то есть IPv6, и упирается в недостижимую сеть. При этом сам почтовый сервер работал прекрасно, потому что у него в конфигурации стоит

inet_protocols = ipv4

и он в AAAA даже не смотрит. Мой тест проверял путь, которым сервис не пользуется.

Через несколько дней я наступил на тот же грабель другим ботинком. Решил проверить SMTP утилитой swaks, добросовестно передал ей флаг -4:

*** Error connecting to mail.example.net:25:
*** 	IO::Socket::INET6: connect: Connection refused

Флаг не применился, и она всё равно полезла через IPv6-сокет. Пришлось подставлять адрес литералом.

Правило: если на хосте нет глобального IPv6, а в DNS есть AAAA, любой сетевой тест надо явно прибивать к IPv4. В Python это фильтр по AF_INET:

_orig = socket.getaddrinfo
socket.getaddrinfo = lambda *a, **k: [
    r for r in _orig(*a, **k) if r[0] == socket.AF_INET
]

И не верьте флагам вроде -4, пока не увидели в ошибке подтверждение, что они сработали.

2. systemctl list-timers без sudo врёт в лицо

Перед рискованной правкой сети я взвёл страховку - transient-таймер, который через пять минут откатит конфигурацию:

systemd-run --on-active=300 --unit=netplan-rollback /root/netplan-rollback.sh

Применил изменения, проверил таймер:

systemctl list-timers
# 0 timers listed.

Испугался, что страховки нет. Потом посмотрел под sudo и увидел список.

А дальше выяснилось кое-что похуже: transient-таймер после применения сетевой конфигурации из systemd пропал, так и не сработав. В журнале пусто, файлы не восстановлены. То есть первое наблюдение было ложным по причине прав, а настоящая проблема была совсем другая и обнаружилась случайно.

Два правила отсюда. Первое: systemctl list-timers, list-units и родственников смотреть только под sudo, иначе вы видите не реальность, а свой срез прав. Второе: transient-таймер - плохая единственная страховка при правках сети. Если он должен вас спасти, проверьте его существование после опасного действия, а не до.

3. Репозиторий отдаёт 200 на пустоту

Надо было понять, встанет ли современный агент мониторинга на старый Debian. Пошёл проверять наличие пакета прямо в индексах репозитория.

GET /zabbix/7.0/debian/dists/stretch/Release
200 OK

Каталог существует, Release отдаётся, в индексе шестнадцать пакетов. Вывод: поддержка есть, можно обновлять.

Вывод опять неверный. Нужного пакета zabbix-agent среди этих шестнадцати нет. То есть apt update пройдёт совершенно чисто, а apt install скажет, что кандидата не нашёл. Я чуть не отправил в работу план обновления, который развалился бы на первом же хосте.

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

curl -s https://repo.zabbix.com/zabbix/7.0/debian/dists/stretch/main/binary-amd64/Packages.gz \
  | gunzip -c \
  | awk '/^Package: zabbix-agent$/{f=1} f&&/^Version:/{print $2; exit}'

Пусто - значит пакета нет, какие бы двухсотые ни отдавал сервер.

Это общий класс проблемы: многие сайты отдают 200 на несуществующие пути, иногда со страницей-заглушкой, иногда с пустотой. Проверка «код ответа не 404, значит есть» не работает нигде, где вы не контролируете сервер.

4. configtest сказал Syntax OK, и Apache лёг

Добавлял в веб-сервер OCSP stapling. Положил директиву внутрь виртуального хоста, проверил конфигурацию как положено:

apache2ctl configtest
# Syntax OK

Перезапустил. Веб-сервер не поднялся целиком, вместе со всеми остальными сайтами:

AH01958: SSLStapling: no stapling cache available
AH02311: Fatal error initialising mod_ssl, exiting

Причина содержательная: кэш для stapling объявляется исключительно на уровне сервера, а не внутри виртуального хоста. Директива без кэша синтаксически безупречна и семантически смертельна.

Но интересна тут не причина, а то, что configtest этого не видит. Он проверяет разбор конфигурации, а не инициализацию модулей. Всё, что падает на этапе запуска, он пропускает с чистой совестью.

Правило: проверки SSL и вообще любых модульных подсистем не считаются сделанными до реального systemctl restart. И делать этот рестарт лучше тогда, когда вы готовы к тому, что сервис не вернётся, а не в конце рабочего дня.

5. Локаль молча ломает разбор вывода

Проверял, какие версии пакетов доступны, конвейером вида

apt-cache policy some-package | sed -n 's/^  Candidate: //p'

Получил пустоту по всем пакетам и спокойно записал: в репозиториях их нет.

На системе была русская локаль. apt-cache policy на ней пишет не Candidate:, а Кандидат:. Мой фильтр не совпадал ни с чем и честно возвращал ничего. Пакеты при этом были на месте.

Беда этого класса ошибок в том, что они дают пустой результат вместо ошибки. Пустота читается как «ничего нет», хотя означает «я не понял, что мне ответили».

Правило простое и не подлежащее обсуждению:

export LC_ALL=C

перед любым разбором вывода системных утилит. В скриптах - в первой строке, не задумываясь. Это ровно тот случай, когда перестраховка бесплатна.

6. Регулярка, которая ничего не нашла, ничего не доказывает

Проверял, приходит ли на почтовый сервер внешняя почта. Написал выборку по логу:

grep -E 'postfix/smtpd.*: [A-Z0-9]+: client=' /var/log/mail.log | grep -v localhost

Ноль строк. Доложил: внешних приёмов нет.

Через сутки заметил несоответствие с другими признаками и посмотрел внимательно. Идентификаторы очереди Postfix выглядят так:

4j87ls6DV2zRkDb

Это смешанный регистр, а мой шаблон требовал [A-Z0-9]+. Он не матчил ни одной живой строки. Внешняя почта шла всё это время, и вполне бодро.

Правильный вариант оказался проще неправильного:

grep -a 'client=' /var/log/mail.log | grep -v localhost

Правило: когда поисковый шаблон возвращает ноль, у этого есть две равновероятные причины - искомого нет, или шаблон негоден. Прежде чем докладывать результат, проверьте шаблон на строке, которая заведомо должна найтись. Тридцать секунд против суток неверного вывода.

Попутно: на бинарных и смешанных логах не забывайте -a, иначе grep может решить, что файл двоичный, и выдать одну строчку вместо совпадений.

7. pgrep -f находит сам себя

Запустил долгую операцию обслуживания и хотел понять, закончилась ли она:

pgrep -c -f "occ maintenance:repair"
# 1

Единица. Значит работает, ждём дальше. Подождал изрядно.

Процесса давно не было. Единицу дала сама проверка: аргумент -f ищет по всей командной строке, а командная строка обёртки, внутри которой выполнялся pgrep, содержала эту самую подстроку. Проверка нашла себя и отрапортовала об успехе.

Лечится по-разному. Можно исключить себя из поиска хитростью с квадратными скобками:

pgrep -c -f "[o]cc maintenance:repair"

Можно проверять по PID, если вы его сохранили при запуске. Можно смотреть на результат работы, а не на наличие процесса, - это вообще самый надёжный путь.

Правило: любая проверка «работает ли X» через поиск по списку процессов обязана быть проверена на заведомо отсутствующем процессе. Если она и там говорит «работает» - она бесполезна.

Общий знаменатель

Если свести все семь в одну мысль, она такая: проверка - это тоже код, и в ней бывают баги. Мы привыкли относиться к диагностике как к объективу, через который смотрим на систему. А это ещё одна программа, со своими умолчаниями, своим разрешением имён, своей локалью и своим правом смотреть.

Из этого следуют три привычки, которые окупаются быстрее всего остального.

Проверяйте проверку на заведомом результате. Если тест должен находить процессы - запустите его там, где процесса точно нет. Если регулярка должна ловить строки - прогоните её по строке, которая обязана совпасть. Вы удивитесь, как часто инструмент не ловит ничего вообще.

Сверяйте два независимых признака. Логи и сетевой тест. Код ответа и содержимое. Таймер и факт его срабатывания. Один признак может врать убедительно, два расходящихся признака сразу показывают, где именно.

Относитесь к пустому результату с подозрением. Пустота - это не «нет», это «я ничего не нашёл». Между этими утверждениями лежат локаль, права, регистр и несовпавший шаблон.

И последнее, уже скорее про самочувствие. Во всех семи случаях я ошибался уверенно: у меня был вывод инструмента, он был однозначный, и я на него опирался. Привычка задавать себе вопрос «а точно ли моя проверка видит то, что я думаю» не делает работу медленнее. Она делает её один раз вместо двух.