Pull to refresh

Comments 52

Где же истории, как защищаться от ядерного Оомкиллера, когда система уже решила, что памяти не хватает и пора кого-нибудь прикончить? Я гарантирую, что любые колдунства над Malloс не дают гарантий, что оомкиллер в ядре не придёт в Вашу программу

Runtime библиотеки на macOS уж очень не стабильны и норовят упасть при каждом «удобном случае»
а можно поподробнее?
Даже вызов printf на macOS может привести к SIGSEGV/SIGBUS.
ну если запихать в него каку, он где угодно упадет

Промежуточные косяки
Чтобы бросить исключение, например std::bad_alloc, необходимо выделить память под объект исключения. И, внезапно, память под объект исключения тоже может быть не выделена, если мы сталкиваемся с OOM
bad_alloc::what() может возвращать статическую строку. И резервировать память под объекты исключений отдельно. Хотя вы об этом написали ниже, но
На macOS никаких запасных буферов выявлено не было
а вы где выявляли? __cxa_allocate_exception из libc++ вызывает __aligned_malloc_with_fallback, который при нехватке памяти вызывает fallback_malloc, который как раз берет память из небольшого статического массива.
UPDATE: Как уже было сказано ранее, новые версии macOS / Xcode не имеют этой проблемы.
ой, всё
Поправьте статью плз… Ну и, как верно было замечено, нет смысла пытаться обрабатывать нехватку памяти до тех пор, пока вы не сможете гарантировать что ОС не сложит вашу приложуху SIGKILL'ом раньше
а можно поподробнее?

  1. Про 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, как мы видим, в данном случае не подаётся.


  2. Запуск потока с использованием std::thread в условиях OOM приводит к падениям на macOS старее High Sierra.


  3. Так же сталкивались с, что при попытки динамической загрузки Framework'ов вместо сообщений об ошибках получали падения внутри системных функций.



а вы где выявляли?

Из личного опыта и объективных результатов тестирования. Если __cxa_allocate_exception не может выделить память мы получаем аварийное завершение работы приложения даже на macOS Mojave: https://travis-ci.org/github/kutelev/overthrower/jobs/734673895


[ RUN      ] Overthrower.ThrowingException
...
libc++abi.dylib: terminating

На Catalina с последним Xcode этого не происходит.

Ничего нелегального в fprintf, как мы видим, в данном случае не подаётся.
значит проблема не в printf?
приводит к падениям на macOS старее High Sierra… На 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Мб надо? Бери и пользуйся навсегда. Надо это делать эффективно.
UFO just landed and posted this here
Тут надо чётко разделить эффективность и безопасность. Если вашему демону прийдет ООМ и прибьёт его — каковы будут убытки? А если (дико утрирую и в курсе, что такого не делают) ООМ заглянет в гости к демону, который стержни на АЭС двигает и она по аварии отключится?
UFO just landed and posted this here
Висит себе мой демон на сервере, виртуальные резиновые изделия пинает 99% времени. А раз в неделю прилетает ему расписание для кинотеатра, согласно которому ему надо в 14 залов выгрузить новое

Ну, так запускайте его, когда надо. Та же лямбда (серверлесс) именно за этим нужна.

UFO just landed and posted this here
Лямбды для того и сущесвуют. Это событийная штука. Как прилетит событие так и запустится.
Пример: https://aws.amazon.com/ru/lambda/
UFO just landed and posted this here
Второе приложение, которое вы хотите запускать для выгрузки останется без сокета

какой-то очень слабый аргумент, кмк

UFO just landed and posted this here

эта проблема будто решаема, стоит только захотеть. Ну, тут уж Вы сами решаете — что Вам нужнее, 500МБ раз в неделю и пиковые потребления, либо нормальная архитектура

UFO just landed and posted this here

Мой вариант — допилить свой софт. Про оборудование я вообще ничего не говорил, между прочим. Можно и с существующим обойтись.


что кому-то хочется чтобы 500 метров были недоступны 99% времени?

я это не предлагал, опять же. Читайте выше.

Вы пытаетесь распространить свой очень частный сценарий на всех. Это так не работает.
У вас там что-то странное. Со странной архитектурой. У людей обычно таких проблем нет.

Покупать в каждый из 2500+ узлов больше планок памяти (а иногда и новую материнку) из-за того
Если у вас bare metal серваки, то… ссзб

у них скорее edge computing :-) такое же в крупных ритейлерах встречается (М-Видео, Х5).

Раз в неделю и запускайте. Мониторинг отдельно, тяжелая загрузка отдельно.

Рядом про лямбды уже написали. Можно вообще запуск контейнера по событию соорудить. Оверкилл, но с другой стороны раз в неделю всего. Может и стоит того. Надо по месту смотреть.

Сейчас именно так софт и пишут. Оптимизация это именно вот оно.
вы определитесь какой процесс важный а какой — нет. Приостанавливать более важный процесс из-за того, что менее важный залез в его резерв памяти, глупо. Если у вас в контейнере выделено 50 гигов, значит пусть процесс их все и утилизирует как умеет. Ну либо он не таким уж и важным получается.

В конейтере обычно не один процесс, и не всегда это контейнер. "Важный" это такой без которого система в принципе бесполезна, хотя в контексте речь шла про "важность" с точки зрения OOM killer.


Как я уже говорил выше, может быть несколько процессов которым в разное время нужно много памяти, но не одновременно — зачем мне выделять 50 гиг на случай если вдруг они все сойдут с ума и потребуют её одновременно, если 99% времени одновременно нужно только 10 гиг максимум? Проще обрабатывать вариант отказа в выделении памяти конкретному процессу и ставить его на паузу пока память не появится.


Ясное дело, если у меня что-то настолько важное (скажем, буквально жизненно важное) что оно не имеет права не получить память — тогда оно получит и 50 и даже 100 гиг, но так в моей практике почти не бывает (уже).

В контейнере должен быть один процесс. Это азы проектирования ПО. Успехов с отладкой и эксплуатацией более одного процесса на контейнер.

Что-то важное, так же как и неважное, имеет лимит по памяти и живет в нем.
Превысил — умер. В этом лимит никто никогда не лезет.

Скажите об этом Gitlab omnibus. Есть разные подходы и все не упирается в «один процесс — один контейнер». Скорее более верный подход «один сервис — один контейнер», но сервис может быть многотредовый

В контейнере должен быть один процесс.

А если это что-то типа Postgres или Dovecot? Мне хардкорно поправить их архитектуру чтобы в одном контейнере был один процесс, несмотря на то что они очень плотно связаны друг с другом?


Успехов с отладкой и эксплуатацией более одного процесса на контейнер.

Вы серьёзно? Любая ось — это тоже своего рода контейнер, и в нём десятки а то и сотни процессов (причём далеко не всегда связанных друг с другом) — как они отлаживались-то всё это время, до появления докера и кубернетов?


Да практически вся индустрия работает с кучей процессов на контейнер (чтобы под ним не подразумевалось) с начала времён — и особых проблем с отладкой нет, а если мы возьмём IoT то там вообще по определению "одна железка — один контейнер", и отлаживают же.

Исключения могут быть. Из любого правила есть исключения. Это нормально. Но это именно исключения, на общее правило не влияющие.

Вы серьёзно? Любая ось — это тоже своего рода контейнер, и в нём десятки а то и сотни процессов (причём далеко не всегда связанных друг с другом) — как они отлаживались-то всё это время, до появления докера и кубернетов?

Все наелись проблем и радостно уехали в виртуалки, а потом и в контейнеры. По контейнеру на приложение. Это просто удобнее.

Сейчас катать пакеты, отслеживать железки, думать об их обновлении или ремонте, распределять софт по разнородным железкам объединенным в логические кластера много лет назад по уже неактуальным соображениям.
Вы точно хотите вернуться в эти времена?
Вместо делоя на нечто полностью виртуальное стандартного Докер образа? Когда о том что под ним думают специальные люди и тебя все это вообще не волнует. Когда под приложение делается кластер именно той топологии которая нужна, а не берется что-то железное 5 летней давности вроде примерно подходящее.

Да практически вся индустрия работает с кучей процессов на контейнер (чтобы под ним не подразумевалось) с начала времён — и особых проблем с отладкой нет, а если мы возьмём IoT то там вообще по определению «одна железка — один контейнер», и отлаживают же.

Мы про бекенды и сервера тут. Иот это свой мир. И нет не работает. Легаси живет черти как. Но все новое уже пишется одно приложение — один контейнер. Старое постепенно адаптируется к такой жизни.

Вы говорили про отладку. В чём принципиальная разница отладки приложения в распоряжении которого отдельный контейнер и приложения рядом с которым ещё десяток приложений, особенно если они под другим uid?


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


Мои приложения максимально абстрактны с точки зрения среды выполнения — им нужны память, немножко (очень мало) файлы и сокеты (много), всё что им нужно из библиотек и прочих зависимостей лежит рядом в одном дереве (то есть в теории они даже в системных библиотеках не нуждаются) — всё, имея это они могут работать как внутри контейнеров так и вне контейнеров, совершенно не осознавая среды. Я могу их отлаживать где угодно, и выполнять где угодно (пока это "где угодно" даёт файлы и сокеты) — что я выиграю если буду их насильно контейниризировать и отлаживать в контейнерах?

что я выиграю если буду их насильно контейниризировать и отлаживать в контейнерах?

возможность запускать на (или "в") оркестраторах контейнеров — на масштабах 10-100-1000 вычислительных узлов и в сотнях реплик?

Вы говорили про отладку. В чём принципиальная разница отладки приложения в распоряжении которого отдельный контейнер и приложения рядом с которым ещё десяток приложений, особенно если они под другим uid?

Оркестрация и изоляция. Это не только баззворды для собеседований, но и действительно полезные вещи.

Когда у вас их десяток, то понять почему ООМ становится на порядки сложнее.
Понять почему ЦПУ затроттлилось нереально вообще.
Ну и всякие мелочи вроде конфликтов за порты, пересечение логов, невозможность обновить одно из приложений не трогая остальные и прочие радости работы на bare metal. Вы его и изображаете.

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

Производительности теряется на уровне погрешности. Можно игнорировать. Зато можно удобно поднимать все окружение. Я помню эту магию с переопределением портов, баш скриптами для поднимания дев среды и секретными файликами разбросаными по всей файловой системе. И естесвенно из сурсконтроля парой команд ничего не поднять. Всегда есть длинная и запутанная инструкция. Хорошо если работающая.

Мои приложения максимально абстрактны с точки зрения среды выполнения — им нужны память, немножко (очень мало) файлы и сокеты (много), всё что им нужно из библиотек и прочих зависимостей лежит рядом в одном дереве (то есть в теории они даже в системных библиотеках не нуждаются) — всё, имея это они могут работать как внутри контейнеров так и вне контейнеров, совершенно не осознавая среды. Я могу их отлаживать где угодно, и выполнять где угодно (пока это «где угодно» даёт файлы и сокеты) — что я выиграю если буду их насильно контейниризировать и отлаживать в контейнерах?

Оркестрация
Каждое приложение становится самодостаточной единицей деплоя. Оно отдельно катается, отдельно рестартуется, отдельно маштабируется. Эксплуатация становится просто на порядок удобнее.

И изоляция
У приложения есть все порты, в его файловую систему никто не пишет, его память никто не использует, его ЦПУ никто не использует. Ресурсы и их использование можно посмотреть UI той или иной степени удобства. Обычно достаточно удобно.
Все взаимодействия с соседими сервисами делается через типовые и более-менее стандартизованние интерфейсы. Во времена железа лично видел общение сервисов через локальный файлик. Очень глубоко запрятанный файлик. Не надо нам такого.

Отладка пока пишешь может делаться как угодно. Если сложного окружения не надо или подойдет общее для всех окружение, то можно и просто локально. Сам так люблю разрабатывать.
Я скорее про отладку прода. Когда что-то идет не так.
зачем мне выделять 50 гиг на случай если вдруг они все сойдут с ума и потребуют её одновременно, если 99% времени одновременно нужно только 10 гиг максимум?
чтобы у вас раз в 0.01% времени не умирал критически важный процесс.
Проще обрабатывать вариант отказа в выделении памяти конкретному процессу и ставить его на паузу пока память не появится.
ставить критический процесс на паузу, ага. В надежде что его ООМ киллер не прибьет раньше чем «память появится». Да даже если… как вы собрались возобновлять процесс по волшебному «память появилась»? И какая принципиальная разница между упавшей приложухой и поставленной на паузу?

А если у вас процессы таки не критически важные, на порядки проще реализовать обработку отказов.
ставить критический процесс на паузу, ага

Я где-то говорил что он "критический"? Речь идёт просто о процессе которому нужна память но её нет "здесь и сейчас".


И какая принципиальная разница между упавшей приложухой и поставленной на паузу?

Примерно такая же как между "накопили данные, обработали, упали перед тем как сохранили результат" и "накопили данные, обработали, ждём пока сможем сбросить".

Я где-то говорил что он «критический»?
А если у вас процессы таки не критически важные, на порядки проще реализовать обработку отказов.
У меня была «замечательная» ситуация, когда моему бинарнику дядюшка Оом Киллер прибил mysqld посреди транзакции. Софт естественно аварийно завершился, нагадив в логи, что запись в мускль обломилась и мускля потом оно не нашло. И как я не пытайся в таком случае сделать суперскую обработку всех ошибок, без действий ручками я слабо себе представляю, как там можно было всё исправить в случае, когда всё крутится на одной машине.
OOM киллер убивает не того кто просит память а того кто занимает больше всего памяти, при этом учитывая его важность для системы (OOM score).

вообще-то это вроде настраивается!? JerleShannara есть что добавить?

Настраивается, путём записи в /proc/$pid/oom_score_adj. Правда, если вы какой-то процесс настроите так чтобы он не убивался (это тоже возможно), но он сойдёт с ума и сожрёт всю память — может быть намного хуже.


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

Да нечего, пока сервис крутится на одной машине/виртуалке/контейнере ничего 100% результативного для обработки дядюшки ООМа сделать нельзя, т.к. максимум, что можно — это в своём софте адскую обработку всех исключений и сигналов + мониторинг и качественные логи.
Всё-же не до конца понятна мотивация — зачем отлавливать каждый случай исчерпания памяти? Ну, допустим, если память закончилась, мы можем попытаться что-то сохранить, но нету гарантии, что это удастся и программа не завалится вторично.
ИМХО лучше не пытаться ловить каждый случай, а просто проектировать программу так, чтобы данные никогда не повреждались при падении. Это будет полезно также и в тех случаях, когда программа падает по другим причинам, или вообще, убивается извне.
просто проектировать программу так, чтобы данные никогда не повреждались при падении

И как вы это сделаете без обработки ситуации "караул, память не дали"? К примеру, если у вас есть внутренние буфера, ещё не отправленные куда нужно с помощью write()/send()/etc, они банально пропадут, незакрытые транзакции БД могут откатится а могут и нет (зависит от БД и режима), и ещё много вещей которые зависнут в памяти приложения при жёстком завале, или даже просто внешнее состояние которое останется "грязным".


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

Суть моей идеи не в том, чтобы иметь возможность всегда сохранить данные, находящиеся в программе, а в том, чтобы не допустить возможность нарушения состояния внешнего мира в таких ситуациях. Пусть программа упадёт и потеряет часть данных, но внешний мир (файлы, базы данных и т. д.) остались в консистентном состоянии.

незакрытые транзакции БД могут откатится а могут и нет

По идее, в базе данных должны быть средства защиты от не завершивших транзакции клиентов. Если таких средств нету, надо или самому такие средства эмулировать, или вообще, смириться с возможной потерей данных от падений, происходящих из-за исчерпания памяти или из-за чего-то ещё.
Если программа убивается извне, это тоже можно (и нужно) обрабатывать

Почему это нужно? Штатное закрытие да, обрабатывать надо. Но именно что SIGKILL или что-то подобное означает, что пользователем/системой ожидается, что программа сдохнет немедленно, а значит что-то пытаться сохранить она не должна.
Пусть программа упадёт и потеряет часть данных, но внешний мир (файлы, базы данных и т. д.) остались в консистентном состоянии.

Вы уверены? Представьте что вы работаете в чём-то типа IDE, и в момент поиска чего-то оно падает, теряя ваши последние 15 минут работы (важный баг-фикс). Конечно, современные IDE достаточно часто сохраняют файлы так что ситуация маловероятна — но вот оператор набивающий форму с данными которые получает по телефону будет очень расстроен, если приложение которое эту форму ему показывает вдруг умрёт в конце ввода, а форма может быть отправлена/сохранена только полностью.


Есть и другой пример, немножко обратный — когда программа меняет внешнее состояние "под себя" (на время работы) но должна его "вернуть взад" при завершении (любом — штатном или нет), причём транзакции тут неприменимы (самый просто пример — всякие демоны для рутинга типа quagga/bird).


Штатное закрытие да, обрабатывать надо.

А SIGINT/SIGHUP — штатное закрытие? Приложение может получить сигнал от системы (злобный админ решил сделать shutdown пока пользователи ходили на обед) — и его нужно корректно обработать. Ясный пень, SIGKILL как раз на крайний случай, но всё остальное (сигналы не связанные с аппаратными источниками) я бы всё равно отнёс к "штатным".


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

оператор набивающий форму с данными которые получает по телефону будет очень расстроен, если приложение которое эту форму ему показывает вдруг умрёт в конце ввода

В идеале такое приложение может по мере набора сохранять данные куда-то во временный файл и в случае падения и последующего перезапуска подгружать эти данные из файла. Это тоже входит в комплекс архитектурных мер против падений.

Есть и другой пример, немножко обратный — когда программа меняет внешнее состояние «под себя» (на время работы) но должна его «вернуть взад» при завершении (любом — штатном или нет), причём транзакции тут неприменимы

Тут опять же, ситуация зависит от конкретной программы. Если это какой-то системный ресурс, то, в идеале, сама система должна его контролировать. К примеру, если программа меняет разрешение экрана, то при падении система должна вернуть исходное разрешение. Если для конкретных ресурсов система такого сделать не может, то да, надо делать это самому — программно.
Но большой вопрос — стоит ли это делать методом обработки всех возможных проблем в каждой точке программы, или же рациональней было бы повесить действия по откату внешнего мира на обработчик вроде at_exit. Кажется, намного проще будет завести такой обработчик и выполнить в нём необходимый минимум действий, чем пытаться 100% обработать каждый bad_alloc.

Какой ещё временный файл? Оло? У нас нынче все приложения в несколько реплик запущены. С временным файлом вы обрекаете себя на необходимость привязывать клиента к конкретной реплике. Стики сешшионс и все такое. Самое норм — рядом базульку типа редиса и данные анкеты лить туда. А вот отказоустойчивость этой базульки- это уже другой вопрос

Какой ещё временный файл? Оло? У нас нынче все приложения в несколько реплик запущены.

Ну, есть ещё динозавры, которые считают, что для персональных компьютеров можно разрабатывать отдельные приложения :) А не открывать вкладочку в браузере, где через SPA заполняется эта самая форма.


Впрочем, у браузеров тоже есть local storage, и браузер тоже может упасть, пусть и частично.

Sign up to leave a comment.

Articles