Cơn ác mộng kiến trúc lúc 2 giờ sáng
Điện thoại của tôi rung lên lúc 2 giờ sáng với cảnh báo sự cố P1. Một lập trình viên vừa đẩy một bản hotfix quan trọng, và trong khi pipeline CI/CD báo xanh, một nửa số node production của chúng tôi lại bị kẹt trong trạng thái CrashLoopBackOff. Các dòng log tiết lộ một lỗi khó hiểu và gây ức chế: standard_init_linux.go:211: exec user process caused "exec format error".
Vấn đề tuy đơn giản nhưng để lại hậu quả nặng nề. Gần đây chúng tôi đã chuyển các worker node sang AWS Graviton (ARM64) để tận dụng tỷ lệ hiệu năng trên giá thành tốt hơn 40%. Tuy nhiên, máy chủ build của chúng tôi vẫn là máy Intel (AMD64) truyền thống. Chúng tôi đã đẩy các tập lệnh x86 tới các bộ vi xử lý ARM vốn không hiểu ngôn ngữ đó. Đây là cái bẫy kinh điển của DevOps hiện đại: chúng ta làm việc trong một thế giới đa kiến trúc, nhưng các công cụ build của chúng ta thường vẫn bị kẹt lại ở quá khứ.
Tại sao Docker Image đơn kiến trúc lại thất bại
Cuộc sống từng rất đơn giản khi mọi thứ đều chạy trên AMD64. Giờ đây, ARM64 có mặt ở khắp mọi nơi. Nó cung cấp sức mạnh cho chip M3 của Apple, AWS Graviton3 và các instance Tau T2A của Google Cloud. Nếu bạn build một image trên MacBook cá nhân và đẩy nó lên máy chủ Linux, bạn sẽ gặp rủi ro không tương thích. Điều tương tự cũng xảy ra khi một CI runner build một image mà cuối cùng lại được triển khai trên một thiết bị edge hoặc Raspberry Pi.
Lỗi “Exec Format Error”
Khi bạn gặp lỗi exec format error, file thực thi (binary) bên trong container của bạn không tương thích với tập lệnh CPU của máy host. Bạn không thể ép nó hoạt động. Bạn cần một Docker image đa kiến trúc giải quyết vấn đề này bằng cách gộp nhiều nền tảng dưới một tag duy nhất. Docker image đa kiến trúc giải quyết vấn đề này bằng cách gộp nhiều nền tảng dưới một tag duy nhất.
Giải pháp: Docker Buildx và QEMU
Bạn không cần phải duy trì các Dockerfile riêng biệt hay các pipeline phức tạp cho việc này. Thay vào đó, chúng ta sử dụng Buildx và QEMU để xử lý các công việc nặng nhọc.
Buildx là gì?
Buildx là một plugin Docker CLI thay thế lệnh docker build tiêu chuẩn. Nó tận dụng engine Moby BuildKit để tạo ra các “builder instance”. Các instance này có thể biên dịch cho nhiều nền tảng cùng lúc và gói chúng vào một danh sách manifest duy nhất.
QEMU xóa nhòa khoảng cách như thế nào
Một máy AMD64 không thể chạy các tập lệnh ARM một cách thuần túy. QEMU (Quick Emulator) đóng vai trò như một lớp thông dịch. Nó cho phép Buildx chạy các lệnh không thuần túy (non-native) trong quá trình build. Ví dụ, QEMU cho phép một máy host Intel chạy npm install hoặc apt-get cho mục tiêu ARM bằng cách mô phỏng môi trường ARM thông qua phần mềm.
Triển khai thực tế: Build cho cả hai thế giới
Tôi đã sử dụng quy trình này trong môi trường production hơn hai năm. Đây là cách ổn định nhất để đảm bảo image của bạn chạy được ở mọi nơi mà không cần can thiệp thủ công.
Bước 1: Khởi tạo Multi-Arch Builder
Driver mặc định của Docker không hỗ trợ build đa kiến trúc. Bạn phải tạo một builder instance mới bằng driver docker-container để mở khóa các tính năng này.
# Tạo một builder mới có tên là 'mybuilder'
docker buildx create --name mybuilder --use
# Khởi động builder và kiểm tra các tính năng hỗ trợ
docker buildx inspect --bootstrap
Kiểm tra đầu ra để xem các nền tảng được hỗ trợ. Nếu không có linux/arm64, bạn cần kích hoạt giả lập ở bước tiếp theo.
Bước 2: Kích hoạt giả lập với QEMU
Trên hầu hết các bản phân phối Linux và các GitHub Actions runner tiêu chuẩn, bạn cần đăng ký các handler QEMU. Việc này sẽ cho nhân hệ điều hành (kernel) biết cách xử lý các file thực thi lạ.
# Đăng ký các handler binfmt_misc
docker run --privileged --rm tonistiigi/binfmt --install all
Chạy lại lệnh docker buildx ls. Giờ đây bạn sẽ thấy một danh sách dài các nền tảng được hỗ trợ, bao gồm linux/amd64, linux/arm64 và thậm chí cả linux/riscv64.
Bước 3: Build và Push
Đây là lúc sự điều phối diễn ra. Chúng ta thay thế docker build bằng docker buildx build. Flag --platform xác định các mục tiêu của chúng ta và --push sẽ gửi toàn bộ gói lên registry của bạn.
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t your-registry.com/my-app:v1.0.2 \
--push .
Docker sẽ build image hai lần—mỗi lần cho một kiến trúc. Sau đó, nó đẩy cả hai phiên bản lên registry cùng với một Danh sách Manifest (Manifest List). Khi người dùng pull my-app:v1.0.2, engine Docker sẽ tự động phát hiện CPU của máy host và tải về phiên bản chính xác. Mọi thứ hoạt động trơn tru.
Tự động hóa với GitHub Actions
Build thủ công thì ổn khi kiểm tra cục bộ, nhưng môi trường production yêu cầu sự tự động hóa. GitHub Actions cung cấp các module chính thức giúp việc thiết lập này trở nên dễ dàng.
Dưới đây là một workflow sẵn sàng cho production (.github/workflows/docker-build.yml):
name: Build Image Đa Kiến Trúc
on:
push:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Kiểm tra mã nguồn (Checkout)
uses: actions/checkout@v4
- name: Thiết lập QEMU
uses: docker/setup-qemu-action@v3
- name: Thiết lập Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Đăng nhập vào DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build và push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: user/app:latest
Các sai lầm thường gặp cần tránh
Ngay cả với những công cụ phù hợp, vẫn có một vài cái bẫy có thể làm hỏng quá trình build hoặc làm chậm tiến độ của bạn.
- Tốc độ Build: Giả lập rất ngốn tài nguyên. Build một image ARM64 trên runner AMD64 thông qua QEMU có thể chậm hơn gấp 5 lần so với build thuần túy. Nếu quá trình build 5 phút của bạn bỗng nhiên mất 25 phút, hãy cân nhắc sử dụng các runner ARM64 thuần túy.
- File thực thi bị gán cứng (Hardcoded Binaries): Tránh dùng
RUN curl -O https://example.com/tool-x86_64trong Dockerfile của bạn. Điều này sẽ làm hỏng phiên bản ARM. Khi xây dựng Docker Image, hãy sử dụng đối sốTARGETARCHđể tải về đúng file thực thi một cách linh hoạt.
# Xử lý kiến trúc một cách linh hoạt
ARG TARGETARCH
RUN curl -L https://github.com/some/tool/releases/download/v1.0/tool-${TARGETARCH} -o /usr/local/bin/tool
Kết luận
Build đa kiến trúc là một yêu cầu bắt buộc đối với hạ tầng đám mây hiện đại. Chúng loại bỏ cái cớ “chạy tốt trên máy tôi” khi máy của bạn là Mac M3 còn máy chủ lại dùng bộ vi xử lý Xeon. Bằng cách tích hợp Buildx và QEMU vào pipeline CI/CD, bạn giúp phần mềm của mình có khả năng di động và không bị lỗi thời trong tương lai. Cuối cùng, bạn có thể ngừng lo lắng về các sự cố do kiến trúc gây ra và tập trung vào việc phát triển tính năng.

