Программист — это звучит гордо
Если пройтись по образам на docker hub, то подавляющее большинство окажется построенными на базе Ubuntu и Debian. Хотя уже встречаются робкие попытки, в официальных сборках популярных пакетов, выкладывать альтернативные образы на базе Alpine linux. Но в общей массе это капля в море. А между тем Alpine подходит для базового образа куда лучше, чем такие знакомые и родные Debian с Ubuntu.
Размер имеет значение
Если сравнивать размеры этих трёх дистрибутивов, то получим вот такую картину:
Дистрибутив | Размер | % от ubuntu:14.04 |
---|---|---|
ubuntu:14.04 | 187.9 MB | 100. % |
ubuntu:16.04 | 126.6 MB | 67.4 % |
debian:wheezy | 84.91 MB | 45.1 % |
debian:jessie | 125.1 MB | 66.5 % |
alpine:3.3 | 4.793 MB | 2.55 % |
alpine:3.4 | 4.795 MB | 2.55 % |
Представители Ubuntu осознавая, что разница в 40 раз вызывает вопросы, оправдываются. Один из основных аргументов в том, что у автора канал хороший и поэтому образ скачивается быстро, остаётся только порадоваться за него. Второй аргумент в том, что базовый образ мол скачивается один раз и благодаря особенностям файловой системы Docker линкуется в остальные. Скажем честно — аргумент лукавый, по опыту, если скачать десяток произвольных контейнеров, есть вероятность собрать на диске всю коллекцию версий Ubuntu и Debian.
Набор софта
У меня наиболее часто используемые операции в базовом образе это: скачать (wget/curl), разархивировать (unzip/tar), отредактировать (nano/vi) и посмотреть (less). Судя по размеру, alpine должен быть лишён всех этих базовых полезных возможностей. Однако реальность удивляет:
Дистрибутив | wget/curl | unzip | nano/vi | less |
---|---|---|---|---|
ubuntu:14.04 | — | — | vi | + |
ubuntu:16.04 | — | — | — | — |
debian:wheezy | — | — | — | — |
debian:jessie | — | — | — | — |
alpine:3.3 | wget | + | vi | + |
alpine:3.4 | wget | + | vi | + |
А что же тогда хранят образы Ubuntu и Debian? Вот набор наибольших по размеру вещей (в зависимости от версий набор варьируется): systemd, udev, python3+python2, bash и т.п. Уж конечно без этого мы собирая образ обойтись никак не могли.
Менеджер пакетов
У Alpine есть легковесный apk, с кучей свежих пакетов на все случаи жизни. Хотя тут он убунтовскомим репозиториям по количеству софта уступает. Но зато под реалии docker’а подходит куда лучше. Давайте разберём типичную проблему: в Dockerfile мы ставим пакет, что-то с его помощью делаем и удаляем, что бы не болтался и не раздувал размер образа. Конкретный пакет не важен, пусть это будет «zip»:
apk add --no-cache zip apk del zip
Размер образа до выполнения скрипта: 4.795 MB, после — 4.813 MB, разница 18 KB. Накладные расходы есть, но терпимо, ключ «–no-cache» помог не выкачивать индекс пакетов, а воспользоваться тем, что лежит в сети.
Теперь то же для apt (ubuntu:16.04), тут обойтись без обновления индекса у меня не вышло:
apt-get update apt-get install zip apt-get remove zip
Размер образа после этого изменился с 126 MB до 166 MB, разница в 40 MB! Неплохой «штраф» за то, что пакетом попользовался 5 сек. Вычистить конечно мусор вручную можно, но повозиться прийдётся не слабо и всё равно разницу в 18 KB вы врядли получите.
Система инициализации
По умолчанию используется гентушный OpenRC, а у конкурентов в последних версиях горячо любимая многими systemd. Не подумайте, что я недолюбливаю творение Поттеринга, напротив по моему, от того, что во всех дистрибутивах будет единообразный способ инициализации, все только выиграют. Но для контейнеров это перебор, уж очень сложно и неудобно там делать многие вещи. Да и по моему никто внутри докера systemd и не использует, не видел ни разу. OpenRC меня кстати тоже не впечатлил, он писался не для docker и не помогает решать специфические задачи, которые возникают в контейнерах.
- «Повесить» набор скриптов на старт и остановку контейнера.
- Декларативно описать назначение прав на директории и файлы, вместо беспорядочных chmod и chown в разных местах.
- Для каждого сервиса можно написать скрипт запуска под нужным пользователем, и скрипт, который будет выполняться при завершении контейнера.
- Ну и конечно единообразное логирование каждого шага.
Документация
В целом проблем с Alpine не возникает, в основном из-за простоты системы, но если что — всегда на помощь прийдёт wiki, по наполнению она конечно поменьше знаменитой archwiki, но решение большинства проблем в ней есть. Проблемы с установкой ПО, проще всего решаются поиском на docker hub, с большой вероятностью кто-то уже написал нужный Dockerfile, где можно подсмотреть решение. Так же существует forum и судя по всему достаточно активный, но настолько сложных проблем, что бы туда написать у меня ещё не возникало.
Самый маленький Docker-образ — меньше 1000 байт
Прим. перев.: Автор этого материала — архитектор в Barclays и Open Source-энтузиаст из Великобритании Ian Miell. Он задаётся целью сделать удобный образ Docker (со «спящим» бинарником), который не нужно скачивать, а достаточно просто копировать через copy & paste. Методом проб, ошибок и экспериментов с Assembler-кодом он достигает цели, подготовив образ размером менее килобайта.
Как я к этому пришёл?
Однажды коллега показал Docker-образ, который он использовал для тестирования кластеров Kubernetes. Он ничего не делал: просто запускал под и ждал, пока вы его убьёте.
И тут мне стало любопытно, какой же минимальный образ Docker я смогу создать. Хотелось получить такой, что можно было бы закодировать в base64 и отправлять буквально куда угодно простым copy & paste. Поскольку Docker-образ — это просто tar-файл, а tar-файл — это всего лишь файл, всё должно получиться.
Крохотный бинарник
В первую очередь мне был нужен очень маленький Linux-бинарник, которые ничего не делает. Потребуется немного волшебства — и вот две замечательные, содержательные и достойные прочтения статьи о создании маленьких исполняемых файлов:
SECTION .data msg: db "Hi World",10 len: equ $-msg SECTION .text global _start _start: mov edx,len mov ecx,msg mov ebx,1 mov eax,4 int 0x80 mov ebx,0 mov eax,1 int 0x80
nasm -f elf64 hw.asm -o hw.o ld hw.o -o hw strip -s hw
Получается бинарник в 504 байта.
Но всё-таки нужен не «Hello World»… Во-первых, я выяснил, что излишни секции .data или .text и не требуется загрузка данных. Вдобавок, верхняя половина секции _start занимается выводом текста. В итоге, я попробовал следующий код:
global _start _start: mov ebx,0 mov eax,1 int 0x80
И он скомпилировался уже в 352 байта.
Но это ещё не искомый результат, потому что программа просто завершает свою работу, а нам нужно, чтобы она спала. В результате дополнительных исследований выяснилось, что команда mov eax заполняет регистр процессора соответствующим номером системного вызова Linux, а int 0x80 производит сам вызов. Подробнее это описано здесь.
А здесь я нашёл нужный список. Syscall 1 — это exit , а нужный нам — это syscall 29:pause . Получилась такая программа:
global _start _start: mov eax, 29 int 0x80
Мы сэкономили ещё 8 байтов: компиляция выдала результат в 344 байта, и теперь это подходящий нам бинарник, который ничего не делает и ожидает сигнала.
Копаясь в hex’ах
Настало время достать бензопилу и разобраться с бинарником… Для этого я использовал hexer, который по сути vim для бинарных файлов с возможностью прямого редактирования hex’ов. После продолжительных экспериментов я получил из такого:
Данный код делает то же самое, но обратите внимание, сколько строк и пробелов ушло. В процессе своей работы я руководствовался таким документом, но по большому счёту это был путь проб и ошибок.
Итак, размер уменьшился до 136 байт.
Меньше 100 байт?
Хотелось узнать, можно ли пойти дальше. Прочитав это, я предположил, что получится дойти до 45 байт, однако — увы! — нет. Описанные там фокусы рассчитаны только на 32-битные бинарники, а для 64-битных не проходили.
Лучшее же, что мне удалось, — взять эту 64-битную версию программы и встроить в свой системный вызов:
BITS 64 org 0x400000 ehdr: ; Elf64_Ehdr db 0x7f, "ELF", 2, 1, 1, 0 ; e_ident times 8 db 0 dw 2 ; e_type dw 0x3e ; e_machine dd 1 ; e_version dq _start ; e_entry dq phdr - $$ ; e_phoff dq 0 ; e_shoff dd 0 ; e_flags dw ehdrsize ; e_ehsize dw phdrsize ; e_phentsize dw 1 ; e_phnum dw 0 ; e_shentsize dw 0 ; e_shnum dw 0 ; e_shstrndx ehdrsize equ $ - ehdr phdr: ; Elf64_Phdr dd 1 ; p_type dd 5 ; p_flags dq 0 ; p_offset dq $$ ; p_vaddr dq $$ ; p_paddr dq filesize ; p_filesz dq filesize ; p_memsz dq 0x1000 ; p_align phdrsize equ $ - phdr _start: mov eax, 29 int 0x80 filesize equ $ - $$
Результирующий образ — 127 байт. На этом я прекратил попытки уменьшать размер, но принимаю предложения.
Крохотный Docker-образ
Теперь, когда есть бинарник, реализующий бесконечное ожидание, остаётся положить его в Docker-образ.
Чтобы сэкономить каждый возможный байт, я создал бинарник с файловым именем из одного байта — t — и поместил его в Dockerfile , создавая практически пустой образ:
Обратите внимание, что в Dockerfile нет CMD , поскольку это увеличило бы размер образа. Для запуска понадобится передавать команду через аргументы к docker run .
Далее командой docker save был создан tar-файл, а затем — сжат с максимальной компрессией gzip. Получился портируемый файл Docker-образа размером менее 1000 байт:
$ docker build -t t . $ docker save t | gzip -9 - | wc -c 976
Ещё я попытался уменьшить размер tar-файла, экспериментируя с manifest-файлом Docker, но тщетно: из-за специфики формата tar и алгоритма сжатия gzip такие изменения приводили только к росту финального gzip’а. Пробовал и другие алгоритмы компрессии, но gzip оказался лучшим для этого маленького файла.
P.S. от переводчика
Читайте также в нашем блоге: