- PVSM.RU - https://www.pvsm.ru -

Как ускорить контейнер: тюнингуем OpenVZ

image
OpenVZ — это OpenSource-реализация технологии контейнерной виртуализации для ядра Linux, которая позволяет запускать на одной системе с ядром OpenVZ множество виртуальных окружений с различными дистрибутивами Linux внутри. За счет своих особенностей (контейнерная виртуализация идет на уровне ядра, а не железа) по ряду показателей производительности – плотности, эластичности, требованиям к размеру оперативной памяти, скорости отклика и т.д. – она работает лучше, чем другие технологии виртуализации. Например, тут [1] можно посмотреть сравнения производительности OpenVZ с традиционными системами гипервизорной виртуализации. Но, помимо этого, в Linux и OpenVZ есть и масса вариантов тонкой настройки.
В данной статье мы рассмотрим нетривиальные варианты настроек контейнеров ядра OpenVZ, которые позволяют улучшить производительность всей системы OpenVZ.

Общие настройки

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

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

# vzctl set 100 --ram 4G --save

Очень часто заниженные лимиты в контейнере приводят к отказу в выделении памяти в том или ином месте в ядре или приложении, поэтому при использовании контейнеров крайне полезно мониторить содержимое файла /proc/user_beancounters. Ненулевые значения в колонке failcnt означают, что какие-то из лимитов слишком малы и нужно либо уменьшить working set в контейнере (например, уменьшить количество нитей apache или postgresql сервера), либо же увеличить лимит на память с помощью опции --ram. Для удобного мониторинга файла /proc/user_beancounters можно воспользоваться утилитой vzubc, которая позволяет, например, смотреть только счетчики, близкие к failcnt, или обновлять показания с какой-то периодичностью (top-like mode). Подробнее про утилиту vzubc можно почитать здесь [2].

Помимо установки лимита на физическую память, рекомендуется установить лимит на размер swap для контейнера. Для настройки размера swap контейнера воспользуемся опцией --swap команды vzctl:

# vzctl set 100 --ram 4G --swap 8G --save

Сумма значений --ram и --swap — это максимальный объем памяти, которым может воспользоваться контейнер. После достижения лимита --ram контейнера, страницы памяти, принадлежащие процессам контейнера, начнут вытесняться в так называемый “виртуальный swap (VSwap)”. При этом не будет происходить реального дискового I/O, а производительность контейнера будет искусственно занижаться для создания эффекта реального swapping.

При конфигурации контейнеров рекомендуется, чтобы сумма ram+swap по всем контейнерам не превышала сумму ram+swap на host node. Для проверки настроек можно воспользоваться утилитой vzoversell.

Для управления максимальным количеством доступных процессоров для контейнера необходимо использовать опцию --cpus, например:

# vzctl set 100 --cpus 4 --save

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

Оверкоммит по памяти

Ядро OpenVZ позволяет выделить всем контейнерам суммарно больший объём памяти чем полный размер физической памяти, доступный на хосте. Данная ситуация называется memory overcommit и в этом случае ядро будет управлять памятью динамически, балансируя её между контейнерами, ведь разрешённая к использованию контейнерами память не обязательно будет ими востребована и ядро может управлять ей по своему усмотрению. При оверкоммите по памяти ядро будет эффективно управлять различными кэшами (page cache, dentry cache) и стараться уменьшать их в контейнерах пропорционально установленному лимиту по памяти. Например, если вы хотите изолировать друг от друга сервисы какого-нибудь высоконагруженного веб сайта состоящего из фронт-энда, бэк-энда и базы данных ради улучшения безопасности, то вы можете поместить их в отдельные контейнеры, но при этом каждому выделить максимально доступное на хосте количество памяти, например:

# vzctl set 100 --ram 128G --save
# vzctl set 101 --ram 128G --save
# vzctl set 102 --ram 128G --save

В этом случае с точки зрения управления памяти ситуация не будет отличаться ничем от той, когда ваши сервисы работали на одном хосте и балансировка памяти будет по-прежнему максимально эффективной. Вам не нужно думать о том, в каком из контейнеров нужно поставить больше памяти – во фронт-энд контейнере для большего размера page cache для статических данных веб сайта, или же в контейнере с базой данных – для большего размера кэша самой базы данных. Ядро будет балансировать всё автоматически.

Хотя оверкоммит позволяет ядру максимально эффективно балансировать память между контейнерами, он также обладает определёнными неприятными свойствами. Когда общее количество выделенной анонимной памяти, то есть суммарный working set для всех процессов всех контейнеров приблизится к общему размер памяти в хосте, то при попытке выделения новой памяти каким-нибудь процессом или ядром может возникнуть глобальное исключение “нехватка памяти” и OOM-killer убьёт один из процессов в системе. Чтобы проверить, случались ли такие исключения на хосте или в контейнере можно воспользоваться командой:

# dmesg | grep oom
[3715841.990200] 645043 (postmaster) invoked oom-killer in ub 600 generation 0 gfp 0x2005a
[3715842.557102] oom-killer in ub 600 generation 0 ends: task died

При этом важно отметить, что OOM-killer убьёт процесс не обязательно в том же контейнере, в котором как раз и пытались выделить память. Для управления поведением OOM-killer’a служит команда:

# vzctl set 100 --oomguarpages 2G --save

позволяющая установить лимит, гарантирующий неприкосновенность процессов контейнера в рамках заданного лимита. Поэтому для контейнеров, в которых запущенны жизненно важные сервисы, можно установить этот лимит равный лимиту памяти.

Оверкоммит по процессорам

Точно также как и в случае с памятью оверкоммит по процессорам позволяет выделять контейнерам суммарное количество процессоров превышающее общее количество логических процессоров на хосте. И также как и в случае с памятью оверкоммит по процессорам позволяет достичь максимально эффективной общей производительности системы. Например, в случае с тем же веб-сервером, “разложенным” на три контейнера для фронт-энда, бэк-энда и базы данных можно также выставить неограниченное количество CPU для каждого контейнера и достичь максимальной суммарной производительности системы. Опять же ядро само определит каким процессам из каких контейнеров выделять процессорное время.

В отличие от памяти процессор – «эластичный» ресурс, то есть нехватка CPU не приводит к каким-либо исключениям или ошибкам в системе, кроме замедления работы каких-то активных процессов. Поэтому использование оверкоммита по процессорам является более безопасным трюком для разгона системы, чем оверкоммит по памяти. Единственным негативным эффектом оверкоммита по процессорам является возможное нарушение принципа честности выделения процессорного времени для контейнеров, что может быть плохо, например, для клиентов VPS [3] хостинга, которые могут недополучать оплаченное процессорное время. Для сохранения честного распределения процессорного времени необходимо выставить “веса” контейнерам, в соответствии с оплаченым процессорным временем, используя опцию --cpuunits команды vzctl (подробнее можно прочитать здесь [4]).

Оптимизация работы контейнеров на NUMA-хостах

В случае запуска контейнеров на хосте с NUMA (Non-Uniform Memory Access) возможна ситуация, когда процессы контейнера исполняются на одной NUMA-ноде, а часть (или вся память) этих процессов была выделена на другой NUMA-ноде. В этом случае каждое обращение к памяти будет медленнее, чем обращение к памяти локальной NUMA-ноды и коэффициент замедления будет зависеть от дистанции между нодами. Ядро Linux будет стараться не допускать такой ситуации, но для гарантированного исполнения контейнера на локальной NUMA-ноде можно выставить для каждого контейнера CPU mask, которая ограничит набор процессоров, на которых разрешено выполнение процессов контейнера.

Посмотреть доступные на хосте NUMA-ноды можно с помощью команды numactl:

# numactl -H
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3
node 0 size: 16351 MB
node 0 free: 1444 MB
node 1 cpus: 4 5 6 7
node 1 size: 16384 MB
node 1 free: 10602 MB
node distances:
node   0   1
  0:  10  21
  1:  21  10

В данном примере на хосте присутствуют две NUMA-ноды, в каждой из которых 4 процессорных ядра и 16Гб памяти.

Для установки ограничения на набор процессоров для контейнера воспользуемся командой vzctl:

# vzctl set 100 --cpumask 0-3 --save
# vzctl set 101 --cpumask 4-7 --save

В данном примере мы разрешили контейнеру 100 исполняться только на процессорах с 0 по 3, а контейнеру 101 — с 4 по 7. При этом нужно понимать, что если какой-либо процесс из, например, контейнера 100 уже выделил память на NUMA-ноде 1, то каждый доступ к этой памяти будет медленнее, чем доступ к локальной памяти. Поэтому рекомендуется перезапустить контейнеры после выполнения этих команд.

Стоит отметить, что в новом релизе vzctl 4.8, появилась опция --nodemask, которая позволяет прикреплять контейнер к конкретной NUMA-ноде не указывая список процессоров данной ноды, а оперируя только номером NUMA-ноды.

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

Управление поведением fsync в контейнерах

Как известно, для гарантированной записи данных на диск приложению необходимо выполнить системный вызов fsync() на каждый измененный файл. Данный системный вызов запишет данные файла из write-back cache на диск и инициирует сброс данных из кэша диска на постоянный энергонезависимый носитель. При этом, даже если приложение записывает данные на диск, минуя write-back cache (так называемое Direct I/O), данный системный вызов все равно необходим для гарантированного сброса данных из кэша самого диска.

Частое выполнение системного вызова fsync() может существенно замедлить работу дисковой подсистемы. Среднестатистический жесткий диск способен выполнить 30-50 syncs/sec.

При этом зачастую известно, что для всех или части контейнеров такие строгие гарантии записи данных не нужны, и потеря части данных в случае аппаратного сбоя не является критической. Для таких случаев ядро OpenVZ предоставляет возможность игнорировать fsync()/fdatasync()/sync() запросы для всех или части контейнеров. Настроить поведение ядра можно, используя файл /proc/sys/fs/fsync-enable. Возможные значения данного файла в случае настройки на host node (глобальные настройки):

  0 (FSYNC_NEVER)       fsync()/fdatasync()/sync() запросы от контейнеров игнорируются
  1 (FSYNC_ALWAYS)      fsync()/fdatasync()/sync() запросы от контейнеров работают как обычно, 
                        данные всех inodes на всех файловых системах хостовой машины будут записаны
  2 (FSYNC_FILTERED)    fsync()/fdatasync() запросы от контейнеров работают как обычно,
                        sync() запросы от контейнеров затрагивают только файлы контейнера (значение по умолчанию)

Возможные значения данного файла в случае настройки внутри конкретного контейнера:

  0       fsync()/fdatasync()/sync() запросы от данного контейнера игнорируются 
  2       использовать глобальные настройки, установленные на host node (значение по умолчанию)

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

Управление поведением Direct I/O в контейнерах

По умолчанию запись во все файлы, открытые без флага O_DIRECT, осуществляется через write-back cache. Это не только уменьшает latency (время ожидания) записи данных на диск для приложения (системный вызов write() завершится, как только данные будут скопированы в write-back cache, не дожидаясь реальной записи на диск), но и позволяет планировщику I/O ядра эффективнее распределять ресурсы диска между процессами, группируя I/O запросы от приложений.

При этом некоторые категории приложений, например, базы данных, сами эффективно управляют записью своих данных, выполняя большие последовательные I/O запросы. Поэтому зачастую такие приложения открывают файлы с флагом O_DIRECT, который указывает ядру писать данные в такой файл, минуя write-back cache, напрямую из памяти пользовательского приложения. В случае работы одной базы данных на хосте такой подход оказывается эффективнее, чем запись через кэш, поскольку I/O запросы от базы данных уже выстроены оптимальным образом и нет необходимости в дополнительном копировании памяти из пользовательского приложения в write-back cache.

В случае работы нескольких контейнеров с базами данных на одном хосте данное предположение оказывается неверным, так как планировщик I/O в ядре Linux не может оптимально распределять ресурсы диска между приложениями, использующими Direct I/O. Поэтому по умолчанию в ядре OpenVZ Direct I/O для контейнеров выключено, и все данные пишутся через write-back cache. Это вносит небольшие накладные расходы в виде дополнительного копирования памяти из пользовательского приложения в write-back cache, при этом позволяя планировщику I/O ядра эффективнее распределять ресурсы диска.

Если заранее известно, что подобной ситуации на хосте не будет, то можно избежать дополнительных накладных расходов и разрешить использование Direct I/O всем или части контейнеров. Настроить поведение ядра можно, используя файл /proc/sys/fs/odirect_enable. Возможные значения данного файла в случае настройки на host node (глобальные настройки):

  0        флаг O_DIRECT игнорируется для контейнеров, вся запись происходит через write-back cache (значение по умолчанию)
  1        флаг O_DIRECT в контейнерах работает как обычно

Возможные значения данного файла в случае настройки внутри конкретного контейнера:

  0        флаг O_DIRECT игнорируется для данного контейнера, вся запись происходит через write-back cache
  1        флаг O_DIRECT для данного контейнера работает как обычно
  2        использовать глобальные настройки (значение по умолчанию)

Заключение

Вообще ядро Linux в целом, и OpenVZ в частности, предоставляют большое количество возможностей тонкой настройки производительности под конкретные задачи пользователя. Виртуализация на базе OpenVZ позволяет обеспечить максимально возможную производительность за счёт гибкого менеджмента ресурсов и различных настроек. В данной статье мы привели лишь небольшую часть специфических для контейнеров настроек. В частности, я не стал расписывать про три параметра CPUUNITS/CPULIMIT/CPUS и как они все три друг на друга влияют. Но готов это и многое другое пояснить в комментариях.
Для более полной информации читайте vzctl man page и массу ресурсов в интернете, например, openvz.livejournal.com.

Автор: pbor

Источник [5]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/open-source/71580

Ссылки в тексте:

[1] тут: http://openvz.org/Performance

[2] здесь: http://openvz.livejournal.com/42146.html

[3] VPS: https://www.reg.ru/?rlink=reflink-717

[4] здесь: https://openvz.org/Resource_shortage#cpuunits

[5] Источник: http://habrahabr.ru/post/240197/