Comments 52
Где же истории, как защищаться от ядерного Оомкиллера, когда система уже решила, что памяти не хватает и пора кого-нибудь прикончить? Я гарантирую, что любые колдунства над Malloс не дают гарантий, что оомкиллер в ядре не придёт в Вашу программу
Runtime библиотеки на macOS уж очень не стабильны и норовят упасть при каждом «удобном случае»а можно поподробнее?
Даже вызов printf на macOS может привести к SIGSEGV/SIGBUS.ну если запихать в него каку, он где угодно упадет
Чтобы бросить исключение, например std::bad_alloc, необходимо выделить память под объект исключения. И, внезапно, память под объект исключения тоже может быть не выделена, если мы сталкиваемся с OOMbad_alloc::what() может возвращать статическую строку. И резервировать память под объекты исключений отдельно. Хотя вы об этом написали ниже, но
На macOS никаких запасных буферов выявлено не былоа вы где выявляли? __cxa_allocate_exception из libc++ вызывает __aligned_malloc_with_fallback, который при нехватке памяти вызывает fallback_malloc, который как раз берет память из небольшого статического массива.
UPDATE: Как уже было сказано ранее, новые версии macOS / Xcode не имеют этой проблемы.ой, всё
а можно поподробнее?
Про
printf
всё очень просто, следующий код завершается аварийно на macOS, даже на Catalina при сборке с использованием Xcode 12:
int main(int argc, char** argv) { OverthrowerConfiguratorStep overthrower_configurator(0U); activateOverthrower(); // Start failing ALL allocations. printf("Some integer number: %d, some floating point number: %f, some string: %s\n", 100500, 100.500f, "100500"); deactivateOverthrower(); // Do not fail any allocations anymore. return 0; }
Ничего нелегального в
fprintf
, как мы видим, в данном случае не подаётся.
Запуск потока с использованием
std::thread
в условиях OOM приводит к падениям на macOS старее High Sierra.
Так же сталкивались с, что при попытки динамической загрузки Framework'ов вместо сообщений об ошибках получали падения внутри системных функций.
а вы где выявляли?
Из личного опыта и объективных результатов тестирования. Если __cxa_allocate_exception
не может выделить память мы получаем аварийное завершение работы приложения даже на macOS Mojave: https://travis-ci.org/github/kutelev/overthrower/jobs/734673895
[ RUN ] Overthrower.ThrowingException
...
libc++abi.dylib: terminating
На Catalina с последним Xcode этого не происходит.
Вот это вот все не работает.
OOM киллер убивает не того кто просит память а того кто занимает больше всего памяти, при этом учитывая его важность для системы (OOM score).
Т.е. если ваш процесс не самый толстый в системе и к тому же имеет пониженный OOM score, то шанс его убийства довольно низок, хотя и остаётся открытым вопрос что случится если убитый процесс важен для работы вашего (или системы в целом).
В любом случае лучше попытаться (если возможно) аккуратно завершить работу в случае если память не дали (сбросить буфера, закрыть соединения, отменить транзакцию etc) чем просто умереть и потерять данные, на этот случай даже можно предусмотреть аварийный пул памяти (запрашиваемый в самом начале работы) которая может потребоваться на случай её нехватки в процессе нормальной работы.
Т.е. если ваш процесс не самый толстый в системеесли наш процесс не самый толстый в системе, существуют и более надежные способы. Например тем или иным способом предоставить ему заведомо достаточное количество памяти.
Единственный способ предоставить процессу заведомо достаточное количество памяти — это жёстко её выделить изначально, а это не всегда целесообразно — к примеру, это может быть процесс который обычно требует 10 Мб для работы, но изредка ему нужно (на короткое время) 100/200/500 Мб — если в этот самый момент когда оно нужно памяти нет, всё же лучше это обработать (к примеру, приостановить работу пока не появится). Выделять ему сразу потенциальный максимум — это просто бесполезная трата ресурса, своп тоже не всегда имеется (или может быть медленным до непрактичности).
Процесс потребляющий на пике 500мб требует выделенных 500мб. Не надо такие процессы делать. Точнее 500Мб мелочи. А вот ступенька 1-50 Гб это больно. Приходится 50Гб выделять на постоянку. И доставать тикет на улучшение этого места из беклога.
ага, requests/limits — будьте добры ТОЧНО сказать сколько вам памяти надо — иначе на мороз, простите, в Вальгаллу для сервисов-неудачников ))))
Если следовать этой логике то overcommit (как дисков так и памяти) нужно в принципе исключить, со всеми вытекающими из этого последствиями, и в итоге мы получим системы которые не используют большую часть ресурсов большую часть времени, а это чрезвычайно расточительно.
Докеры и кубернетисы (которые, кстати, далеко не у всех, хотя и очень популярны) расчитаны на то чтобы ресурсы использовались максимально эффективно — это не карт-бланш на игнорирование ситуаций когда ресурсы временно могут оказаться недоступны.
И 500 MB совсем не мелочи — если у вас таких процессов с сотню, но эти самые 500 MB нужны далеко не всем одновременно. И ничего "неправильного" в таких процессах нет — всё зависит от задач.
Вот так и живем. Утилизация за счет удобного и маштабируемого разделения контейнеров по железкам. Пока все жили bare metal была частая ситуация когда сервера недозагружены. Забыли, забили, не умеют, не уверены что там с ресурсами, просто не могут софт так раскидать по кластерам. Разные причины. Сейчас это делается почти само. Когда один контейнер процентов 10-20 (или меньше) от железки можно набить плотненько.
Сейчас софт надо писать так чтобы пикового потребления не было. Старый постепенно переписывать. Крутись как хочешь, жизнь такая. 500Мб надо? Бери и пользуйся навсегда. Надо это делать эффективно.
Висит себе мой демон на сервере, виртуальные резиновые изделия пинает 99% времени. А раз в неделю прилетает ему расписание для кинотеатра, согласно которому ему надо в 14 залов выгрузить новое
Ну, так запускайте его, когда надо. Та же лямбда (серверлесс) именно за этим нужна.
Пример: https://aws.amazon.com/ru/lambda/
Второе приложение, которое вы хотите запускать для выгрузки останется без сокета
какой-то очень слабый аргумент, кмк
эта проблема будто решаема, стоит только захотеть. Ну, тут уж Вы сами решаете — что Вам нужнее, 500МБ раз в неделю и пиковые потребления, либо нормальная архитектура
Мой вариант — допилить свой софт. Про оборудование я вообще ничего не говорил, между прочим. Можно и с существующим обойтись.
что кому-то хочется чтобы 500 метров были недоступны 99% времени?
я это не предлагал, опять же. Читайте выше.
Вы пытаетесь распространить свой очень частный сценарий на всех. Это так не работает.
У вас там что-то странное. Со странной архитектурой. У людей обычно таких проблем нет.
Покупать в каждый из 2500+ узлов больше планок памяти (а иногда и новую материнку) из-за тогоЕсли у вас bare metal серваки, то… ссзб
Рядом про лямбды уже написали. Можно вообще запуск контейнера по событию соорудить. Оверкилл, но с другой стороны раз в неделю всего. Может и стоит того. Надо по месту смотреть.
Сейчас именно так софт и пишут. Оптимизация это именно вот оно.
В конейтере обычно не один процесс, и не всегда это контейнер. "Важный" это такой без которого система в принципе бесполезна, хотя в контексте речь шла про "важность" с точки зрения OOM killer.
Как я уже говорил выше, может быть несколько процессов которым в разное время нужно много памяти, но не одновременно — зачем мне выделять 50 гиг на случай если вдруг они все сойдут с ума и потребуют её одновременно, если 99% времени одновременно нужно только 10 гиг максимум? Проще обрабатывать вариант отказа в выделении памяти конкретному процессу и ставить его на паузу пока память не появится.
Ясное дело, если у меня что-то настолько важное (скажем, буквально жизненно важное) что оно не имеет права не получить память — тогда оно получит и 50 и даже 100 гиг, но так в моей практике почти не бывает (уже).
Вот именно из-за такого подхода мы имеем проблемы вроде
https://habr.com/ru/company/oleg-bunin/blog/431536/
Что-то важное, так же как и неважное, имеет лимит по памяти и живет в нем.
Превысил — умер. В этом лимит никто никогда не лезет.
Скажите об этом Gitlab omnibus. Есть разные подходы и все не упирается в «один процесс — один контейнер». Скорее более верный подход «один сервис — один контейнер», но сервис может быть многотредовый
В контейнере должен быть один процесс.
А если это что-то типа Postgres или Dovecot? Мне хардкорно поправить их архитектуру чтобы в одном контейнере был один процесс, несмотря на то что они очень плотно связаны друг с другом?
Успехов с отладкой и эксплуатацией более одного процесса на контейнер.
Вы серьёзно? Любая ось — это тоже своего рода контейнер, и в нём десятки а то и сотни процессов (причём далеко не всегда связанных друг с другом) — как они отлаживались-то всё это время, до появления докера и кубернетов?
Да практически вся индустрия работает с кучей процессов на контейнер (чтобы под ним не подразумевалось) с начала времён — и особых проблем с отладкой нет, а если мы возьмём IoT то там вообще по определению "одна железка — один контейнер", и отлаживают же.
Вы серьёзно? Любая ось — это тоже своего рода контейнер, и в нём десятки а то и сотни процессов (причём далеко не всегда связанных друг с другом) — как они отлаживались-то всё это время, до появления докера и кубернетов?
Все наелись проблем и радостно уехали в виртуалки, а потом и в контейнеры. По контейнеру на приложение. Это просто удобнее.
Сейчас катать пакеты, отслеживать железки, думать об их обновлении или ремонте, распределять софт по разнородным железкам объединенным в логические кластера много лет назад по уже неактуальным соображениям.
Вы точно хотите вернуться в эти времена?
Вместо делоя на нечто полностью виртуальное стандартного Докер образа? Когда о том что под ним думают специальные люди и тебя все это вообще не волнует. Когда под приложение делается кластер именно той топологии которая нужна, а не берется что-то железное 5 летней давности вроде примерно подходящее.
Да практически вся индустрия работает с кучей процессов на контейнер (чтобы под ним не подразумевалось) с начала времён — и особых проблем с отладкой нет, а если мы возьмём IoT то там вообще по определению «одна железка — один контейнер», и отлаживают же.
Мы про бекенды и сервера тут. Иот это свой мир. И нет не работает. Легаси живет черти как. Но все новое уже пишется одно приложение — один контейнер. Старое постепенно адаптируется к такой жизни.
Вы говорили про отладку. В чём принципиальная разница отладки приложения в распоряжении которого отдельный контейнер и приложения рядом с которым ещё десяток приложений, особенно если они под другим uid?
Я вот никакой разницы не наблюдаю, хотя нет, чуть-чуть вру — если это приложение в докере — то это масса дополнительного инструментария (и в зависимости от обстоятельств — потеря производительности), а в остальном — никаких различий.
Мои приложения максимально абстрактны с точки зрения среды выполнения — им нужны память, немножко (очень мало) файлы и сокеты (много), всё что им нужно из библиотек и прочих зависимостей лежит рядом в одном дереве (то есть в теории они даже в системных библиотеках не нуждаются) — всё, имея это они могут работать как внутри контейнеров так и вне контейнеров, совершенно не осознавая среды. Я могу их отлаживать где угодно, и выполнять где угодно (пока это "где угодно" даёт файлы и сокеты) — что я выиграю если буду их насильно контейниризировать и отлаживать в контейнерах?
что я выиграю если буду их насильно контейниризировать и отлаживать в контейнерах?
возможность запускать на (или "в") оркестраторах контейнеров — на масштабах 10-100-1000 вычислительных узлов и в сотнях реплик?
Вы говорили про отладку. В чём принципиальная разница отладки приложения в распоряжении которого отдельный контейнер и приложения рядом с которым ещё десяток приложений, особенно если они под другим uid?
Оркестрация и изоляция. Это не только баззворды для собеседований, но и действительно полезные вещи.
Когда у вас их десяток, то понять почему ООМ становится на порядки сложнее.
Понять почему ЦПУ затроттлилось нереально вообще.
Ну и всякие мелочи вроде конфликтов за порты, пересечение логов, невозможность обновить одно из приложений не трогая остальные и прочие радости работы на bare metal. Вы его и изображаете.
Я вот никакой разницы не наблюдаю, хотя нет, чуть-чуть вру — если это приложение в докере — то это масса дополнительного инструментария (и в зависимости от обстоятельств — потеря производительности), а в остальном — никаких различий.
Производительности теряется на уровне погрешности. Можно игнорировать. Зато можно удобно поднимать все окружение. Я помню эту магию с переопределением портов, баш скриптами для поднимания дев среды и секретными файликами разбросаными по всей файловой системе. И естесвенно из сурсконтроля парой команд ничего не поднять. Всегда есть длинная и запутанная инструкция. Хорошо если работающая.
Мои приложения максимально абстрактны с точки зрения среды выполнения — им нужны память, немножко (очень мало) файлы и сокеты (много), всё что им нужно из библиотек и прочих зависимостей лежит рядом в одном дереве (то есть в теории они даже в системных библиотеках не нуждаются) — всё, имея это они могут работать как внутри контейнеров так и вне контейнеров, совершенно не осознавая среды. Я могу их отлаживать где угодно, и выполнять где угодно (пока это «где угодно» даёт файлы и сокеты) — что я выиграю если буду их насильно контейниризировать и отлаживать в контейнерах?
Оркестрация
Каждое приложение становится самодостаточной единицей деплоя. Оно отдельно катается, отдельно рестартуется, отдельно маштабируется. Эксплуатация становится просто на порядок удобнее.
И изоляция
У приложения есть все порты, в его файловую систему никто не пишет, его память никто не использует, его ЦПУ никто не использует. Ресурсы и их использование можно посмотреть UI той или иной степени удобства. Обычно достаточно удобно.
Все взаимодействия с соседими сервисами делается через типовые и более-менее стандартизованние интерфейсы. Во времена железа лично видел общение сервисов через локальный файлик. Очень глубоко запрятанный файлик. Не надо нам такого.
Отладка пока пишешь может делаться как угодно. Если сложного окружения не надо или подойдет общее для всех окружение, то можно и просто локально. Сам так люблю разрабатывать.
Я скорее про отладку прода. Когда что-то идет не так.
зачем мне выделять 50 гиг на случай если вдруг они все сойдут с ума и потребуют её одновременно, если 99% времени одновременно нужно только 10 гиг максимум?чтобы у вас раз в 0.01% времени не умирал критически важный процесс.
Проще обрабатывать вариант отказа в выделении памяти конкретному процессу и ставить его на паузу пока память не появится.ставить критический процесс на паузу, ага. В надежде что его ООМ киллер не прибьет раньше чем «память появится». Да даже если… как вы собрались возобновлять процесс по волшебному «память появилась»? И какая принципиальная разница между упавшей приложухой и поставленной на паузу?
А если у вас процессы таки не критически важные, на порядки проще реализовать обработку отказов.
ставить критический процесс на паузу, ага
Я где-то говорил что он "критический"? Речь идёт просто о процессе которому нужна память но её нет "здесь и сейчас".
И какая принципиальная разница между упавшей приложухой и поставленной на паузу?
Примерно такая же как между "накопили данные, обработали, упали перед тем как сохранили результат" и "накопили данные, обработали, ждём пока сможем сбросить".
OOM киллер убивает не того кто просит память а того кто занимает больше всего памяти, при этом учитывая его важность для системы (OOM score).
вообще-то это вроде настраивается!? JerleShannara есть что добавить?
Настраивается, путём записи в /proc/$pid/oom_score_adj. Правда, если вы какой-то процесс настроите так чтобы он не убивался (это тоже возможно), но он сойдёт с ума и сожрёт всю память — может быть намного хуже.
И уж точно если вы делаете его неубиваемым — нужно обрабатывать ситуации когда он не может получить память, иначе это не имеет смысла (разве что он не содержит данных которые нужно сохранять).
ИМХО лучше не пытаться ловить каждый случай, а просто проектировать программу так, чтобы данные никогда не повреждались при падении. Это будет полезно также и в тех случаях, когда программа падает по другим причинам, или вообще, убивается извне.
просто проектировать программу так, чтобы данные никогда не повреждались при падении
И как вы это сделаете без обработки ситуации "караул, память не дали"? К примеру, если у вас есть внутренние буфера, ещё не отправленные куда нужно с помощью write()/send()/etc, они банально пропадут, незакрытые транзакции БД могут откатится а могут и нет (зависит от БД и режима), и ещё много вещей которые зависнут в памяти приложения при жёстком завале, или даже просто внешнее состояние которое останется "грязным".
Если программа убивается извне, это тоже можно (и нужно) обрабатывать, кроме, разумеется, SIGKILL — но в последнем случае вы в принципе не сможете спроектировать приложение так чтобы данные не терялись пока эти данные хранятся в самом приложении, точнее, в выделенной ему памяти.
незакрытые транзакции БД могут откатится а могут и нет
По идее, в базе данных должны быть средства защиты от не завершивших транзакции клиентов. Если таких средств нету, надо или самому такие средства эмулировать, или вообще, смириться с возможной потерей данных от падений, происходящих из-за исчерпания памяти или из-за чего-то ещё.
Если программа убивается извне, это тоже можно (и нужно) обрабатывать
Почему это нужно? Штатное закрытие да, обрабатывать надо. Но именно что SIGKILL или что-то подобное означает, что пользователем/системой ожидается, что программа сдохнет немедленно, а значит что-то пытаться сохранить она не должна.
Пусть программа упадёт и потеряет часть данных, но внешний мир (файлы, базы данных и т. д.) остались в консистентном состоянии.
Вы уверены? Представьте что вы работаете в чём-то типа IDE, и в момент поиска чего-то оно падает, теряя ваши последние 15 минут работы (важный баг-фикс). Конечно, современные IDE достаточно часто сохраняют файлы так что ситуация маловероятна — но вот оператор набивающий форму с данными которые получает по телефону будет очень расстроен, если приложение которое эту форму ему показывает вдруг умрёт в конце ввода, а форма может быть отправлена/сохранена только полностью.
Есть и другой пример, немножко обратный — когда программа меняет внешнее состояние "под себя" (на время работы) но должна его "вернуть взад" при завершении (любом — штатном или нет), причём транзакции тут неприменимы (самый просто пример — всякие демоны для рутинга типа quagga/bird).
Штатное закрытие да, обрабатывать надо.
А SIGINT/SIGHUP — штатное закрытие? Приложение может получить сигнал от системы (злобный админ решил сделать shutdown пока пользователи ходили на обед) — и его нужно корректно обработать. Ясный пень, SIGKILL как раз на крайний случай, но всё остальное (сигналы не связанные с аппаратными источниками) я бы всё равно отнёс к "штатным".
Так или иначе не очень сложно разработать архитектуру таким образом чтобы сохранять то что возможно (и что жалко потерять) в случае неожиданностей, включая нехватку памяти — пользователи будут безмерно благодарны.
оператор набивающий форму с данными которые получает по телефону будет очень расстроен, если приложение которое эту форму ему показывает вдруг умрёт в конце ввода
В идеале такое приложение может по мере набора сохранять данные куда-то во временный файл и в случае падения и последующего перезапуска подгружать эти данные из файла. Это тоже входит в комплекс архитектурных мер против падений.
Есть и другой пример, немножко обратный — когда программа меняет внешнее состояние «под себя» (на время работы) но должна его «вернуть взад» при завершении (любом — штатном или нет), причём транзакции тут неприменимы
Тут опять же, ситуация зависит от конкретной программы. Если это какой-то системный ресурс, то, в идеале, сама система должна его контролировать. К примеру, если программа меняет разрешение экрана, то при падении система должна вернуть исходное разрешение. Если для конкретных ресурсов система такого сделать не может, то да, надо делать это самому — программно.
Но большой вопрос — стоит ли это делать методом обработки всех возможных проблем в каждой точке программы, или же рациональней было бы повесить действия по откату внешнего мира на обработчик вроде at_exit. Кажется, намного проще будет завести такой обработчик и выполнить в нём необходимый минимум действий, чем пытаться 100% обработать каждый bad_alloc.
Какой ещё временный файл? Оло? У нас нынче все приложения в несколько реплик запущены. С временным файлом вы обрекаете себя на необходимость привязывать клиента к конкретной реплике. Стики сешшионс и все такое. Самое норм — рядом базульку типа редиса и данные анкеты лить туда. А вот отказоустойчивость этой базульки- это уже другой вопрос
Какой ещё временный файл? Оло? У нас нынче все приложения в несколько реплик запущены.
Ну, есть ещё динозавры, которые считают, что для персональных компьютеров можно разрабатывать отдельные приложения :) А не открывать вкладочку в браузере, где через SPA заполняется эта самая форма.
Впрочем, у браузеров тоже есть local storage, и браузер тоже может упасть, пусть и частично.
Тестирование приложений в условиях нехватки памяти