Кроме домашнего сервера у меня есть небольшой арендованный сервер за пределами дома: на нём сервис, которым пользуются домашние устройства, и почта. Неделями он «падал» по вечерам: то работает, то нет. Я искал виноватого у хостера, в маршруте, в домашнем канале. А причина лежала на самом сервере и была банальной до обидного — кончилось место на диске. Плюс вторая история: защита от шторма переподключений, которая через четыре дня заперла систему изнутри.
Это тринадцатая часть цикла «Домашняя инфраструктура».
Симптом, который сбивал с толку
Сеть до сервера была чистой: 0% потерь, 56 мс. Мелкие запросы через сервис проходили. А объёмные — например, отправка картинки — вставали намертво, 0 КБ/с. Такая картина очень похожа на проблемы с сетью: «мелкое пролезает, крупное нет». Поэтому я и копал в сторону маршрута и MTU.
А на сервере:
$ df -h /
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 15G 14G 367M 98% /
Сервис писал строку журнала на каждый запрос (уровень info) плюс журнал DNS-запросов. JSON-журнал его контейнера разросся до 928 МБ. Когда место почти кончилось, запись в журнал начала блокироваться — а вместе с ней вставала и обработка запросов, которые эту запись порождали. Мелкие запросы успевали проскочить, объёмные — нет.
После чистки — 5368 КБ/с (около 44 Мбит/с) вместо нуля. И задним числом это объяснило всю историю: 70 записей «сервер не отвечает» за неделю при чистой сети и по 12–14 срабатываний в сутки в предыдущий месяц.
Что занимало 15 ГБ и что с этим сделал
| Что | Сколько | Что сделано |
|---|---|---|
| JSON-журналы контейнеров | 1,6 ГБ (928 + 230 + 169 + 96 МБ) | обнулены, последние 2000 строк сохранены |
| журнал systemd | 713 МБ без лимита | SystemMaxUse=200M |
| кэш apt и старые ревизии snap | — | apt-get clean, удалены |
| хранилище образов containerd | 5 ГБ | не трогал: там рабочие образы |
Итог: занято 82% вместо 98%, свободно 2,6 ГБ. На диске в 15 ГБ это и есть разумный запас.
Грабля 1: ротация журналов Docker не действует на старые контейнеры
Лимит в /etc/docker/daemon.json ("log-opts": {"max-size": "50m", "max-file": "3"}) применяется только к контейнерам, созданным после правки. Уже работающие продолжают писать без ограничений, пока их не пересоздать. До пересоздания помогает logrotate — но только с copytruncate: без него Docker продолжает писать в переименованный файл, и место не освобождается.
Грабля 2: vacuum журнала освободил ноль
$ journalctl --vacuum-size=200M
Vacuuming done, freed 0B of archived journals.
Все 713 МБ лежали в активном файле журнала, а vacuum чистит только архивные. Помогло systemctl restart systemd-journald (активный файл закрывается и становится архивным) и повтор команды.
Грабля 3: уровень журнала откатывается сам
Уровень журнала сервиса я понизил до warning в его файле настроек — и после перезапуска он вернулся на info. Файл генерировался панелью управления из её базы и перетирался при каждом старте. Править пришлось в самой панели. Результат: было около 900 МБ журнала в сутки, стала одна строка раз в 12 секунд.
Сторожа за местом на диске не было — а именно он предотвратил бы всю историю. Проверка свободного места раз в час с тревогой при 85% — самый дешёвый мониторинг, который только бывает. Единственный подвох: если тревога уходит почтой, а почта живёт на том же сервере, она не уйдёт тогда, когда нужнее всего.
Правило «сначала самое глупое»
Через пару недель сервер лёг целиком — на полтора часа: ни пинга, ни портов, и из дома, и с другого сервера в дата-центре. В панели хостера «всё нормально». Девять падений за сутки — проблема была в сети хостера, и вместе с сервером, конечно, легла почта. Из обоих случаев вышло правило проверки при «всё легло», по порядку:
- жив ли домашний интернет (открывается ли что угодно ещё);
- жив ли сервер — с двух разных точек, не только из дома;
- есть ли место на диске и память —
df -h,free -m; - и только потом — свои настройки, маршруты и версии.
Защита, которая заперла систему изнутри
Вторая история связана с тем же сервисом. Однажды пропавший канал до внешнего узла устроил шторм: соединения устанавливались, но данных по ним не шло, и закрывать их было некому — по умолчанию ядро проверяет мёртвые соединения раз в два часа. Устройства в сети переспрашивали каждые несколько секунд, и одновременных соединений набралось 3149.
Первое лекарство — ограничить число одновременных соединений и скорость создания новых:
iptables -A OUTPUT -p tcp --syn -m connlimit --connlimit-above 300 --connlimit-mask 0 -j REJECT --reject-with tcp-reset
iptables -A OUTPUT -p tcp --syn -m hashlimit --hashlimit-above 40/sec --hashlimit-burst 80 \
--hashlimit-name syn -j REJECT --reject-with tcp-reset
Через четыре дня сервис встал при живом сервере. Характерный признак — ровно 300 установленных соединений и 21 815 отказов в счётчике правила. --connlimit-mask 0 значит «считать все соединения вместе, не разбирая источников», и обычная нагрузка домашней сети упёрлась в потолок. Поднял потолок до 1000 — и снова: 827 отказов за час при трёх реальных соединениях с моей стороны. Обычная рабочая точка сети оказалась около 954 одновременных соединений, то есть любой разумный потолок был ниже нормы.
Итог: connlimit удалён совсем. Осталось только ограничение скорости создания новых соединений — от шторма оно защищает, а нормальную работу не трогает. А мёртвые соединения теперь закрываются быстро: в настройках сервиса включён keepalive на исходящих соединениях — проверка через 30 секунд простоя, повтор через 10. Системные net.ipv4.tcp_keepalive_* тоже задают эти интервалы, но действуют только на сокеты, где программа сама включила SO_KEEPALIVE, — поэтому смотреть надо в настройки сервиса.
Keepalive лучше, чем урезать время простоя: мёртвое соединение умирает за минуту, а живое, но просто молчащее, никто не рвёт. И имя счётчика hashlimit держите коротким: у модуля есть ограничение на длину имени.
И урок, который стоит повторить: прежде чем чинить «нестабильность» удалённого сервера, проверьте, не ваша ли защита её создаёт. 12 записей «сервер мёртв» за сутки сгрудились ровно в окне, когда работал ограничитель.
Сервер «иногда не отвечает», а причина не находится?
Пришлите df, free и правила файрвола — проверю место, журналы и ограничители, которые могли запереть систему.
// загружаю комментарии…