scratch
и крошечного http-сервера на основе этой сборки, я смог ужать результат до 6.32kB!
Если предпочитаете видео, вот ролик по статье, выложенный на YouTube!
Раздутые контейнеры
Контейнеры часто превозносятся как панацея, позволяющая справиться с любыми вызовами, связанными с эксплуатацией ПО. Притом, как мне нравятся контейнеры, на практике мне часто попадаются контейнерные образы, отягощенные разнообразными проблемами. Распространенная беда – размер контейнера; у некоторых образов он достигает многих гигабайт!
Поэтому я решил устроить челлендж себе и всем желающим и попытаться создать настолько компактный образ, насколько возможно.
Задача
Правила довольно просты:
- Контейнер должен выдавать содержимое файла по http на выбранный вами порт
- Монтирование томов не допускается (так называемое «Правило Марека» )
Упрощенное решение
Чтобы узнать размер базового образа, можно воспользоваться node.js и создать простой сервер
index.js
:const fs = require("fs");
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'content-type': 'text/html' })
fs.createReadStream('index.html').pipe(res)
})
server.listen(port, hostname, () => {
console.log(`Server: http://0.0.0.0:8080/`);
});
и сделать из него образ, запуская официальный базовый образ node:
FROM node:14
COPY . .
CMD ["node", "index.js"]
Этот завесил на
943MB
! Уменьшенный базовый образ
Один из простейших и наиболее очевидных тактических подходов к уменьшению размера образа – выбрать более компактный базовый образ. Официальный базовый образ node существует в варианте
slim
(по-прежнему на основе debian, но с меньшим количеством предустановленных зависимостей) и вариант alpine
на основе Alpine Linux.С применением
node:14-slim
и node:14-alpine
в качестве базового удается уменьшить размер образа до 167MB
и 116MB
соответственно.Поскольку образы docker аддитивны, где каждый уровень надстраивается над следующим, здесь уже практически нечего сделать, чтобы еще сильнее уменьшить решение с node.js.
Компилируемые языки
Чтобы вывести ситуацию на новый уровень, можно перейти к компилируемому языку, где гораздо меньше зависимостей времени исполнения. Есть ряд вариантов, но для создания веб-сервисов часто применяется golang.
Я создал простейший файловый сервер
server.go
:package main
import (
"fmt"
"log"
"net/http"
)
func main() {
fileServer := http.FileServer(http.Dir("./"))
http.Handle("/", fileServer)
fmt.Printf("Starting server at port 8080\n")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
И встроил его в контейнерный образ, воспользовавшись официальным базовым образом golang:
FROM golang:1.14
COPY . .
RUN go build -o server .
CMD ["./server"]
Который завесил на…
818MB
. Здесь есть проблема: в базовом образе golang установлено много зависимостей, которые полезны при создании программ на go, но не нужны для запуска программ.
Многоступенчатые сборки
В Docker есть возможность под названием многоступенчатые сборки, с которыми просто собирать код в среде, содержащей все необходимые зависимости, а потом скопировать полученный исполняемый файл в иной образ.
Это полезно по нескольким причинам, но одна из наиболее очевидных – размер образа! Выполнив рефакторинг dockerfile вот так:
### этап сборки ###
FROM golang:1.14-alpine AS builder
COPY . .
RUN go build -o server .
### этап запуска ###
FROM alpine:3.12
COPY --from=builder /go/server ./server
COPY index.html index.html
CMD ["./server"]
Размер полученного образа – всего
13.2MB
! Статическая компиляция + образ Scratch
13 MB – совсем неплохо, но у нас в запасе осталась еще пара трюков, позволяющих еще сильнее ужать этот образ.
Есть базовый образ под названием scratch, который однозначно пуст, его размер равен нулю. Поскольку внутри
scratch
ничего нет, любой образ, построенный на его основе, должен нести в себе все необходимые зависимости.Чтобы это было возможно на основе нашего сервера на go, нужно добавить несколько флагов на этапе компиляции, чтобы обеспечить статическую линковку всех необходимых библиотек в исполняемый файл:
### этап сборки ###
FROM golang:1.14 as builder
COPY . .
RUN go build -ldflags "-linkmode external -extldflags -static" -a server.go
### этап запуска ###
FROM scratch
COPY --from=builder /go/server ./server
COPY index.html index.html
CMD ["./server"]
В частности, мы задаем
external
в качестве режима линковки и передаем флаг -static
внешнему линковщику.Благодаря двум этим изменениям удается довести размер образа до
8.65MB
ASM как залог победы!
Образ размером менее 10MB, написанный на языке вроде Go, отчетливо миниатюрен почти для любых обстоятельств… но можно сделать еще меньше! Пользователь nemasu выложил на Github полноценный http-сервер, написанный на ассемблере. Он называется assmttpd.
Все, что потребовалось для его контейнеризации – установить несколько зависимостей сборки в базовый образ Ubuntu, прежде, чем запустить предоставленный рецепт
make release
:### этап сборки ###
FROM ubuntu:18.04 as builder
RUN apt update
RUN apt install -y make yasm as31 nasm binutils
COPY . .
RUN make release
### этап запуска ###
FROM scratch
COPY --from=builder /asmttpd /asmttpd
COPY /web_root/index.html /web_root/index.html
CMD ["/asmttpd", "/web_root", "8080"]
Затем полученный в результате исполняемый файл
asmttpd
копируется в scratch-образ и вызывается через командную строку. Размер полученного образа – всего 6,34kB!
xxxcoltxxx
Не вижу проблем в больших базовых образах. Слои переиспользуются и редко меняются. Но чисто ради спортивного интереса — почему бы и нет
Areso
Гонять гигабайты трафика между девмашиной, докер-регистри и демо/продмашинами — так себе удовольствие.
Особенно, если вас 400 человек в одном помещении и все пытаются пропихнуть гигабайты :)
xxxcoltxxx
Слои базового образа скачаются только 1 раз для 1 ноды. В дальнейшем они не будут скачиваться, пока их хеш не изменится. А он не изменится, если ты не сделаешь изменений в базовом образе
Areso
Понимаю это.
Но вот прихожу я на мероприятие. У меня уже скачан Python:3.6. Команда такая — а мы всё на ноде делаем. И я начинаю качать гигабайт с небольшим ноды.
Я говорю — делаем всё как взрослые, с БД. Вся остальная команда начинает качать Postgress.
Отлично, все всё скачали, началась работа. Первый билд… Собрали. Кто-то запушил в докер регистри. Ждёт, пока гигабайт уедет.
Что-то поменяли — снова пуш — снова ожидание. Если в команде есть художники или непрограммисты, которые сами не собирают — они каждый раз будут с регистри выкачивать актуальный 1,5 ГБ готовый образ с приложением.
В результате, что вафля, что 3g/4g в этой локации в этот момент времени подаёт признаки издыхающего ленивца.
xxxcoltxxx
А потом вы делаете аналог proxy_pass в nginx и ваш сервис ломается, потому что нет ни одного корневого сертификата, а вам нужен https. Базовые образы не просто так придуманы
Areso
Ну, надо разделять промышленную разработку и такие вот мероприятия, где есть цель выкатить прототип побыстрее и обогнать других NN команд. Там очень редко до https доходит, по моему опыту :)
xxxcoltxxx
Вот поэтому я и написал про спортивный интерес. Понятно, что нужно стремиться к уменьшению образа, но не ставить это в самый важный приоритет. Разница между 10 мб и 10 кб несущесттвенна, но она может избавить от большого количества проблем, о которых ты не подумал
nomhoi
На докер хабе образы в сжатом виде лежат. Опцию --compress используют?
OnYourLips
К сожалению, это не так. Через самое непродолжительное время у вас будут сервисы, последние версии которых собраны в разное время, и у них будут разные версии базового образа.
Этого можно избежать, только если вы запретите обновлять хеш базового образа при создании своих. И я даже не хочу представлять последствия такого решения.
xxxcoltxxx
И слои нового базового образа тоже скачаются только 1 раз! Причем только те слои, которые изменились. И при запуске контейнер не займет места весом в образ. И при масштабировании тоже. Место займут только изменения в фс в рантайме
Если эта статья в контексте какого-то соревнования по мегабайтам, то конечно я не прав. Но выглядит как без контекста. Т.е., ничего страшного в этом нет