Все, что программа требует у системы и что может быть выражено количественно, мы будем называть здесь ресурсами (в отличие от свойств, которые выражаются качественно). Можно найти множество разных ресурсов, но некоторый их набор следует считать критическими ресурсами:
Названные три ресурса - критические. Это означает, что 1) они нужны всем процессам, а не некоторым; 2) они могут требоваться не только во время старта и обычной работы програмной системы, но и во время ее операций по завершению работ, как плановому, так и внеплановому, даже при высококачественном программировании; 3) их недостаток, пусть даже временный, трудно диагностируется, часто не обнаруживается (это уже ошибки программистов, но чрезвычайно распространенные ошибки) и приводит к трудноуловимым глюкам, или же к нештатным срывам работы.
Простой пример на недостаток такого критического ресурса. Функции getpwnam(), getpwuid(), getpwent(), которые являются штатным средством получения данных по зарегистрированным в системе пользователям, так были изначально определены, что невозможно по результату их работы определить, был ли какой-то сбой в работе, или же запрошенный пользователь отсутствует в системных базах данных. SUSv2 уже специфицирует метод поиска ошибки; но масса систем еще не приведена в соответствие с его требованиями, и простой возврат NULL может означать любой из описанных вариантов. Следовательно, кратковременное отсутствие возможности открыть файл может привести к логическому сбою в работе (и тяжесть этого сбоя заранее совершенно не предсказуема и может быть сколь угодно велика).
Мне приходилось наблюдать два раза, по одному разу на хост, последствия исчерпания файловых дескрипторов на системе на основе RedHat 6.2. Механизм воздействия остался невыясненным, но результатом исчерпания файловых дескрипторов стали систематические неправильные ответы на DNS-запросы. Перезапуск единственного постоянно работающего компонента системы резолвинга - named'а - ничего не дал, и лечением стала только перезагрузка.:(
Но больше всего вопросов возникает по такому ресурсу, как виртуальная память; особенности современных реализаций подсистемы виртуальной памяти (VM) и стиль написания программ приводят к тому, что, с одной стороны, фактически невозможно подсчитать, сколько же именно памяти занимает программа; с другой стороны, не применяется фактически никаких мер к управлению затратами памяти.
Показательным примером того, как не надо делать, стал стиль, пропагандируемый программистами Free Software Foundation (она же GNU). Большинство программ производства GNU знает единственный метод реакции на невозможность выделения памяти: немедленное завершение программы, системным вызовом _exit() или его вариантом с минимумом оберточных действий. Обоснование, приводимое ими, сводится к тому, что если памяти не дали, то системе в этот момент уже очень плохо и немедленное самоубийство - лучшее, что сможет один процесс в такой ситуации сделать;(( Возможность наличия, например, лимита на используемую одним пользователем память, принципиально в эту "концепцию" не укладывается. Зато, облегчается программирование (за счет пользователей, которые будут выгребать проблемы при любой относительно существенной нагрузке).
Невозможность точного подсчета памяти, занятой процессом, возникает из возможности разделения несколькими процессами одной области виртуальной памяти. Несколько процессов, запущенных из одной программы, будут разделять существенную часть кода этой программы - все неизмененные по сравнению с образом на диске страницы, коих может быть значительно больше половины. Более того, так как эти страницы можно по необходимости снова загрузить с диска, занятую ими память можно посчитать с еще меньшим коэффициентом. Если какой-то процесс разделился на два, то в момент разделения у них все страницы кода, данных, стека общие; постепенно накапливаются изменения, но система совершенно не может предсказать, останется ли из них существенная часть общими, или же очень скоро все страницы станут разными, пройдя модификацию в каком-то из этих процессов. Если процессов, выросших из одного, не два, а больше, различие между случаями мелких и крупных изменений может быть еще больше. И часто эти различия зависят от случайностей - например, от порядка инициализации данных, которые приводят к другой группировке изменяемых данных по страницам виртуальной памяти.
В этих условиях, представим себе ситуацию. В системе исчерпалась виртуальная память. Один из процессов захотел себе еще памяти, вызвал brk() или аналог и получил отказ. Отказ он попытается нормально отработать, возбудив, например, исключение. Исключение вызывает размотку стека, массовый вызов деструкторов упомянутых по дороге объектов; это вызывает модификацию имевших признак copy-on-write страниц. На создание персональных для процесса копий страниц снова требуется виртуальная память, но ее уже нет! В результате, процесс помирает по SIGSEGV или SIGKILL, которые ему были выданы "чтоб быстрее отмучился"...
Для интернет-сервера типичного "старого" образца, у которого нагрузка - пачка sendmail'ов, попперов, апачей или еще чего-то подобного, отстрел какой-то части процессов в случае недостатка памяти - ситуация не дающая тяжелых долговременных последствий. Письмо будет отправлено или забрано еще раз, на странице нажмут reload (разве что будет некоторое недовольство). Хотя и таким серверам это может навредить: может тихо умереть какой-то демон, и отсутствие не всех из них мониторинг может заметить (заметьте-ка неработу cron'а, если мониторинг запускается периодически этим же кроном!;)) Но для них подобные ситуации редки и дают кратковременные легко устранимые проблемы. Если же будет убит - без возможности выполнить стандартные сверточные операции - сервер БД, то это уже приведет к перезапуску с размоткой журнала транзакций, выполнением откатов в БД, и хорошо, если автоматически - что-то мне лично приходилось часто видеть слетевший и требующий ручной перестройки индексов postgres.;(
Можно долго приводить примеры и аргументы - и на описанный круг вопросов, и на специфику lazy commit'а, который дает еще больше неприятных "чудес" к описанным, но суммарный вывод, пожалуй, уже четко описан. Стандартная на сейчас схема построения VM большинства unix-систем, а особенно open source систем (о коммерческих подробнее см. ниже), не обладает устойчивым и предсказуемым поведением в условиях нехватки виртуальной памяти и не дает приложениям методы аккуратного и достойного выхода из ситуации.
Так как ресурс VM - самый критический, даже по сравнению с таким ресурсом, как файловые дескрипторы, - а ограничения могут происходить из разных источников - общесистемные пределы, пределы на всех кроме рута, пределы на один процесс, пределы на группу процессов... - то надо иметь возможность 1) не быть убитым как только стало не хватать памяти, 2) иметь возможность в случае нехватки ресурсов получить достаточно ресурсов для того, чтобы аккуратно свернуться, полностью или частично.
В первый момент может показаться, что запретить lazy commit там, где он явно не требуется, достаточно для выполнения этих требований: выделяем достаточный резерв памяти и работаем с ней. Но это было бы хорошо для VM без объединения неизмененных страниц разных процессов (то, что отражается в copy-on-write признаках у разных процессов). Наличие copy-on-write страниц следует учитывать и использовать: они действительно полезны тем, что сокращают расход VM. Какие же выходы из этой ситуации?
Первый "инженерный" выход - расширение виртуальной памяти за счет дисковой. Это используется в ряде коммерческих систем - например, в HP-UX. При нехватке виртуальной памяти, часть памяти процесса, не сдвигаясь с места и не теряя своего содержимого, меняет реализацию в VM: вместо выделенной напрямую из VM области - отображенный в память файл на диске, обычно в /tmp или аналогичной по режиму использования файловой системе.
Второй "инженерный" выход - аллоцирование процессом резервной группы страниц, которые становятся жестко закрепленными за ним (и, соответственно, числящимися за ним), но явно помеченных для перераспределения на отработку неявных запросов памяти, определяемых системой VM - на создание личных копий copy-on-write страниц и страниц из областей, которым задан lazy commit. Процесс должен иметь возможность контролировать объем доступного резерва, и регулярно (например, при запросах памяти целевым кодом приложения) выполнять действия по поддержанию резерва на достаточном уровне, а в случае невозможности такого поддержания - сигнализировать и запускать операции по свертке (полной или частичной) активности. В настоящее время мне неизвестны реализации, которые применяют этот метод.
(C) 2002 Valentin Nechayev. All rights reserved.
Допускается свободное распространение данного текста в некоммерческих целях
без изменения содержимого и реквизитов.
$Id: cres.html,v 1.2 2015/06/06 06:01:08 netch Exp $