MAATRIX / Блог / Лимиты systemd, о которых узнают в аварии: TasksMax, LimitNOFILE и мёртвый сервис

Лимиты systemd, о которых узнают в аварии: TasksMax, LimitNOFILE и мёртвый сервис

MAATRIX

Сервис работал месяцами без нареканий, а потом под возросшей нагрузкой начал падать с непонятной ошибкой — то fork: retry: Resource temporarily unavailable, то Too many open files, хотя на сервере полно и памяти, и файловых дескрипторов на уровне ядра. Разгадка почти всегда одна: у каждого юнита systemd есть собственные лимиты, независимые от общесистемных, и их никто не выставлял специально — они просто стоят по умолчанию. Разберём, какие лимиты есть у юнита из коробки, как посмотреть их фактические значения и как переопределить безопасно, не трогая штатный unit-файл.

Почему это вообще существует: изоляция ресурсов на уровне юнита

Начиная с systemd, интегрированного с cgroups, каждый сервис — это не просто процесс, а обособленная контрольная группа со своими лимитами на память, число задач (процессов и потоков), CPU-квоту и файловые дескрипторы. Идея правильная: один взбесившийся сервис не должен положить весь сервер, наплодив тысячи потоков или открыв миллион сокетов. Проблема в том, что часть лимитов задаётся неявно — не в вашем unit-файле, а в дефолтах самого systemd или в конфиге дистрибутива, и вы про них не думаете до тех пор, пока сервис не упрётся в потолок под нагрузкой, которой не было при первом деплое.

Отсюда типичный сценарий аварии: нагрузка растёт постепенно (больше клиентов, воркеров, параллельных соединений), система работает месяцами, а затем число процессов, потоков или открытых файлов конкретного юнита пересекает порог — и сервис ведёт себя так, будто ресурсы кончились, хотя free -h и df -h на хосте показывают запас. Диагностика осложняется тем, что ошибка вылезает из недр приложения (fork failed, Too many open files, Cannot allocate memory) и выглядит как баг в коде, а не административный потолок.

Какие лимиты есть у юнита по умолчанию

Три лимита стоит держать в голове в первую очередь — они реально стреляют на практике:

  • TasksMax — максимальное число задач (процессов и потоков суммарно) внутри cgroup юнита. Начиная с systemd v228 имеет ненулевое значение по умолчанию: типично DefaultTasksMax=15% от PidsMax системы (видно в /etc/systemd/system.conf или его дропинах), что превращается в конкретное число тысяч задач — вроде бы много, но многопоточные worker-пулы, форкающиеся демоны и особенно приложения, плодящие поток на соединение, съедают этот запас быстрее, чем кажется.
  • LimitNOFILE — жёсткий и мягкий лимит файловых дескрипторов для процессов юнита. Именно он определяет, сколько дескрипторов увидит сервис, а не значение из /etc/security/limits.conf — то применяется только к сессиям через PAM (логин, su, sudo), а не к сервисам, которые systemd запускает напрямую как PID 1 → fork. Подробно про сам механизм — в отдельном разборе настройки лимитов открытых файлов через ulimit.
  • MemoryMax (и связанные MemoryHigh, MemoryLow, MemorySwapMax) — потолок памяти cgroup. По умолчанию для большинства юнитов не задан явно (infinity), но если его однажды прописали — скопировав unit-файл с другого сервиса — юнит будет упираться в чужой лимит независимо от того, сколько RAM свободно на сервере. Разбор конкретного инцидента с сервисом, который уходил в своп из-за унаследованного MemoryMax, стоит прочитать, если у вас похожая картина: своп растёт, а free показывает запас.

Менее очевидные, но тоже реальные:

  • LimitNPROC — устаревающий, но иногда встречающийся параметр числа процессов (в современных версиях фактически вытеснен TasksMax, который считает потоки и процессы вместе — старый LimitNPROC через PAM/limits.conf считает процессы пользователя, TasksMax — задачи именно этой cgroup).
  • CPUQuota — по умолчанию не ограничена, но если задана, приводит к троттлингу под пиковой нагрузкой, который выглядит как «процессор не загружен, а сервис тормозит».
  • IPAddressAllow/IPAddressDeny, DeviceAllow и прочие sandboxing-директивы — не лимиты объёма ресурса, но так же неожиданно режут функциональность, если unit-файл собирали из строгого security-шаблона.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Как посмотреть реальные значения: systemctl show и cgroup напрямую

Единственный надёжный способ узнать, с чем работает конкретный юнит прямо сейчас — не читать unit-файл (там могут быть не указаны директивы, которые всё равно действуют по дефолту), а спросить у самого systemd:

systemctl show my-service.service --property=TasksMax,TasksCurrent,LimitNOFILE,LimitNOFILESoft,MemoryMax,MemoryCurrent,CPUQuotaPerSecUSec

Пример вывода:

TasksMax=4915
TasksCurrent=612
LimitNOFILE=524288
LimitNOFILESoft=1024
MemoryMax=infinity
MemoryCurrent=891234304
CPUQuotaPerSecUSec=infinity

На это стоит обратить внимание сразу: LimitNOFILE (жёсткий лимит) может быть большим — 524288 — а LimitNOFILESoft (мягкий, реально действующий, пока процесс сам не поднимет его через setrlimit) остаться дефолтным — 1024. Многие приложения setrlimit не делают и живут с мягким лимитом, даже если жёсткий позволяет намного больше. Это одна из частых причин Too many open files при формально высоком LimitNOFILE в выводе systemctl show — разбор такой ошибки и трёх мест, где на самом деле стоит лимит, есть в статье про ошибку Too many open files.

Без фильтра по свойствам systemctl show выводит несколько сотен строк — удобнее сразу grep’ать нужное:

systemctl show my-service.service | grep -E '^(Tasks|Limit|Memory|CPUQuota)'

Текущее фактическое число задач и файл с лимитом можно посмотреть и напрямую через cgroup v2, без обёртки systemctl — это полезно, когда нужно свериться, что значение действительно применилось к работающему процессу:

cat /sys/fs/cgroup/system.slice/my-service.service/pids.max
cat /sys/fs/cgroup/system.slice/my-service.service/pids.current

А фактический лимит дескрипторов конкретного процесса (не юнита в целом, а именно PID) — через /proc:

cat /proc/$(systemctl show my-service.service -p MainPID --value)/limits | grep -i "open files"
Max open files            1024                 524288               files

Здесь первое число — мягкий лимит, действующий по умолчанию для процесса, второе — жёсткий потолок, до которого процесс может поднять себя сам. Если soft-значение маленькое, а приложение не вызывает setrlimit, оно упрётся именно в 1024, сколько бы ни было прописано в hard-лимите юнита.

Типичные симптомы упора в лимит

Ошибки, с которыми упор в лимит долетает до логов приложения, обычно не называют настоящую причину напрямую — это системные ошибки уровня ядра, транслированные наверх:

Симптом в логах/поведенииКакой лимит вероятно упёртКуда смотреть
fork: retry: Resource temporarily unavailable, Cannot forkTasksMax (или PidsMax cgroup)TasksCurrent vs TasksMax в systemctl show
Too many open files, EMFILE, обрывы соединений под нагрузкойLimitNOFILE (чаще soft)/proc/<pid>/limits, LimitNOFILESoft
Резкое торможение при формально свободном CPUCPUQuota / троттлинг cgroupCPUQuotaPerSecUSec, cpu.stat (поле nr_throttled)
Своп растёт, free показывает запас памяти на хостеMemoryMax / MemoryHighmemory.current vs memory.max в cgroup
Сервис перезапускается сам по себе без видимой причины в логах приложенияOOMPolicy + упор в MemoryMaxjournalctl -u my-service на предмет killed, oom

Общая черта всех этих симптомов — они появляются не сразу после деплоя, а спустя недели или месяцы, когда нагрузка (число клиентов, воркеров, параллельных задач) естественным образом дорастает до потолка, который никто явно не выбирал.

fork failed не всегда означает упор именно в TasksMax — иногда виновата исчерпанная память для нового процесса при живой памяти в целом (эффект MemoryMax), а не число задач cgroup.

Как переопределить лимит безопасно: override-файлы

Худший способ поправить лимит — редактировать unit-файл пакета напрямую в /lib/systemd/system/ или /usr/lib/systemd/system/: правки затрутся при обновлении пакета. Правильный способ — drop-in override, который переживает обновления и явно виден в systemctl status:

systemctl edit my-service.service

Команда открывает редактор и создаёт (или дополняет) файл /etc/systemd/system/my-service.service.d/override.conf. Пример содержимого для повышения лимитов под возросшую нагрузку:

[Service]
TasksMax=8192
LimitNOFILE=262144
# Явный soft = hard убирает разрыв между "видно в systemctl show" и "реально работает"
LimitNOFILESoft=262144

Путь при ручной правке (без systemctl edit) должен быть именно таким — /etc/systemd/system/<имя-юнита>.service.d/override.conf. После правки — обязательные два шага:

systemctl daemon-reload
systemctl restart my-service.service

daemon-reload перечитывает unit-файлы и дропины, но не перезапускает уже работающие сервисы — без последующего restart новый лимит не применится к текущему процессу, вы увидите его только в systemctl show для «желаемой» конфигурации, а не в /proc/<pid>/limits для реально работающего.

Для срочной диагностики без правки файлов есть runtime-вариант — systemctl set-property, который меняет значение cgroup немедленно:

systemctl set-property my-service.service TasksMax=8192 --runtime

Флаг --runtime делает изменение временным (до перезагрузки) — удобно, чтобы проверить гипотезу «дело в лимите» до того, как вносить постоянную правку. Без --runtime команда запишет изменение сразу в постоянный drop-in — это стоит иметь в виду, если ожидали чисто диагностическую проверку.

Снимать TasksMax совсем (infinity) — плохая идея на проде, даже если он сейчас мешает: смысл лимита не в том, чтобы мешать разработчику, а в том, чтобы не дать одному сервису с багом исчерпать PidsMax всей системы и заблокировать создание процессов везде, включая SSH-сессию для диагностики. Правильная правка — поднять лимит с запасом от реальной пиковой нагрузки, а не убрать его концептуально. Похожая история с процессным лимитом на уровне пользователя (nproc/PAM, а не cgroup юнита) разобрана в статье про инцидент, где лимит процессов на пользователя обрубил деплой на середине флота — путать эти два механизма при диагностике легко.

Особый случай: сервисы под docker и systemd одновременно

Если сервис запускается через docker run или docker compose, а сам демон Docker управляется юнитом docker.service, лимиты действуют на двух независимых уровнях: у самого контейнера (--ulimit, --pids-limit, mem_limit в compose) и у юнита docker.service, который их не наследует контейнерам автоматически. Поднятый в override для docker.service LimitNOFILE не долетает до контейнеров — каждый получает собственный дефолт, если явно не передать --ulimit nofile=262144:262144 при запуске или не прописать это в compose-файле. Диагностика тоже отдельная: docker inspect <container> --format '{{.HostConfig.Ulimits}}' и cgroup самого контейнера (/sys/fs/cgroup/system.slice/docker-<id>.scope/), а не юнита демона.

Профилактика: как не узнавать про лимит в аварии

Несколько практик, которые снимают этот класс проблем до того, как он станет инцидентом на проде:

  • Снимайте базовую линию лимитов при первом деплое сервиса, а не когда он уже упал. Прогон systemctl show <unit> | grep -E '^(Tasks|Limit|Memory)' сразу после запуска и сохранение результата в репозиторий с конфигами экономит часы диагностики через полгода, когда никто уже не помнит, что задано явно, а что досталось по дефолту.
  • Считайте реальный потолок нагрузки заранее. Если сервис создаёт поток или процесс на клиента, посчитайте, сколько параллельных клиентов приведёт TasksCurrent к TasksMax, и держите лимит с запасом — вдвое-втрое выше ожидаемого пика.
  • При копировании unit-файлов между сервисами — сверяйте каждое числовое значение. Унаследованный MemoryMax, TasksMax или LimitNOFILE со старого сервиса с другим профилем нагрузки — частая причина таких аварий: конфиг валиден, сервис работает нормально, пока нагрузка не дорастёт до чужого потолка.
  • Мониторьте TasksCurrent/TasksMax и MemoryCurrent/MemoryMax отдельно от общесистемных CPU/RAM/диска. Общий мониторинг здесь обычно молчит — сервер здоров, юнит уже упирается в свой потолок. Алерт на «значение подобралось к 80% от лимита юнита» ловит проблему за дни до даунтайма.
  • Разделяйте soft- и hard-лимиты явно в override, не полагаясь на то, что приложение само поднимет мягкий лимит через setrlimit. Многие этого не делают и остаются на дефолтном 1024, даже если systemd готов дать намного больше.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Почему ulimit -n в интерактивной сессии показывает одно значение, а сервис под systemd видит другое?

ulimit в шелле и /etc/security/limits.conf действуют через PAM — только для сессий, начатых через login, su, sudo. Сервисы, которые systemd запускает напрямую как дочерние процессы PID 1, PAM-стек не проходят и получают лимиты из LimitNOFILE конкретного unit-файла или из дефолтов systemd — это два независимых набора значений.

Нужно ли перезапускать сервис после systemctl daemon-reload, если правил только override-файл?

Да, обязательно. daemon-reload только перечитывает конфигурацию юнитов, но не применяет новые лимиты к уже работающему процессу. Для override-файлов через systemctl edit — всегда daemon-reload и затем restart.

Можно ли посмотреть лимиты юнита, который сейчас не запущен?

Частично — systemctl show <unit> покажет статические значения из unit-файла и дефолтов, но динамические вроде TasksCurrent или MemoryCurrent будут нулевыми или отсутствовать, потому что cgroup процесса создаётся только при запуске.

Дефолтный TasksMax одинаков на всех дистрибутивах?

Нет, он зависит от версии systemd и от DefaultTasksMax в /etc/systemd/system.conf, а эти значения отличаются между дистрибутивами и даже между их версиями. Не полагайтесь на цифру из документации — проверяйте systemctl show <unit> --property=TasksMax на конкретном сервере.

MemoryMax и TasksMax считаются одной и той же cgroup или разными?

Одной и той же — оба лимита действуют в рамках cgroup конкретного юнита (/sys/fs/cgroup/system.slice/<unit>.service/). Рядом лежат и pids.max/pids.current, и memory.max/memory.current — согласованная картина одного изолированного контейнера ресурсов.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →