داکرفایل چند مرحله ای برای ایمیج سبک و سریع در 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 سنگین تر اجسام در عالمبه همین دلیل برای حل مشکل و کاهش زمان Build دست به کار شدم و بعد از جستجو متوجه شدم، دوستانی که Node و React و پروژه هایی که نیاز به node_module دارد به راه‌حل‌های خوبی رسیدند. من نیز با توجه به روش‌های آن‌ها با اضافه‌کردن یک مرحله جدید به روش “چند مرحله‌ای” قبلی، راه‌حلی برای رفع این مشکل در Golang به شرح زیر پیدا کردم:ابتدا فایل go.mod به تنهایی در یک Image قرار می‌دهیم.بعد از آن دستور `go mod download` اجرا می‌شود که ماژول‌های مورد نیاز در یک Image واسط ذخیره شود.در مرحله بعد یک کد بر اساس این Image واسط تولید می‌شود.در انتها مشابه حالت قبل، خروجی در Image کم حجم alpine قرار داده می‌شود.نکته:با انجام این کار مادامی که تغییری در فایل go.mod پروژه اتفاق نیافتد،‌ مرحله گرفتن ماژول‌های مورد نیاز برای کامپایل پروژه skip می‌شود و سرعت ایجاد Image پروژه افزایش می‌یابد.نتیجه‌گیریزمان ایجاد Image در پروژه بالا که تعداد ماژول‌های مورد نیاز کمی داشت، در حالت اول به مدت ۱:۱۰ ثانیه است و در حالت دوم به ۳۷ ثانیه می‌رسد، این مقدار در شرایطی با ماژول‌های مورد نیازِ بیشتر، محسوس‌تر است، مثلا در پروژه لایو آپارات چیزی حدود ۴ دقیقه صرفه‌جویی در زمان داشتیم.