داکرفایل چند مرحله ای برای ایمیج سبک و سریع در Golang

سرعت خروجی گرفتن در فرآیندهای توسعه و پیاده‌سازی برای اعمال سریع تغییرات در کد بسیار مهم است، بنابراین در پروسه‌های روزمره‌ای که در توسعه ابزارهای زیرساختی در آپارات داشتم به تجربه‌ای تحت عنوان داکرفایل چند مرحله‌ای برای ایجاد Image های سبک و سریع در زبان Golang رسیدم و در این مقاله می‌خواهم این تجربه رو با شما در میان بگذارم.

خیلی از ما وقتی که می‌خواهیم اپلکیشنِ Golang خودمان را داکرایز کنیم، از Image رسمی پیشفرض Golang استفاده‌ می‌کنیم.

https://docs.docker.com/samples/library/golang/

همانطور که Dockerfile این Image را می‌ببینید با خطوط زیر شروع می‌شود :

FROM golang
FROM nginx
FROM openjdk

حالا اگر این Image را دریافت کنیم، متوجه  خواهید‌شد که ۸۰۳ مگابایت حجم دارد. باید گفت، برای یک Image خالی که هیچ کدی درون آن نیست حجم خیلی زیادی است، اینطور نیست؟

docker pull golang:1.13
$ docker image list
golang                                     1.13                3a7408f53f79        2 months ago        803MB

از سویی دیگر یک Image کم حجم به نام Alpine برای Golang وجود دارد که ۳۶۰ مگابایت حجم دارد درست است که حجم کمتری نسبت به Image رسمی golang دارد اما برای یک Image در حالت Production حجم خیلی زیادی است.

FROM golang:alpine
$ docker image list
golang                                     alpine              459ae5e869df        10 days ago         370MB

استفاده از روش “چند مرحله‌ای برای کاهش حجم

به لطف استفاده از روش “چند مرحله‌ای” داکر در ایجاد Image ها می‌توانیم فایل خروجی نرم‌افزار خود را در Image golang:alpine بسازیم و یک Image بر اساس alpine تولید کنیم که تنها، فایل اجرایی نرم‌افزار ما در آن باشد.

https://docs.docker.com/develop/develop-images/multistage-build/

برای روشن‌تر شدن موضوع بیایید باهم یک وب سرور ساده با Go بنویسیم و در ادامه سراغ Dockerfile پروژه برویم، ابتدا از Image Alpine به عنوان Builder استفاده می‌کنیم. می‌توانید حجم Image را در تصویر زیر مشاهده کنید:

https://gist.github.com/subzerobo/0296050e7f1ef7e6035aa6d87f67fd0d


https://gist.github.com/subzerobo/c26797205286ff1330c8e1a145722f60


حالا وقتش هست که ببینیم Image برنامه به چه شکل ایجاد می‌شود و چه حجمی خواهد داشت؟

$ docker build -t subzero/sample .
Sending build context to Docker daemon   25.6kB
Step 1/10 : FROM golang:alpine AS builder 
---> 459ae5e869df
Step 2/10 : WORKDIR /src ---> Using cache 
---> 6cf33681f8bb
Step 3/10 : COPY . /src---> b2e0287ec4f1
Step 4/10 : ENV GO111MODULE=on
---> Running in b2a8def41333
Removing intermediate container b2a8def41333
---> 33858d537c8c
Step 5/10 : RUN go mod download
---> Running in a57d93861ca6
Removing intermediate container a57d93861ca6 
---> bc14349da12c
Step 6/10 : RUN go build -o server main.go
---> Running in 306afd335c6cRemoving intermediate container 306afd335c6c---> 970ebfd59bd1
Step 7/10 : FROM scratch 
---> 
Step 8/10 : WORKDIR /app
---> Running in 0c4735c45471
Removing intermediate container 0c4735c45471
---> d2ee5378c6d8
Step 9/10 : COPY --from=builder /src/server /app/
---> 4644b56bd11e
Step 10/10 : ENTRYPOINT ./server
---> Running in 18c324363942
Removing intermediate container 18c324363942
---> 2fa59a876599
Successfully built 2fa59a876599
Successfully tagged subzero/sample:latest

