Alpine nhỏ vì nó dùng musl thay cho glibc — thư viện C mà gần như mọi bản Linux khác dùng. Đổi thư viện C không phải chuyện nhỏ như đổi tên gói: nó đổi cách chương trình của bạn cấp phát bộ nhớ, phân giải tên miền, và khởi tạo luồng.
Bài này đo ba khác biệt, và một trong ba đủ lớn để đảo ngược quyết định chọn image nền.
Cấp phát bộ nhớ đa luồng
Chương trình C: mỗi luồng cấp phát và giải phóng 400 000 lần, kích thước thay đổi từ 32 đến 544 byte, có ghi và đọc lại để trình biên dịch không vứt vòng lặp đi.
| Số luồng | glibc | musl | musl chậm hơn |
|---|---|---|---|
| 1 | 45,8 triệu lần/giây | 6,26 triệu | 7,3× |
| 2 | 93,5 triệu | 2,30 triệu | 40,7× |
| 4 | 185,4 triệu | 1,90 triệu | 97,5× |
| 8 | 342,4 triệu | 1,64 triệu | 209× |
Đọc theo cột thì thấy hai hành vi ngược nhau. glibc tăng gần tuyến tính — 45,8 lên 342,4 triệu khi từ 1 lên 8 luồng, tức gấp 7,5 lần. musl đi lùi — 6,26 xuống 1,64 triệu, tức chậm đi 3,8 lần khi có thêm luồng.
Nguyên nhân là thiết kế: glibc cấp cho mỗi luồng một vùng cấp phát riêng nên chúng không tranh nhau. musl dùng một khoá chung, nên càng nhiều luồng thì càng nhiều thời gian chờ khoá.
Con số 209 lần chỉ đúng cho tải cấp phát dày đặc như phép đo này. Ứng dụng thật ít khi làm mỗi việc cấp phát, nên đừng chờ đợi dịch vụ của bạn chậm 209 lần. Nhưng những thứ hay cấp phát và hay chạy đa luồng thì nằm đúng vào vùng nguy hiểm: JVM, Node với nhiều worker, Python đa tiến trình, và bất cứ máy chủ nào tạo đối tượng cho mỗi request.
Đây cũng là lý do bạn hay nghe "ứng dụng của tôi chạy chậm hẳn khi chuyển sang Alpine" mà không ai chỉ ra được dòng mã nào chậm — vì không có dòng nào cả, chậm nằm ở tầng dưới.
Một lưu ý về cách đo: lần chạy đầu tiên tôi được 0,00 giây và 11 tỉ lần cấp phát mỗi giây. Đó là trình biên dịch vứt cả vòng lặp đi vì kết quả không ai dùng. Phải thêm biến volatile hút giá trị đọc lại thì con số mới thật — cùng một cái bẫy đã gặp khi đo JIT của Java.
Wheel Python: lời khuyên cũ đã lỗi thời
Lời khuyên quen thuộc: đừng dùng Alpine cho Python, vì không có wheel dựng sẵn nên pip phải biên dịch từ mã nguồn. Tôi đo pandas:
| Thời gian | Dung lượng | ||
|---|---|---|---|
python:3.12-slim |
37,9 s | 87,4 MB | dùng wheel có sẵn |
python:3.12-alpine |
31,5 s | 66,1 MB | dùng wheel có sẵn |
Alpine nhanh hơn và nhỏ hơn. Từ khi có chuẩn musllinux (PEP 656), các gói phổ biến đã phát hành wheel cho musl, và pandas là một trong số đó.
Nhưng không phải gói nào cũng vậy. Thử grpcio:
slim : 5,6 s | wheel co san
alpine: THAT BAI sau 11,2 s
Lỗi đầy đủ:
FileNotFoundError: [Errno 2] No such file or directory: 'c++'
Không có wheel cho musl, pip chuyển sang biên dịch từ mã nguồn, và image Alpine không có trình biên dịch. Cách vá hiển nhiên là cài công cụ build — và đây là chỗ mọi thứ đảo ngược:
| Dung lượng | |
|---|---|
python:3.12-alpine trần |
17,7 MB |
+ build-base linux-headers |
104,7 MB |
python:3.12-slim trần |
41,5 MB |
Alpine cộng công cụ biên dịch lớn gấp 2,5 lần slim trần. Bạn chọn Alpine để image nhỏ, rồi phải thêm 87 MB công cụ để cài được gói, và kết thúc với image to hơn lựa chọn bạn đã bỏ qua.
Multi-stage build gỡ được phần dung lượng — như phần 9 đã đo, tầng build không đi vào image cuối. Nhưng thời gian biên dịch thì vẫn phải trả, mỗi lần cache hỏng.
Kết luận thực dụng: kiểm tra chính danh sách gói của bạn, đừng theo lời khuyên chung. Một dự án toàn gói có wheel musl thì Alpine tốt hơn thật; một gói duy nhất thiếu wheel là đủ đảo ngược.
Ngăn xếp luồng: 128 KB so với 8 MB
musl (alpine): ngan xep mac dinh moi luong: 131072 byte (128 KB)
glibc (debian): ngan xep mac dinh moi luong: 8388608 byte (8192 KB)
Khác 64 lần. Đây là nguồn của một lớp lỗi rất khó chẩn đoán: chương trình đệ quy sâu, phân tích cú pháp lồng nhau, hoặc thư viện dùng mảng lớn trên ngăn xếp sẽ tràn ngăn xếp trên Alpine trong khi chạy tốt ở nơi khác. Và triệu chứng thường là một Segmentation fault trần trụi, không kèm thông tin gì.
Chữa được bằng cách khai tường minh khi tạo luồng, hoặc đặt ulimit -s, nhưng phải biết mình đang tìm gì.
Còn gì nữa
Hai khác biệt tôi không đo trong bài này nhưng đáng biết để đi tìm khi có sự cố:
- Phân giải tên miền. musl xử lý
/etc/resolv.confkhác glibc, đặc biệt vớisearchdomain và với việc thử tuần tự nhiều máy chủ DNS. Trong Kubernetes, nơi tên dịch vụ dựa nhiều vàosearch, đây là nguồn lỗi có thật. - Múi giờ và locale. Alpine không cài sẵn
tzdata, nênTZ=Asia/Ho_Chi_Minhim lặng không có tác dụng cho tới khi bạnapk add tzdata.
Vậy có nên dùng Alpine không
Số đo cho một câu trả lời có điều kiện, không phải một câu khẩu hiệu:
Nên khi ứng dụng là tệp nhị phân tĩnh không dùng libc của hệ thống — Go với CGO_ENABLED=0, Rust liên kết tĩnh. Ở đó musl không tham gia lúc chạy, và bạn chỉ hưởng phần dung lượng nhỏ.
Nên khi ứng dụng ít cấp phát và ít luồng, và mọi phụ thuộc đều có wheel musl.
Cân nhắc lại khi ứng dụng đa luồng và cấp phát nhiều — bảng đầu bài là lý do. JVM là ví dụ điển hình, và các bản phân phối Java chính thức cho Alpine đều là bản riêng vì lý do này.
Đừng chọn Alpine chỉ vì con số dung lượng trên docker images. Chênh lệch giữa alpine và debian:12-slim là khoảng 23 MB — nhỏ hơn nhiều so với 87 MB công cụ biên dịch bạn có thể phải thêm vào, và nhỏ hơn rất nhiều so với cái giá của việc gỡ một lỗi tràn ngăn xếp lúc nửa đêm.
Thử ba mươi giây
Xem ứng dụng của bạn có thật sự dùng libc của hệ thống không:
docker run --rm <anh-cua-ban> sh -c 'ldd /duong/dan/toi/chuong-trinh 2>&1 | head -5'
Kết quả not a dynamic executable hoặc statically linked nghĩa là musl không tham gia lúc chạy, và mọi thứ trong bài này không ảnh hưởng tới bạn.
Còn nếu bạn đang cân nhắc chuyển sang Alpine, phép thử rẻ nhất là dựng cả hai và bắn tải:
docker build -t thu:alpine -f Dockerfile.alpine .
docker build -t thu:slim -f Dockerfile.slim .
# roi do cung mot kich ban tai len ca hai
Phần sau đo chuyện chạy container bằng người dùng không phải root: cái gì hỏng, và vá thế nào cho volume.