среда, 26 октября 2011 г.

Запуск большого количества параллельных задач в надежных песочницах

Недавно возникла одна задача, удовлетворяющего меня на 100% ответа на которую я пока не смог найти (вариантов разных придумалась масса).

Предлагаю порассуждать на тему возможных решений.

Итак, есть крупное Enterprise-решение, написанное на Java. Как часть своей функциональности, оно предлагает пользователям возможность создавать собственные скрипты, написанные на некотором языке, которые запускаются на выполнение в песочницах, и возвращают некоторый результат. По замыслу, это именно что чистые функции — принимают значения, производят вычисления над ними, возвращают результат, все. Никаких обращений к внешним ресурсам, никаких side effects. Скриптов этим может запускаться на выполнение много — сотни в минуту, примерно так. Возникает вопрос, как организовать надежную песочницу для них.



Предположим, мы будем использовать в качестве языка для этих скриптов Jython/Groovy/JRuby, какой угодно скриптовый язык под JVM, так как остальная часть системы написана на Java.

Сразу проблема — штатные средства JVM/архитектура Windows/Unix не позволяют организовать песочницу в рамках родительского процесса, в котором выполняется основная программа. Ограничение доступа к частям JDK по списку разрешенных пакетом, система пермишенов на доступ к сети, файлам, и все прочее хорошо, но одно но — нет никакого нормального способа запретить скрипту выполнить new int[10000000000000] и свалить JVM с OutOfMemory, тупо потому, что весь heap расшаривается между всеми потоками процесса, и ничего с этим поделать нельзя (строго говоря, единственный теоритический способ, это перехватывать все операции выделения памяти с помощью jvm agent, и не запускать на выполнение байткоды, запрашивающие выделение памяти через new, без проверки того, сколько памяти запрашивается… это потребует написание агента, который модифицирует байткод всех загружаемых классов, и вставляет нужные проверки вокруг всех операций выделения памяти, в том числе внутри самих классов JDK… мрак).

Т.е. для каждого скрипта нам нужен отдельный процесс. Но раз так, то JVM нам сразу для этого мало подходит. Потому что она изначально не предназначена для выполнения одноразовых скриптов, она имеет большой оверхед по запуску, инициализации рантайма (даже для client jvm), она потребляет много памяти на каждый свой процесс (в том числе потому, что на каждый процесс требуется создавать свой perm gen, в который грузятся все классы… и даже class data sharing вряд ли радикально сократит потребление памяти, хотя тут я еще собираюсь поэкспериментировать).

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

Питон как язык удобен для того, чтобы пользователи писали скрипты именно на нем. Возникает вопрос, как обстоят дела с sandboxing у Питона, по аналогии с Java Security Manager.

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

10 комментариев:

  1. А разве OOM роняет всю JVM? Мне казалось, это всего лишь исключение, как многие другие. Есть у него некоторые особенности, но не более. Запускаем все скрипты в отдельных потоках, вешаем обработчики необработанных исключений, и все. Кажется :)

    Другое дело, что, конечно, изоляция несколько хромать будет -- в части перфоманса. То есть скрипт, выделивший 100Гб оперативки всю систему не завалит, но напряжет порядочно -- пока он свалится, пока GC уберет за ним мусор -- тормозить будет все, это да. Но тут рождается второе решение -- основную часть системы в одной JVM, все пользовательские скрипты -- во второй. Получается такой агент для запуска скриптов. Пользовательские скрипты в этой модели могут мешать друг другу -- но это не страшно, если они "чистые функции" их всегда можно просто перезапустить. А до основной системы они не достанут.

    Ну и третий вариант -- просто очень сильно прижать юзеров по части доступного функционала. То есть вместо анализа байткода и перехвата вызовов просто вообще запретить им создавать массивы явно. Дать им набор фабрик типа createIntArray(size), и в реализации фабрик уже ограничивать память. Не 100% решение, но вероятность косяков уменьшит.

    ОтветитьУдалить
  2. OOM в Java роняет всю JVM, завершая процесс. Надежно перехватить его нельзя, можно только записать на диск дамп памяти для дальнейшего изучения :)

    Запретить юзеру создавать массив - если юзер пишет скриптна Groovy, например, то запретить ему создавать массив нельзя - это часть языка, это нельзя запретить через Java Secutiry Manager. И, кстати, на сайте Oracle, в разделе про Java Security Architecture, прямо указано, что она не предназначена для защиты неавторизованных действия, запрета использования определенных пакетов, классов, рефлекции и прочего, чтения файлов..но не предназначена для защиты от таких ошибок как выделение слишком большого количества памяти, создания слишком большого количества потоков etc.

    Т.е. варианты такие- если это JVM, то отдельный процесс на каждый скрипт, и держать пул таких процессов чтобы крутить в них скрипты, иначе никак.

    Или, использовать язык типа Erlang-a, где защита от таких ошибок встроена в Runtime (точнее, там есть так называемые эрланговские процессы, которые умирают при таких ошибках, но это не убивает системный процесс).

    ОтветитьУдалить
  3. Немного писал на эрланг. Очень экзотично... Сложно мозги перестроить, да и поддержка будет дорогой.
    ИМХО, пул jvm - наиболее оптимальное решение.
    Память сейчас стоит дешево.

    ОтветитьУдалить
  4. Роняет всю JVM???

    public class OOMTest {

    public static void main( final String[] args ) throws Exception {
    final double[][] array = new double[1000][];
    for ( int i = 0; i < 1000; i++ ) {
    try {
    array[i] = new double[Integer.MAX_VALUE];
    } catch ( Throwable e ) {
    e.printStackTrace();
    }
    }
    }
    }

    Код честно выполняется 1000 раз, пытаясь каждый раз выделить память, и каждый раз обламываясь. Что я делаю не так?

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

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

    ОтветитьУдалить
  5. Насчет запрета на создание массивов. Я не имел дела с груви, но я когда-то делал подобное для JavaScript (Rhino) - о нем и буду говорить. Поскольку rhino реализован поверх JVM, то создание всех джаваскриптовых объектов (как и вообще все операции) реально делегируется некому рантайму, написанному на джаве, и создающему какие-то джавовские хелперы. Встроиться в этот рантайм, и добавить туда свои проверки -- задачка не на 5 минут, но и не на год -- за недельку можно управиться вчерне. Плюс запретить или сильно ограничить liveconnect (возможность дергать непосредственно java libs). И почти все в шоколаде.

    ОтветитьУдалить
  6. BegemoT

    - хм, точно, запустил пример который вы привели - у меня работает. У меня осталось в памяти просто, что после выброса любой ошибки, производной от java.lang.VirtualMachineError, не гарантируется что JVM находится в стабильном состоянии. Постараюсь найти линк для подтверждения/опровержения этого. А вы на такое не натыкались?

    Надо думаю спросить инженеров Oracle.

    Под LinkerError вы имеете в виду UnsatisfiedLinkError, видимо, когда native lib (dll/so) не подгружается для JNI?

    Насчет пула процессов - пока что для меня это выглядит самым нормальным решением (учитывая, что кроме памяти, есть еще некоторые другие моменты).

    ОтветитьУдалить
  7. Насчет встраивания в рантайм языка, работающего поверх JVM - с точки зрения хакерства задача конечно безумно интересная, но с точки зрения getting things done - сомнительная. Это же потом надо будет все поддерживать при апдейте все это..+ для меня это выглядит как задача, где можно запросто убить две-три недели застрять на какой-нибудь мелочи, из-за которой достичь результата не получится.

    ОтветитьУдалить
  8. Кстати, еще комментарий насчет стабильности...У меня скорее был даже другой concern -- если скрипт который мы запускаем, идет в бесконечный цикл, нам надо его прибить с помошью Thread.stop() - другого способа нет. Вопрос, что в этом случае произойдет с мониторами (насильное освобождение), и насколько стабильны после этого они. Тут я пока еще не копал.

    ОтветитьУдалить
  9. Насчет кроссплатформенности пула, кстати, это не обязательно такая уж проблема. Вполне можно сказать, что все эти скрипты вполне могут работать пол *nix, и соотв. писать этот пул только под Линукс.

    ОтветитьУдалить
  10. Корректная работа после OOME действительно не может гарантироваться -- потому что оно же разной степени "тяжести" может быть. У меня-то был простейший (и самый частый) случай -- отказ в аллокации большого объекта. Но можно ведь permgen засрать -- это в 1.6 по моему не лечится. Или памяти может не хватить самому GC для внутренних нужд -- и непонятно, в каком состоянии он тогда выпадет. В общем, тут никаких гарантий, это да.

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

    Я тут подумал, со времен комментария -- да не так уж и сложно этот пул реализовать. Уж в линуксе-то с его вполне четко прописанными средствами управления потоками -- точно. Собственно, запускать процессы можно штатными средствами явы (тут только будет известная проблема с памятью при fork-е). А прибивать -- хоть kill -9 выдавай, хоть тот же kill через JNA дергай.

    Да, пожалуй, пул процессов -- оптимален в этом случае

    ОтветитьУдалить