$ docker image list
REPOSITORY                                 TAG                 IMAGE ID                         SIZE
subzero/sample                             latest              2fa59a876599                13MB

خیلی بهتر شد نه؟‌

باید بگویم هنوز هم جا برای کم شدن حجم این فایل وجود دارد، به طور مثال؛

اگر هنگام ایجاد فایل خروجی، اطلاعات مربوط به Debug را حذف کنیم و فایل را مستقیم برای سیستم‌ عامل لینوکس خروجی بگیریم، این حجم باز کاهش پیدا می‌کند. برای این کار کافیست خط دهم داکرفایلی را که ایجاد کردیم به این شکل زیر تغییر دهیم:

RUN go build -o server main.go
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags=&quot-w -s&quot -o server main.go

REPOSITORY                                 TAG                 IMAGE ID                    SIZE
subzero/sample                             latest              23ad308c5a7b           9.31MB

خوب با همین تغییر بازهم حجم خروجی حدود ۲۵ درصد کاهش پیدا کرد.

استفاده از روش “چند مرحله‌ای” برای کاهش زمان Build

با توجه به اینکه برای استفاده از پکیج‌های زبان Go در ایران با مشکلات زیادی مواجه هستیم و برای Build کردن Image از پروکسی استفاده می‌کنیم، کاهش مدت زمان Build در محیط Production  یک دغدغه اصلی است.

به طور معمول اگر کد در مرحله Production باشد دیگر تغییرات زیادی در پکیج‌های استفاده شده، ایجاد نمی‌شود یا حداقل در پروژه‌هایی که من تا به امروز کار کردم، این موضوع زیاد پیش آمده‌است،

برای مثال، شرایطی را در نظر بگیرید که در روز، چیزی حدود ۱۰ بار تغییرات دارید و هنوز پروژه به روال عادی CI/CD شرکت اضافه نشده و از طرفی و مجبور هستید کد را به شکل دستی روی سرور Deploy کنید، با توجه به زمان Build می‌توانید تصور کنید چه سختی‌ای را به همراه دارد؟

در تصویر زیر به طنز، شاهد مقایسه اندازه وزن خورشید، ستاره نوترونی، سیاه چاله و node_module هستید. درست است که حجم پیکج‌ها و ماژول‌های Golang خیلی زیاد نیست ولی مشکل پروکسی سرور برای دریافت پکیج‌های Golang با همین مقدار حجم، بسیار اذیت کننده‌است،

meme سنگین تر اجسام در عالم
meme سنگین تر اجسام در عالم


به همین دلیل برای حل مشکل و کاهش زمان Build دست به کار شدم و بعد از جستجو متوجه شدم، دوستانی که Node و React و پروژه هایی که نیاز به node_module دارد به راه‌حل‌های خوبی رسیدند. من نیز با توجه به روش‌های آن‌ها با اضافه‌کردن یک مرحله جدید به روش “چند مرحله‌ای” قبلی، راه‌حلی برای رفع این مشکل در Golang به شرح زیر پیدا کردم:

  • ابتدا فایل go.mod به تنهایی در یک Image قرار می‌دهیم.
  • بعد از آن دستور `go mod download` اجرا می‌شود که ماژول‌های مورد نیاز در یک Image واسط ذخیره شود.
  • در مرحله بعد یک کد بر اساس این Image واسط تولید می‌شود.
  • در انتها مشابه حالت قبل، خروجی در Image کم حجم alpine قرار داده می‌شود.

نکته:

با انجام این کار مادامی که تغییری در فایل go.mod پروژه اتفاق نیافتد،‌ مرحله گرفتن ماژول‌های مورد نیاز برای کامپایل پروژه skip می‌شود و سرعت ایجاد Image پروژه افزایش می‌یابد.

نتیجه‌گیری

زمان ایجاد Image در پروژه بالا که تعداد ماژول‌های مورد نیاز کمی داشت، در حالت اول به مدت ۱:۱۰ ثانیه است و در حالت دوم به ۳۷ ثانیه می‌رسد، این مقدار در شرایطی با ماژول‌های مورد نیازِ بیشتر، محسوس‌تر است، مثلا در پروژه لایو آپارات چیزی حدود ۴ دقیقه صرفه‌جویی در زمان داشتیم.