Тестирование приложений в условиях нехватки памяти

    Вопрос о том надо ли проверять то, что возвращает malloc является спорным и всегда порождает жаркие дискуссии.


    Часть людей считает, что надо пытаться обрабатывать все виды runtime ошибок, в т.ч. и OOM ситуации. Другие считают, что с OOM всё равно мало что можно сделать и лучше дать приложению просто упасть. На стороне второй группы людей ещё и тот факт, что дополнительная логика обработки OOM с трудом поддаётся тестированию. А если код не тестируется, то почти наверняка он не работает.


    Я полностью согласен с тем, что не стоит реализовывать логику обработки ошибок которую вы не собираетесь тестировать. Почти наверняка она ничего не улучшит, а может и того хуже — всё испортит.


    Вопрос о том надо ли пытаться обрабатывать OOM ситуации в библиотеках/приложениях является противоречивым и мы не будем его здесь касаться. В рамках данной публикации я лишь хочу поделиться опытом того как можно тестировать реализованную логику обработки OOM ситуаций в приложениях написанных на C/C++. Разговор будет идти об операционных системах Linux и macOS. Ввиду ряда причин, Windows будет обойдён стороной.


    Введение


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


    • Объем RAM всегда ограничен.
    • SWAP не всегда включен.
    • Приложения не всегда ведут себя адекватно и порой пытаются выделить нереально большие объёмы памяти мешая себе и другим.
    • 32-битные приложения всё ещё существуют.
    • overcommit не всегда включен.
    • Потребление памяти могут ограничить с помощью ulimit, например.
    • Через тот же LD_PRELOAD приложению могут выдать специфический аллокатор который может просто не выдавать памяти сверх назначенного предела.

    Если вы дочитали до этого места, то будем считать, что вы согласны с тем, что обрабатывать OOM важно и нужно. Почему это важно для себя каждый решает сам, со своей стороны могу кратко сказать, что ко многим компонентам надо которыми работал я предъявлялись следующие требования:


    • Компонент никогда не должен падать, приложения его использующие не должны падать из-за ошибок в таких компонентах.
    • При возникновении OOM ситуаций приложение должно уметь завершить свою работу не падая. Данные над которыми работает ПО не должны повреждаться даже в случае нехватки памяти.
    • При временной нехватке памяти ПО должно переходить в режим пониженного потребления ресурсов и при возможности продолжать выполнять свои функции пусть даже с низкой производительностью. Если памяти начинает снова хватать, приложение должно выходить на нормальный режим работы.

    Одним из примеров библиотек, которые обрабатывают все виды ошибок, является SQLite. Как её не ломай, она не ломается. Внутренняя логика не допускает того чтобы ваша база данных превратилась в тыкву при возникновении нехватки ресурсов. SQLite обрабатывает ошибки и так же не допускает чтобы в ходе обработки этих ошибок возникли какие либо утечки ресурсов.


    Сразу скажем, что, к сожалению, порой мы бессильны и не можем гарантировать выполнение данных требований. К нам может прийти OOM Killer, от него можно частично защитится, но не всегда. Ещё одним сюрпризом может оказаться то, что вам не всегда может удаться бросить объект исключения в C++, об этом мы расскажем чуть позже.


    Шаг 1. Наивный подход или лучше чем ничего


    На первых шагах может показаться, что создать приложению OOM ситуации для целей тестирования легко. Можно просто создать пару функций my_malloc и my_free и везде в коде использовать их вместо нативных malloc и free.


    К слову переопределение my_free опционально. И так же не стоит забывать про потенциальную необходимость реализации my_realloc.


    В нормальных условиях my_malloc будет просто оборачивать malloc не внося никакой логики. Для целей тестирования в my_malloc можно заложить некоторую логику, которая будет приводить к возвращению NULL в определённых условиях.


    Не будем вдаваться в детали, а сразу скажем почему данный подход недостаточно хорош:


    • Нет никакой возможность покрыть тестами 3rd party код не изменяя его.
    • Многие библиотечные функции используют malloc под капотом и будут продолжать вызывать его напрямую. Одним из широко-используемых примеров таких функций является strdup.
    • Оборачивание malloc’а порождает дополнительные накладные расходы, в большинстве случаев они будут незначительны, но всё же не равны нулю.
    • Данный подход слабо подходит для тестирования C++ кода в котором редко происходят явные вызовы malloc и free.

    Не смотря на все недостатки наивного подхода в некоторых случаях он вполне жизнеспособен и даже используется кое-где на практике.


    Шаг 2. Метод грубой силы


    Те кто тесно работают с Linux наверняка в курсе что такое LD_PRELOAD. С помощью данной переменной окружения можно принудительно заставить загрузить свою библиотеку вперёд других. С её помощью чаще всего и переопределяют поведение таких стандартных функций как malloc. Возможно это по причине того, что такие функции как malloc/realloc/free исторически являются слабыми (weak). Сразу скажем, что на macOS есть брат LD_PRELOAD, зовут его DYLD_INSERT_LIBRARIES.


    И так, в целях тестирования, с помощью LD_PRELOAD и DYLD_INSERT_LIBRARIES можно подменить стандартные malloc/realloc своими реализациями которые и помогут нам возвращать NULL когда нам этого нужно.


    Дело за малым, осталось найти библиотеку которая позволяла бы "заваливать" некоторые аллокации памяти. Честно скажем, готовые решения мы сильно не искали ибо требовалась возможность гибкой кастомизации которую бы вряд ли бы кто предоставил.


    Опять же, если просто "заваливать" случайным образом все аллокации, не беря в расчёт контекст, мы мало чего сможем добиться. Суровая реальность преподносит следующие сюрпризы:


    • Зачастую приложение может упасть даже не дойдя до выполнения функции main. С этим сделать мы ничего не можем, соответственно и симулировать подобные ситуации нет никакого смысла.
    • Runtime библиотеки на macOS уж очень не стабильны и норовят упасть при каждом "удобном случае". Опять же за чужой код мы не отвечаем, если мы знаем, что он хрупкий, то не стоит пытаться его уронить.
    • Даже вызов printf на macOS может привести к SIGSEGV/SIGBUS.
    • Чтобы бросить исключение, например std::bad_alloc, необходимо выделить память под объект исключения. И, внезапно, память под объект исключения тоже может быть не выделена, если мы сталкиваемся с OOM. В этих ситуациях приложение просто получает std::terminate. Данная ситуация также малоинтересна и её не стоит допускать в процессе тестирования.
    • Создание потока с помощью std::thread в условиях нехватки памяти может привести к std::terminate на macOS.

    UPDATE: По результатам тестирования с помощью Travis CI можно сказать, что с новыми версиями macOS / Xcode ситуация улучшилась, бросать std::bad_alloc можно даже когда кончилась память, создание потока с помощью std::thread больше не приводит к std::terminate.


    Для того чтобы принять в расчёт все известные факторы суровой реальности и была написана библиотека Overthrower. Она позволяет несколькими псевдо-случайными способами заставлять malloc возвращать NULL. Одновременно с этим Overthrower позволяет не допускать падений из-за кода, который нам неподвластен.


    Шаг 3. Берём в расчёт суровую реальность


    Гарантируем возможность дойти до main


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


    К слову, когда мы покидаем main мы тоже мало на что можем повлиять. То есть библиотека Overthrower, которая будет синтетически создавать OOM ситуации должна иметь возможность отложенного запуска и раннего останова. Необходим некий способ который позволит проинформировать библиотеку Overthrower о том, когда ей стоит начать работать и когда стоит прекратить.


    Самым простым способом оказалось создать пару функций которые будут видны тестируемому приложению:


    • activateOverthrower
    • deactivateOverthrower

    Чтобы иметь возможность увидеть эти функции в тестируемое приложение необходимо добавить следующее:


    #ifdef __cplusplus
    extern "C" {
    #endif
    void activateOverthrower() __attribute__((weak));
    unsigned int deactivateOverthrower() __attribute__((weak));
    #ifdef __cplusplus
    }
    #endif

    В исходных кодах библиотеки есть заголовочный файл с объявлениями этих функций.


    Если библиотека Overthrower не подгружена с помощью механизма LD_PRELOAD, эти функции будут указывать в NULL, очевидно, что пытаться вызывать их в данном случае не стоит.


    Итак, в самом простом случае, протестировать некий код на предмет устойчивости можно следующим образом:


    int main(int argc, char** argv)
    {
        activateOverthrower();
        // Some code we want to test ...
        deactivateOverthrower();
    }

    На практике вызывать activateOverthrower/deactivateOverthrower можно где угодно и сколько угодно раз, на практике это можно делать в модульных тестах следующим образом:


    TEST(Foo, Bar)
    {
        activateOverthrower();
        // Some code we want to test ...
        deactivateOverthrower();
    }

    Иногда, например в случаях когда нам нужно вызвать что-то, что точно упадёт, может понадобиться поставить Overthrower на паузу, для это есть пара других функций:


    #ifdef __cplusplus
    extern "C" {
    #endif
    void pauseOverthrower(unsigned int duration) __attribute__((weak));
    void resumeOverthrower() __attribute__((weak));
    #ifdef __cplusplus
    }
    #endif

    Использовать это можно следующим образом:


    TEST(Foo, Bar)
    {
        activateOverthrower();
        // Some code we want to test ...
        pauseOverthrower(0);
        // Some fragile code we can not fix ...
        resumeOverthrower();
        // Some code we want to test ...
        deactivateOverthrower();
    }

    Реальные примеры использования библиотеки Overthrower можно найти в тестах самой библиотеки.


    Гарантируем возможность бросить исключение


    Для выделения памяти под объект исключения используется функция __cxa_allocate_exception, она, о сюрприз, может вызвать под капотом malloc, который может вернуть NULL. Тут стоит отметить, что на Linux, при невозможности выделить память с помощью malloc, __cxa_allocate_exception будет пытаться использовать запасной буфер (emergency buffer), данный буфер аллоцируется статически и в большинстве случаев позволяет без проблем бросать небольшие исключения даже тогда, когда память на куче закончилась совсем. Дополнительное подробности могут быть найдены тут.


    На macOS никаких запасных буферов выявлено не было, вследствие этого, если память на куче закончилась, то при попытке бросить любое исключение, в том числе и std::bad_alloc, работа приложения будет аварийно завершена с помощью std::terminate.


    UPDATE: Как уже было сказано ранее, новые версии macOS / Xcode не имеют этой проблемы.


    Чтобы гарантировать возможность тестирования кода, который пишем мы, нам не остаётся ничего иного как гарантировать то, что __cxa_allocate_exception всегда сможет выделить память под объект исключения с помощью вызова malloc. Из-за этого Overthrower’у приходится немного анализировать стек вызовов на каждый вызов malloc. Overthrower никогда не заваливает malloc который приходит из __cxa_allocate_exception.


    Также, некоторые широко используемые функции славятся тем, что никогда явно не освобождают за собой память, например на macOS этим славится __cxa_atexit, на Linux dlerror. Есть и другие примеры.


    Overthrower ожидает, что всё что выделяется с помощью malloc будет освобождено с помощью free. При вызове упомянутых выше функций Overthrower’у может показаться, что в коде вызываемом между activateOverthrower и deactivateOverthrower есть утечки, о чём он обязательно пожалуется:


    overthrower got deactivation signal.
    overthrower will not fail allocations anymore.
    overthrower has detected not freed memory blocks with following addresses:
    0x0000000000dd1e70  -       2  -         128
    0x0000000000dd1de0  -       1  -         128
    0x0000000000dd1030  -       0  -         128
    ^^^^^^^^^^^^^^^^^^  |  ^^^^^^  |  ^^^^^^^^^^
          pointer       |  malloc  |  block size
                        |invocation|
                        |  number  |

    Overthrower в курсе, что некоторые системные функции не освобождают за собой память и не ожидает, что память выделяемая ими будет освобождена.


    Поиск утечек памяти не есть основная функция Overthrower’а, для этого есть инструменты получше, такие как valgrind. Однако, эти инструменты не могут быть использованы при запуске приложения в условиях OOM. Поскольку мы требуем, что в приложении не возникает утечек ни при каких обстоятельствах, мы используем Overthrower в том числе и для поиска возможных утечек памяти. Если Overthrower считает, что он нашёл утечки памяти, функция deactivateOverthrower вернёт количество не освобождённых блоков памяти, а в stderr будет выведен краткий отчёт.


    Выбор стратегии заваливания


    Overthrower имеет 3 стратегии для заваливания аллокаций:


    • Random — заваливает аллокации для которых rand() % duty_cycle == 0. Параметр duty_cycle, можно установить в желаемое значение.
    • Step — начинает заваливать все аллокации после достижения указанного момента (malloc_seq_num >= delay), delay регулируется по желанию.

    <--- delay --->
    --------------+
                  |
                  | All further allocations fail
                  |
                  +------------------------------

    • Pulse — заваливает указанное количество аллокаций после некоторой задержки (malloc_seq_num > delay && malloc_seq_num <= delay + duration), параметры delay и duration могут быть настроены согласно требованиям.

    <--- delay --->
    --------------+                +------------------------------
                  |                |
                  |                | All further allocations pass
                  |                |
                  +----------------+
                  <--- duration --->

    Выбор стратегии и её настройка выполняются с помощью переменных окружения:


    • OVERTHROWER_STRATEGY
    • OVERTHROWER_SEED
    • OVERTHROWER_DUTY_CYCLE
    • OVERTHROWER_DELAY
    • OVERTHROWER_DURATION

    Переменные окружение можно выставить в любой момент до вызова activateOverthrower. Если переменные окружения не заданы, Overthrower выбирает стратегию и её параметры случайным образом, в качестве источника случайных данных используется устройство /dev/urandom.


    В некотором виде возможные способы тонкой настройки описаны в файле README.md.


    Заключение


    • Overthrower позволяет завалить любой вызов malloc по отдельности и проверить как это обрабатывает тестируемое приложение/библиотека.
    • Можно тестировать отдельные части приложения по отдельности многократно задавая разные параметры заваливания.
    • Overthrower можно использовать для тестирования кода написанного на голом Си.
    • Хрупкие фрагменты кода можно защитить от падения поставив Overthrower на паузу.
    • Overthrower замедляет работу приложения, но не катастрофически.
    • Побочным эффектом использования Overthrower’а является возможность обнаруживать утечки памяти которые могут возникнуть в том числе и вследствие неправильной обработки ошибочных ситуаций.
    • Для того чтобы интегрировать Overthrower в процесс тестирования необходимо писать Overthrower-aware тесты. В принципе, сложного в этом ничего нет.
    • Сам Overthrower тестируется на Ubuntu (начиная с версии 14.04) и macOS (начиная с Sierra (10.12) и Xcode 8.3). В процессе тестирования Overthrower пытается в том числе уронить сам себя.
    • Если в системе возникает реальный OOM, Overthrower делает всё чтобы не упасть самому.

    Средняя зарплата в IT

    111 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 7 268 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 52

      +2

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

        +2
        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'ом раньше
          0
          а можно поподробнее?

          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 этого не происходит.

            0
            Ничего нелегального в fprintf, как мы видим, в данном случае не подаётся.
            значит проблема не в printf?
            приводит к падениям на macOS старее High Sierra… На Catalina с последним Xcode этого не происходит.
            ну и забейте
          +2
          В век контейнеров приходит ООМ киллер и убивает.
          Вот это вот все не работает.
            0

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


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


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

              0
              Т.е. если ваш процесс не самый толстый в системе
              если наш процесс не самый толстый в системе, существуют и более надежные способы. Например тем или иным способом предоставить ему заведомо достаточное количество памяти.
                0

                Единственный способ предоставить процессу заведомо достаточное количество памяти — это жёстко её выделить изначально, а это не всегда целесообразно — к примеру, это может быть процесс который обычно требует 10 Мб для работы, но изредка ему нужно (на короткое время) 100/200/500 Мб — если в этот самый момент когда оно нужно памяти нет, всё же лучше это обработать (к примеру, приостановить работу пока не появится). Выделять ему сразу потенциальный максимум — это просто бесполезная трата ресурса, своп тоже не всегда имеется (или может быть медленным до непрактичности).

                  0
                  У всех сейчас докеры с кубернетисами. Запросил больше положенного — умри. Негде там настраивать.
                  Процесс потребляющий на пике 500мб требует выделенных 500мб. Не надо такие процессы делать. Точнее 500Мб мелочи. А вот ступенька 1-50 Гб это больно. Приходится 50Гб выделять на постоянку. И доставать тикет на улучшение этого места из беклога.
                    0

                    ага, requests/limits — будьте добры ТОЧНО сказать сколько вам памяти надо — иначе на мороз, простите, в Вальгаллу для сервисов-неудачников ))))

                      0

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


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


                      И 500 MB совсем не мелочи — если у вас таких процессов с сотню, но эти самые 500 MB нужны далеко не всем одновременно. И ничего "неправильного" в таких процессах нет — всё зависит от задач.

                        0
                        Оверкоммит это очень плохой сценарий. Он ведет к непонятным тормозам или багам в проде. Не надо так делать.

                        Вот так и живем. Утилизация за счет удобного и маштабируемого разделения контейнеров по железкам. Пока все жили bare metal была частая ситуация когда сервера недозагружены. Забыли, забили, не умеют, не уверены что там с ресурсами, просто не могут софт так раскидать по кластерам. Разные причины. Сейчас это делается почти само. Когда один контейнер процентов 10-20 (или меньше) от железки можно набить плотненько.

                        Сейчас софт надо писать так чтобы пикового потребления не было. Старый постепенно переписывать. Крутись как хочешь, жизнь такая. 500Мб надо? Бери и пользуйся навсегда. Надо это делать эффективно.
                          +1
                          Сейчас софт надо писать так чтобы пикового потребления не было.

                          Извините, а как вы себе это представляете? Висит себе мой демон на сервере, виртуальные резиновые изделия пинает 99% времени. А раз в неделю прилетает ему расписание для кинотеатра, согласно которому ему надо в 14 залов выгрузить новое расписание, закачать контент, настроить рекламу из-за чего он может дожраться до 500 метров. А спустя пять минут ресурсы освобождаются и он снова кушает обычные 500-600 КБ. Ваш подход предлагает эти 500 метров навсегда закрепить за процессом таким образом максимально НЕэффективно используя системные ресурсы, не давая другим их использовать.
                            0
                            Тут надо чётко разделить эффективность и безопасность. Если вашему демону прийдет ООМ и прибьёт его — каковы будут убытки? А если (дико утрирую и в курсе, что такого не делают) ООМ заглянет в гости к демону, который стержни на АЭС двигает и она по аварии отключится?
                              0
                              Стержни двигает помесь механики с системами QNX либо на аналогичных, там нет ООМ.

                              А в качестве убытков можете представить следующее — апгрейд памяти на 2500+ узлах так, чтобы каждый демон мог работать в таком режиме или если нас пришиб ООМ — переотправка данных через пять минут автоматикой; что выгоднее?
                              0
                              Висит себе мой демон на сервере, виртуальные резиновые изделия пинает 99% времени. А раз в неделю прилетает ему расписание для кинотеатра, согласно которому ему надо в 14 залов выгрузить новое

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

                                0
                                Откуда мне знать, когда прилетит команда? Плюс «пинание» не всегда ничего не делание. Сбор live информации с оборудования почти ничего не «ест» в плане ресурсов, но это нужно делать всегда, а с держать параллельного демона нельзя — ко многому оборудованию позволяется только один коннект.
                                  0
                                  Лямбды для того и сущесвуют. Это событийная штука. Как прилетит событие так и запустится.
                                  Пример: https://aws.amazon.com/ru/lambda/
                                    0
                                    Еще раз — мониторинг использует единственное доступное соединение. Второе приложение, которое вы хотите запускать для выгрузки останется без сокета. Поэтому это сделано в рамках одного демона.
                                      0
                                      Второе приложение, которое вы хотите запускать для выгрузки останется без сокета

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

                                        0
                                        Слабый аргумент что отдельное приложение для выгрузки не сможет работать потому что оборудование поддерживает только один коннект?
                                          0

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

                                            0
                                            Решаемая как? Менять во всех кинотеатрах оборудование за сотни миллионов рублей?
                                            Нормальная архитектура чего? Покупать в каждый из 2500+ узлов больше планок памяти (а иногда и новую материнку) из-за того, что кому-то хочется чтобы 500 метров были недоступны 99% времени?
                                              0

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


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

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

                                                0

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

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

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

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

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

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

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


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


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

                                  0

                                  Вот именно из-за такого подхода мы имеем проблемы вроде
                                  https://habr.com/ru/company/oleg-bunin/blog/431536/

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

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

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

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

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


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

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


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

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

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

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

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

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

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

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


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


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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

                                        0

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


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

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

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


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

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

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

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

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

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


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


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

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


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

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

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

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

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

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

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

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


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

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                      Самое читаемое