Как известно, приложение не всегда использует одинаковое количество ресурсов, но благодаря функции автоматического вертикального масштабирования, в Jelastic изменяется размер контейнера под приложение. Соответственно пользователю не нужно переплачивать за зарезервированные ресурсы, которые не используются, как в случае с другими PaaS. Ведь действительно, бизнес-модель всей хостинговой индустрии и старого поколения PaaS решений основана на «overselling». Таким образом, важность справедливой оплаты за фактическое потребление ресурсов очевидна.

imageПонятное дело, что в то время, когда разрабатывали JVM, никто не знал об облаках или виртуализации, и, тем более, никто даже не задумывался о плотности в PaaS. Сегодня виртуализация изменила ход игры в хостинговой индустрии, и эта революция будет пр��должаться. Теперь мы можем более эффективно использовать ресурсы. Майкл Видстендт, один из главных архитекторов JVM в Oracle, подтвердил, что JVM совсем не предназначена для PaaS, но Oracle делает все возможное, чтобы изменить это. Плюс ребята из IBM двигаются в том же направлении. Некоторые примечания о динамическом поведением JVM можно найти в IBM JavaOne Keynote 2012 Highlights.

imageЧестно говоря, «изобрести» вертикальное масштабирование было нелегко в силу ряда технических ограничений. Одно из таких ограничений связано непосредственно с JVM. В этой статье мы хотели бы поделиться некоторой информацией, которая может быть полезна для понимания вертикального масштабирования и вышеупомянутых ограничений.

Для начала мы создали приложение, которое само контролировало потребление ресурсов и через API, когда оно доходило до верхней границы, давало команду инфраструктуре увеличить лимит, и наоборот, когда ресурсы не нужны были, давало команду забрать их обратно. Такой алгоритм работал достаточно неплохо, но в процессе тестирования выяснился еще один нюанс: Java не очень хорошо работает с масштабированием, так как она может стартануть с маленьким объемом памяти, потом взять дополнительные ресурсы, когда это необходимо, но обратно в OC она их не возвращает. Это специально было продумано инженерами JVM, чтобы ускорить процесс получения памяти, когда она понадобится в следующий раз. Разумеется, что для нашей облачной модели это не приемлемо.

Затем мы провели еще серию экспериментов, как ведет себя Java, точнее garbage collectors, такие как: Serial, Parallel, Concurrent Mark Sweep и G1.

Как работает Garbage Collection


В Java, динамического выделения объектов достигается с помощью нового оператора. Объект, как только создается, использует часть памяти, а память остается зарезервированной до тех пор, пока нет обращения к объекту. В случае, когда обращение к объекту не происходит, предполагается, что он больше не нужен, и память, занимаемая им, может быть освобождена. Нет явной необходимости уничтожать сам объект, так как Java может автоматически переместить его в памяти. Сбор мусора как раз является тем методом, который решает данную задачу. Программы, вовремя не освобождающие ненужные уже участки памяти, могут вызвать неконтролируемое уменьшение объёма свободной памяти — так называемую «утечку памяти». В Java, сбор мусора происходит автоматически на протяжении всего жизненного цикла программы, устраняя необходимость в освобождении памяти и, таким образом, предотвращает утечки. Более подробную информацию о Garbage Collection в Java можно найти в книге Java Enterprise Performance Book или на блоге Javarevisted.

Serial Garbage Collector (-XX:+UseSerialGC)


Начнем с Serial. Он использует один поток для выполнения всех работ по сборке мусора. Лучше всего подходит для однопроцессорных машин, иногда может быть полезен и для многопроцессорных, на которых запускаются приложения с небольшими наборами данных (примерно до 100 МБ).

��тот garbage collector показал очень хороший результат относительно масштабирования. Мы выяснили, что он обладает свойствами компактинации, то есть можно сказать, что он делает дефрагментацию памяти, и неиспользуемые ресурсы возвращает в OС.
Давайте еще раз в этом убедимся, протестировав Serial Garbage Collector на JVM 6 и JVM 7.

Java 6

Если точнее, будем тестировать Java версии 1.6.0_27.

Запускаем утилиту Visual JavaVM, которая дает возможность мониторить все Java-поцессы, запущенные на компьютере, а также отслеживать, сколько эти процессы потребляют памяти.

Для тестирования будем использовать программу, которая динамически выделает и освобождает память в бесконечном цикле:
public class Memoryleak {
 public static void main(String[] args) {
         System.out.println("START....");
         while (true) {
            System.out.println("next loop...");
            try {
int count =  1000 * 1024;
byte [] array = new byte[1024 * count];
 
                Thread.sleep(5000);
                array = null;
                System.gc();
System.gc();
Thread.sleep(5000);
} catch (InterruptedException ex) {
         }
        }
     }
 }
 

Запускаем JVM с такими параметрами:

-XX:+UseSerialGC -Xmx1024m -Xmn64m -Xms128m -Xminf0.1 -Xmaxf0.3, где

-XX:+UseSerialGC — использовать Garbage Collector Serial;
-Xmx1024m — максимальное использование ОЗУ – 1024 Мб;
-Xmn64m — heap-шаг равный 64 Мб;
-Xms128m – минимальное значение heap равное 128 Мб;
-Xminf0.1 — этот параметр определяет минимальное свободное пространство в heap и поручает JVM расширить его, если после выполнения сборки мусора нет, по крайней мере, 10% свободного пространства;
-Xmaxf0.3 — этот параметр определяет насколько heap расширен и поручает JVM уплотнить его, если объем свободного пространства превышает 30%.

По умолчанию значения для Xminf и Xmaxf равны 0.3 и 0.6, соответственно, но для эксперимента мы существенно уменьшили эти пределы, чтобы увеличить амплитуду вертикального масштабирования.

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

image

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

image

Java 7

Будем тестировать версию jdk 1.7.0_03

Используем ту же программу и устанавливаем те же параметры для JVM. Как видим с JDK 7 все тоже гладко:
image

Хороший результат, но, к сожалению, как мы уже упоминали выше, данный garbage collector работает только с однопроцессорными машинами и небольшими объемами памяти, чего недостаточно для функционирования больших, громоздких приложений.

Parallel Garbage Collector (-XX:+UseParallelGC)


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

Давайте повторим наш эксперимент для Parallel Garbage Collector.

Результат по суммарно потребляемой памяти для Java 6:
image

И для Java 7:
image

Parallel, конечно, обладает многими преимуществом по сравнению с Serial, например: может работать с мультипоточными приложениями, мультипроцессорными машинами, быстро справляется с большими объемами памяти. Но, увы, как мы видим на графиках, он не возвращает неиспользуемые ресурсы обратно в операционную систему.

Concurrent Mark Sweep Garbage Collector (-XX:+UseConcMarkSweepGC)


Concurrent Mark Sweep выполняет большую часть работ, пока приложение все еще запущено. Это позволяет существенно уменьшить паузы на сборку мусора. Он предназначен для приложений со средним и большим набором данных, для которых время отклика важнее, чем общая пропускная способность.

Повторим процедуру еще раз для Concurrent Mark Sweep.

И опять ситуация аналогична предыдущей:

Java 6:

image

Java 7:

image

G1 Garbage Collector (-XX:+UseG1GC)


И тут появляется G1, который обладает всеми преимуществами Parallel и Concurrent Mark Sweep, и удовлетворяет всем нашим требованиям.

Мы приступили к его тестированию, и выяснилось, что при продолжительной работе G1 наблюдается стабильная константа утечки памяти.

Java 6:
image


Конечно же, мы обратились с этой проблемой к представителям Oracle. Сотрудники Oracle Иван Крылов и Владимир Иванов из Санкт-Петербурга оперативно отреагировали на наш запрос и в результате данная проблема устранена в JVM 7. Давайте проверим:

image


Как видим, действительно в Java 7 все ok, усовершенствованный G1 выполнил свою миссию полностью.

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

Видео ниже объясняет, как автоматическое вертикальное масштабирование работает в Jelastic:



*По неизвестным причинам официальный баг по нашему отчету об утечке памяти в G1 был удален из базы данных Oracle. Более подробную информацию об этом баге можно найти здесь.