Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 44 — Multi-arch Image — ARM64 & x86

Đã đọc 0/19 mục · còn khoảng 12 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn là DevOps engineer tại một startup fintech ở TP.HCM. Nhóm dev của bạn dùng MacBook đời mới — tức là chip Apple Silicon (ARM64). Server production thì chạy trên AWS, và sếp vừa yêu cầu chuyển sang dùng instance Graviton để tiết kiệm chi phí — đó cũng là ARM64. Nhưng CI runner của GitHub Actions mặc định lại là x86 (amd64). Còn một số máy tester vẫn dùng laptop Windows/Intel cũ — x86.

Câu hỏi đặt ra: image Docker bạn build ra chạy được ở đâu? Nếu bạn build trên máy Mac M2 rồi push lên registry, image đó là ARM64. Khi CI runner x86 kéo về chạy test, nó sẽ báo lỗi exec format error. Ngược lại, image build trên CI x86 đẩy xuống Graviton production cũng dính đúng vấn đề đó — hoặc chạy được nhưng qua lớp giả lập (emulation) chậm gấp 5-10 lần.

Đây chính là bài toán mà multi-arch image (image đa kiến trúc) sinh ra để giải quyết. Thay vì phải nhớ "image này cho chip nào", bạn build một tag duy nhất — ví dụ myapp:1.4.0 — chứa cả bản ARM64 lẫn x86 bên trong. Bất kỳ máy nào pull về, Docker tự động chọn đúng biến thể cho kiến trúc CPU của nó. Người dùng không cần biết, không cần quan tâm.

Trong bối cảnh 2026, đây không còn là kỹ năng "nice to have" nữa. Với việc AWS Graviton, Apple Silicon và các thiết bị edge ARM ngày càng phổ biến, một pipeline CI/CD hiện đại gần như bắt buộc phải build được multi-arch. Bài này sẽ giúp bạn hiểu bản chất, tự tay dựng pipeline, và tránh những cái bẫy khiến build chậm hàng chục phút.

Khái niệm cốt lõi

Kiến trúc CPU và vì sao image không "chạy khắp nơi"

Một Docker image thực chất chứa các file binary đã được biên dịch cho một tập lệnh CPU cụ thể. x86_64 (còn gọi là amd64) và arm64 (còn gọi aarch64) là hai họ tập lệnh khác nhau. Binary biên dịch cho x86 không thể chạy trực tiếp trên CPU ARM và ngược lại — giống như đĩa DVD vùng 1 không phát được trên đầu đĩa vùng 3.

Khi bạn viết FROM node:20 trong Dockerfile, Docker sẽ lấy biến thể phù hợp với máy đang build. Nếu không để ý, bạn dễ vô tình khóa cứng image vào một kiến trúc.

Vì sao ARM64 đang trỗi dậy mạnh mẽ

Có ba động lực chính khiến ARM64 trở thành mối quan tâm hàng đầu:

  • AWS Graviton — rẻ hơn 20-40% so với x86 với cùng hiệu năng. Đây là lý do kinh tế trực tiếp. Cùng một workload, chuyển từ instance m6i (Intel) sang m6g/m7g (Graviton) có thể cắt giảm đáng kể hóa đơn hàng tháng. Với các công ty Việt Nam đang cân đo từng đô chi phí cloud, con số này rất hấp dẫn.
  • Máy dev dùng Apple Silicon. Từ 2020 Apple chuyển sang chip M-series (ARM64). Đa số developer bây giờ code trên máy ARM. Nếu image chỉ có bản x86, dev phải chạy qua giả lập QEMU rất chậm khi test cục bộ.
  • Raspberry Pi và thiết bị edge. Các dự án IoT, edge deployment (ví dụ đặt server nhỏ tại từng cửa hàng, nhà máy) thường dùng Raspberry Pi hoặc board ARM giá rẻ. Cùng một image ứng dụng cần chạy được cả trên cloud lẫn trên các thiết bị này.

Manifest list — "danh bạ" của multi-arch image

Đây là khái niệm quan trọng nhất cần nắm. Một multi-arch image không phải là một file image khổng lồ gộp mọi thứ. Nó là một manifest list (còn gọi image index theo chuẩn OCI) — một bản kê khai nhỏ trỏ tới nhiều image con, mỗi image con gắn với một cặp os/arch cụ thể.

Cấu trúc trông như thế này:

myapp:1.4.0  (manifest list)
├── linux/amd64  → sha256:aaa... (image thật cho x86)
└── linux/arm64  → sha256:bbb... (image thật cho ARM)

Khi máy ARM64 chạy docker pull myapp:1.4.0, Docker đọc manifest list, thấy có entry linux/arm64, và chỉ kéo về image con tương ứng. Nó không tải bản x86 — tiết kiệm băng thông. Cơ chế này hoàn toàn trong suốt với người dùng.

Buildx và QEMU — hai công cụ nền tảng

Docker Buildx là plugin build thế hệ mới (dựa trên BuildKit), cho phép build cho nhiều nền tảng trong một lệnh và tự động tạo manifest list. Đây là công cụ chuẩn cho multi-arch.

QEMU là bộ giả lập cho phép máy x86 "đóng vai" CPU ARM (và ngược lại) để biên dịch. Khi bạn build ARM64 trên runner x86, QEMU sẽ giả lập từng lệnh — chạy được nhưng chậm. Đây là gốc rễ của nhiều vấn đề hiệu năng mà ta sẽ bàn ở phần lỗi thường gặp.

Tình huống thực tế

Tình huống 1 — Sàn TMĐT chuyển sang Graviton để cắt chi phí

Một công ty thương mại điện tử tại Hà Nội (giả định tên ChợNhanh) chạy khoảng 40 microservice trên EKS (Kubernetes trên AWS), toàn bộ dùng instance Intel m5.xlarge. Hóa đơn EC2 khoảng 12.000 USD/tháng. Sau khi phân tích, đội platform quyết định thử chuyển sang node group Graviton m7g.xlarge.

Vấn đề phát sinh ngay: toàn bộ image trong registry đều là x86-only. Khi pod được lịch lên node ARM, container không khởi động — báo exec format error. Đội buộc phải dừng, cấu hình lại toàn bộ pipeline CI để build multi-arch bằng Buildx.

Sau khi hoàn tất, họ chạy hỗn hợp: 70% node ARM, 30% node x86 (giữ lại cho vài service dùng thư viện native chưa hỗ trợ ARM tốt). Kết quả: hóa đơn EC2 giảm còn khoảng 8.400 USD/tháng — tiết kiệm khoảng 30%, tương đương hơn 43.000 USD/năm.

Bài học: Đừng đợi đến lúc cần migrate mới build multi-arch. Nếu ngay từ đầu image đã đa kiến trúc, việc thử nghiệm Graviton chỉ là đổi một dòng cấu hình node group thay vì phải rà lại cả pipeline. Multi-arch là "khoản đầu tư bảo hiểm" cho tương lai hạ tầng.

Tình huống 2 — Startup với đội dev toàn máy Mac M-series

Một startup SaaS ở Đà Nẵng có 8 developer, tất cả đều dùng MacBook Pro chip M1/M2. Pipeline CI trên GitHub Actions build image x86 (mặc định runner ubuntu-latest). Khi dev pull image về chạy docker compose up để dev cục bộ, Docker Desktop chạy qua giả lập x86 trên chip ARM.

Hậu quả: service startup mất 45 giây thay vì 6 giây, thi thoảng còn crash do một thư viện native không giả lập ổn định. Dev phàn nàn liên tục, năng suất giảm.

Giải pháp của họ: chuyển pipeline sang build cả linux/amd64 và linux/arm64. CI vẫn push image x86 để deploy production (lúc đó production còn dùng x86), nhưng bản ARM64 giúp dev chạy native trên máy Mac. Thời gian startup cục bộ trở lại bình thường.

Bài học: Multi-arch không chỉ phục vụ production. Trải nghiệm developer (DX) cũng quan trọng. Một image chạy native trên máy dev giúp vòng lặp phát triển nhanh và ổn định hơn nhiều so với giả lập.

Tình huống 3 — Dự án edge với Raspberry Pi tại chuỗi cửa hàng

Một chuỗi cà phê giả định (CafeChain, 120 chi nhánh) triển khai hệ thống điểm bán hàng (POS) offline-first: mỗi cửa hàng có một Raspberry Pi 4 (ARM64) chạy container ứng dụng đồng bộ dữ liệu về cloud. Trung tâm dữ liệu thì chạy x86.

Cùng một ứng dụng phải chạy trên cả hai. Nếu maintain hai tag riêng (myapp:pi và myapp:cloud), đội vận hành thường xuyên deploy nhầm — có lần đẩy nhầm bản x86 xuống Pi khiến 30 cửa hàng offline nửa ngày.

Sau khi chuyển sang một tag multi-arch duy nhất pos-app:2.1.0, mọi thiết bị chỉ cần lệnh pull giống hệt nhau, Docker tự chọn đúng biến thể. Sự cố deploy nhầm kiến trúc biến mất hoàn toàn.

Bài học: Một tag duy nhất cho mọi kiến trúc loại bỏ cả một lớp lỗi vận hành do con người. Đây là giá trị lớn nhất của multi-arch trong môi trường phân tán edge.

Hướng dẫn từng bước

Ta sẽ dựng một pipeline build và push multi-arch image lên registry bằng GitHub Actions. Đây là cách phổ biến và ổn định nhất hiện nay.

Bước 1 — Kiểm tra Dockerfile không khóa cứng kiến trúc

Tránh những dòng như RUN wget .../amd64/binary hoặc FROM --platform=linux/amd64 .... Hãy dùng các biến build tự động mà BuildKit cung cấp:

FROM --platform=$BUILDPLATFORM golang:1.22 AS builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src
COPY . .
# Cross-compile cho đúng kiến trúc đích
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /app ./cmd/server

FROM alpine:3.20
COPY --from=builder /app /app
ENTRYPOINT ["/app"]

Ở đây có một mẹo hay: stage builder chạy trên nền tảng của runner ($BUILDPLATFORM, thường là x86 — nhanh, không giả lập), nhưng Go tự cross-compile ra binary cho $TARGETARCH. Nhờ đó ta tránh được giả lập QEMU chậm chạp trong bước biên dịch.

Bước 2 — Cấu hình Buildx và QEMU trong CI

Trong file workflow .github/workflows/build.yml:

name: Build multi-arch image
on:
  push:
    tags: ['v*']

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

setup-qemu-action đăng ký khả năng giả lập ARM trên runner x86. setup-buildx-action khởi tạo builder BuildKit hỗ trợ multi-platform.

Bước 3 — Build và push với nhiều platform

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ghcr.io/myorg/myapp:${{ github.ref_name }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Điểm mấu chốt: platforms: linux/amd64,linux/arm64. Buildx sẽ build cả hai biến thể, tạo manifest list và push tất cả trong một lần. Dòng cache-from/cache-to bật cache của GitHub Actions — cực kỳ quan trọng để build lần sau nhanh hơn (sẽ nói kỹ ở phần lỗi).

Bước 4 — Kiểm tra kết quả

Sau khi pipeline chạy xong, xác minh manifest list:

docker buildx imagetools inspect ghcr.io/myorg/myapp:v1.4.0

Bạn sẽ thấy liệt kê cả linux/amd64 và linux/arm64. Nếu chỉ thấy một, tức là có gì đó sai ở bước cấu hình platform.

Bước 5 (nâng cao) — Tách build song song để tăng tốc

Với image nặng, thay vì build tuần tự hai kiến trúc trên một runner, bạn có thể dùng native runner cho từng kiến trúc: một job chạy trên runner x86, một job chạy trên runner ARM (GitHub đã cung cấp runner ARM), rồi gộp manifest ở cuối bằng docker buildx imagetools create. Cách này tránh QEMU hoàn toàn và nhanh hơn nhiều — ta sẽ đề cập ở phần mẹo.

Lỗi thường gặp & mẹo

Lỗi 1 — Build ARM qua QEMU quá chậm. Đây là than phiền phổ biến nhất. Build ARM64 trên runner x86 bằng giả lập QEMU có thể chậm gấp 5-10 lần, đặc biệt với các bước biên dịch nặng (C/C++, Rust) hoặc npm install với nhiều native module. Một build 8 phút có thể phình lên 40 phút. Cách khắc phục: (a) Ưu tiên cross-compile trong Dockerfile như ở Bước 1 để né giả lập ở bước build. (b) Nếu bắt buộc chạy code trong container khi build (ví dụ compile native), hãy dùng native ARM runner thay vì QEMU. (c) Luôn bật layer caching.

Lỗi 2 — Quên bật cache khiến mỗi lần build lại từ đầu. Không có cache-from/cache-to, mỗi lần chạy pipeline build lại toàn bộ layer cho cả hai kiến trúc. Với multi-arch, chi phí này nhân đôi. Cách khắc phục: Luôn dùng type=gha (GitHub Actions cache) hoặc registry cache. Chú ý cache phải tách theo từng platform để tránh dùng nhầm.

Lỗi 3 — docker build thường không tạo được multi-arch. Lệnh docker build cổ điển chỉ build cho một platform và không đẩy manifest list được. Nhiều người copy lệnh cũ rồi thắc mắc sao image không đa kiến trúc. Cách khắc phục: Luôn dùng docker buildx build với --platform. Muốn có manifest list thì phải --push thẳng lên registry (không load được đa platform vào local daemon).

Lỗi 4 — Base image không có bản ARM. Một số image cơ sở cũ hoặc niche chỉ có bản amd64. Khi build ARM sẽ báo no matching manifest for linux/arm64. Cách khắc phục: Kiểm tra base image bằng docker buildx imagetools inspect <image> trước. Ưu tiên official image (hầu hết đã multi-arch). Nếu thư viện native chưa hỗ trợ ARM, cân nhắc chỉ build arch mà production thực sự cần.

Lỗi 5 — Test không phủ hết kiến trúc. Build được multi-arch không có nghĩa cả hai biến thể đều chạy đúng. Có bug chỉ xuất hiện trên ARM (ví dụ khác biệt về endianness, floating point, hoặc thư viện native). Cách khắc phục: Chạy smoke test trên cả hai kiến trúc — ít nhất docker run --platform linux/arm64 myapp --version qua QEMU trong CI, lý tưởng hơn là test trên native ARM runner.

Mẹo vàng: Chỉ build những kiến trúc bạn thực sự triển khai. Nếu production chỉ dùng x86, đừng build ARM chỉ để "cho đủ" — nó làm chậm pipeline và tốn tài nguyên. Danh sách platform nên phản ánh đúng thực tế hạ tầng của bạn.

Bài tập thực hành

  • Kiểm tra một image quen thuộc. Chạy docker buildx imagetools inspect node:20 và liệt kê tất cả các cặp os/arch mà nó hỗ trợ. Ghi lại xem có linux/arm64 không.
  • Viết Dockerfile cross-compile. Lấy một app Go hoặc Node đơn giản, viết Dockerfile dùng $BUILDPLATFORM, $TARGETOS, $TARGETARCH như ở Bước 1. Mục tiêu là stage build không cần QEMU.
  • Dựng pipeline hoàn chỉnh. Tạo GitHub Actions workflow build multi-arch (linux/amd64 + linux/arm64), push lên GitHub Container Registry (ghcr.io), bật cache type=gha. Verify bằng imagetools inspect.
  • Đo hiệu năng. Chạy pipeline hai lần: lần một build ARM qua QEMU (chạy code trong container), lần hai dùng cross-compile. So sánh thời gian build và viết vài dòng nhận xét về mức chênh lệch.
  • Tình huống ra quyết định. Giả sử công ty bạn đang cân nhắc chuyển 50% workload sang Graviton để tiết kiệm chi phí. Viết một đoạn ngắn (5-7 câu) đề xuất lộ trình: cần thay đổi gì trong pipeline, rủi ro nào cần test, và kiến trúc nào nên build.

Tóm tắt

Multi-arch image cho phép một tag duy nhất phục vụ nhiều kiến trúc CPU — chủ yếu là x86 (amd64) và ARM64 (arm64) — thông qua manifest list trỏ tới các image con theo từng os/arch. Máy nào pull về sẽ tự nhận đúng biến thể, hoàn toàn trong suốt với người dùng.

Động lực chính đẩy ARM64 lên hàng đầu là: AWS Graviton rẻ hơn 20-40%, máy dev Apple Silicon, và thiết bị edge như Raspberry Pi. Trong bối cảnh chi phí cloud ngày càng được siết chặt ở các công ty Việt Nam, khả năng chạy trên Graviton là một lợi thế kinh tế thực sự.

Công cụ nền tảng là Docker Buildx (tạo manifest list, build đa platform trong một lệnh) và QEMU (giả lập kiến trúc chéo). Chìa khóa để pipeline nhanh là: ưu tiên cross-compile trong Dockerfile để né giả lập, luôn bật layer cache, và cân nhắc native ARM runner cho workload nặng.

Ba nguyên tắc thực chiến cần nhớ: (1) chỉ build những kiến trúc bạn thực sự deploy; (2) luôn verify manifest bằng imagetools inspect; (3) test trên cả hai kiến trúc vì "build được" không đồng nghĩa "chạy đúng". Nắm vững những điều này, bạn đã sẵn sàng đưa ứng dụng chạy mượt mà từ Graviton trên cloud xuống tới Raspberry Pi ngoài cửa hàng.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi