Nội dung bài viết
Hi các bạn 👋. Mình là Hùng Anh.
Đây là bài thứ tư trong series Kubernetes Fundamentals. Ở bài trước, mình và bạn đã tìm hiểu về cơ chế Virtual Machine và ở cuối bài mình có đề cập tới việc phần lớn các server thực ra đều sử dụng Linux kernel — vậy nên việc mỗi VM đều phải chứa riêng hẳn 1 bản kernel đầy đủ có phần hơi thừa.
Container đã ra đời và có hướng tiếp cận khác. Nó vẫn tạo ra được các môi trường cô lập khác nhau nhưng tất cả đều sử dụng chung Linux kernel của máy host. Nhờ vậy mà nó đã khắc phục được những nhược điểm của cơ chế VM, đồng thời cũng tối ưu hơn cho bài toán trong đó các application của mình đều dùng chung 1 loại kernel.
Hãy cùng mình tìm hiểu kỹ hơn về Container trong bài này nhé.
Bắt đầu thôi.
1. Container là gì

Container đơn giản là 1 tiến trình bình thường trên máy host và được cô lập bởi 2 cơ chế namespace và cgroups trong Linux sandbox.
Khác với VM, container không có kernel riêng. Nó cũng không đi qua hypervisor — nó chạy thẳng trên kernel thật của host và bị cô lập khỏi những tiến trình khác đang chạy cùng trên máy host đó.
2. Container image
Để application của mình chạy được dưới dạng một container, chúng ta cần phải đóng gói application trong một container image (bước này còn gọi là build container image).
Container image là 1 package chứa toàn bộ những thứ mà app cần để chạy như source code, runtime (Node, JVM, ...), các thư viện, tiện ích hệ thống, và cả cấu hình đi kèm. Nói cách khác, toàn bộ "môi trường" để chạy được app đã được đóng gói cố định vào trong image ngay từ lúc build.
Khi chạy, container chỉ cần dùng Linux kernel từ máy host, mà tập syscall của kernel thì lại rất ổn định giữa các phiên bản kernel khác nhau, và giữa các distro khác nhau. Vì vậy cùng 1 image, dù chạy trên Ubuntu, CentOS, hay trên cloud, app đều thấy môi trường y hệt nhau — sự khác biệt về môi trường giữa các máy là gần như không có, vì thế mà câu chuyện "container máy này chạy được nhưng máy khác lại không chạy được" sẽ hiếm khi xảy ra.
3. Container Runtime

Namespace và cgroups chỉ là cơ chế có sẵn trong kernel để tạo một sandbox — vẫn cần 1 thành phần thực sự đứng ra gọi đúng các cơ chế đó để tạo, khởi động và quản lý các container. Container runtime đã được sinh ra để đảm nhiệm vai trò này.
Container Runtime Interface (CRI) là 1 chuẩn do Kubernetes tạo ra. Nó định nghĩa 1 tập API (qua gRPC) để Kubernetes có thể dễ dàng giao tiếp với container runtime. Nhờ vậy mà bất kỳ container runtime nào tuân thủ theo chuẩn này đều deploy được trên Kubernetes, và Kubernetes cũng không bị khóa cứng vào 1 runtime cụ thể nào cả. Dưới đây là 2 implementation phổ biến nhất của chuẩn này:
- containerd — tách ra từ Docker, hiện là runtime phổ biến nhất.
- CRI-O — 1 implementation tuân thủ đúng chuẩn CRI do cộng đồng Kubernetes phát triển.
4. Cách Container hoạt động
Khi 1 container khởi động, container runtime làm 1 việc khá đơn giản:
- Tạo 1 tiến trình mới trên host (việc này diễn ra nhanh, y hệt như khởi động bất kỳ chương trình nào khác).
- Dùng cơ chế namespace và cgroups của Linux sandbox để cô lập tiến trình đó.
- Namespace — container sẽ không thấy được tiến trình, file, mạng của host hay của container khác.
- cgroups — container bị giới hạn tối đa được dùng bao nhiêu CPU, RAM, disk I/O.
Các container sẽ gọi lệnh syscall thẳng tới kernel thật của host, không qua lớp giả lập nào. Đây chính là khác biệt cốt lõi so với VM, và cũng là lý do container nhẹ và khởi động nhanh hơn hẳn.
- Máy host có thấy. Với host, container chỉ là 1 tiến trình bình thường. Bạn đứng từ host chạy
pslà thấy nó ngay, chỉ khác là PID trên host sẽ khác với PID mà tiến trình tự thấy bên trong container. - Container khác không thấy. Mỗi container nằm trong 1 namespace riêng, nên dù chạy chung 1 máy, container này không có cách nào nhìn thấy tiến trình của container kia — kể cả khi tiến trình bên trong chạy với quyền root, vì root đó cũng chỉ có hiệu lực trong namespace của chính nó.
- Bản thân container cũng không thấy host. Từ bên trong container, tiến trình không thấy được tiến trình nào của host.
Tóm lại, namespace chỉ giới hạn tầm nhìn của tiến trình bên trong nó, chứ không giấu tiến trình đó khỏi host.
5. Container đã giải quyết bài toán ở bài trước như nào
Bài toán: Phần lớn các server đều dùng chung 1 loại kernel, nên việc mỗi VM đều phải chứa riêng hẳn 1 bản kernel đầy đủ có phần hơi thừa. Liệu có cách nào tối ưu hơn không?
Trả lời:
Container đã bỏ qua hoàn toàn hypervisor và guest OS. Mọi container trên cùng 1 host dùng chung đúng 1 kernel của máy host đó, thay vì mỗi cái tự có kernel riêng như VM. Nhờ vậy mà nó tránh được mọi thứ khiến VM trở nên nặng nề:
- Không cần boot cả 1 hệ điều hành (khởi động 1 container chỉ là khởi động 1 tiến trình — dưới 1 giây, không phải vài phút).
- Không tốn vài GB chỉ để nuôi sống 1 guest OS.
Cũng chính vì nhẹ hơn nên số lượng container chạy được trên cùng 1 phần cứng sẽ nhiều hơn hẳn so với VM.
6. Nhược điểm của Container
- Cô lập yếu hơn VM: Vì mọi container trên host đều dùng chung 1 kernel, 1 lỗ hổng ở kernel đó có thể ảnh hưởng tới mọi container đang chạy trên đó — khác với VM, mỗi VM có hẳn 1 kernel tách biệt hoàn toàn.
- Phụ thuộc vào kernel của host: 1 container Linux cần có Linux kernel bên dưới mới chạy được. Muốn chạy container Windows, bạn sẽ cần 1 host chạy Windows kernel.
Phần lớn các image container đều là container Linux, mà Windows kernel thì lại không chạy được container Linux. Vì vậy khi cài Docker Desktop trên Windows, nó âm thầm chạy 1 VM Linux nhẹ phía sau qua WSL2, và mọi "container Linux" bạn chạy thực chất đang chạy bên trong chiếc VM ẩn đó, không phải trực tiếp trên Windows kernel.
7. Tổng kết
Container chỉ đơn giản là 1 tiến trình bình thường, bị cô lập bằng namespace và cgroups, chạy thẳng trên chính kernel của host — không hypervisor, không guest OS. Đó là lý do nó nhẹ và nhanh nhưng trade off là sự cô lập sẽ yếu hơn VM và phụ thuộc chặt chẽ vào kernel của host.
Trên môi trường production, mình thường cần triển khai nhiều container trên nhiều server khác nhau để đảm bảo tính HA (High Availability). Nhưng nếu làm việc này thủ công thì lại tốn quá nhiều công sức và thời gian — mình cần 1 công nghệ có thể tự động điều phối, quản lý, quyết định xem container nào chạy ở đâu, restart nếu nó chết, hay scale thêm instance khi traffic tăng. Google cũng đã nhận ra vấn đề này từ rất sớm và đã cho ra mắt công nghệ Kubernetes để giải quyết đúng bài toán này. Bài sau mình sẽ cùng tìm hiểu kỹ hơn về Kubernetes và các thành phần của nó nhé.
Giờ thì tạm biệt và hẹn gặp lại.
Happy reading! 🍵
Tài liệu tham khảo
- What even is a container: namespaces and cgroups - Julia Evans
- Container Runtime Interface (CRI) - Kubernetes documentation
Bài viết liên quan
Kubernetes Cluster Architecture
1 cluster Kubernetes thực sự gồm những gì? Control Plane ra quyết định cái gì chạy ở đâu, các node thực sự chạy chúng, còn các addon lo phần còn lại.
Virtual Machine
Virtual Machine giải quyết bài toán '1 máy, 1 môi trường' của server vật lý bằng cách nào, và cách triển khai này có những nhược điểm gì?
Server vật lý và các giới hạn
Server vật lý thực sự trông như thế nào, và vì sao không thể cứ deploy mãi lên cùng 1 server?
