Два трюка и трюка, чтобы уменьшить размер образа Docker на 99%

Docker

Оригинальная ссылка: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Зеркальный контент.

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

Преимущества использования динамической компоновки очевидны:

  1. Для экономии места на диске разные программы могут использовать общие библиотеки.
  2. Чтобы сэкономить память, общие библиотеки нужно загрузить с диска в память только один раз, а затем использовать совместно с различными программами.
  3. Легче поддерживать, после обновления файла библиотеки все программы, использующие библиотеку, не нужно перекомпилировать.

Строго говоря, сочетание динамической библиотеки и разделяемой библиотеки позволяет добиться эффекта экономии памяти. Расширение для динамических библиотек в 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, и ответьте на официальную учетную запись ◉Добавить группу◉, чтобы присоединиться к нашей облачной коммуникационной группе и обсудить облачные технологии с Сунь Хунляном, директором Чжаном, Ян Мином и другими важными шишками.