Принцип реализации контейнера Docker и введение в изоляцию контейнера

Docker Kubernetes Service Mesh
Подобно официальному лозунгу Docker: «Построить один раз, запустить где угодно, настроить один раз, запустить что угодно», Docker был помечен следующим образом: легкий вес, запуск второго уровня, управление версиями, переносимость и т. д. Эти преимущества, появившиеся в начале, получили большое внимание. Теперь Docker — это не только инструмент, используемый на этапе разработки и тестирования, но и широко используемый в производственной среде. Сегодня мы представим «пит» про изоляцию контейнеров. Перед этим давайте рассмотрим основные принципы реализации контейнеров Docker.

Базовая реализация контейнера

Все мы знаем, что базовые принципы реализации виртуальных машин и контейнеров различаются, как показано на следующем рисунке:
vm_container_archs.png
Метод виртуальной машины для достижения изоляции ресурсов заключается в использовании независимой гостевой ОС и использовании гипервизора для виртуализации ЦП, памяти, устройств ввода-вывода и т. д. Например, для виртуализации памяти гипервизор создает теневую таблицу страниц.Обычно таблицу страниц можно использовать для трансляции из виртуальной памяти в физическую память. По сравнению с решением виртуальной машины для изоляции ресурсов и сред, Docker гораздо более лаконичен. Он не перезагружает ядро ​​операционной системы, как виртуальная машина. Загрузка и загрузка ядра операционной системы — процесс, требующий много времени и ресурсов. Воспользовавшись преимуществами изоляции, обеспечиваемой функциями ядра Linux, запуск контейнера выполняется почти так же быстро, как запуск процесса напрямую.

Что касается принципа реализации Docker, краткое изложение выглядит следующим образом:
  • Изоляция системной среды достигается с помощью пространств имен.Пространства имен позволяют процессу и его дочерним процессам получить изолированную область, которая видна только им самим из общих ресурсов ядра хоста (сетевой стек, список процессов, точка монтирования и т. д.). , позволяя одному и тому же Все процессы в Namespace воспринимать изменения друг друга и ничего не знать о внешних процессах, как если бы они работали в монопольной операционной системе;
  • Используйте CGroups, чтобы ограничить использование ресурсов этой среды, например, 16-ядерный компьютер с 32 ГБ, разрешите контейнеру использовать только 2 ядра по 4 ГБ. Используя CGroups, вы также можете устанавливать веса для ресурсов, рассчитывать использование, управлять запуском и остановкой задачи (процесса или потока) и т. д.;
  • Используя функцию управления образами, многослойность образов Docker, копирование при записи, адресацию содержимого и технологию совместного монтирования для получения полного набора контейнерных файловых систем и операционных сред в сочетании с репозиторием образов, образы можно быстро загружать и совместно использовать. , что удобно для развертывания в нескольких средах.

Поскольку Docker не виртуализирует гостевую ОС, как виртуальную машину, а использует ресурсы хоста для совместного использования ядра с хостом, существуют следующие проблемы:

Примечание: проблема не обязательно означает угрозу безопасности.Docker, как одна из наиболее ориентированных на безопасность контейнерных технологий, предоставляет конфигурации по умолчанию с надежной защитой во многих аспектах, включая: ограничения возможностей корневого пользователя контейнера, фильтрацию системных вызовов Seccomp, Контроль доступа к MAC-адресам Apparmor, ограничение ulimit, поддержка pid-limits, механизм подписи изображения и т. д.
1. Docker использует CGroups для достижения ограничения ресурсов, что может ограничивать только максимальное потребление ресурсов, но не может изолировать другие программы от занятия собственных ресурсов;

2. 6 элементов изоляции пространства имен кажутся завершенными, но на самом деле они все еще не полностью изолируют ресурсы Linux, такие как /proc , /sys , /dev/sd* и другие каталоги не изолированы полностью, и вся информация, кроме SELinux, времени, системного журнала и других существующих пространств имен, не изолирована.

Яма, на которую наступил контейнер в изоляции

При использовании контейнеров вы могли столкнуться со следующими проблемами:
  1. Выполните такие команды, как top и free в контейнере Docker, и вы обнаружите, что использование ресурсов, которое вы видите, является использованием ресурсов хоста, а нам нужно, сколько процессора, памяти и сколько процессов в текущем контейнере использует. ;
  2. Измените /etc/sysctl.conf в контейнере, вы получите сообщение «sysctl: ошибка установки ключа 'net.ipv4....': файловая система только для чтения»;
  3. Программа запускается в контейнере, вызывает API для получения системной памяти и ЦП и получает размер ресурсов хоста;
  4. Для многопроцессорных программ обычно можно установить количество рабочих процессов на автоматическое, чтобы адаптироваться к количеству ядер ЦП системы.Однако, если вы установите это в контейнере, количество полученных ядер ЦП будет неверным.Например, Nginx может могут быть получены другими приложениями. Неверно, необходимо проверить.

Суть этих проблем одна и та же.В среде Linux многие команды подсчитывают использование ресурсов, читая файлы в каталоге /proc или /sys.В качестве примера возьмем команду free:
lynzabo@ubuntu:~$ strace free
execve("/usr/bin/free", ["free"], [/* 66 vars */]) = 0
...
statfs("/sys/fs/selinux", 0x7ffec90733a0) = -1 ENOENT (No such file or directory)
statfs("/selinux", 0x7ffec90733a0) = -1 ENOENT (No such file or directory)
open("/proc/filesystems", O_RDONLY) = 3
...
open("/sys/devices/system/cpu/online", O_RDONLY|O_CLOEXEC) = 3
...
open("/proc/meminfo", O_RDONLY) = 3
+++ exited with 0 +++
lynzabo@ubuntu:~$

Включая различные языки, такие как Java, NodeJS, вот пример NodeJS:
const os = require('os');
const total = os.totalmem();
const free = os.freemem();
const usage = (free - total) / total * 100;

Реализация NodeJS также получает информацию о памяти, читая файл /proc/meminfo. Ява похожа.

Все мы знаем, что максимальный размер кучи JVM по умолчанию составляет 1/4 системной памяти.Если физическая память машины составляет 10 ГБ, если вы не укажете размер кучи вручную, размер кучи по умолчанию для JVM составляет 2,5 ГБ. JavaSE8 (не говорите .open JDK.java.net/browse/JDK-…). Любой, кто знаком со структурой памяти JVM, знает, что куча JVM — это модель памяти, которая только увеличивается, а не уменьшается, память кучи будет только увеличиваться, а не уменьшаться. Когда в контейнере используется Java, если размер кучи не установлен для JVM, куча получает размер памяти хост-компьютера.Когда размер кучи достигает размера памяти контейнера, система запускает OOM контейнера, и процесс Java завершится аварийно. Общие распечатки системного журнала следующие:
memory: usage 2047696kB, limit 2047696kB, failcnt 23543
memory+swap: usage 2047696kB, limit 9007199254740991kB, failcnt 0
......
Free swap = 0kB
Total swap = 0kB
......
Memory cgroup out of memory: Kill process 18286 (java) score 933 or sacrifice child

Для приложений Java ниже приведены два метода настройки кучи.

