تغییرات مهمی در صنعت نرم افزار رخ می دهد. با انتقال همه دستگاه‌های اپل به سمت سیلیکون مبتنی بر ARM و AWS که بهترین نسبت عملکرد به ازای هزینه را با نمونه‌های Graviton2 خود ارائه می‌دهد، دیگر نمی‌توان انتظار داشت که همه نرم‌افزارها فقط باید روی پردازنده‌های x86 اجرا شوند. اگر با کانتینرها کار می‌کنید، زمانی که تیم‌های توسعه‌دهنده شما از معماری‌های متفاوتی استفاده می‌کنند یا می‌خواهید در معماری متفاوتی از معماری که روی آن توسعه می‌دهید، مستقر شوید، ابزار خوبی برای ساخت تصاویر چند پلتفرمی در دسترس است. در این پست، الگوهایی را نشان خواهم داد که اگر می‌خواهید بهترین عملکرد را از چنین ساخت‌هایی به دست آورید، می‌توانید از آنها استفاده کنید.

برای ساخت تصاویر کانتینر چند پلتفرمی، از docker buildx فرمان. Buildx یک مؤلفه Docker است که بسیاری از ویژگی‌های ساخت قدرتمند را با تجربه کاربری آشنای Docker فعال می‌کند. همه ساخت‌ها از طریق buildx اجرا می‌شوند و با موتور سازنده Moby Buildkit اجرا می‌شوند. Buildx همچنین می‌تواند به‌صورت مستقل یا مثلاً برای اجرای ساخت‌ها در یک خوشه Kubernetes استفاده شود. در نسخه بعدی Docker CLI، فرمان docker build نیز به‌طور پیش‌فرض شروع به استفاده از Buildx می‌کند.

به‌طور پیش‌فرض، ساختی که با Buildx اجرا می‌شود، تصویری را برای معماری مطابق با دستگاه شما می‌سازد. به این ترتیب، تصویری دریافت می کنید که روی همان دستگاهی که روی آن کار می کنید اجرا می شود. به منظور ساختن برای معماری متفاوت، می‌توانید پرچم --platform را تنظیم کنید، به عنوان مثال. --platform=linux/arm64. برای ساختن چندین پلتفرم با هم، می‌توانید چندین مقدار را با جداکننده کاما تنظیم کنید.

# ساخت یک تصویر برای دو پلتفرم
docker buildx build --platform=linux/amd64,linux/arm64.

برای ساختن تصاویر چند پلتفرمی، همچنین باید یک نمونه سازنده ایجاد کنیم، زیرا ساخت تصاویر چند پلتفرمی در حال حاضر فقط در صورت استفاده پشتیبانی می شود. BuildKit با درایورهای docker-container و kubernetes. تنظیم یک پلتفرم هدف واحد در همه درایورهای buildx مجاز است.

docker buildx create --use
# ساخت یک تصویر برای دو پلتفرم
docker buildx build --platform=linux/amd64,linux/arm64.

هنگام ساختن یک تصویر چند پلتفرمی از یک Dockerfile، عملاً Dockerfile شما یک بار برای هر پلتفرم ساخته می شود. در پایان ساخت، همه این تصاویر با هم در یک تصویر چند پلتفرمی ادغام می شوند.

FROM alpine.
RUN echo "Hello" > /hello

برای مثال، در مورد یک Dockerfile ساده مانند این که برای دو معماری ساخته شده است، BuildKit دو نسخه مختلف از تصویر Alpine را می کشد، یکی شامل باینری های x86 و دیگری حاوی باینری های arm64 است.

روش های مختلف ساخت

به طور کلی، CPU دستگاه شما فقط می تواند باینری ها را برای معماری اصلی خود اجرا کند. CPU x86 نمی تواند باینری های ARM را اجرا کند و بالعکس. بنابراین وقتی مثال بالا را روی یک ماشین اینتل اجرا می کنیم، چگونه می تواند باینری پوسته را برای ARM اجرا کند؟ این کار را با اجرای باینری از طریق شبیه‌ساز نرم‌افزار به‌جای انجام مستقیم این کار انجام می‌دهد. اگر آنها را برای سیستم خود فهرست نمی‌بینید، می‌توانید آنها را با تصویر tonistiigi/binfmt نصب کنید.

استفاده از شبیه‌ساز به این روش بسیار آسان است. ما اصلاً نیازی به اصلاح Dockerfile خود نداریم و می‌توانیم به طور خودکار برای چندین پلتفرم بسازیم. اما بدون جنبه های منفی به دست نمی آید. باینری هایی که به این روش اجرا می شوند باید به طور مداوم دستورالعمل های خود را بین معماری ها تبدیل کنند و بنابراین با سرعت اصلی اجرا نمی شوند. گاهی اوقات ممکن است موردی پیدا کنید که یک باگ در لایه شبیه‌سازی ایجاد کند.

یکی از راه‌های جلوگیری از این سربار این است که Dockerfile خود را تغییر دهید تا طولانی‌ترین دستورات از طریق شبیه‌ساز اجرا نشوند. در عوض، می‌توانیم از مرحله کامپایل متقابل استفاده کنیم. ما فقط از باینری های ساخته شده برای معماری بومی خود با یک گزینه پیکربندی خاص استفاده می کنیم که باعث می شود آنها باینری های جدیدی را برای معماری هدف ما تولید کنند. همانطور که از نامش می‌گوید، این تکنیک را نمی‌توان برای همه فرآیندها استفاده کرد، اما بیشتر زمانی که یک کامپایلر را اجرا می‌کنید. خوشبختانه این دو تکنیک را می توان با هم ترکیب کرد. به عنوان مثال، Dockerfile شما می‌تواند از شبیه‌سازی برای نصب بسته‌ها از مدیر بسته استفاده کند و از کامپایل متقابل برای ساخت کد منبع شما استفاده کند. روی دستگاه Intel/AMD اجرا شود. آبی حاوی باینری‌های x86، باینری‌های ARM زرد است.

هنگام تصمیم‌گیری در مورد استفاده از شبیه‌سازی یا کامپایل متقابل، مهمترین چیزی که باید در نظر بگیرید این است که آیا فرآیند شما از قدرت پردازش زیادی CPU استفاده می‌کند یا خیر. شبیه سازی معمولاً یک روش خوب برای نصب بسته ها یا در صورت نیاز به ایجاد برخی فایل ها یا اجرای یک اسکریپت یکباره است. اما اگر استفاده از کامپایل متقابل می‌تواند ساخت‌های شما را (احتمالاً ده‌ها) دقیقه سریع‌تر کند، احتمالاً ارزش به‌روزرسانی Dockerfile را دارد. اگر می خواهید تست هایی را به عنوان بخشی از ساخت اجرا کنید، کامپایل متقابل نمی تواند به آن دست یابد. برای بهترین عملکرد در این مورد، گزینه دیگر استفاده از یک خوشه ساخت از راه دور با چندین ماشین با معماری های مختلف است. . متداول‌ترین الگوی استفاده در ساخت‌های چند مرحله‌ای، تعریف مرحله (های) ساخت است که در آن مصنوعات ساخت خود را آماده می‌کنیم و یک مرحله زمان اجرا که به عنوان تصویر نهایی صادر می‌کنیم. ما از همین روش در اینجا با یک شرط اضافی استفاده خواهیم کرد که می‌خواهیم مرحله ساخت ما همیشه باینری‌ها را برای معماری بومی ما اجرا کند و مرحله زمان اجرا ما حاوی باینری‌هایی برای معماری هدف باشد.

وقتی یک مرحله ساخت را با دستوری مانند شروع می‌کنیم. FROM debian به سازنده دستور می‌دهد تصویر Debian را که با مقداری که با پرچم --platform تنظیم شده مطابقت دارد، بکشد. کاری که می‌خواهیم انجام دهیم این است که مطمئن شویم این تصویر دبیان همیشه بومی ماشین فعلی ما است. وقتی روی یک سیستم x86 هستیم، می‌توانیم در عوض از دستوری مانند FROM --platform=linux/amd64 debian استفاده کنیم. حالا مهم نیست که در طول ساخت چه پلتفرمی تنظیم شده است، این مرحله همیشه بر اساس amd64 خواهد بود. به جز اینکه اگر به یک دستگاه ARM مانند مک های جدید اپل سوئیچ کنیم، اکنون چه اتفاقی می افتد؟ آیا اکنون باید همه Dockerfiles خود را تغییر دهیم؟ پاسخ منفی است، و به جای نوشتن یک مقدار پلتفرم ثابت در Dockerfile خود، باید از متغیری استفاده کنیم، FROM --platform=$BUILDPLATFORM debian.

BUILDPLATFORM بخشی از مجموعه a است. آرگومان‌هایی که به‌طور خودکار تعریف شده‌اند (گستره جهانی) که می‌توانید از آنها استفاده کنید. همیشه با پلتفرم یا سیستم فعلی شما مطابقت دارد و سازنده مقدار صحیح را برای ما پر می‌کند.

در اینجا فهرست کاملی از این متغیرها وجود دارد:

BUILDPLATFORM — با دستگاه فعلی مطابقت دارد. (به عنوان مثال linux/amd64)

BUILDOS - جزء سیستم عامل BUILDPLATFORM، به عنوان مثال. لینوکس

BUILDARCH - به عنوان مثال. amd64، arm64، riscv64

BUILDVARIANT - برای تنظیم نوع ARM، به عنوان مثال، استفاده می شود. v7

TARGETPLATFORM - مقدار تنظیم شده با پرچم --platform در ساخت

TARGETOS - جزء سیستم عامل از --platform، به عنوان مثال. لینوکس

TARGETARCH - معماری از --platform، به عنوان مثال. بازو64

TARGETVARIANT

اکنون در مرحله ساخت، می‌توانیم کد منبع خود را وارد کنیم، بسته کامپایلری را که می‌خواهیم استفاده کنیم، نصب کنیم، و غیره. Dockerfile مبتنی بر شبیه‌سازی.

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

برای عبور از معماری هدف، می‌توانیم از همان ساختار تعریف‌شده به‌طور خودکار استفاده کنیم. آرگومان‌هایی که قبلاً نشان داده شده‌اند، این بار با پیشوند TARGET*. از آنجایی که ما از این آرگومان‌های ساخت داخل مرحله استفاده می‌کنیم، باید در محدوده محلی باشند و قبل از استفاده با یک فرمان  ARG اعلام شوند. ] آلپاین AS ساخت
# اجرا
# کپی.
ARG TARGETPLATFORM
RUN compile –target=$TARGETPLATFORM -o /out/mybinary

تنها کاری که اکنون باید انجام شود ایجاد یک مرحله زمان اجرا است که در نتیجه ساخت خود صادر خواهیم کرد. برای این مرحله، از --platform در تعریف FROM استفاده نخواهیم کرد. می‌توانیم FROM --platform=$TARGETPLATFORM بنویسیم، اما به هر حال این مقدار پیش‌فرض برای تمام مراحل ساخت است، بنابراین استفاده از یک پرچم اضافی است.

FROM alpine.
# RUN 
COPY --from=build /out/mybinary /bin

برای تأیید، بیایید ببینیم چه اتفاقی می‌افتد اگر Dockerfile بالا برای دو پلتفرم با docker buildx build --platform=linux ساخته شود /amd64,linux/arm64 . روی سیستم‌های مبتنی بر ARM64 مانند دستگاه‌های جدید Apple M1 فراخوانی شده است.

ابتدا، سازنده تصویر Alpine را برای ARM64 پایین می‌آورد، وابستگی‌های ساخت را نصب می‌کند و روی منبع کپی می‌کند. توجه داشته باشید که این مراحل فقط یک بار اجرا می شوند، حتی اگر ما برای دو پلتفرم مختلف می سازیم. BuildKit آنقدر هوشمند است که بفهمد هر دوی این پلتفرم‌ها به کامپایلر و کد منبع یکسانی وابسته هستند و به‌طور خودکار مراحل را کپی می‌کند.

اکنون دو نمونه جداگانه از کانتینرهایی که فرآیند کامپایلر را اجرا می‌کنند، با یک کد فراخوانی می‌شوند. مقدار متفاوتی به پرچم --target ارسال شد.

برای مرحله صادرات، BuildKit اکنون هر دو نسخه ARM64 و x86 تصویر Alpine را پایین می‌آورد. اگر از بسته های زمان اجرا استفاده شده باشد، نسخه های x86 با کمک لایه شبیه سازی نصب می شوند. همه این مراحل قبلاً به موازات مرحله ساخت انجام می شد زیرا وابستگی مشترکی نداشتند. به عنوان آخرین مرحله، باینری ایجاد شده توسط فرآیند کامپایلر مربوطه در مرحله کپی می‌شود.

فرمان‌های Dockerfile را در صورت اجرا در دستگاه Apple M1 اجرا می‌کنید. آبی شامل باینری‌های x86، ARM زرد است.

سپس هر دو مرحله زمان اجرا به یک تصویر OCI تبدیل می‌شوند و BuildKit یک ساختار OCI Image Index (که فهرست مانیفست نیز نامیده می‌شود) آماده می‌کند که حاوی هر دوی این تصاویر است.[19659036]Go example

برای یک مثال کاربردی، اجازه دهید به یک پروژه نمونه نوشته شده در زبان برنامه نویسی Go نگاه کنیم.

یک Dockerfile چند مرحله ای معمولی که یک برنامه Go ساده را بسازد، چیزی شبیه به این است:

FROM golang: ساخت 1.17 آلپاین AS
WORKDIR /src
کپی 🀄 . .
RUN go build -o /out/myapp را اجرا کنید.

از کوهستان
COPY --from=build /out/myapp /bin

استفاده از کامپایل متقابل در Go بسیار آسان است. تنها کاری که باید انجام دهید این است که معماری هدف را با متغیرهای محیطی منتقل کنید. go build متغیرهای محیطی GOOS ، GOARCH را می‌فهمد. همچنین GOARM برای تعیین نسخه ARM برای سیستم‌های 32 بیتی وجود دارد. مقادیر  و TARGETARCH که قبلاً دیدیم که BuildKit در Dockerfile در دسترس قرار می‌دهد.

وقتی همه مراحلی را که قبلاً یاد گرفته‌ایم اعمال می‌کنیم: اصلاح مرحله ساخت به پلتفرم ساخت، تعریف 459 0[19AR] متغیرهای TARGET*، و با ارسال پارامترهای کامپایل متقابل به کامپایلر، خواهیم داشت:

FROM --platform=$BUILDPLATFORM golang:1.17-buildal-pine
WORKDIR /src
کپی 🀄 . .
ARG TARGETOS TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/myapp .

از کوهستان
COPY --from=build /out/myapp /bin

همانطور که می بینید ما فقط نیاز به سه تغییر کوچک داشتیم و Dockerfile ما بسیار قدرتمندتر است. توجه داشته باشید که هیچ نقطه ضعفی برای این تغییرات وجود ندارد، Dockerfile هنوز قابل حمل است و در تمام معماری ها کار می کند. همین الان که ما برای یک معماری غیر بومی می سازیم، ساخت های ما بسیار سریعتر هستند.

بیایید به چند بهینه سازی اضافی که ممکن است بخواهید در نظر بگیرید نیز نگاه کنیم. یا شامل منابع وابستگی‌ها در فهرست راهنمای فروشنده یا اگر پروژه آنها شامل چنین فهرستی نباشد، کامپایلر Go وابستگی‌های فهرست شده در فایل go.mod را می‌کشد در حالی که فایل فرمان go build در حال اجرا است.

در مورد دوم، به این معنی است که (اگرچه کد منبع خودمان فقط یک بار کپی شده است) زیرا فرآیند go build دو بار برای k فراخوانی شده است. ساخت چند پلت فرم ما، این وابستگی ها نیز دو بار کشیده می شوند. بهتر است قبل از اینکه مرحله ساخت خود را با فرمان  ARG TARGETARCH انشعاب دهیم، به Go برای دانلود این وابستگی ها بگویید.
WORKDIR /src
go.mod go.sum را کپی کنید.
دانلود مود را اجرا کنید
کپی. .
ARG TARGETOS TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH برو build -o /out/myapp .FROM alpine
COPY –from=build /out/myapp /bin

اکنون وقتی دو فرآیند go build اجرا می‌شوند، از قبل به وابستگی‌های از پیش کشیده شده دسترسی دارند. ما همچنین فقط فایل‌های go.mod و go.sum را قبل از دانلود بسته‌ها کپی کردیم تا زمانی که کد منبع معمولی ما تغییر می‌کند، حافظه پنهان را برای بارگیری‌های ماژول باطل نکنیم.[196590] ]برای کامل‌تر شدن، اجازه دهید مانت‌های کش را در داخل Dockerfile خود نیز قرار دهیم. RUN --mount گزینه‌ها اجازه می‌دهد نقاط نصب جدید را در معرض فرمانی قرار دهید که ممکن است برای دسترسی به کد منبع، اسرار ساخت، دایرکتوری‌های موقت و حافظه پنهان استفاده شود. نصب‌های کش دایرکتوری‌های دائمی ایجاد می‌کنند که در آن می‌توانید فایل‌های کش مخصوص برنامه خود را بنویسید که دفعه بعد که سازنده را دوباره فراخوانی کردید دوباره ظاهر می‌شوند. هنگامی که پس از ایجاد تغییرات در کد منبع خود، ساخت‌های افزایشی را انجام می‌دهید، باعث افزایش عملکرد می‌شود.

در Go، دایرکتوری‌هایی که می‌خواهید به نصب کش تبدیل کنید عبارتند از /root/.cache/go-build و /go/pkg . اولی محل پیش‌فرض حافظه پنهان ساخت Go است و دومی جایی است که go mod ماژول‌ها را دانلود می‌کند. این فرض می‌کند کاربر شما root و GOPATH /go  است.

 .

 .

 .

. .cache/go-build 
 --mount=type=cache,target=/go/pkg 
    GOOS=$TARGETOS GOARCH=$TARGETARCH برو build -o /out/myapp.

همچنین می‌توانید از یک type=bind mount (نوع پیش‌فرض) برای نصب در کد منبع خود استفاده کنید. این کمک می‌کند تا از هزینه کپی واقعی فایل‌ها با COPY جلوگیری شود. در کامپایل متقابل در Dockerfile، گاهی اوقات مهم است که نخواهید منبع خود را قبل از تعریف ARG TARGETPLATFORM کپی کنید، زیرا تغییرات در کد منبع باعث بی اعتبار شدن حافظه پنهان وابستگی‌های خاص هدف شما می‌شود. توجه داشته باشید که پایه‌های type=bind به‌طور پیش‌فرض فقط خواندنی نصب می‌شوند. اگر دستوراتی که اجرا می‌کنید نیاز به نوشتن فایل‌ها در کد منبع شما دارند، ممکن است همچنان بخواهید از COPY استفاده کنید یا گزینهrw را برای mount تنظیم کنید.

این به ما منجر می‌شود. کامپایل متقابل کامل و کاملاً بهینه شده Go Dockerfile:

FROM --platform=$BUILDPLATFORM golang:1.17-alpine AS build
WORKDIR /src
ARG TARGETOS TARGETARCH
RUN --mount=target=. 
    --mount=type=cache,target=/root/.cache/go-build 
    --mount=type=cache,target=/go/pkg 
    GOOS=$TARGETOS GOARCH=$TARGETARCH برو build -o /out/myapp .

از کوهستان
COPY --from=build /out/myapp /bin

به عنوان مثال، مدت زمان لازم برای ساخت باینری Docker CLI را با Dockerfile چند مرحله‌ای که با آن شروع کردیم و سپس با بهینه‌سازی‌های اعمال شده اندازه‌گیری کردم. همانطور که می بینید، تفاوت کاملاً شدید است.

https://github.com/docker/cli زمان ساخت با Dockerfiles آزمایشی (ثانیه، کمتر بهتر است)

برای ساخت اولیه فقط برای معماری بومی ما، تفاوت حداقل است - تنها یک تغییر کوچک از عدم نیاز به اجرای دستورالعمل COPY. اما زمانی که ما یک تصویر برای CPU های ARM و x86 می سازیم، تفاوت بسیار زیاد است. برای Dockerfile جدید ما، دوبرابر کردن معماری‌ها زمان ساخت را تنها تا 70% افزایش می‌دهد (زیرا برخی از قسمت‌های ساخت‌ها به اشتراک گذاشته شده بودند)، در حالی که وقتی معماری دوم با شبیه‌سازی QEMU ساخته می‌شود، زمان ساخت ما تقریباً هفت برابر بیشتر می‌شود.

با موارد اضافی. با کمک مانت‌های کش که اضافه کردیم، بازسازی‌های تدریجی ما با تغییرات کد منبع Go در مقایسه با Dockerfile قدیمی به محدوده بهبود سرعت 100 برابری مضحک می‌رسند.