Предлагаю читателям «Хабрахабра» перевод публикации «How I shrunk a Docker image by 98.8% – featuring fanotify».

Несколько недель назад я делал внутренний доклад о 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 слушателя о потере сообщения. Это именно то, что нам нужно. Ненавязчивый, вся точка монтирование за раз, прост в настройке (ну, исходя из того что вы найдете документацию конечно…). Это может показаться смешным, но это действительно важно, как я узнал позже.

В использовании он очень прост


  1. Инициализируем 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);

  2. Подписываемся на 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)
    );

  3. Считываем сообщения из файлового дескриптора, который мы получили от 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.

Примечания


  1. Признаюсь, в действительности мне было интересно поэкспериментировать с системными вызовами. Образы Docker скорее хороший предлог;
  2. Вообще-то вполне можно было бы использовать FAN_UNLIMITED_QUEUE, вызывая fanotify_init для обхода этого ограничения, при условии, что вызывающий процесс по крайней мере CAP_SYS_ADMIN;
  3. Он так же в 2.4 раза меньше, чем образ на 6.13MB, который я упомянул в начале этой статьи, но сравнение не является справедливым.

Комментарии (44)


  1. Jeditobe
    28.05.2015 16:58

    Не совсем понятно. Решение все-таки рабочее или нет?


    1. middle
      28.05.2015 17:12
      +1

      Решение не идеальное ;)


    1. grossws
      28.05.2015 17:56
      +4

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


  1. egorsmkv
    28.05.2015 19:09

    А есть ли какая-нибудь утилита для очистки неиспользуемых мной программ в Linux? Например, я знаю, что буду использовать в docker-контейнере. Утилита тянет информацию с кэша пакетных менеджеров (секции «зависимости»), а все остальные приложения удаляет.


    1. egorsmkv
      28.05.2015 19:15

      Неужели только «apt-get autoremove» единым?


    1. hmage
      30.05.2015 00:32

      deborphan --guess-all -n


  1. hardex
    28.05.2015 19:25
    +5

    В основном это «catch-all» root возможность

    Охладите уже углепластик


    1. sandricmora Автор
      28.05.2015 19:46
      +2

      Честно, сам перечитывая думал может оставить как capability, но все таки решил перевести, тем более что в тут как раз «возможности» в русском man'е.


      1. hardex
        08.06.2015 18:25

        Универсальный capability, присущий пользователю root.
        Не зная заранее о чем идет речь, распарсить "«catch-all» root возможность" нормальному человеку не представляется возможным.


  1. frol
    28.05.2015 20:03
    +2

    Интересный способ себя занять, это даже более геморно, чем страдания с Alpine образом (5МБ базовый образ — busybox + apk manager).

    Для интересующихся: в Alpine используется musl libc, которому просто не хватает поддержки и некоторые вещи (компиляторы и другие крупные/старые проекты) очень сложно собирать, но пакетов уже достаточно много, так что не всё уж так плохо.


  1. evg_krsk
    28.05.2015 20:28
    +2

    Отличный подход, сначала накидаем неизвестно чего, потом будем «сжимать», потом в продакт. Мне нравится.


    1. amarao
      28.05.2015 21:04
      +9

      Это новый путь. Чем меньше думаешь о безопасности и продакшене, тем выше скорость разработки.


  1. lexa0
    28.05.2015 22:07
    +1

    Для того чтобы найти все все файлы который открывает процесс можно было использовать старый как мир strace.

    strace -f -e trace=open /bin/ls


    1. foxmuldercp
      28.05.2015 23:01
      +1

      Просветите, открывает ли он реально ВСЕ файлы, или таки только то, что вот надо прям сейчас, и может, если понадобится, через часик откроет еще вон ту пачку библиотек?


      1. lexa0
        28.05.2015 23:05
        +1

        А это вы уже спросите у разработчика ПО который хотите окучивать, а не у меня.


    1. naum
      28.05.2015 23:03

      strace будет мониторить файлы открытые дочерними процессами? простите, в устройства *nix не силен и вопрос может звучать глупо.


      1. lexa0
        28.05.2015 23:04
        +4

        с ключиком -f будет мониторить и дочерние процессы.


  1. mikhailov
    29.05.2015 00:12

    По atime файлы отсеить и удалить вcе, что отфильтровано, не?


    1. sply
      29.05.2015 01:05

      Да, это самый простой способ. А если atime отсутствует, то есть strace и ftrace/trace-cmd, DTrace, SystemTap. Но тут автор не ищет легких путей :)

      Хотя главная ошибка, что он ловит только open, а нужено еще и отслеживать stat, т.к. часто бывает проверки наличие файлов/каталогов, без того, чтобы открывать их.


      1. grossws
        29.05.2015 01:07

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


        1. sply
          29.05.2015 01:16

          Для докера с большими приложениями — да, не подходит принципиально. Но очень хорошо подходит для собирания initrd, практикой проверено.


          1. grossws
            29.05.2015 01:26

            Для initrd подходит с аналогичными ограничениями.

            Вы получите минимальный образ, который будет работать в той же конфигурации. Например, если initrd выполняет определенные действия только, если выполняется специальное условие. Например, выполняет btrfs scrub только если есть точки монтирования btrfs в /etc/fstab. Или условное выполнения действия при наличии определенного устройства.

            В общем, сводится к тому, есть ли реакция на «внешний» мир. Если его зафиксировать (например, собирая initrd под определенную конфигурацию), то этот подход нормален. Если не фиксировать — рано или поздно будет ошибка.


    1. grossws
      29.05.2015 01:05
      +1

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

      Представьте, что у вас nginx, например, который раздаёт статику из /var/www. За время профилирования вы запросили не все файлы, которые были в /var/www, удалили те, к которым не было обращений. В итоге получается явное удаления части необходимых файлов.


      1. Zelgadis
        29.05.2015 19:52
        -1

        файлы из в /var/www вероятно должен быть скопированы в образ с хост системы перед упаковкой…


        1. grossws
          29.05.2015 19:58
          +1

          Это пример. С тем же успехом это может быть какая-нибудь библиотека из /usr/lib.


          1. Zelgadis
            29.05.2015 20:01

            Это плохой пример. Прогоните системные тесты в приложении, если не все библиотеки были использованы — плохие тесты у вас.


            1. grossws
              29.05.2015 20:08

              Не ведаю, что за «системные тесты». Если вы имели ввиду unit/func/integration для приложения, то они обычно не включаются в итоговое приложение. Если же профилировать сборку с запуском тестов, то вы получите в списке «необходимых» для работы ещё систему сборки и всю тестовую машинерию.


              1. Zelgadis
                29.05.2015 20:14

                Тесты которые гоняют весь стэй без всяких mock'ов. Учитывая, что вероятно 90% докер контейнеров это уеб приложения. В чем сложность их запускать вне контейнера?

                Что это за приложение такое которое не все подгружает при запуске? Я думал в продакшене мы загружаем все в память. Это раз.
                Два. Что мешает включить мозг свой и сделать strace или просто знать, что libzaebis используется в приложении? Или Вы один из тех разработчиков которые не понимают их приложение работает?


                1. grossws
                  30.05.2015 18:17
                  +2

                  Учитывая, что вероятно 90% докер контейнеров это уеб приложения.
                  Откуда вы почерпнули 90%, а не, скажем, 40%? Мне неизвестна реальная статистика. Для них, конечно, стоит прогонять интеграционные + smoke тесты. Вопрос, как всегда, полноты покрытия.

                  Что это за приложение такое которое не все подгружает при запуске? Я думал в продакшене мы загружаем все в память.
                  Бывают модульные приложения. Бывает необходимость включить подсистему только тогда, когда загрузили определенный плагин/таск. Простейший пример из столь любимого вами «уеба» — appserver'а. Какой-нибудь JDBC-драйвер базы не будет загружен пока не будет настроен соответствующий jndi-ресурс (и иногда может требоваться, чтобы было задеплоено приложение, использующее этот ресурс).

                  Что мешает включить мозг свой и сделать strace или просто знать, что libzaebis используется в приложении? Или Вы один из тех разработчиков которые не понимают их приложение работает?
                  Не хамите.

                  Несколькими сообщениями выше я уже пытался объяснить, что имеется ввиду. Эта проблема сродни проблеме останова, для произвольного приложения невозможно заранее сказать к каким зависимостям обратится данное приложение в процессе работы, и strace/ptrace/atime может помочь только в частных случаях. Рабочий вариант — ориентироваться на то, что разработчики корректно описали зависимости.


  1. flashvoid
    29.05.2015 04:17
    +2

    Почти то же самое на примере nginx и в исполнении разработчиков докера — типа best practice.


  1. ToSHiC
    29.05.2015 11:14

    А можете рассказать, зачем вообще пытаться делать минимальный размер образа? Ведь фича образов докера в том, что они состоят из слоёв вплоть до запуска контейнера. Если использовать aufs/overlayfs, то тот самый жирный базовый слой будет смонтирован только один раз, и будет пошарен между всеми контейнерами. При этом он будет один раз в page cache, что тоже положительно скажется на скорости работы и потреблении памяти.

    В вашем же случае каждый контейнер уникальный, профита от переиспользования базового образа никакого. То есть, если запустить 100 контейнеров, собранных вашим способом, и 100 контейнеров, не ужатых, но с overlayfs, то второй вариант победит по потреблению page cache, а места на диске потребует незначительно больше.


    1. 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 образов есть с аналогичными сравнениями)


      1. ToSHiC
        29.05.2015 20:13

        А я вас полностью поддерживаю :) Единственное что, 5МБ в качестве первого слоя не всегда круто, всё же нужен баланс между количеством слоёв и их объёмом. Конечно, тут уже нужно смотреть на свои образы и по ним изучать, что же можно назвать общей базой для бОльшей части из них.


        1. frol
          29.05.2015 20:21

          В том-то и дело, что, например, официальный Python образ страдает во многом из-за убогости выбранного базового образа — первая команда у них в Dockerfile: apt-get purge python.*, но это уже не вернёт утраченное место.

          Да, найти баланс действительно сложно, Debian всё-таки «привычнее», все знают apt-get и слова поперёк не скажут если в репах не будет какого-то пакета или он будет старый, а вот в Alpine хоть и достаточно много пакетов, но если что-то экзотическое (например из последнего, haskell, которому для сборки себя нужен haskell), то musl libc станет поперёк горла, или просто проприетарные бинарники, которые нужно заталкивать через установку glibc. Но я пока держусь на Alpine и только для особо замороченных случаев (Hadoop, FreeIPA) приходится использовать тяжёлую артиллерию.


      1. 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 сервера.


        1. 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 такое часто, когда модули имеют опциональные зависимости, но в приложении эти функции не используются, но модули будут пробовать подключать эти опциональные зависимости и они будут включены в финальную сборку если пользоваться методом описанным в статье, а в случае минимальных базовых образов такого не случится.


    1. 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, что приводило к ещё большему разбуханию образа.


  1. JIghtuse
    29.05.2015 18:56

    Забавный эксперимент, хоть и неудачный. Было интересно почитать.
    По fanotify обещал написать статью Michael Kerrisk, да что-то всё никак не возьмётся. У него на LWN была серия статей по механизмам уведомлений файловой системы: dnotify, inotify. Хорошо, что хотя бы в manpage есть пример использования.


  1. sandricmora Автор
    29.05.2015 19:08

    Спасибо за ссылки, открыл для себя нового автора). Я вот как то наоборот — слышал про inotify и fanotify, но не слышал про dnotify. Вообще есть еще забавная, правда давно заброшенная loggedfs, а если уж совсем мониторить по полной — то можно и auditd в ядре включить.
    Но только почему эксперимент неудачный? То есть понятно, что это вариант далеко не для всех задач — об этом и выше люди писали, а скорее для полностью детерминированного выполнения, и перевел я его просто потому что он показался мне остроумным, но в чем конкретно неудача?


    1. sandricmora Автор
      29.05.2015 19:14

      Черт, это должен был быть ответ пользователю JIghtuse, я до этого не сталкивался с комментариями на хабре — ни удалить, ни отредактировать после истечения какого то времени.


    1. JIghtuse
      29.05.2015 19:18

      Автор хороший, мэйнтэйнер manpages =) есть у него книга отличная — The Linux Programming Interface. Многие бывалые рекомендуют.

      Неудачный — я к тому, что с натяжкой работает даже для ls. То есть польза от эксперимента определённо есть — приобретённые знания и опыт, но едва ли его можно применять где-то. Сомневаюсь в существовании детерминированного выполнения, посмотрите хотя бы на количество флагов ls. Кто знает, к каким файлам он обращается при вызове каждого. Что уж говорить о более сложных инструментах.

      Только сейчас заметил, что это перевод — обычно метка висит. Хорошая работа, корявостей в языке не заметил.


      1. sandricmora Автор
        29.05.2015 20:23
        +1

        Метки нет потому что статья из песочницы, насколько я понял там такую метку установить нельзя. Спасибо, старался)


  1. inook
    31.05.2015 21:56

    Присмотритесь в сторону Rocket Container!


    1. farcaller
      31.05.2015 23:21

      А как rkt решает эту проблему?