Несколько недель назад я делал внутренний доклад о Docker. Во время презентации один из админов спросил простой на первый взгляд вопрос: «Есть ли что-то вроде „программы похудения для Docker образов“»?
Для решения этой проблемы вы можете найти несколько вполне адекватных подходов в интернете, вроде удаления директорий кэша, временных файлов, уменьшение разных избыточных пакетов, если не всего образа. Но если подумать, действительно ли нам необходима полностью рабочая Linux система? Какие файлы нам действительно необходимы в отдельно взятом образе? Для Go binary я нашел радикальный и довольно эффективный подход. Он был собран статически, почти без внешних зависимостей. Конечный образ — 6.12 МB.
Да, уж! Но существует ли шанс сделать что-либо подобное с любым другим приложением?
Оказывается, такой подход может существовать. Идея проста: мы можем так или иначе профилировать образ во время исполнения, чтобы определить, какие файлы подвергались обращению/открытию/…, и удалить все оставшиеся файлы, которые за таким замечены не были. Эмм, звучит многообещающе, давайте же напишем PoC для этой идеи.
Исходные данные
- Образ: Ubuntu (~200MB)
- Приложение, которое должно быть запущено: /bin/ls
- Цель: Создать образ с наименьшим возможным размером
/bin/ls это хороший пример: довольно простой для проверки идеи, без подводных камней, но все же не тривиальный, ведь он использует динамическое связывание.
Теперь, когда у нас есть цель, давайте определимся с инструментом. Основная идея — это мониторинг события доступа к файлу. Будь то stat или open. Существует пара хороших кандидатов для этого. Мы могли бы использовать inotify, но его необходимо настраивать и каждый watch должен быть назначен отдельному файлу, что в итоге приведет к целой куче этих самых watch’ей. Мы могли бы использовать LD_PRELOAD, но, во-первых — использование его радости лично мне не доставляет, а во-вторых — он не будет перехватывать системные вызовы напрямую, ну и в-третьих — он не будет работать для статически собранных приложений (кто сказал golang’ов?). Решением, которое бы работало даже для статически собранного приложения, было бы использование ptrace для трассировки системных вызовов в реальном времени. Да, у него тоже существуют тонкости в настройке, но все же это было бы надежное и гибкое решение. Менее известный системный вызов — fanotify и, как уже стало ясно из названия статьи, использоваться будет именно он.
fanotify был изначально создан как «достойный» механизм для анти-вирусных вендоров для перехвата событий файловой системы, потенциально на всей точке монтирования за раз. Звучит знакомо? В то время как он может использоваться для отказа в доступе, или же просто осуществлять не блокирующий мониторинг доступа к файлу, потенциально отбрасывая события, если очередь ядра переполняется. В последнем случае специальное сообщение будет сгенерировано для уведомления user-space слушателя о потере сообщения. Это именно то, что нам нужно. Ненавязчивый, вся точка монтирование за раз, прост в настройке (ну, исходя из того что вы найдете документацию конечно…). Это может показаться смешным, но это действительно важно, как я узнал позже.
В использовании он очень прост
- Инициализируем fanotify в FAN_CLASS_NOTIFICATION моде используя системный вызов fanotify_init:
// Open ``fan`` fd for fanotify notifications. Messages will embed a // filedescriptor on accessed file. Expect it to be read-only fan = fanotify_init(FAN_CLASS_NOTIF, O_RDONLY);
- Подписываемся на FAN_ACCESS и FAN_OPEN события в "/" FAN_MARK_MOUNTPOINT используя системный вызов fanotify_mark:
// Watch open/access events on root mountpoint fanotify_mark( fan, FAN_MARK_ADD | FAN_MARK_MOUNT, // Add mountpoint mark to fan FAN_ACCESS | FAN_OPEN, // Report open and access events, non blocking -1, "/" // Watch root mountpoint (-1 is ignored for FAN_MARK_MOUNT type calls) );
- Считываем сообщения из файлового дескриптора, который мы получили от fanotify_init и проходим по ним итератором используя FAN_EVENT_NEXT:
// Read pending events from ``fan`` into ``buf`` buflen = read(fan, buf, sizeof(buf)); // Position cursor on first message metadata = (struct fanotify_event_metadata*)&buf; // Loop until we reached the last event while(FAN_EVENT_OK(metadata, buflen)) { // Do something interesting with the notification // ``metadata->fd`` will contain a valid, RO fd to accessed file. // Close opened fd, otherwise we'll quickly exhaust the fd pool. close(metadata->fd); // Move to next event in buffer metadata = FAN_EVENT_NEXT(metadata, buflen); }
В итоге мы напечатаем полное имя каждого файла, к которому был осуществлен доступ, и добавим обнаружение переполнение очереди. Для наших целей этого должно вполне хватить (комментарии и проверки ошибок опущены ради иллюстрации решения).
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <sys/fanotify.h>
int main(int argc, char** argv) {
int fan;
char buf[4096];
char fdpath[32];
char path[PATH_MAX + 1];
ssize_t buflen, linklen;
struct fanotify_event_metadata *metadata;
// Init fanotify structure
fan = fanotify_init(FAN_CLASS_NOTIF, O_RDONLY);
// Watch open/access events on root mountpoint
fanotify_mark(
fan,
FAN_MARK_ADD | FAN_MARK_MOUNT,
FAN_ACCESS | FAN_OPEN,
-1, "/"
);
while(1) {
buflen = read(fan, buf, sizeof(buf));
metadata = (struct fanotify_event_metadata*)&buf;
while(FAN_EVENT_OK(metadata, buflen)) {
if (metadata->mask & FAN_Q_OVERFLOW) {
printf("Queue overflow!\n");
continue;
}
// Resolve path, using automatically opened fd
sprintf(fdpath, "/proc/self/fd/%d", metadata->fd);
linklen = readlink(fdpath, path, sizeof(path) - 1);
path[linklen] = '\0';
printf("%s\n", path);
close(metadata->fd);
metadata = FAN_EVENT_NEXT(metadata, buflen);
}
}
}
Собираем:
gcc main.c --static -o fanotify-profiler
Грубо говоря, теперь мы имеем инструмент для мониторинга любого доступа к файлу на активной «/» точке монтирование в реальном времени. Отлично.
Что дальше? Давайте создадим Ubuntu контейнер, стартуем наш мониторинг, и выполним /bin/ls. Fanotify’ю необходима CAP_SYS_ADMIN возможность. В основном это «catch-all» root возможность. В любом случае это лучше, чем выполнять в —privileged моде.
# Run image
docker run --name profiler_ls --volume $PWD:/src --cap-add SYS_ADMIN -it ubuntu /src/fanotify-profiler
# Run the command to profile, from another shell
docker exec -it profiler_ls ls
# Interrupt Running image using
docker kill profiler_ls # You know, the "dynamite"
Результат выполнения:
/etc/passwd
/etc/group
/etc/passwd
/etc/group
/bin/ls
/bin/ls
/bin/ls
/lib/x86_64-linux-gnu/ld-2.19.so
/lib/x86_64-linux-gnu/ld-2.19.so
/etc/ld.so.cache
/lib/x86_64-linux-gnu/libselinux.so.1
/lib/x86_64-linux-gnu/libacl.so.1.1.0
/lib/x86_64-linux-gnu/libc-2.19.so
/lib/x86_64-linux-gnu/libc-2.19.so
/lib/x86_64-linux-gnu/libpcre.so.3.13.1
/lib/x86_64-linux-gnu/libdl-2.19.so
/lib/x86_64-linux-gnu/libdl-2.19.so
/lib/x86_64-linux-gnu/libattr.so.1.1.0
Прекрасно! Сработало. Теперь мы знаем наверняка, что в конечном счете необходимо для выполнения /bin/ls. Так что теперь мы просто скопируем все это в «FROM scratch» Docker образ — и готово.
Но не тут-то было… Однако давайте не забегать наперед, все по порядку.
# Export base docker image
mkdir ubuntu_base
docker export profiler_ls | sudo tar -x -C ubuntu_base
# Create new image
mkdir ubuntu_lean
# Get the linker (trust me)
sudo mkdir -p ubuntu_lean/lib64
sudo cp -a ubuntu_base/lib64/ld-linux-x86-64.so.2 ubuntu_lean/lib64/
# Copy the files
sudo mkdir -p ubuntu_lean/etc
sudo mkdir -p ubuntu_lean/bin
sudo mkdir -p ubuntu_lean/lib/x86_64-linux-gnu/
sudo cp -a ubuntu_base/bin/ls ubuntu_lean/bin/ls
sudo cp -a ubuntu_base/etc/group ubuntu_lean/etc/group
sudo cp -a ubuntu_base/etc/passwd ubuntu_lean/etc/passwd
sudo cp -a ubuntu_base/etc/ld.so.cache ubuntu_lean/etc/ld.so.cache
sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/ld-2.19.so ubuntu_lean/lib/x86_64-linux-gnu/ld-2.19.so
sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/ld-2.19.so ubuntu_lean/lib/x86_64-linux-gnu/ld-2.19.so
sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libselinux.so.1 ubuntu_lean/lib/x86_64-linux-gnu/libselinux.so.1
sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libacl.so.1.1.0 ubuntu_lean/lib/x86_64-linux-gnu/libacl.so.1.1.0
sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libc-2.19.so ubuntu_lean/lib/x86_64-linux-gnu/libc-2.19.so
sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libpcre.so.3.13.1 ubuntu_lean/lib/x86_64-linux-gnu/libpcre.so.3.13.1
sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libdl-2.19.so ubuntu_lean/lib/x86_64-linux-gnu/libdl-2.19.so
sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libattr.so.1.1.0 ubuntu_lean/lib/x86_64-linux-gnu/libattr.so.1.1.0
# Import it back to Docker
cd ubuntu_lean
sudo tar -c . | docker import - ubuntu_lean
Запустим наш образ:
docker run --rm -it ubuntu_lean /bin/ls
В итоге получаем:
# If you did not trust me with the linker (as it was already loaded when the profiler started, it does not show in the ouput)
no such file or directoryFATA[0000] Error response from daemon: Cannot start container f318adb174a9e381500431370a245275196a2948828919205524edc107626d78: no such file or directory
# Otherwise
/bin/ls: error while loading shared libraries: libacl.so.1: cannot open
Да уж. Но что пошло не так? Помните, я упомянул что этот системный вызов изначально создавался для работы с антивирусом? Антивирус в реальном времени должен обнаруживать доступ к файлу, проводить проверки и по результату принимать решения. Что здесь имеет значение, так это содержимое файла. В частности, состояния гонки в файловой системе должны обходиться всеми силами. Это причина, по которой fanotify выдает файловые дескрипторы вместо путей, к которым осуществлялся доступ. Вычисление физического пути файла выполняется пробированием /proc/self/fd/[fd]. К тому же, он не в состоянии сказать, какая символьная ссылка подверглась доступу, только файл на который она указывает.
Для того, чтобы заставить это заработать, нам нужно найти все ссылки на найденные fanotify’ем файлы, и установить их в отфильтрованном образе таким же образом. Команда find нам в этом поможет.
# Find all files refering to a given one
find -L -samefile "./lib/x86_64-linux-gnu/libacl.so.1.1.0" 2>/dev/null
# If you want to exclude the target itself from the results
find -L -samefile "./lib/x86_64-linux-gnu/libacl.so.1.1.0" -a ! -path "./
Это может быть легко автоматизировано циклом:
for f in $(cd ubuntu_lean; find)
do
(
cd ubuntu_base
find -L -samefile "$f" -a ! -path "$f"
) 2>/dev/null
done
Что в итоге дает нам список недостающих семантических ссылок. Это все библиотеки:
./lib/x86_64-linux-gnu/libc.so.6
./lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
./lib/x86_64-linux-gnu/libattr.so.1
./lib/x86_64-linux-gnu/libdl.so.2
./lib/x86_64-linux-gnu/libpcre.so.3
./lib/x86_64-linux-gnu/libacl.so.1
Теперь давайте скопируем их из исходного образа и пересоздадим результирующий образ.
# Copy the links
sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libc.so.6 ubuntu_lean/lib/x86_64-linux-gnu/libc.so.6
sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 ubuntu_lean/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libdl.so.2 ubuntu_lean/lib/x86_64-linux-gnu/libdl.so.2
sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libpcre.so.3 ubuntu_lean/lib/x86_64-linux-gnu/libpcre.so.3
sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libacl.so.1 ubuntu_lean/lib/x86_64-linux-gnu/libacl.so.1
sudo cp -a ubuntu_base/lib/x86_64-linux-gnu/libattr.so.1 ubuntu_lean/lib/x86_64-linux-gnu/libattr.so.1
# Import it back to Docker
cd ubuntu_lean
docker rmi -f ubuntu_lean; sudo tar -c . | docker import - ubuntu_lean
Важное замечание: данный метод ограничен. К примеру, он не вернет ссылки на ссылки, так же как и абсолютные ссылки. Последнее требует по крайней мере chroot. Или выполняться должно из исходного образа, при условии что find или его альтернативна в нем присутствует.
Запустим результирующий образ:
docker run --rm -it ubuntu_lean /bin/ls
Теперь все работает:
bin dev etc lib lib64 proc sys
Итог
ubuntu: 209MB
ubuntu_lean: 2.5MB
В результате мы получили образ, в 83.5 раз меньше. Это сжатие на 98.8%.
Послесловие
Как и все методы, основанные на профилировании, он в состоянии сказать, что в действительности сделано/использовалось в данном сценарии. К примеру, попробуйте выполнить /bin/ls -l в конечном образе и увидите всё сами.
Техника профилирования не без изъяна. Она не позволяет понять, как именно файл был открыт, только что это за файл. Это проблема для символьные ссылок, в частности cross-filesytems (читай cross-volumes). При помощи fanotify, мы потеряем оригинальную символическую ссылку и сломаем приложение.
Если бы мне пришлось построить такой «сжиматель», готовый для использования в продакшене, я скорее всего использовал бы ptrace.
Примечания
- Признаюсь, в действительности мне было интересно поэкспериментировать с системными вызовами. Образы Docker скорее хороший предлог;
- Вообще-то вполне можно было бы использовать FAN_UNLIMITED_QUEUE, вызывая fanotify_init для обхода этого ограничения, при условии, что вызывающий процесс по крайней мере CAP_SYS_ADMIN;
- Он так же в 2.4 раза меньше, чем образ на 6.13MB, который я упомянул в начале этой статьи, но сравнение не является справедливым.
Комментарии (44)
egorsmkv
28.05.2015 19:09А есть ли какая-нибудь утилита для очистки неиспользуемых мной программ в Linux? Например, я знаю, что буду использовать в docker-контейнере. Утилита тянет информацию с кэша пакетных менеджеров (секции «зависимости»), а все остальные приложения удаляет.
hardex
28.05.2015 19:25+5В основном это «catch-all» root возможность
Охладите уже углепластикsandricmora Автор
28.05.2015 19:46+2Честно, сам перечитывая думал может оставить как capability, но все таки решил перевести, тем более что в тут как раз «возможности» в русском man'е.
hardex
08.06.2015 18:25Универсальный capability, присущий пользователю root.
Не зная заранее о чем идет речь, распарсить "«catch-all» root возможность" нормальному человеку не представляется возможным.
frol
28.05.2015 20:03+2Интересный способ себя занять, это даже более геморно, чем страдания с Alpine образом (5МБ базовый образ — busybox + apk manager).
Для интересующихся: в Alpine используется musl libc, которому просто не хватает поддержки и некоторые вещи (компиляторы и другие крупные/старые проекты) очень сложно собирать, но пакетов уже достаточно много, так что не всё уж так плохо.
lexa0
28.05.2015 22:07+1Для того чтобы найти все все файлы который открывает процесс можно было использовать старый как мир strace.
strace -f -e trace=open /bin/lsfoxmuldercp
28.05.2015 23:01+1Просветите, открывает ли он реально ВСЕ файлы, или таки только то, что вот надо прям сейчас, и может, если понадобится, через часик откроет еще вон ту пачку библиотек?
lexa0
28.05.2015 23:05+1А это вы уже спросите у разработчика ПО который хотите окучивать, а не у меня.
mikhailov
29.05.2015 00:12По atime файлы отсеить и удалить вcе, что отфильтровано, не?
sply
29.05.2015 01:05Да, это самый простой способ. А если atime отсутствует, то есть strace и ftrace/trace-cmd, DTrace, SystemTap. Но тут автор не ищет легких путей :)
Хотя главная ошибка, что он ловит только open, а нужено еще и отслеживать stat, т.к. часто бывает проверки наличие файлов/каталогов, без того, чтобы открывать их.grossws
29.05.2015 01:07Это не убирает принципиального недостатка такого подхода. Приложение могло ни разу не обращаться к файлу, к которому обратится потом по внешнему запросу в ходе работы.
sply
29.05.2015 01:16Для докера с большими приложениями — да, не подходит принципиально. Но очень хорошо подходит для собирания initrd, практикой проверено.
grossws
29.05.2015 01:26Для initrd подходит с аналогичными ограничениями.
Вы получите минимальный образ, который будет работать в той же конфигурации. Например, если initrd выполняет определенные действия только, если выполняется специальное условие. Например, выполняет btrfs scrub только если есть точки монтирования btrfs в /etc/fstab. Или условное выполнения действия при наличии определенного устройства.
В общем, сводится к тому, есть ли реакция на «внешний» мир. Если его зафиксировать (например, собирая initrd под определенную конфигурацию), то этот подход нормален. Если не фиксировать — рано или поздно будет ошибка.
grossws
29.05.2015 01:05+1Проблема та же. Приложение могло за время работы не обратиться к файлу, соответственно atime останется прежним (в предположении, что он вообще включен).
Представьте, что у вас nginx, например, который раздаёт статику из /var/www. За время профилирования вы запросили не все файлы, которые были в /var/www, удалили те, к которым не было обращений. В итоге получается явное удаления части необходимых файлов.Zelgadis
29.05.2015 19:52-1файлы из в /var/www вероятно должен быть скопированы в образ с хост системы перед упаковкой…
grossws
29.05.2015 19:58+1Это пример. С тем же успехом это может быть какая-нибудь библиотека из /usr/lib.
Zelgadis
29.05.2015 20:01Это плохой пример. Прогоните системные тесты в приложении, если не все библиотеки были использованы — плохие тесты у вас.
grossws
29.05.2015 20:08Не ведаю, что за «системные тесты». Если вы имели ввиду unit/func/integration для приложения, то они обычно не включаются в итоговое приложение. Если же профилировать сборку с запуском тестов, то вы получите в списке «необходимых» для работы ещё систему сборки и всю тестовую машинерию.
Zelgadis
29.05.2015 20:14Тесты которые гоняют весь стэй без всяких mock'ов. Учитывая, что вероятно 90% докер контейнеров это уеб приложения. В чем сложность их запускать вне контейнера?
Что это за приложение такое которое не все подгружает при запуске? Я думал в продакшене мы загружаем все в память. Это раз.
Два. Что мешает включить мозг свой и сделать strace или просто знать, что libzaebis используется в приложении? Или Вы один из тех разработчиков которые не понимают их приложение работает?grossws
30.05.2015 18:17+2Учитывая, что вероятно 90% докер контейнеров это уеб приложения.
Откуда вы почерпнули 90%, а не, скажем, 40%? Мне неизвестна реальная статистика. Для них, конечно, стоит прогонять интеграционные + smoke тесты. Вопрос, как всегда, полноты покрытия.
Что это за приложение такое которое не все подгружает при запуске? Я думал в продакшене мы загружаем все в память.
Бывают модульные приложения. Бывает необходимость включить подсистему только тогда, когда загрузили определенный плагин/таск. Простейший пример из столь любимого вами «уеба» — appserver'а. Какой-нибудь JDBC-драйвер базы не будет загружен пока не будет настроен соответствующий jndi-ресурс (и иногда может требоваться, чтобы было задеплоено приложение, использующее этот ресурс).
Что мешает включить мозг свой и сделать strace или просто знать, что libzaebis используется в приложении? Или Вы один из тех разработчиков которые не понимают их приложение работает?
Не хамите.
Несколькими сообщениями выше я уже пытался объяснить, что имеется ввиду. Эта проблема сродни проблеме останова, для произвольного приложения невозможно заранее сказать к каким зависимостям обратится данное приложение в процессе работы, и strace/ptrace/atime может помочь только в частных случаях. Рабочий вариант — ориентироваться на то, что разработчики корректно описали зависимости.
flashvoid
29.05.2015 04:17+2Почти то же самое на примере nginx и в исполнении разработчиков докера — типа best practice.
ToSHiC
29.05.2015 11:14А можете рассказать, зачем вообще пытаться делать минимальный размер образа? Ведь фича образов докера в том, что они состоят из слоёв вплоть до запуска контейнера. Если использовать aufs/overlayfs, то тот самый жирный базовый слой будет смонтирован только один раз, и будет пошарен между всеми контейнерами. При этом он будет один раз в page cache, что тоже положительно скажется на скорости работы и потреблении памяти.
В вашем же случае каждый контейнер уникальный, профита от переиспользования базового образа никакого. То есть, если запустить 100 контейнеров, собранных вашим способом, и 100 контейнеров, не ужатых, но с overlayfs, то второй вариант победит по потреблению page cache, а места на диске потребует незначительно больше.frol
29.05.2015 19:33Позвольте мне ответить. В таком извращении, которым занимался автор (не переводчик), смысла я тоже не вижу, а вот в минимизации образа я вижу смысл. Только для минимизации образа лучше просто брать маленький базовый образ, например, сейчас мне очень нравится Alpine (5МБ, которые включают busybox и apk пакетный менеджер). То есть /bin/ls вы получаете ценой в 5МБ и тут уже не получится кричать, что образ ужался на 98% (он бы ещё Haskell образ (1ГБ, официальный образ между прочим...) взял ради /bin/ls).
Маленький образ — это меньшее количество уязвимостей. Я уже писал об этом здесь. Судя по всему, нужно дописать уже статью, а то по комментариям хожу проповедую :)
Кроме того, есть две большие разницы — базовый образ внутри компании, который все используют и ничего внешнего не приносят, и образы, которые выложены на Docker Hub, где каждый кто во что горазд (ubuntu, debian, google/debian, fedora, centos — собрать такой зоопарк, если не следить за тем что там авторы наваяли, не составит труда, а это уже 1ГБ, а потом они ещё обновляются кто во что горазд — наследуемые образы НЕ перестраиваются после обновления базового если этого явно не указать в настройках (мало кто указывает)). Таким образом 1ГБ базовых образов можно собрать за первый день и потом в течение месяца из-за обновлений одних образов и не обновлений других — можно новый 1ГБ собрать.
Главное заблуждение: официальные образы на Docker Hub — хороши. Да ничего подобного! Большинство образов на троечку собраны. Самые наглядные примеры:
- Python: мои образы основанные на Alpine (50МБ) VS официальный slim (216МБ, а «обычный» — 750МБ) — тыц
- Golang: мой образ (126МБ) VS официальный (517МБ) — тыц
(у меня ещё 7 образов есть с аналогичными сравнениями)ToSHiC
29.05.2015 20:13А я вас полностью поддерживаю :) Единственное что, 5МБ в качестве первого слоя не всегда круто, всё же нужен баланс между количеством слоёв и их объёмом. Конечно, тут уже нужно смотреть на свои образы и по ним изучать, что же можно назвать общей базой для бОльшей части из них.
frol
29.05.2015 20:21В том-то и дело, что, например, официальный Python образ страдает во многом из-за убогости выбранного базового образа — первая команда у них в Dockerfile:
apt-get purge python.*
, но это уже не вернёт утраченное место.
Да, найти баланс действительно сложно, Debian всё-таки «привычнее», все знают apt-get и слова поперёк не скажут если в репах не будет какого-то пакета или он будет старый, а вот в Alpine хоть и достаточно много пакетов, но если что-то экзотическое (например из последнего, haskell, которому для сборки себя нужен haskell), то musl libc станет поперёк горла, или просто проприетарные бинарники, которые нужно заталкивать через установку glibc. Но я пока держусь на Alpine и только для особо замороченных случаев (Hadoop, FreeIPA) приходится использовать тяжёлую артиллерию.
sandricmora Автор
30.05.2015 09:08Да, конечно, минимизация образов абсолютно необходима, большинство из них необоснованно огромны как по мне, но использовать для этого busybox или alpine — допустим вам необходим образ ИМЕННО с федорой или дебианом определенной версии, под которой должно запускаться приложение в контейнере, и любой другой базовый образ системы вам не подходит? Конечно, можно попытаться привести alpine/busybox к необходимым зависимостям, но время, которое на это потратится скомпрометирует саму идею смысла перехода на контейнеры для dev-ops тим.
В этом смысле мне пока нравиться подход Kelsey Hightower из coreos — изначально используется не busybox, а debian, и используется он внутри компании, так что остальные члены комманды могут добавлять в него свои слои. Но на этапе деплоя, контейнер передается в ci, который каким то образом его ужимает — в примере, так как ничего другого не нужно было, просто копировалась конечная папка с go бинарниками в новый, «продакшн» образ с дебианом как основной системой.
То есть ясно, когда можнo использовать busybox/scratch/alpine то использовать стоит их, но когда это уж очень сложно и тянет кучу зависимостей — думаю больший смысл имеет использовать большой образ внутри, но ужать его при деплое. И я думаю такие механизмы «ужатия» будут появляться в будущем.
Поэтому я не вижу будущего за отдельными образами (busybox/scratch/alpine), а скорее за самим докером или скорее за rkt, как унифицированной спецификацией контейнеров, где этот механизм можно будет реализовать.
Просто пока, такой единственный механизм я увидел только в этой статье, при помощи профилирования, которая ясно подходит далеко не для всего, но на сколько я знаю пока единственная, которую можно автоматизировать переведя в функцию ci сервера.frol
30.05.2015 09:34+1Внутри компании можно хоть 1ГБ образ использовать, лишь бы все от него наследовались и обновлялись вместе с базовым. Вот на Docker Hub заливать стоит, как мне кажется, только с наследованием от debian (официальный, а не один из 100500 кастомных) или минимальных бразов (scratch, busybox, progrium/busybox, alpine, cirros). К другим базовым образам нужно подходить с полным осознанием, что на debian это не взлетит (FreeIPA, например, имеет только rpm пакеты и все скрипты рассчитаны на Red Hat/CentOS/Fedora и проект достаточно большой) или вам нужна конкретная версия Python, например, тогда целесообразно использовать
python:3.2.1-slim
.
Да, иногда трудно с Alpine, но я уже спокойно с ним уживаюсь и мне интересно развитие musl libc. У меня есть целый проект, в котором я использую только образы наследованные от Alpine и проект работает замечательно. Всё-таки Alpine помогает иногда понять что на самом деле у моего приложения за зависимости, нужен ли мне bash и тд. Опять же, бывают случаи, например, в Python такое часто, когда модули имеют опциональные зависимости, но в приложении эти функции не используются, но модули будут пробовать подключать эти опциональные зависимости и они будут включены в финальную сборку если пользоваться методом описанным в статье, а в случае минимальных базовых образов такого не случится.
frol
29.05.2015 19:41+2Ещё один нюанс — Over 30% of Official Images in Docker Hub Contain High Priority Security Vulnerabilities. То есть базовые образы таки надо обновлять и обновлять регулярно! Долгое время в Ubuntu образе был shellock, который каждый исправлял уже в своём образе через apt-get dist-upgrade, что приводило к ещё большему разбуханию образа.
JIghtuse
29.05.2015 18:56Забавный эксперимент, хоть и неудачный. Было интересно почитать.
По fanotify обещал написать статью Michael Kerrisk, да что-то всё никак не возьмётся. У него на LWN была серия статей по механизмам уведомлений файловой системы: dnotify, inotify. Хорошо, что хотя бы в manpage есть пример использования.
sandricmora Автор
29.05.2015 19:08Спасибо за ссылки, открыл для себя нового автора). Я вот как то наоборот — слышал про inotify и fanotify, но не слышал про dnotify. Вообще есть еще забавная, правда давно заброшенная loggedfs, а если уж совсем мониторить по полной — то можно и auditd в ядре включить.
Но только почему эксперимент неудачный? То есть понятно, что это вариант далеко не для всех задач — об этом и выше люди писали, а скорее для полностью детерминированного выполнения, и перевел я его просто потому что он показался мне остроумным, но в чем конкретно неудача?sandricmora Автор
29.05.2015 19:14Черт, это должен был быть ответ пользователю JIghtuse, я до этого не сталкивался с комментариями на хабре — ни удалить, ни отредактировать после истечения какого то времени.
JIghtuse
29.05.2015 19:18Автор хороший, мэйнтэйнер manpages =) есть у него книга отличная — The Linux Programming Interface. Многие бывалые рекомендуют.
Неудачный — я к тому, что с натяжкой работает даже для ls. То есть польза от эксперимента определённо есть — приобретённые знания и опыт, но едва ли его можно применять где-то. Сомневаюсь в существовании детерминированного выполнения, посмотрите хотя бы на количество флагов ls. Кто знает, к каким файлам он обращается при вызове каждого. Что уж говорить о более сложных инструментах.
Только сейчас заметил, что это перевод — обычно метка висит. Хорошая работа, корявостей в языке не заметил.sandricmora Автор
29.05.2015 20:23+1Метки нет потому что статья из песочницы, насколько я понял там такую метку установить нельзя. Спасибо, старался)
Jeditobe
Не совсем понятно. Решение все-таки рабочее или нет?
middle
Решение не идеальное ;)
grossws
Нет, работает только в частных случаях (когда приложение за время профилирования, как минимум обратилось ко всем зависимостям, которые будут использоваться в production).