Docker увеличить память для контейнера
Если вы думаете о запуске ваших веб-приложений в контейнерах Docker или вы уже адаптировались к этой технологии, вам следует позаботиться об одном из самых важных аспектов, а именно о том, сколько ресурсов хоста используют ваши контейнеры..
По умолчанию нет ограничений на объем памяти или ЦП, которые может использовать контейнер. Он может использовать столько, сколько позволяет планировщик ядра хоста. Важно, чтобы работающий контейнер не занимал слишком много памяти хост-машины или ЦП. Если ядро обнаруживает, что осталось очень мало свободной памяти, оно выдает Исключение из нехватки памяти и запускает процессы уничтожения, и это может привести к отключению всей системы в некоторых случаях.
Чтобы избежать этих обстоятельств, вы всегда должны запускать тесты для своего приложения и выяснять количество необходимых ресурсов, а затем ограничивать контейнеры для использования адекватного количества ресурсов..
Настройка Ubuntu 18.04 для использования функции ограничения ресурсов Docker
Итак, начнем. Прежде всего, вы должны проверить, если ваше ядро позволяет эти функции. Для этого вам нужно выполнить следующую команду.
Информация о докере $ sudo
Если вы увидите следующее предупреждение в конце вывода, вам придется внести некоторые изменения в системные файлы.
В этом случае выполните следующие действия в Ubuntu 18.04.
- Войдите в Ubuntu host как пользователь с привилегиями sudo.
- Изменить / И т.д. / по умолчанию / личинка файл и добавьте в него следующую строку.
GRUB_CMDLINE_LINUX ="cgroup_enable = память подкачки = 1"
- Сохраните и закройте файл после добавления вышеуказанной строки. Вам также потребуется обновить GRUB с помощью следующей команды.
Изменения вступят в силу после перезагрузки системы. Подтвердите, что изменения вступили в силу, снова выполнив следующую команду
Если вы не видите предупреждение снова, вы можете идти.
Теперь есть два типа основных ресурсов, о которых мы должны позаботиться.
Ограничение доступа к памяти контейнера
При запуске контейнера с Docker Run Командой мы можем указать разные варианты. Ниже приведены примеры.
Ограничение использования ЦП контейнера
Вывод
Ограничение ресурсов контейнера очень сильно зависит от конфигурации ядра хост-машины. Хотя очень важно знать требования вашего контейнера и соответствующим образом их ограничивать, вы также должны быть знакомы со средой хост-машин, которая в данном случае называется Ubuntu 18.04. Всегда выполняйте несколько тестов в своих приложениях, чтобы получить представление о требованиях к ресурсам. Аккуратное использование ресурсов может сэкономить много затрат.
Вы можете увидеть статистику для ваших докерских контейнеров в рабочем состоянии, выполнив статистика докера команда, указав имя контейнера или имена и проверить ограничения и конфигурации, которые вы наложили.
При запуске Node.js-приложений в контейнерах Docker традиционные настройки памяти не всегда работают так, как ожидается. Материал, перевод которого мы сегодня публикуем, посвящён поиску ответа на вопрос о том, почему это так. Здесь же будут приведены практические рекомендации по управлению памятью, доступной Node.js-приложениям, работающим в контейнерах.
Обзор рекомендаций
Предположим, Node.js-приложение выполняется в контейнере с установленным лимитом памяти. Если речь идёт о Docker, то для установки этого лимита могла быть использована опция --memory . Нечто подобное возможно и при работе с системами оркестрации контейнеров. В таком случае рекомендуется, при запуске Node.js-приложения, использовать опцию --max-old-space-size . Это позволяет сообщить платформе о том, какой объём памяти ей доступен, а так же учесть то, что этот объём должен быть меньше лимита, заданного на уровне контейнера.
Когда Node.js-приложение выполняется внутри контейнера, задавайте ёмкость доступной ему памяти в соответствии с пиковым значением использования активной памяти приложением. Это делается в том случае, если ограничения памяти контейнера можно настраивать.
Теперь поговорим о проблеме использования памяти в контейнерах подробнее.
Лимит памяти Docker
По умолчанию контейнеры не имеют ограничений по ресурсам и могут использовать столько памяти, сколько позволяет им операционная система. У команды docker run имеются опции командной строки, позволяющие задавать лимиты, касающиеся использования памяти или ресурсов процессора.
Команда запуска контейнера может выглядеть так:
Обратите внимание на следующее:
- x — это лимит объёма памяти, доступной контейнеру, выраженный в единицах измерения y .
- y может принимать значение b (байты), k (килобайты), m (мегабайты), g (гигабайты).
Здесь лимит памяти установлен в 1000000 байт.
Для проверки лимита памяти, установленного на уровне контейнера, можно, в контейнере, выполнить следующую команду:
Поговорим о поведении системы при указании с помощью ключа --max-old-space-size лимита памяти Node.js-приложения. При этом данный лимит памяти будет соответствовать лимиту, установленному на уровне контейнера.
То, что в имени ключа называется «old-space», представляет собой один из фрагментов кучи, управляемой V8 (то место, где размещаются «старые» JavaScript-объекты). Этот ключ, если не вдаваться в детали, которых мы коснёмся ниже, контролирует максимальный размер кучи. Подробности о ключах командной строки Node.js можно почитать здесь.
В общем случае, когда приложение пытается использовать больше памяти, чем доступно в контейнере, его работа завершается.
В следующем примере (файл приложения называется test-fatal-error.js ) в массив list , с интервалом в 10 миллисекунд, помещают объекты MyRecord . Это приводит к бесконтрольному росту кучи, имитируя утечку памяти.
Обратите внимание на то, что все примеры программ, которые мы будем тут рассматривать, помещены в образ Docker, который можно загрузить с Docker Hub:
Вы можете воспользоваться этим образом для самостоятельных экспериментов.
Кроме того, можно упаковать приложение в контейнер Docker, собрать образ и запустить его с указанием лимита памяти:
Здесь ravali1906/dockermemory — это имя образа.
Теперь можно запустить приложение, указав лимит памяти для него, превышающий лимит контейнера:
Здесь ключ --max_old_space_size представляет собой лимит памяти, указываемый в мегабайтах. Метод process.memoryUsage() даёт сведения об использовании памяти. Значения показателей выражены в байтах.
Работа приложения в некий момент времени принудительно завершается. Происходит это тогда, когда объём использованной им памяти переходит некую границу. Что это за граница? О каких ограничениях на объём памяти можно говорить?
Ожидаемое поведение приложения, запущенного с ключом --max-old-space-size
По умолчанию максимальный размер кучи в Node.js (вплоть до версии 11.x) составляет 700 Мб на 32-битных платформах, и 1400 Мб на 64-битных. О настройке этих значений можно почитать здесь.
В теории, если установить с помощью ключа --max-old-space-size лимит памяти, превышающий лимит памяти контейнера, можно ожидать, что приложение будет завершено защитным механизмом ядра Linux OOM Killer.
В реальности этого может и не случиться.
Реальное поведение приложения, запущенного с ключом --max-old-space-size
Приложению, сразу после запуска, не выделяется вся память, лимит которой указан с помощью --max-old-space-size . Размер JavaScript-кучи зависит от нужд приложения. О том, какой размер памяти использует приложение, можно судить на основании значения поля heapUsed из объекта, возвращаемого методом process.memoryUsage() . Фактически, речь идёт о памяти, выделенной в куче под объекты.
В результате мы приходим к выводу о том, что приложение будет принудительно завершено в том случае, если размер кучи окажется больше лимита, заданного ключом --memory при запуске контейнера.
Но в реальности этого тоже может не случиться.
При профилировании ресурсоёмких Node.js-приложений, которые запущены в контейнерах с заданным лимитом памяти, можно наблюдать следующие паттерны:
- OOM Killer срабатывает гораздо позже того момента, когда значения heapTotal и heapUsed оказываются значительно превышающими ограничения на объём памяти.
- OOM Killer никак не реагирует на превышение ограничений.
Объяснение особенностей поведения Node.js-приложений в контейнерах
Контейнер наблюдает за одним важным показателем приложений, которые в нём выполняются. Это — RSS (resident set size). Этот показатель представляет некую часть виртуальной памяти приложения.
Более того, он представляет собой фрагмент памяти, которая выделена приложению.
Но и это ещё не всё. RSS — это часть активной памяти, выделенной приложению.
Активной может быть не вся память, выделенная приложению. Дело в том, что «выделенная память» не обязательно физически выделяется до тех пор, пока процесс не начнёт ей по-настоящему пользоваться. Кроме того, в ответ на запросы на выделение памяти от других процессов, операционная система может сбросить в файл подкачки неактивные фрагменты памяти приложения и передать освободившееся пространство другим процессам. А когда приложению снова понадобятся эти фрагменты памяти, они будут взяты из файла подкачки и возвращены в физическую память.
Показатель RSS указывает на объём активной и доступной приложению памяти в его адресном пространстве. Именно он влияет на принятие решения о принудительном завершении работы приложения.
Доказательства
▍Пример №1. Приложение, которое выделяет память под буфер
В следующем примере, buffer_example.js , показана программа, которая выделяет память под буфер:
Для того чтобы объём памяти, выделяемой программой, превысил бы лимит, заданный при запуске контейнера, сначала запустим контейнер следующей командой:
После этого запустим программу:
Как видно, система не завершила выполнение программы, хотя при этом выделенная программой память и превышает лимит контейнера. Произошло это из-за того, что программа не работает со всей выделенной памятью. Показатель RSS очень мал, он не превышает лимит памяти контейнера.
▍Пример №2. Приложение, заполняющее буфер данными
В следующем примере, buffer_example_fill.js , память не просто выделяется, а ещё и заполняется данными:
После этого запустим приложение:
Как видно, даже теперь приложение не завершается! Почему? Дело в том, что когда объём активной памяти достигает лимита, заданного при запуске контейнера, и при этом в файле подкачки есть место, некоторые из старых страниц памяти процесса перемещаются в файл подкачки. Освобождённая память оказывается доступной тому же самому процессу. По умолчанию Docker выделяет под файл подкачки пространство, равное лимиту памяти, заданному с помощью флага --memory . Учитывая это можно сказать, что у процесса есть 2 Гб памяти — 1 Гб в активной памяти, и 1 Гб — в файле подкачки. То есть, благодаря тому, что приложение может пользоваться своей же памятью, содержимое которой временно перемещается в файл подкачки, размер показателя RSS находится в пределах лимита контейнера. В результате приложение продолжает работать.
▍Пример №3. Приложение, заполняющее буфер данными, выполняющееся в контейнере, в котором файл подкачки не используется
Вот код, с которым мы будем здесь экспериментировать (это — тот же файл buffer_example_fill.js ):
На этот раз запустим контейнер, явным образом настроив особенности работы с файлом подкачки:
Общие рекомендации
Когда Node.js-приложения запускают с ключом --max-old-space-size , значение которого превышает лимит памяти, заданный при запуске контейнера, может показаться, что Node.js «не обращает внимания» на лимит контейнера. Но, как видно из предыдущих примеров, явной причиной подобного поведения является тот факт, что приложение просто не использует весь объём кучи, заданный с помощью флага --max-old-space-size .
Помните о том, что приложение не всегда будет вести себя одинаково в том случае, если оно использует больше памяти, чем доступно в контейнере. Почему? Дело в том, что на активную память процесса (RSS) влияет множество внешних факторов, на которые не может воздействовать само приложение. Они зависят от нагруженности системы и от особенностей окружения. Например — это особенности самого приложения, уровень параллелизма в системе, особенности работы планировщика операционной системы, особенности работы сборщика мусора, и так далее. Кроме того, эти факторы, от запуска к запуску приложения, могут меняться.
Ограничения ресурсов Docker (память, ЦП, ввод-вывод) подробные статьи
На хосте Docker работает несколько контейнеров, для каждого из которых требуются ресурсы ЦП, памяти и I0. Для таких технологий виртуализации, как KVM и VMware, пользователи могут контролировать, сколько ресурсов ЦП и памяти выделяется каждой виртуальной машине. Для контейнеров Docker также предоставляет аналогичный механизм для предотвращения того, чтобы контейнер занимал слишком много ресурсов и влиял на другие контейнеры и даже на весь хост.
производительность.
Как и в операционной системе, память, которую может использовать контейнер, состоит из двух частей: физической памяти и подкачки.
Docker использует следующие два набора параметров для управления использованием памяти контейнера.
(1) -m или --memory: установить предел использования памяти, например 100 МБ, 2 ГБ
(2) --memory-swap: установить лимит использования памяти + своп
Когда мы выполняем следующую команду
Его значение состоит в том, чтобы позволить контейнеру использовать до 200 МБ памяти и 100 МБ подкачки. По умолчанию два вышеуказанных набора параметров равны -1, что означает отсутствие ограничений на использование памяти контейнера и подкачки.
Ниже мы будем использовать изображение прогриума / стресса, чтобы узнать, как выделить память для контейнера. Этот образ можно использовать для выполнения стресс-тестирования контейнера. Выполните следующие команды:
- --vm1: запустить 1 рабочий поток памяти.
- --vm-bytes 280M: каждый поток выделяет 280 МБ памяти.
Запустите результат, как показано ниже
Поскольку 280 МБ находятся в пределах доступного диапазона (300 МБ), рабочий поток может работать нормально. Процесс следующий:
(1) Выделите 280 МБ памяти.
(2) Освободите 280 МБ памяти.
(3) Выделите еще 280 МБ памяти.
(4) Освободите еще 280 МБ памяти.
(5) Одна прямая петля .
Если объем памяти, выделенной рабочим потоком, превышает 300 МБ, результат показан на рисунке.
Выделенная память превышает предел, стресс-поток сообщает об ошибке, и контейнер выходит.
Если вы укажете только -m без указания -memoryswap при запуске контейнера, то -memory-swap по умолчанию будет вдвое больше значения -m, например:
Контейнер может использовать до 200 МБ кашемировой памяти и 200 замен.
Ограничение ЦП
По умолчанию все контейнеры могут использовать ресурсы ЦП хоста одинаково, и ограничений нет.。
Docker может использовать -c или -pu-share для установки веса использования ЦП контейнера. Если не указано, значение по умолчанию 1024 。
В отличие от ограничения памяти, общий ресурс процессора, установленный параметром -c, является не абсолютным количеством ресурсов ЦП, а значением относительного веса. Ресурсы ЦП, которые контейнер может в конечном итоге выделить, зависят от доли его доли ЦП в сумме всех общих ресурсов ЦП контейнера.
Другими словами: с помощью общего ресурса cpu вы можете установить приоритет контейнера для использования CPU.
Например, на хосте запущены два контейнера:
Доля ЦП для containerA составляет 1024, что вдвое больше, чем для containerB. Когда обоим контейнерам требуются ресурсы ЦП, ЦП, который может получить containerA, в два раза больше, чем у containerB.
Важно отметить, что такое распределение веса ЦП будет происходить только тогда, когда ресурсы ЦП ограничены. Если контейнер containerA в это время простаивает, чтобы в полной мере использовать ресурсы ЦП, контейнер B также можно выделить для всех доступных ЦП.
Далее мы продолжим экспериментировать с прогрием / стрессом.
(1) Start (container_ A, доля ЦП - 1024
--cpu используется для установки количества рабочих потоков. Поскольку у хоста в настоящее время только 1 ЦП, один рабочий поток может заполнить ЦП. Если у хоста несколько процессоров, вам необходимо соответственно увеличить количество процессоров.
Docker великолепен, и в этом нет сомнений. Пару лет назад он представил новый способ сборки, доставки и запуска любых рабочих нагрузок, демократизируя использование контейнеров и чрезвычайно упрощая управление их жизненным циклом.
Такой подход предоставил разработчику возможность запускать любые приложения, не загрязняя локальную машину. Однако, когда мы запускаем контейнеры, развертываем сложные стеки приложений, создаем собственные образы и извлекаем их, потребление памяти в файловой системе хоста может значительно увеличиться.
Если мы не очищали локальную машину в течение некоторого времени, то результаты этой команды могут удивить:
Эта команда показывает использование диска Docker в нескольких категориях:
- Образы (Images): размер образов, извлеченных из реестра и созданных локально.
- Контейнеры (Containers): дисковое пространство, занимаемое слоями чтения-записи каждого из контейнеров, работающих в системе.
- Локальные тома (Local Volumes): в случае, если хранение осуществляется на хосте, но вне файловой системы контейнера.
- Кэш сборки (Build Cache): кэш, сгенерированный процессом сборки образа (касается BuildKit в Docker 18.09).
Из вывод а выше видно, что большое количество дискового пространства можно восстановить. Поскольку оно не используется Docker, его можно вернуть хост-машине.
Использование диска контейнерами
Каждый раз, когда создается контейнер, в папке /var/lib/docker на хост-машине появляется несколько папок и файлов. Среди них:
- Папка /var/lib/docker/containers/ID (ID — уникальный идентификатор контейнера). Если контейнер использует драйвер логгирования по умолчанию, все логи будут сохранены в файле JSON внутри нее. Создание слишком большого количества логов может повлиять на файловую систему хост-машины.
- Папка в /var/lib/docker/overlay2 , содержащая слой чтения-записи контейнера (overlay2 является предпочтительным драйвером хранилища в большинстве дистрибутивов Linux). Если контейнер сохраняет данные в своей собственной файловой системе, они будут храниться в /var/lib/docker/overlay2 на хост-машине.
Представим, что у нас есть совершенно новая система, в которой только что был установлен Docker.
Во-первых, запустим контейнер NGINX:
Снова запустив команду df , мы увидим:
- один образ размером 126 Мбайт. Его загрузил NGINX: 1.16, когда мы запустили контейнер;
- контейнер www, запускающийся из образа NGINX.
Поскольку контейнер запущен, а образ в данный момент используется, освобождаемого пространства пока нет. Так как его размер (2B) незначителен, и поэтому его нелегко отследить в файловой системе, создадим пустой файл размером 100 Мбайт в файловой системе контейнера. Для этого мы используем удобную команду dd из контейнера www.
Этот файл создается в слое чтения-записи, связанном с этим контейнером. Если мы снова проверим вывод команды df , то увидим, что он теперь занимает некоторое дополнительное дисковое пространство.
Где находится этот файл на хосте? Проверим:
Не вдаваясь глубоко в детали — этот файл был создан на слое чтения-записи контейнера, который управляется драйвером overlay2. Если мы остановим контейнер, используемое им дисковое пространство станет пригодным для восстановления. Посмотрим:
Как можно восстановить это пространство? Путем удаления контейнера, что приведет к удалению связанного с ним слоя чтения-записи.
Следующие команды позволяют нам удалить все остановленные контейнеры сразу и освободить место на диске, которое они используют:
Из вывода видно, что больше нет места, используемого контейнерами, и, поскольку образ больше не используется (контейнер не работает), пространство, которое он использует в файловой системе хоста, может быть восстановлено:
Примечание: если образ используется хотя бы одним контейнером, занимаемое им дисковое пространство не может быть освобождено.
Подкоманда prune , которую мы применяли выше, удаляет остановленные контейнеры. Если нам нужно удалить все — и запущенные, и остановленные, мы можем использовать одну из следующих команд (обе эквивалентны):
Примечание: во многих случаях стоит использовать флаг --rm при запуске контейнера, чтобы он автоматически удалялся при остановке процесса PID 1, тем самым немедленно освобождая неиспользуемое пространство.
Использование диска образами
Существует несколько видов образов, которые не видны напрямую конечному пользователю:
- Промежуточные — те, что ссылаются на другие (дочерние) и не могут быть удалены.
- Висящие — это те, на которые больше нет ссылок. Они занимают некоторое место на диске и могут быть удалены.
Следующие команды отображают висящие образы в системе:
Чтобы удалить их, можно пойти долгим путем:
$ docker image rm $(docker image ls -f dangling=true -q)
Или использовать подкоманду prune :
В случае, если нужно удалить все образы сразу (а не только висящие), можно запустить следующую команду. Однако это не позволит удалить те, которые используются контейнером в данный момент:
Использование диска томами
Тома используются для хранения данных вне файловой системы контейнера. Например, когда контейнер запускает приложение с сохранением состояния, данные должны оставаться за его пределами и быть отделены от его жизненного цикла. Тома также используются потому, что тяжелые операции файловой системы внутри контейнера плохо влияют на производительность.
Допустим, мы запускаем контейнер на основе MongoDB, а затем используем его для тестирования бэкапа, который мы сделали ранее (он доступен локально в файле bck.json):
Данные в файле бэкапа будут храниться на хосте в папке /var/lib/docker/volumes . Почему эти данные не сохраняются в слое контейнера? Причина в том, что в Dockerfile образа mongo расположение /data/db (где mongo хранит свои данные по умолчанию) определяется как том.
Примечание: многие образы, часто связанные с приложениями с сохранением состояния, определяют тома для управления данными за пределами слоя контейнера.
Окончив тестирование бэкапа, мы останавливаем или удаляем контейнер. Однако том не удаляется, если мы не сделаем этого явно — он остается, потребляя дисковое пространство. Тогда мы можем пойти долгим путем:
Или использовать prune :
Использование диска кэшем сборки
В релизе Docker 18.09 представлены усовершенствования процесса сборки с помощью BuildKit. Использование этого инструмента позволяет повысить производительность, управление хранилищем, функциональность и безопасность. Мы не будем подробно его описывать, а просто посмотрим, как его включить и как он влияет на использование диска.
Рассмотрим следующее фиктивное приложение Node.Js и связанный с ним Dockerfile :
Dockerfile определяет, как построить образ из приведенного выше кода:
Создадим образ как обычно и без включенного BuildKit:
При проверке использования диска мы увидим только базовый ( node:13-alpine был загружен в начале сборки) и конечный образ сборки (app: 1.0):
Теперь соберем версию образа 2.0 с BuildKit. Нужно просто установить DOCKER_BUILDKIT в 1:
При повторной проверке использования диска видим, что был создан кэш сборки (Build Cache):
Для его удаления можно выполнить:
Очистка всего и сразу
В приведенных выше примерах каждая из команд контейнера, образа и тома предоставляет подкоманду prune для освобождения дискового пространства. Она доступна на системном уровне Docker, поэтому удаляет все сразу:
Выполнение этой команды время от времени для очистки диска — хорошая привычка.
Мы узнали, что такое Docker, как он работает и какие преимущества может дать. Однако на пути использования этого мощного инструмента у тебя может возникнуть ряд вопросов, и ты столкнешься с трудностями, которые сложно решить без знания архитектурных деталей Docker. В этом FAQ мы попытались собрать ответы на наиболее частые вопросы и решения для самых серьезных проблем Docker.
Как управлять ресурсами, доступными для контейнера (процессор, память)?
В случае с памятью все просто — ты можешь указать максимальный объем памяти, который будет доступен контейнеру:
С процессором все несколько сложнее. Docker полагается на механизм cgroups для ограничения ресурсов процессора, а он оперирует понятием веса, которое может быть от 1 до 1024. Чем выше вес группы процессов (в данном случае контейнера), тем больше шансов у нее получить процессорные ресурсы. Другими словами, если у тебя будет два контейнера с весом 1024 и один с весом 512, то первые два будут иметь в два раза больше шансов получить ресурсы процессора, чем последний. Это нечто вроде приоритета. Значение по умолчанию всегда 1024, для изменения используется опция -c:
Также доступна опция --cpuset, с помощью которой контейнер можно привязать к определенным ядрам процессора. Например, если указать --cpuset="0,1", то контейнеру будут доступны первые два ядра. Просмотреть статистику использования ресурсов можно с помощью команды docker stats:
Что такое линковка контейнеров и почему она хуже DNS discovery?
Линковка контейнеров — это встроенный в Docker механизм, позволяющий пробрасывать информацию об IP-адресе и открытых портах одного контейнера в другой. На уровне командной строки это делается так:
Данная команда запускает контейнер www с веб-сервером nginx внутри и пробрасывает внутрь него инфу о контейнере с именем db. При этом происходит две вещи:
Казалось бы, это именно то, что нужно, и без всяких заморочек со SkyDNS. Но данный метод имеет ряд проблем. Первая: обновление записей в /etc/hosts происходит только один раз, а это значит, что, если после перезапуска контейнер db получит другой IP-шник, контейнер www его не увидит (возможно, когда ты читаешь эти строки, проблема уже исправлена). Вторая: хостнеймы видны только изнутри слинкованного контейнера (адресата), поэтому для доступа в обратную сторону, а также из хост-системы и тем более с другой машины все так же придется использовать IP-адреса. Третья: при большом количестве контейнеров и связей между ними настройка с помощью --link чревата ошибками и очень неудобна. Четвертая: линковка не имеет побочного эффекта в виде балансировки нагрузки.
Можно ли управлять Docker через веб-интерфейс?
Официального GUI для Docker не существует. Однако можно найти несколько интересных проектов, решающих эту проблему. Первый — это DockerUI, очень простой веб-интерфейс, распространяемый в виде Docker-образа. Для установки и запуска выполняем такую команду:
Другой вариант — это Shipyard, включающий в себя такие функции, как аутентификация, поддержка кластеров и CLI. Основное назначение интерфейса — управление кластерами из множества Docker-хостов. Как и в предыдущем случае, установка очень проста:
Веб-интерфейс будет доступен на порту 8080, юзер admin, пароль shipyard.
Интерфейс DockerUI
Я слышал, что вместо docker exec лучше использовать SSH. Почему?
В небольших проектах docker exec вполне справляется со своей задачей и никакого смысла в использовании SSH нет. Однако SSH дает ряд преимуществ и решает некоторые проблемы docker exec:
- Во-первых, SSH не имеет проблемы «повисших процессов», когда по какой-то причине при выполнении docker exec клиент Docker убивается или падает, а запущенная им команда продолжает работать.
- Во-вторых, для выполнения команды внутри контейнера клиент Docker должен иметь права root, чего не требует клиент SSH. Это вопрос не столько удобства, сколько безопасности.
- В-третьих, в Docker нет системы разграничения прав на доступ к контейнерам. Если тебе понадобится дать кому-то доступ к одному из контейнеров с помощью docker exec, придется открывать полный доступ к docker-хосту.
Доступ к Docker можно получить из контейнера. Достаточно пробросить внутрь него UNIX-сокет:
У меня сложная настройка с зависимостями между контейнерами, поэтому --restart мне не подходит
В этом случае следует использовать стандартную систему инициализации дистрибутива. В качестве примера возьмем сервис MySQL. Чтобы запустить его внутри Docker на этапе инициализации Red Hat, CentOS и любого другого основанного на systemd дистрибутива, нам потребуется следующий unit-файл (mysql меняем на имя нужного контейнера):
Копируем его в каталог /etc/systemd/system/ под именем docker-mysql.service и активируем:
В Ubuntu та же задача выполняется немного по-другому. Создаем файл /etc/init/docker-mysql.conf:
А далее отдаем команду Upstart перечитать конфиги:
А теперь самое главное. Чтобы поставить другой сервис в зависимость от этого, просто создаем новый конфиг по аналогии и меняем строку After=docker.service на After=docker-mysql.service в первом случае или меняем строку start on filesystem and started docker на start on filesystem and started docker-mysql во втором.
Периодически некоторые мои контейнеры наполняются зомби-процессами. Почему так происходит?
Это одна из фундаментальных проблем Docker. В UNIX-системах в зомби превращается некорректно остановленный процесс, завершения которого до сих пор ждет родитель, — процесса уже нет, а ядро все равно продолжает хранить о нем данные. В нормальной ситуации зомби существуют недолго, так как сразу после их появления ядро пинает родителя (сигнал SIGCHLD) и тот разбирается с мертвым потомством. Однако в том случае, если родитель умирает раньше ребенка, попечительство над детьми переходит к первичному процессу (PID 1) и разбираться с зомби, в которых могут превратиться его подопечные, теперь его проблема.
В UNIX-системах первичный процесс — это демон init (или его аналог в лице Upstart или systemd), и он умеет разбираться с зомби корректно. Однако в Docker, с его философией «одно приложение на один контейнер» первичным процессом становится то самое «одно приложение». Если оно порождает процессы, которые сами порождают процессы, а затем умирают, попечительство над внуками переходит к главному процессу приложения, а оно с зомби (если они появятся) справляться совсем не умеет (в подавляющем большинстве случаев).
Решить означенную проблему можно разными способами, включая запуск внутри контейнера Upstart или systemd. Но это слишком большой оверхед и явное излишество, поэтому лучше воспользоваться образом phusion/baseimage (на основе последней Ubuntu), который включает в себя минималистичный демон my_init, решающий проблему зомби-процессов. Просто получаем образ из Docker Hub и используем его для формирования образов и запуска своих приложений:
Кроме my_init, baseimage включает в себя также syslog-ng, cron, runit, SSH-сервер и фиксы apt-get для несовместимых с Docker приложений. Плюс поддержка скриптов инициализации, которые можно использовать для запуска своих сервисов. Просто пропиши нужные команды в скрипт и положи его в каталог /etc/my_init.d/ внутри контейнера.
Почему, собирая образ с помощью Dockerfile, я получаю толстый слоеный пирог?
Команда docker build собирает образ из инструкций Dockerfile не атомарно, а выполняя каждую команду по отдельности. Работает это так: сначала Docker читает команду FROM и берет указанный в ней образ за основу, затем читает следующую команду, запускает контейнер из образа, выполняет команду и делает commit, получая новый образ, затем читает следующую команду и делает то же самое по отношению к сохраненному в предыдущем шаге образу. Другими словами, каждая команда Dockerfile добавляет новый слой к существующему образу, поэтому стоит избегать длинных списков команд вроде таких:
А вместо этого писать все одной строкой:
Ну и в целом не особо увлекаться составлением длинных Dockerfile. Кстати, в Docker есть лимит на количество слоев, и он равен 127. Это искусственное ограничение, введенное с целью не допустить деградации производительности при большом количестве слоев и не позволить админам использовать саму идею слоев не по назначению.
Правильно написанный Dockerfile
Docker поддерживает различные механизмы сборки контейнера из слоев. В чем их различия?
Текущая версия Docker (1.5.0) поддерживает пять различных механизмов для сборки файловой структуры контейнера из слоев: AUFS, Device Mapper, BTRFS, overlay и VFS. Посмотреть, какая технология используется в текущий момент, можно с помощью команды docker info, а выбрать нужную — с помощью флага -s при запуске демона. Различия между технологиями следующие:
- AUFS — технология, применявшаяся в Docker с первых дней существования проекта. Отличается простотой реализации и очень высокой скоростью работы, однако имеет некоторые проблемы с производительностью при открытии громоздких файлов на запись и работе в условиях большого количества слоев и каталогов. Огромный минус: из мейнстримовых дистрибутивов доступна только в ядрах Debian и Ubuntu.
- Device Mapper — комплексная подсистема ядра Linux для создания RAID, шифрования дисков, снапшотинга и так далее. Главное преимущество в том, что Device Mapper доступен в любом дистрибутиве и в любом ядре. Недостаток — что Docker использует обычный заполненный нулями файл для хранения всех образов, а это приводит к серьезным проседаниям производительности при записи файлов.
- Btrfs — позволяет реализовать функциональность AUFS на уровне файловой системы. Отличается высокой производительностью, но требует, чтобы каталог с образами (/var/lib/docker/) находился на Btrfs.
- Overlay — альтернативная реализация функциональности AUFS, появившаяся в ядре 3.18. Отличается высокой производительностью и не имеет ярко выраженных недостатков, кроме требования к версии ядра.
- VFS — самая примитивная технология, опирающаяся на стандартные механизмы POSIX-систем. Фактически отключает механизм разбиения на слои и хранит каждый образ в виде полной структуры каталога, как это делает, например, LXC или OpenVZ. Может пригодиться, если есть проблема вынесения часто изменяемых данных контейнера на хост-систему.
По умолчанию Docker использует AUFS, но переключается на Device Mapper, если поддержки AUFS в ядре нет.
В большинстве случаев Docker будет работать поверх Device Mapper
Расскажите подробнее про Docker Machine, Swarm и Compose
Это три инструмента оркестрации, развиваемых командой Docker. Они все находятся в стадии активной разработки, поэтому пока не рекомендуются к применению в продакшене. Первый инструмент, Docker Machine позволяет быстро развернуть инфраструктуру Docker на виртуальных или железных хостах. Это своего рода инструмент zero-to-Docker, превращающий ВМ или железный сервер в Docker-хост. Бета-релиз Machine уже включает в себя драйверы для двенадцати различных облачных платформ, включая Amazon EC2, VirtualBox, Google Cloud Platform и OpenStack.
Главная задача Machine — позволить системному администратору быстро развернуть кластер из множества Docker-хостов без необходимости заботиться о добавлении репозиториев, установке Docker и его настройке; все это делается в автоматическом режиме. Разработчикам и пользователям Machine также может пригодиться, так как позволяет в одну команду создать виртуальную машину с минимальным Linux-окружением и Docker внутри. Особенно это полезно для юзеров Mac’ов, так как они могут не заморачиваться с установкой Docker с помощью brew или boot2docker, а просто выполнить одну команду:
Второй инструмент, Docker Swarm позволяет добавить в Docker поддержку кластеров из контейнеров. С помощью Swarm можно управлять пулом контейнеров, с автоматической регулировкой нагрузки на серверы и защитой от сбоев. Swarm постоянно мониторит кластер, и, если один из контейнеров падает, он проводит автоматическую ребалансировку кластера с помощью перемещения контейнеров по машинам.
Swarm распространяется в виде контейнера, поэтому создать новый кластер с его помощью можно за считаные секунды:
Работая со Swarm, ты всегда будешь иметь дело с сервером Swarm, а не с отдельными контейнерами, которые теперь будут именоваться нодами. На каждой ноде будет запущен агент Swarm, ответственный за принятие команд от сервера. Команды будут выполнены сразу на всех нодах, что позволяет разворачивать очень большие фермы однотипных контейнеров.
Третий инструмент, Docker Compose (в девичестве fig) позволяет быстро запускать мультиконтейнерные приложения с помощью простого описания на языке YAML. В самом файле можно перечислить, какие контейнеры и из каких образов должны быть запущены, какие между ними должны быть связи (используется механизм линковки), какие каталоги и файлы должны быть проброшены с хост-системы. К примеру, конфигурация для запуска стека LAMP из примера в предыдущей статье будет выглядеть так:
Все, что нужно сделать для его запуска, — просто отдать такую команду:
Но что более интересно — ты можешь указать, сколько контейнеров тебе нужно. Например, ты можешь запустить три веб-сервера и две базы данных:
Выводы
При работе с Docker ты столкнешься со множеством других более мелких проблем и у тебя возникнет множество вопросов по реализации той или иной конфигурации. Однако на первых порах этот FAQ должен помочь.
У Docker прекрасная встроенная справка
Несколько простых советов
- Всегда выноси часто изменяемые данные на хост-систему. Логи, базы данных, каталоги с часто меняющимися файлами — все это не должно храниться внутри контейнера (во-первых, усложняется администрирование, во-вторых, при остановке контейнера данные потеряются). В идеале контейнер должен содержать только код, конфиги и статичные файлы.
- Не лепи новые слои при каждом изменении настроек или обновлении софта внутри контейнера. Используй вместо этого Dockerfile для автоматической сборки нового образа с обновлениями и измененными конфигами.
- Вовремя вычищай завершенные и давно не используемые контейнеры, чтобы избежать возможных конфликтов имен.
- По возможности используй SkyDNS или dnsmasq. Их несложно поднять, но они способны сэкономить уйму времени.
- Внимательно изучи хелп по команде run (docker run --help), там много интересного и полезного.
Шпаргалка по командам Dockerfile
- FROM <имя-образа> — какой образ использовать в качестве базы (должна быть первой строкой в любом Dockerfile).
- MAINTAINER <имя> — имя мантейнера данного Dockerfile.
- RUN <команда> — запустить указанную команду внутри контейнера.
- CMD <команда> — выполнить команду при запуске контейнера (обычно идет последней).
- EXPOSE <порт> — список портов, которые будет слушать контейнер (используется механизмом линковки).
- ENV <ключ> <значение> — создать переменную окружения.
- ADD <путь> <путь> — скопировать файл/каталог внутрь контейнера/образа (первый аргумент может быть URL).
- ENTRYPOINT <команда> — команда для запуска приложения в контейнере (по умолчанию /bin/sh -c).
- VOLUME <путь> — пробросить в контейнер указанный каталог (аналог опции -v).
- USER <имя> — сменить юзера внутри контейнера.
- WORKDIR <путь> — сменить каталог внутри контейнера.
- ONBUILD [ИНСТРУКЦИЯ] — запустить указанную инструкцию Dockerfile только в том случае, если образ используется для сборки другого образа (с помощью FROM).
Евгений Зобнин
Редактор рубрики X-Mobile. По совместительству сисадмин. Большой фанат Linux, Plan 9, гаджетов и древних видеоигр.
Читайте также: