PostgreSQL создаёт семафоры операционной системы заранее — по одному на каждый слот, который сервер резервирует под backendпроцесс, обслуживающий одно клиентское соединение или фоновый процессслужебный процесс сервера, не связанный с клиентом. Сколько именно семафоров запрошено, стало видно только в PostgreSQL 18: появился параметр num_os_semaphores. До этого число приходилось считать вручную по формуле с необъяснимой константой, и формула годами расходилась с реальностью. Разберём, откуда берётся число, как оно связано с лимитами ядра и почему ошибка «could not create semaphores: No space left on device» дожила до наших дней.
Зачем серверу семафоры ОС
Платформенно-независимый API в pg_sema.h определяет семафоры как счётныйсчётный семафор хранит число разрешений: каждая разблокировка увеличивает счётчик, каждая блокировка уменьшает его, а когда счётчик доходит до нуля, следующая блокировка ждёт. Серверу нужен именно такой семафор, потому что разблокировок может накопиться несколько подряд, и столько же последующих блокировок должны пройти без ожидания. Каждый слот, зарезервированный сервером под backend или фоновый процесс, получает один семафор, на котором процесс спит в ожидании легковесной блокировкиlightweight lock — внутренняя блокировка PostgreSQL, которой процессы синхронизируются без обращения к диску. Все семафоры создаются заранее, при старте сервера.
Число этих семафоров и сообщает num_os_semaphores — preset-параметрпараметр с контекстом internal, доступный только для чтения и не задаваемый в конфигурации. Он новый в PostgreSQL 18, а значение вычисляется при каждом запуске, а не вкомпилировано. На установке по умолчанию 18 это 174. Узнать его можно командой:
postgres -D $PGDATA -C num_os_semaphoresФормула и её эволюция
Сумма выглядит так:
max_connections + autovacuum_worker_slots + max_worker_processes + max_wal_senders + 40Слагаемые означают по одному семафору на каждое разрешённое соединение (max_connections), на каждый разрешённый рабочий процесс autovacuum (autovacuum_worker_slots — слоты для процессов автоочистки, которые чистят устаревшие строки таблиц), на каждый разрешённый процесс WAL sender (max_wal_senders — процессы, передающие журнал предзаписи на реплики) и на каждый разрешённый фоновый процесс (max_worker_processes — общий пул фоновых рабочих процессов, из которого берутся, например, параллельные исполнители запросов).
Константа 40 складывается из трёх частей. Два синглтона — autovacuum launcher (процесс, запускающий автоочистку) и slot sync worker (процесс синхронизации слотов репликации). Шесть слотов для auxiliary-процессоввспомогательных процессов: checkpointer, background writer, WAL writer и остальных. И 32 слота для I/O workersфоновых процессов, обслуживающих ввод-вывод, зарезервированных независимо от значений io_method и io_workers — io_method = sync их не освобождает. Подготовленные транзакции семафоров не получают, поэтому max_prepared_transactions в сумму не входит.
Формула «не хотела оставаться фиксированной», потому что её должен был обновлять человек при появлении нового вида процессов, но никто этого не делал. По разбору Nathan Bossart, она была неверна с 9.2 (появление checkpointer) до обнаружения в 9.6, снова неверна в 14 (archiver стал auxiliary-процессом) и снова должна была стать неверной в 17 (добавлен WAL summarizerпроцесс, подготавливающий сводки журнала предзаписи для инкрементального резервного копирования). Исправление в мае 2024 состояло в том, чтобы перестать публиковать сумму и заставить сервер сообщать собственный ответ, вычисляемый во время выполнения, — так же, как shared_memory_size.
Проверить фактическое значение можно командой postgres -D $PGDATA -C num_os_semaphores; в примере она выводит 174. Этот способ работает даже когда сервер не может стартовать — а это и есть основная ситуация, когда он нужен. Пока сервер запущен, команда не сработает: postgres -C останавливается на файле блокировки postmaster.pid для параметров, вычисляемых во время выполнения; в этом случае следует использовать SHOW num_os_semaphores.
Как устроен механизм: POSIX против System V
Две реализации семафоров по-разному работают с ядром.
В POSIX-пути создание идёт через sem_open с именем вида /pgsql-%d и флагами O_CREAT | O_EXCL. При коллизии (EEXIST, EACCES, EINTR) цикл повторяется с новым ключом. Сразу после успеха вызывается sem_unlink, чтобы семафор нельзя было открыть извне и чтобы он исчез при крахе. Удаление — sem_close для именованных или sem_destroy для неименованных семафоров. Сброс счётчика семафора до нуля не имеет прямого API: он выполняется повторными sem_trywait, пока не вернётся EAGAIN или EDEADLK. Ожидание семафора блокирующе вызывает sem_wait.
В SYSV-пути создание идёт через semget(semKey, numSems, IPC_CREAT | IPC_EXCL | IPCProtection). При коллизии возвращается -1 только если вызывающий разрешил повтор (retry_ok), иначе — FATAL. Отдельно отмечается, что EINVAL может означать, что SEMMSL меньше numSems, и это не лечится сменой ключа. Удаление — semctl(semId, 0, IPC_RMID, semun). Инициализация значения — semctl с SETVAL, при ERANGE подсказка поднять SEMVMX. Ожидание семафора выполняет semop с sem_op = -1. При ENOSPC подсказка про лимиты SEMMNI/SEMMNS.
Ограничения тоже различаются. POSIX-путь при именованных семафорах не требует разделяемой памяти (запрос разделяемой памяти под семафоры ничего не делает), а при неименованных нужен массив PGSemaphoreData в разделяемой памяти. SYSV-путь упирается в лимиты SEMMNI/SEMMNS и SEMVMX. В sysv_sema.c создание семафоров запрещено в backend: Assert(!IsUnderPostmaster); — создавать семафоры можно только в postmaster.
Наборы семафоров System V
Семафоры группируются в наборы, потому что при создании нового семафора проверяется, не исчерпан ли текущий набор: если nextSemaNumber >= SEMAS_PER_SET, создаётся ещё один набор, а счётчик сбрасывается в 0. Число наборов ограничено вычисленным максимумом maxSemaSets = (maxSemas + SEMAS_PER_SET - 1) / SEMAS_PER_SET;, и при превышении выдаётся elog(PANIC, "too many semaphores created").
В каждом наборе 17-й семафор служит для распознавания «своих» наборов: набор семафоровгруппа семафоров System V, создаваемая одним вызовом semget и адресуемая парой «идентификатор набора + номер семафора в наборе». При поиске свободного ключа сервер должен отличить набор, оставшийся от прежнего запуска PostgreSQL, от набора, созданного другим приложением, — иначе он попытался бы использовать чужие семафоры. Для этого при создании набора 17-й семафор инициализируется значением PGSemaMagic-1 и затем увеличивается до PGSemaMagic — PGSemaMagicмагическое число, которым помечается 17-й семафор набора, чтобы сервер мог опознать набор как свой. При поиске свободного ключа проверяется, что этот семафор содержит магическое число; иначе набор принадлежит другому приложению. Ограничение SEMMNI — предел числа наборов семафоров в системе, поэтому оно должно быть не меньше ceil(num_os_semaphores / 16).
Цикл «Give up after trying 1000»
IpcSemaphoreCreate перебирает ключи IPC, увеличивая nextSemaKey на каждой итерации и считая попытки в num_tries. Ключ IPC — это числовой идентификатор, под которым набор семафоров регистрируется в ядре; ключ может быть уже занят другим набором семафоров, и тогда попытка создать набор с тем же ключом заканчивается коллизией. На каждой попытке вызывается InternalIpcSemaphoreCreate(nextSemaKey, numSems + 1, num_tries < 1000), где третий аргумент retry_ok разрешает тихо вернуть -1 при коллизии, пока число попыток меньше 1000. Успешное создание прерывает цикл.
Если ключ занят, код проверяет через semget, не остался ли набор от мёртвого процесса Postgres: сверяет значение зарезервированного семафора с PGSemaMagic, читает PID создателя и, если создатель — сам процесс или уже не существует, удаляет набор через semctl(semId, 0, IPC_RMID, semun) и повторяет создание с retry_ok = true.
Когда num_tries достигает 1000, retry_ok становится ложным, и при коллизии InternalIpcSemaphoreCreate вместо возврата -1 выполняет ereport(FATAL, ...) с сообщением «could not create semaphores: %m» — процесс аварийно завершается, а не продолжает перебор. После успешного создания набор помечается как созданный этим процессом: инициализируется значением PGSemaMagic-1, затем PGSemaphoreUnlock увеличивает семафор до PGSemaMagic и записывает PID.
Ошибка «could not create semaphores: No space left on device»
semget() возвращает ENOSPC, когда превышается либо системный лимит на число наборов семафоров (SEMMNI), либо общий системный лимит на число семафоров (SEMMNS). Это не связано с дисковым пространством: в коде при saved_errno == ENOSPC выдаётся подсказка, что ошибка «does not mean that you have run out of disk space», а возникает при превышении SEMMNI или SEMMNS.
Документация подтверждает: SEMMNS должен быть не меньше num_os_semaphores плюс по одному дополнительному семафору на каждые 16 требуемых, а SEMMNI должен быть не меньше ceil(num_os_semaphores / 16). Каждый набор из 16 семафоров содержит 17-й семафор с магическим числом для обнаружения коллизий с наборами других приложений. Снижение max_connections — временный обходной путь для таких сбоев, которые обычно вводят в заблуждение формулировкой «No space left on device» от функции semget.
Почему ошибка жила 12 лет
Вплоть до версии 17 документация предлагала вычислять число семафоров самостоятельно по формуле с необъяснимой константой, которую должен был обновлять кто-то при появлении нового вида процесса, но никто этого не делал. Формула была неверна с 9.2 (появление checkpointer) до 9.6, снова неверна в 14 (archiver стал auxiliary-процессом) и должна была снова стать неверной в 17 (добавлен WAL summarizer).
Проблема стала массовой из-за изменений в конфигурации по умолчанию и в самой формуле. Пока число семафоров оставалось небольшим, тесные лимиты ядра не давали о себе знать. С ростом числа процессов, каждый из которых получает семафор, и с появлением новых видов процессов сумма перестала помещаться в типичные лимиты SEMMNI/SEMMNS, и установки начали упираться в ENOSPC при старте.
Настройка лимитов ядра
На Linux рекомендуемые значения задаются командами:
sysctl -w kernel.shmmax=17179869184
sysctl -w kernel.shmall=4194304Чтобы они сохранялись после перезагрузки, нужно изменить /etc/sysctl.conf.
На macOS рекомендуемый способ настройки разделяемой памяти — создать файл /etc/sysctl.conf с присваиваниями переменных, например kern.sysv.shmmax=4194304, kern.sysv.shmmin=1, kern.sysv.shmmni=32, kern.sysv.shmseg=8, kern.sysv.shmall=1024. В некоторых версиях macOS все пять параметров разделяемой памяти должны быть заданы в /etc/sysctl.conf, иначе значения будут проигнорированы. SHMMAX можно задавать только кратным 4096, а SHMALL на этой платформе измеряется в страницах по 4 kB. Все параметры, кроме SHMMNI, можно менять на лету через sysctl, но лучше задавать их через /etc/sysctl.conf, чтобы значения сохранялись между перезагрузками.
Почему лимиты нужно поднимать до initdb
На момент initdb кластера ещё нет и спросить его о требуемом числе семафоров нельзя: «There is no cluster to ask at that point, so do the sum yourself». Поэтому на System V-платформе лимиты следует поднимать до initdb, а не после первого сбоя.
Если лимиты окажутся тесными, но не безнадёжными, initdb не падает, а сам понижает max_connections, перебирая значения 100, 50, 40, 30 и 20, и оставляет первое, с которым сервер запустился. В результате можно получить кластер с урезанным max_connections, например 50, и единственным уведомлением будет одна строка вывода initdb. Если же лимитов совсем не хватает, сервер отказывается стартовать с ошибкой semget: FATAL: could not create semaphores: No space left on device.
Что видит клиент при сбое
Когда backend не может создать семафор, клиент видит фатальную ошибку с текстом «could not create semaphores: %m», где %m подставляет системное сообщение об ошибке, например «No space left on device». Дополнительно выводится DETAIL с указанием неудавшегося системного вызова semget и его аргументов, а при errno == ENOSPC добавляется HINT, объясняющий, что это не связано с нехваткой места на диске.
HINT прямо говорит, что ошибка возникает при превышении системного лимита на число наборов семафоров (SEMMNI) или на общее число семафоров (SEMMNS), и предлагает поднять соответствующий параметр ядра либо уменьшить потребление семафоров, снизив max_connections. Отличить эту ситуацию от исчерпания max_connections можно по наличию HINT: он появляется только при ENOSPC, тогда как при других ошибках создания семафора hint не выводится. Кроме того, при нехватке семафоров initdb может не упасть, а подобрать меньшее значение max_connections из ряда 100, 50, 40, 30 и 20, и тогда единственным признаком будет одна строка вывода initdb.
Как семафоры очищаются
При завершении работы или переинициализации разделяемой памяти PostgreSQL регистрирует обратный вызов on_shmem_exit(ReleaseSemaphores, 0), который затем удаляет все созданные семафоры. Для именованных POSIX-семафоров ReleaseSemaphores вызывает PosixSemaphoreKill для каждого указателя из mySemPointers и освобождает массив, а для неименованных — для каждого элемента sharedSemas. PosixSemaphoreKill удаляет семафор через sem_close для именованных или sem_destroy для неименованных, логируя ошибку при неудаче. В System V-реализации IpcSemaphoreKill удаляет набор семафоров через semctl(semId, 0, IPC_RMID, semun).
При создании набора System V-семафоров IpcSemaphoreCreate ищет свободный ключ, и если находит набор, помеченный PGSemaMagic, создатель которого мёртв или это тот же процесс, он удаляет его через semctl(semId, 0, IPC_RMID, semun) и пытается создать заново. Так «висящие» семафоры от умерших процессов не накапливаются.
Отдельная угроза — systemd с настройкой RemoveIPC. Этот параметр в logind.conf управляет тем, удаляются ли объекты IPC (включая разделяемую память) при полном выходе пользователя из системы; системные пользователи освобождены от этого. По умолчанию в стандартном systemd он включён, но некоторые дистрибутивы ставят его выключенным. Типичный наблюдаемый эффект при включённом параметре — объекты разделяемой памяти, используемые для параллельного выполнения запросов, удаляются в случайные моменты, что приводит к ошибкам и предупреждениям при попытке их открыть и удалить. Разные типы объектов IPC (разделяемая память против семафоров, System V против POSIX) обрабатываются systemd немного по-разному, но полагаться на эти тонкие различия не рекомендуется. «Выход пользователя» может произойти в рамках задачи обслуживания или вручную, когда администратор входит как пользователь postgres, поэтому это трудно предотвратить в целом.
Что считается «системным пользователем», определяется во время компиляции systemd из настройки SYS_UID_MAX в /etc/login.defs. Сценарии упаковки и развёртывания должны создавать пользователя postgres как системного с помощью useradd -r, adduser --system или эквивалента. Если учётная запись создана неправильно или не может быть изменена, рекомендуется установить RemoveIPC=no.
Для Solaris projects команда projadd создаёт проект с ограничениями, например project.max-shm-memory=(privileged,8GB,deny), а также задаются project.max-shm-ids=(priv,32768,deny) и project.max-sem-ids=(priv,4096,deny).
Как устроены семафоры на Windows
В win32_sema.c семафоры реализованы как анонимные объекты ядра Windows: PGSemaphoreCreate вызывает CreateSemaphore с начальным счётчиком 1 и максимальным 32767, а полученный HANDLE сохраняется в локальном массиве mySemSet. Состояние счётчика живёт в объекте ядра, а не в памяти процесса, поэтому PGSemaphoreShmemRequest ничего не делает («No shared memory needed on Windows»), а PGSemaphoreInit лишь выделяет массив для учёта дескрипторов.
Захват PGSemaphoreLock ожидает одновременно два события — pgwin32_signal_event и сам семафор — через WaitForMultipleObjectsEx, чтобы обрабатывать сигналы. PGSemaphoreUnlock вызывает ReleaseSemaphore, а PGSemaphoreTryLock — WaitForSingleObject с нулевым таймаутом.
Состояние не может храниться в backend-процессе, потому что PGSemaphoreCreate содержит Assert(!IsUnderPostmaster) и комментарий «Can't do this in a backend, because static state is postmaster's»: семафоры создаются в postmaster и наследуются потомками. В posix_sema.c предпочтителен безымянный вариант: sem_t размещаются в массиве в разделяемой памяти, тогда как именованные семафоры несовместимы с EXEC_BACKEND, поскольку их структуры остаются в приватной памяти postmaster. В sysv_sema.c PGSemaphoreData хранит semId и semNum, а сами семафоры создаются через semget как наборы по 16 штук, то есть состояние также находится вне backend-процесса.
Как определяется принадлежность сегмента shared memory
PGSharedMemoryAttach сначала вызывает shmctl для анализа сегмента: если получен EACCES, это означает отсутствие прав на чтение, и сегмент считается чужим. Затем выполняется stat(DataDir, &statbuf), чтобы получить идентификатор каталога данных; при ошибке сегмент не удаётся проанализировать, поэтому решение принимается осторожно. Далее сегмент присоединяется через shmat, и если это не удалось, EINVAL или EIDRM трактуются как исчезновение сегмента, а EACCES — как чужой сегмент.
После присоединения проверяется, что hdr->magic равен PGShmemMagic, а hdr->device и hdr->inode совпадают с st_dev и st_ino из stat(DataDir); при несовпадении сегмент считается чужим. Наконец, по значению shmStat.shm_nattch определяется, есть ли ещё процессы, присоединённые к сегменту: если ноль — сегмент существует, но к нему не присоединён ни один процесс, иначе — сегмент существует и к нему присоединены процессы.
Что из этого следует на практике
num_os_semaphoresв PostgreSQL 18 — единственный надёжный источник числа требуемых семафоров ОС. До 17 его приходилось считать вручную, и формула годами расходилась с реальностью.- На System V-платформах лимиты
SEMMNIиSEMMNSнужно поднимать доinitdb, а не после первого сбоя: на моментinitdbкластера ещё нет, и спросить его о числе семафоров нельзя. SEMMNIдолжен быть не меньшеceil(num_os_semaphores / 16), аSEMMNS— не меньшеnum_os_semaphoresплюс по одному семафору на каждые 16 требуемых.- Ошибка «No space left on device» от
semgetне связана с диском: это исчерпание лимитов на семафоры. Отличить её от исчерпанияmax_connectionsможно по наличиюHINT, который появляется только приENOSPC. - При тесных лимитах
initdbне падает, а молча понижаетmax_connectionsдо 100, 50, 40, 30 или 20 — единственным признаком будет одна строка вывода. - Константа
40в формуле включает 32 слота под I/O workers, зарезервированных независимо отio_methodиio_workers;io_method = syncих не освобождает. RemoveIPC=noв systemd защищает IPC-объекты от преждевременного удаления при выходе пользователя; пользовательpostgresдолжен создаваться как системный, иначе он не освобождён от этой очистки.