1. Для версий JavaSE8 (При запуске докера передайте параметры через переменные среды, чтобы ограничить максимальный размер кучи:
docker run -d -m 800M -e JAVA_OPTIONS='-Xmx300m' openjdk:8-jdk-alpine

2. Для версии JavaSE8 (>8u131) вы можете использовать описанное выше, чтобы вручную указать максимальный размер кучи, или использовать следующий метод, чтобы установить предел памяти адаптивного контейнера.

При запуске докера передайте параметры через переменные среды, чтобы ограничить максимальный размер кучи.
docker run -d -m 800M -e JAVA_OPTIONS='-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1' openjdk:8-jdk-alpine

Если сравнивать эти два метода, то первому не хватает гибкости, и его можно использовать, когда точно известен лимит памяти, а второй метод необходимо использовать в версии JavaSE8 (>8u131).

Когда вы запускаете контейнер, Docker вызывает libcontainer для реализации определенного управления контейнером, включая создание UTS, IPS, Mount и других пространств имен для изоляции контейнеров и использования CGroups для реализации ограничений ресурсов для контейнеров, в которых Docker будет размещать некоторые каталоги контейнера. host монтируются в контейнер в режиме только для чтения, включая каталоги /proc, /dev, /dev/shm, /sys, а также устанавливаются следующие ссылки:
  • /proc/self/fd->/dev/fd
  • /proc/self/fd/0->/dev/stdin
  • /proc/self/fd/1->/dev/stdout
  • /proc/self/fd/2->/dev/stderr

Гарантированно не будет проблем с системным IO, именно поэтому ресурсы хоста получаются в контейнере.

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

Чтение из CGroups

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

В контейнере вы можете запустить команду mount, чтобы просмотреть эти записи монтирования.
...
cgroup on /sys/fs/cgroup/cpuset type cgroup (ro,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/cpu type cgroup (ro,nosuid,nodev,noexec,relatime,cpu)
cgroup on /sys/fs/cgroup/cpuacct type cgroup (ro,nosuid,nodev,noexec,relatime,cpuacct)
cgroup on /sys/fs/cgroup/memory type cgroup (ro,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/devices type cgroup (ro,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/freezer type cgroup (ro,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/blkio type cgroup (ro,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/perf_event type cgroup (ro,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (ro,nosuid,nodev,noexec,relatime,hugetlb)
...

Здесь мы не будем объяснять ограничения ЦП и памяти CGroups, а только представим управление вычислительными ресурсами на основе движка оркестрации Kubernetes и какая поддержка предоставляется для контейнерных CGroups:
  • Когда запросы указаны для пода, request.cpu будет использоваться как--cpu-sharesзначение параметра передаетсяdocker runкоманда, этот параметр вступит в силу, когда на хосте есть несколько контейнеров, конкурирующих за ресурсы ЦП. Чем больше значение параметра, тем легче его выделить ЦП. Requests.memory не будет передаваться в Docker как параметр Этот параметр используется в Kubernetes для управления QoS ресурсов;
  • Когда для пода указаны лимиты, limit.cpu будет использоваться как--cpu-quotaЗначение параметра передаетсяdocker runЗаказ,docker runДругой параметр в команде--cpu-periodЗначение по умолчанию — 100000. Эти два параметра ограничивают максимальное количество ядер ЦП, которое может использовать контейнер.--memoryПараметры, переданные в docker run Команда используется для ограничения памяти контейнера.В настоящее время Kubernetes не поддерживает ограничение размера Swap.Рекомендуется отключать Swap при развертывании Kubernetes.

Kubernetes 1.10 и более поздние версии поддерживают указание фиксированных номеров ЦП для подов. Мы не будем подробно вводить их здесь, а сосредоточимся на обычном управлении вычислительными ресурсами. Давайте кратко поговорим об ограничениях ресурсов CGroups для контейнеров, использующих Kubernetes в качестве механизма оркестровки:

1. Прочитайте количество ядер ЦП контейнера
# 这个值除以100000得到的就是容器核数
~ # cat  /sys/fs/cgroup/cpu/cpu.cfs_quota_us 
400000


2. Получить использование памяти контейнера (USAGE / LIMIT)
~ # cat /sys/fs/cgroup/memory/memory.usage_in_bytes 
4289953792
~ # cat /sys/fs/cgroup/memory/memory.limit_in_bytes 
4294967296

Разделив эти два значения, вы получите процент используемой памяти.

3. Узнайте, установлен ли для контейнера режим OOM и произошел ли OOM.
~ # cat /sys/fs/cgroup/memory/memory.oom_control 
oom_kill_disable 0
under_oom 0
~ #

Здесь нужно пояснить:
  • Значение по умолчанию для oom_kill_disable равно 0, что означает, что oom killer включен, то есть по истечении времени ожидания памяти будет запущен процесс уничтожения. Вы можете отключить oom при использовании docker run, установить для этого значения значение 1 и отключить oom killer;
  • Значение under_oom предназначено только для просмотра, указывающего, было ли текущее состояние CGroups oom.Если да, то это значение будет отображаться как 1.

4. Получите контейнерный дисковый ввод-вывод
~ # cat /sys/fs/cgroup/blkio/blkio.throttle.io_service_bytes
253:16 Read 20015124480
253:16 Write 24235769856
253:16 Sync 0
253:16 Async 44250894336
253:16 Total 44250894336
Total 44250894336

5. Получите входящий/исходящий трафик виртуальной сетевой карты контейнера.
~ # cat /sys/class/net/eth0/statistics/rx_bytes 
10167967741
~ # cat /sys/class/net/eth0/statistics/tx_bytes 
15139291335

Если вам интересно читать CGroups из контейнеров, прочтитеРеализация исходного кода статистики Docker.

Использование LXCFS

По привычке и другим причинам использование top, free и других команд в контейнере все еще является относительно распространенным требованием, но каталоги /proc и /sys в контейнере по-прежнему являются смонтированными хост-каталогами.Есть проект с открытым исходным кодом: LXCFS. LXCFS — это файловая система пользовательского режима, основанная на FUSE. Использование LXCFS позволяет вам продолжать использовать такие команды, как free в контейнере. Обратите внимание, что в настоящее время LXCFS поддерживает только создание следующих файлов для контейнеров:
/proc/cpuinfo
/proc/diskstats
/proc/meminfo
/proc/stat
/proc/swaps
/proc/uptime

Если команда реализована путем разбора этих файлов, ее можно продолжать использовать в контейнере, в противном случае статус ресурса можно получить только чтением CGroups.

Суммировать

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

Обмен этой статьей находится здесь.Я надеюсь, что это будет полезно для всех.Приглашаем к общению с командой Xiaomi Ecological Cloud.