Оригинальная ссылка:Docker Images : Part I - Reducing Image Size
Людей, которые плохо знакомы с контейнерами, легко пугает размер образа Docker, который они создают.Мне нужен только исполняемый файл размером в несколько МБ.Почему размер образа достигает1 GB
выше? В этой статье будут представлены несколько приемов и приемов, которые помогут вам упростить ваши изображения, не жертвуя при этом простотой работы для разработчиков и операторов. Эта серия статей будет разделена на три части:
Первая часть посвящена многоступенчатой сборке, так как это важная часть пути к уменьшению изображения. В этом разделе я объясню разницу между статической и динамической компоновкой, их влияние на зеркалирование и способы избежать этих негативных последствий. Некоторые пары будут перемежаться посерединеAlpine
Знакомство с зеркалированием.
Во второй части будет выбрана подходящая стратегия сокращения для разных языков, в которой в основном обсуждаютсяGo
, также включаетJava
,Node
,Python
,Ruby
иRust
. В этом разделе также будет подробно описано руководство по предотвращению ям для изображений Alpine. Какие? Ты не знаешьAlpine
Какие ямки в зеркале? Я вам скажу.
В части 3 будут рассмотрены общие стратегии прореживания, применимые к большинству языков и фреймворков, такие как использование общих базовых образов, извлечение исполняемых файлов и уменьшение размера каждого слоя. Также вводятся некоторые более экзотические или радикальные инструменты, такие какBazel
,Distroless
,DockerSlim
иUPX
, хотя эти инструменты могут творить чудеса в некоторых конкретных сценариях, в большинстве случаев они могут привести к обратным результатам.
В этой статье представлена первая часть.
1. Корень всех зол
Бьюсь об заклад, каждый, кто впервые создает образ Docker с собственным кодом, будет напуган размером образа, давайте посмотрим на пример.
Давайте отойдем от этого испытанного и истинногоhello world
Программа С:
/* hello.c */
int main () {
puts("Hello, world!");
return 0;
}
И соберите образ со следующим Dockerfile:
FROM gcc
COPY hello.c .
RUN gcc -o hello hello.c
CMD ["./hello"]
Затем вы обнаружите, что размер успешно построенного образа намного больше, чем1 GB
. . . потому что изображение содержит весьgcc
Зеркальный контент.
При использованииUbuntu
image, установите компилятор C и, наконец, скомпилируйте программу, и вы получите примерно300 MB
Размер зеркала, намного меньше, чем зеркало выше. Но все же недостаточно мал, потому что скомпилированный исполняемый файл еще не20 KB
:
$ ls -l hello
-rwxr-xr-x 1 root root 16384 Nov 18 14:36 hello
Точно так же языковая версия Gohello world
получит тот же результат:
package main
import "fmt"
func main () {
fmt.Println("Hello, world!")
}
Использовать базовое изображениеgolang
Размер построенного образа800 MB
, в то время как скомпилированный исполняемый файл имеет только2 MB
размер:
$ ls -l hello
-rwxr-xr-x 1 root root 2008801 Jan 15 16:41 hello
Все еще не идеально, есть ли способ сильно уменьшить размер зеркального изображения? Посмотрите вниз.
Для более интуитивного сравнения размеров разных изображений все изображения используют одно и то же имя изображения и разные метки. Например:hello:gcc
,hello:ubuntu
,hello:thisweirdtrick
и так далее, чтобы вы могли использовать команду напрямуюdocker images hello
Перечислите все зеркала, чьи зеркала названы привет, не беспокоясь о других зеркалах.
2. Многоступенчатая сборка
Чтобы резко уменьшить размер изображения, необходима многоэтапная сборка. Идея многоэтапной сборки проста: «Я не хочу включать в окончательный образ кучу компиляторов C или Go и всю цепочку инструментов сборки, я просто хочу скомпилированный исполняемый файл!»
Многоэтапные сборки могут состоять из несколькихFROM
идентификация инструкции, каждаяFROM
указывает новую фазу сборки, имя фазы можно использовать сAS
спецификация параметра, например:
FROM gcc AS mybuildstage
COPY hello.c .
RUN gcc -o hello hello.c
FROM ubuntu
COPY --from=mybuildstage hello .
CMD ["./hello"]
В этом примере используется базовое изображениеgcc
скомпилировать программуhello.c
, затем начинается новая фаза сборки, которая начинается сubuntu
В качестве базового образа исполняемый файлhello
Скопируйте с предыдущего этапа на финальное изображение. Окончательный размер изображения64 MB
, чем предыдущий1.1 GB
уменьшился95%
:
🐳 → docker images minimage
REPOSITORY TAG ... SIZE
minimage hello-c.gcc ... 1.14GB
minimage hello-c.gcc.ubuntu ... 64.2MB
Можем ли мы продолжить оптимизацию? Конечно. Прежде чем продолжить оптимизацию, напоминание:
При объявлении этапа сборки не обязательно использовать ключевые словаAS
, вы можете напрямую использовать серийный номер, чтобы указать предыдущий этап сборки (начиная с нуля) при копировании файлов на последнем этапе. То есть следующие две строки эквивалентны:
COPY --from=mybuildstage hello .
COPY --from=0 hello .
еслиDockerfile
Содержание не очень сложное, и этапов строительства не так много.Вы можете напрямую использовать серийный номер, чтобы указать этап строительства. Как только Dockerfile становится сложным, а этапы сборки увеличиваются, лучше передать ключевые словаAS
Назовите каждый этап, что также облегчает последующее обслуживание.
Используйте классический базовый образ
Я настоятельно рекомендую использовать классический базовый образ для первого этапа сборки, где классический образ относится кCentOS
,Debian
,Fedora
иUbuntu
зеркала вроде. Возможно, вы также слышали о зеркалировании Alpine, не используйте его! Не пользуйтесь им, по крайней мере, пока, я вам потом расскажу, какие там ямы.
COPY --from
использовать абсолютный путь
При копировании файлов с предыдущего этапа сборки используется путь относительно предыдущего этапа.Корневая директорияиз. если вы используетеgolang
Аналогичные проблемы возникают, когда образ используется в качестве базового на этапе сборки. Предположим, что для сборки образа используется следующий Dockerfile:
FROM golang
COPY hello.go .
RUN go build hello.go
FROM ubuntu
COPY --from=0 hello .
CMD ["./hello"]
Вы увидите такую ошибку:
COPY failed: stat /var/lib/docker/overlay2/1be...868/merged/hello: no such file or directory
Это потому чтоCOPY
Команда хочет скопировать/hello
,иgolang
зеркальныйWORKDIR
да/go
, поэтому реальный путь к исполняемому файлу/go/hello
.
Конечно, вы можете использовать абсолютные пути для решения этой проблемы, но если базовое изображение изменится позжеWORKDIR
что делать? Вы также должны постоянно изменять абсолютный путь, поэтому это решение все еще не очень элегантно. Лучше всего указать на первом этапеWORKDIR
, копировать файлы с использованием абсолютных путей на втором этапе, чтобы даже при изменении базового образаWORKDIR
, это не повлияет на сборку образа. Например:
FROM golang
WORKDIR /src
COPY hello.go .
RUN go build hello.go
FROM ubuntu
COPY --from=0 /src/hello .
CMD ["./hello"]
Окончательный эффект по-прежнему потрясающий, объем зеркала напрямую изменяется от800 MB
сокращено до66 MB
:
🐳 → docker images minimage
REPOSITORY TAG ... SIZE
minimage hello-go.golang ... 805MB
minimage hello-go.golang.ubuntu-workdir ... 66.2MB
3. Магия С нуля
вернуться к нашемуhello world
, размер программы версии на языке C составляет16 kB
, размер программы языковой версии Go составляет2 MB
, то можем ли мы уменьшить изображение до такого маленького размера? Могу ли я собрать образ, содержащий только нужные мне программы, без лишних файлов?
Ответ — да, вам просто нужно изменить базовый образ второго этапа многоэтапной сборки наscratch
Достаточно.scratch
Это виртуальный образ, и его нельзя извлечь или запустить, потому что он означает пустой, ничего! Это означает, что новые изображения создаются с нуля, без каких-либо других слоев изображения. Например:
FROM golang
COPY hello.go .
RUN go build hello.go
FROM scratch
COPY --from=0 /go/hello .
CMD ["./hello"]
Размер изображения, построенного на этот раз, точно2 MB
, идеально!
Однако, используяscratch
В качестве базового образа он принесет много неудобств, позвольте мне рассказать вам по порядку.
отсутствует оболочка
scratch
Первое неудобство зеркалирования – нетshell
, Который означает, чтоCMD/RUN
Строки нельзя использовать в операторах, например:
...
FROM scratch
COPY --from=0 /go/hello .
CMD ./hello
Если вы используете встроенный образ для создания и запуска контейнера, вы столкнетесь со следующей ошибкой:
docker: Error response from daemon: OCI runtime create failed: container_linux.go:345: starting container process caused "exec: \"/bin/sh\": stat /bin/sh: no such file or directory": unknown.
Как видно из сообщения об ошибке, изображение не содержит/bin/sh
, поэтому программа не может быть запущена. Это потому, что когда выCMD/RUN
Когда строки используются в качестве параметров в операторе, эти параметры помещаются в/bin/sh
, то есть следующие два утверждения эквивалентны:
CMD ./hello
CMD /bin/sh -c "./hello"
Решение на самом деле очень простое:Используйте синтаксис JSON вместо синтаксиса строки.Например, будетCMD ./hello
заменитьCMD ["./hello"]
, чтобы Docker запускал программу напрямую, не помещая ее в оболочку для запуска.
Отсутствуют инструменты отладки
scratch
Образ не содержит никаких средств отладки,ls
,ps
,ping
Ничего из этого, разумеется, как и оболочки (упомянутой выше), использовать нельзяdocker exec
Войдите в контейнер и не сможете просмотреть информацию о сетевом стеке и т.д.
Если вы хотите просмотреть файлы в контейнере, вы можете использоватьdocker cp
; если вы хотите просмотреть или отладить сетевой стек, вы можете использоватьdocker run --net container:
или используйтеnsenter
; Чтобы лучше отлаживать контейнеры, Kubernetes также вводит новую концепцию под названиемEphemeral Containers, но все еще является альфа-функцией.
Хотя существует так много разных способов помочь нам в отладке контейнеров, они все усложняют, а мы стремимся к простоте, и чем проще, тем лучше.
Вы можете выбрать компромиссbusybox
илиalpine
зеркало вместоscratch
, хотя они на несколько МБ больше, но в целом это стоит лишь небольшого количества места, пожертвованного ради удобства отладки.
отсутствует libc
Это самая сложная проблема для решения. использоватьscratch
При использовании в качестве базового образа языковая версия Gohello world
Запустив счастливо, версия на языке C не будет работать, или более сложная программа Go не запустится (например, с использованием инструментария, связанного с сетью), вы столкнетесь с ошибками, подобными следующим:
standard_init_linux.go:211: exec user process caused "no such file or directory"
Из сообщения об ошибке мы видим, что отсутствуют файлы, но не сообщает нам, каких файлов не хватает, на самом деле эти файлы являются динамическими библиотеками, необходимыми для работы программы.
Итак, что такое динамическая библиотека? Зачем вам динамические библиотеки?
Так называемая динамическая библиотека, статическая библиотека, относится к программе, скомпилированнойэтап ссылки, способ ссылки на исполняемый файл.статическая библиотекаЭто относится к связыванию и упаковке объектного файла .o, созданного сборкой, вместе с библиотекой, на которую ссылаются, в исполняемый файл на этапе связывания, поэтому вызывается соответствующий метод связывания.статическая ссылка(статическая ссылка). Динамическая библиотека не подключается к объектному коду при компиляции программы, но загружается при ее работе, поэтому вызывается соответствующий метод линковкидинамическая ссылка(динамическая ссылка).
Программы 1990-х были в основном статически связаны, потому что большинство программ в то время работали на гибких дисках или кассетах, а стандартной библиотеки вообще не существовало. Таким образом, программа не имеет ничего общего с библиотекой функций, когда она работает, и ее удобно пересаживать. Однако в системе с разделением времени, такой как Linux, несколько программ будут работать одновременно на одном жестком диске, и эти программы в основном будут использовать стандартную библиотеку C. В настоящее время отражаются преимущества использования динамической компоновки. При использовании динамической компоновки исполняемый файл не содержит файлы стандартной библиотеки, а только индексирует эти файлы библиотеки. Например, программа зависит от библиотечного файла.libtrigonometry.so
серединаcos
иsin
функция, программа найдет и загрузит ее по индексу при запускеlibtrigonometry.so
, а затем программа может вызывать функции в этом библиотечном файле.
Преимущества использования динамической компоновки очевидны:
- Для экономии места на диске разные программы могут использовать общие библиотеки.
- Чтобы сэкономить память, общие библиотеки нужно загрузить с диска в память только один раз, а затем использовать совместно с различными программами.
- Легче поддерживать, после обновления файла библиотеки все программы, использующие библиотеку, не нужно перекомпилировать.
Строго говоря, сочетание динамической библиотеки и разделяемой библиотеки позволяет добиться эффекта экономии памяти. Расширение для динамических библиотек в Linux:.so
(shared object
), а расширение для динамических библиотек в Windows —.DLL
(Dynamic-link library).
Вернемся к первоначальному вопросу: по умолчанию программы C используют динамическую компоновку, как и программы Go. вышеhello world
Программа использует файлы стандартной библиотекиlibc.so.6
, поэтому программа может работать нормально только в том случае, если файл включен в образ. использоватьscratch
Поскольку базовое изображение определенно неприемлемо, используйтеbusybox
иalpine
Ни потому, чтоbusybox
не включает стандартную библиотеку, стандартная библиотека, используемая alpine,musl libc
, с часто используемой стандартной библиотекойglibc
Несовместимые, последующие статьи будут подробно объяснены, поэтому я не буду здесь вдаваться в подробности.
Так как же решить проблему стандартной библиотеки? Есть три варианта.
1. Используйте статическую библиотеку
Мы можем заставить компилятор скомпилировать программу с помощью статической библиотеки, есть много способов, если вы используете gcc в качестве компилятора, просто добавьте параметр-static
:
$ gcc -o hello hello.c -static
Размер скомпилированного исполняемого файла760 kB
, по сравнению с предыдущим16kB
Он намного больше, потому что исполняемый файл содержит библиотечные файлы, необходимые для запуска. Скомпилированную программу можно запустить вscratch
зеркальный.
Если вы используете образ alpine в качестве базового образа для компиляции, результирующий исполняемый файл будет меньше (
2. Скопируйте файлы библиотеки на зеркало
Чтобы узнать, какие файлы библиотеки необходимы для запуска программы, вы можете использоватьldd
инструмент:
$ ldd hello
linux-vdso.so.1 (0x00007ffdf8acb000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007ff897ef6000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007ff8980f7000)
Как видно из вывода, программе достаточноlibc.so.6
этот файл библиотеки.linux-vdso.so.1
сVDSOМеханизм, используемый для ускорения некоторых системных вызовов, является необязательным.ld-linux-x86-64.so.2
Представляет сам динамический компоновщик, включая информацию обо всех зависимых файлах библиотеки.
вы можете выбратьldd
Все перечисленные файлы библиотек копируются на зеркало, но это может быть сложно поддерживать, особенно когда программа имеет большое количество зависимых библиотек. заhello world
Для программ копирование файлов библиотек вполне нормально, но для более сложных программ (например, использующих DNS) возникают непонятные проблемы:glibc
(библиотека GNU C) реализует DNS через довольно сложный механизм, называемыйNSS
(Переключатель службы имен, переключатель службы имен). ему нужен файл конфигурации/etc/nsswitch.conf
и дополнительные библиотеки функций, но используйтеldd
Эти библиотеки функций не отображаются при запуске программы, поскольку эти библиотеки не загружаются до тех пор, пока программа не запустится. Если вы хотите, чтобы разрешение DNS работало правильно, вам необходимо скопировать эти дополнительные файлы библиотеки (/lib64/libnss_*
).
Лично я не рекомендую копировать файлы библиотеки напрямую, потому что их очень сложно поддерживать, их нужно постоянно изменять в будущем, и есть много неизвестных скрытых опасностей.
3. Используйтеbusybox:glibc
как базовое изображение
Есть зеркало, которое прекрасно решает все эти проблемы, и этоbusybox:glibc
. у него есть только5 MB
размера и включаетglibc
и различные инструменты отладки. Если вы хотите выбрать подходящий образ для запуска программ, использующих динамическую компоновку,busybox:glibc
это лучший выбор.
Примечание. Если ваша программа использует библиотеки, отличные от стандартной библиотеки, вам все равно необходимо скопировать файлы этих библиотек на зеркало.
4. Резюме
Наконец, давайте сравним размеры изображений, созданных разными методами сборки:
- Оригинальный метод сборки: 1,14 ГБ
- использовать
ubuntu
Многоэтапная сборка образа: 64,2 МБ - использовать
alpine
зеркало и статикаglibc
: 6,5 МБ - использовать
alpine
Изображения и динамические библиотеки: 5,6 МБ - использовать
scratch
зеркало и статикаglibc
: 940 КБ - использовать
scratch
зеркало и статикаmusl libc
: 94 КБ
В итоге мы уменьшили размер зеркального отображения99.99%
.
Но я не рекомендую использовать sratch в качестве базового образа, потому что очень хлопотно отлаживать, но если хотите, не буду вас останавливать.
Следующая статья будет посвящена стратегии сокращения зеркал языка Go, в которой будет уделено много времени обсуждению альпийского зеркала, потому что оно настолько крутое, что вы должны понять его детали, прежде чем использовать его.
Публичный аккаунт WeChat
Отсканируйте QR-код ниже, чтобы подписаться на официальную учетную запись WeChat, и ответьте на официальную учетную запись ◉Добавить группу◉, чтобы присоединиться к нашей облачной коммуникационной группе и обсудить облачные технологии с Сунь Хунляном, директором Чжаном, Ян Мином и другими важными шишками.