71 คะแนน โดย GN⁺ 2026-01-21 | 9 ความคิดเห็น | แชร์ทาง WhatsApp
  • แม้จะรัน docker run ubuntu ก็ยังคง แชร์เคอร์เนล Linux ของโฮสต์ และ Ubuntu มีหน้าที่เพียง จัดเตรียมเครื่องมือใน user space เท่านั้น
  • ผลลัพธ์ของ uname -r จะแสดงเป็น เวอร์ชันเคอร์เนลของโฮสต์ และมีเพียง /etc/os-release ที่แสดงข้อมูล Ubuntu
  • VM แต่ละตัวมีเคอร์เนลของตัวเองและใช้เวลาบูตหลายนาที ขณะที่คอนเทนเนอร์ เริ่มทำงานในระดับมิลลิวินาที และแชร์เคอร์เนลของโฮสต์ผ่าน การแยกระดับระบบปฏิบัติการโดยไม่ต้องทำ hardware virtualization จึงมี overhead ต่ำ
  • ด้วย ความเสถียรของ Linux system call ABI คอนเทนเนอร์จากดิสทริบิวชันที่หลากหลายจึงทำงานบนเคอร์เนลเดียวกันได้
  • ในสภาพแวดล้อม RAM 16GB ขีดจำกัดที่ใช้งานได้จริงคือคอนเทนเนอร์ขนาดเล็ก 50-100 ตัว, ขนาดกลาง 10-30 ตัว และ คอนเทนเนอร์ขนาดใหญ่ 5-10 ตัว
  • เหตุผลที่ต้องเข้าใจสถาปัตยกรรมนี้คือช่องโหว่ของเคอร์เนลส่งผลกับทุกคอนเทนเนอร์ และ การเลือก base image มีผลโดยตรงต่อความเข้ากันได้และความปลอดภัย

ความหมายของการ “รัน Ubuntu”

  • เมื่อรัน docker run ubuntu:22.04 จะได้ bash prompt ที่ดูเหมือน Ubuntu และสามารถใช้ apt update กับติดตั้งแพ็กเกจได้
  • แต่เมื่อรัน uname -r ภายในคอนเทนเนอร์ จะเห็นเป็น เวอร์ชันเคอร์เนลของโฮสต์ (เช่น 6.5.0-44-generic)
  • ไฟล์ /etc/os-release จะแสดงว่าเป็น Ubuntu 22.04 แต่ เคอร์เนลเป็นของเครื่องโฮสต์ และส่วนที่เป็น “Ubuntu” นั้นเป็นเพียง ไฟล์ซิสเต็มที่ประกอบเป็น user space เท่านั้น

คอนเทนเนอร์ vs เครื่องเสมือน: เปรียบเทียบสถาปัตยกรรม

  • VM ทำ virtualization ของฮาร์ดแวร์ ส่วนคอนเทนเนอร์ทำ virtualization ของระบบปฏิบัติการ
  • ความต่างหลัก:
    • เคอร์เนล: VM แต่ละตัวมีเคอร์เนลของตนเอง ส่วนคอนเทนเนอร์แชร์เคอร์เนลของโฮสต์
    • เวลาบูต: VM ใช้เวลาหลายนาที ส่วนคอนเทนเนอร์ใช้เวลาเพียง มิลลิวินาที
    • memory overhead: VM อยู่ที่ 512MB-4GB ส่วนคอนเทนเนอร์อยู่ที่ 1-10MB
    • การใช้ดิสก์: VM ใช้ 10-100GB ส่วนอิมเมจคอนเทนเนอร์ใช้ 10-500MB
    • ระดับการแยก: VM อยู่ที่ระดับฮาร์ดแวร์ ส่วนคอนเทนเนอร์อยู่ที่ระดับ โปรเซส
    • ประสิทธิภาพ: VM มี overhead ราว 5-10% ส่วนคอนเทนเนอร์ให้ ประสิทธิภาพใกล้เคียง native

องค์ประกอบจริงของ base image

  • เนื้อหาของ tarball ที่ถูกดาวน์โหลดเมื่อ pull ubuntu:22.04:
  • 1. ไบนารีที่จำเป็น (/bin, /usr/bin)

    • /bin/bash (เชลล์), /bin/ls (แสดงรายการไฟล์), /bin/cat (แสดงเนื้อหาไฟล์)
    • /usr/bin/apt (ตัวจัดการแพ็กเกจ), /usr/bin/dpkg (เครื่องมือแพ็กเกจของ Debian)
  • 2. shared library (/lib, /usr/lib)

    • glibc และ shared library อื่น ๆ ที่โปรแกรมใช้ลิงก์
    • /lib/x86_64-linux-gnu/libc.so.6 (C library - พื้นฐานของทุกโปรแกรมภาษา C)
    • ไลบรารีสำคัญอย่าง libpthread.so.0, libm.so.6
  • 3. ไฟล์คอนฟิก (/etc)

    • /etc/apt/sources.list (คลังแพ็กเกจ)
    • /etc/passwd (ฐานข้อมูลผู้ใช้)
    • /etc/resolv.conf (การตั้งค่า DNS ซึ่งโดยปกติมัก mount มาจากโฮสต์)
  • 4. ฐานข้อมูลแพ็กเกจ

    • /var/lib/dpkg/status (แพ็กเกจที่ติดตั้งแล้ว)
    • /var/lib/apt/lists/ (แคชรายการแพ็กเกจที่ใช้งานได้)
  • ไม่มี เคอร์เนล, bootloader หรือไดรเวอร์รวมอยู่ด้วย

เคอร์เนลเหมือนเดิม แต่ทุกอย่างรอบตัวเปลี่ยนไป

  • ฟังก์ชันที่ Linux kernel จัดให้มี: การจัดตารางโปรเซส, การจัดการหน่วยความจำ, การทำงานของไฟล์ซิสเต็ม, network stack, device driver, system call
  • เมื่อโปรเซสในคอนเทนเนอร์เรียก open(), read(), fork() คำขอจะถูก ส่งตรงไปยังเคอร์เนลของโฮสต์
  • เคอร์เนล ไม่รู้และไม่สนใจ ด้วยซ้ำว่าโปรเซสนั้นมาจาก “Ubuntu container” หรือ “Alpine container”
  • ความเสถียรของอินเทอร์เฟซ system call

    • Linux syscall ABI มีความเสถียรสูงมาก
    • เหตุผลที่ไบนารีที่คอมไพล์ด้วย glibc 2.31 (Ubuntu 20.04) ยังทำงานได้บนเคอร์เนลของ Ubuntu 24.04:
      • เคอร์เนลรักษา backward compatibility
      • ไม่มีการเปลี่ยนหมายเลข system call
      • มีการเพิ่มฟีเจอร์ใหม่ แต่แทบไม่ลบฟีเจอร์เดิมออก
    • นี่จึงเป็นเหตุผลที่รันคอนเทนเนอร์ Ubuntu 18.04 ได้บนโฮสต์ที่ใช้เคอร์เนล 6.5

ลองทำเอง: เคอร์เนลเดียวกัน แต่ user space ต่างกัน

  • เมื่อรันคำสั่ง query เคอร์เนลเดียวกันบนหลาย base image จะเห็นได้ว่าทุกอิมเมจ แชร์เคอร์เนลของโฮสต์
  • ubuntu:22.04, debian:12, alpine:3.19, fedora:39, archlinux:latest ต่างก็ใช้เคอร์เนลเวอร์ชันเดียวกัน (6.5.0-44-generic)
  • สิ่งที่ต่างกันในแต่ละคอนเทนเนอร์คือองค์ประกอบของ userland เช่นไบนารี uname และ libc

เหตุผลที่คอนเทนเนอร์มีประสิทธิภาพมาก

  • 1. ไม่มีการทำเคอร์เนลซ้ำซ้อน

    • VM แต่ละตัวโหลดเคอร์เนลเต็มชุดขึ้นหน่วยความจำ (ประมาณ 100-500MB)
    • VM 10 ตัวกินหน่วยความจำสำหรับ 10 เคอร์เนล แต่คอนเทนเนอร์ 10 ตัวใช้ เคอร์เนลเดียว
  • 2. เริ่มทำงานได้ทันที

    • ลำดับการบูตของ VM: BIOS → bootloader → kernel → init system → services
    • คอนเทนเนอร์ใช้เพียงการเรียก fork() และ exec() ก็มี โปรเซสพร้อมทำงานภายในมิลลิวินาที
    • การบูต VM ทั่วไป: 30-60 วินาที / การเริ่มคอนเทนเนอร์: ประมาณ 0.347 วินาที
  • 3. แชร์ image layer

    • หากรันคอนเทนเนอร์ 100 ตัวจาก ubuntu:22.04 base image layer จะมีอยู่ บนดิสก์เพียงครั้งเดียว
    • แต่ละคอนเทนเนอร์จะได้เพียง copy-on-write layer แบบบาง สำหรับเก็บการเปลี่ยนแปลง
  • 4. แชร์หน่วยความจำผ่านเคอร์เนล

    • page cache ของเคอร์เนลถูกแชร์ร่วมกัน
    • หาก 50 คอนเทนเนอร์อ่านไฟล์เดียวกัน เคอร์เนลจะ cache ไว้เพียงครั้งเดียว
    • เมื่อใช้ shared library เดียวกัน ก็สามารถ แชร์ memory page ผ่าน copy-on-write ได้

คำนวณขีดจำกัดการรันคอนเทนเนอร์

  • วิเคราะห์หน่วยความจำ (อิงจาก VM ที่มี RAM 16GB)

    • RAM รวม: 16,384 MB
    • overhead ของโฮสต์ OS: -1,024 MB
    • Docker daemon: -256 MB
    • overhead ของ container runtime: -512 MB
    • หน่วยความจำที่ใช้ได้สำหรับคอนเทนเนอร์: 14,592 MB
  • การใช้หน่วยความจำตามประเภทคอนเทนเนอร์

    • ขั้นต่ำสุด (sleep): ประมาณ 1MB
    • Alpine + แอปขนาดเล็ก: ประมาณ 25MB
    • Ubuntu + แอป Python: ประมาณ 120MB
    • Ubuntu + แอป Java: ประมาณ 500MB
    • บริการ Node.js: ประมาณ 200MB
  • ค่าสูงสุดในทางทฤษฎี

    • คอนเทนเนอร์ขั้นต่ำสุด (1MB): 14,592 ตัว
    • Alpine + แอปขนาดเล็ก (25MB): 583 ตัว
    • Ubuntu + Python (120MB): 121 ตัว
    • Java microservice (500MB): 29 ตัว
  • ขีดจำกัดในโลกจริง

    • นอกเหนือจากหน่วยความจำ ยังมีปัจจัยอื่นที่ต้องคำนึงถึง:
      • CPU scheduling: หากมีคอนเทนเนอร์แย่งกันมากเกินไป จะเกิด latency spike
      • file descriptor: ค่าเริ่มต้นของ ulimit คือ 1024
      • network port: ใช้ได้เพียง 65,535 พอร์ตสำหรับการ map พอร์ต
      • PID: ถูกจำกัดด้วย /proc/sys/kernel/pid_max (ค่าเริ่มต้น: 32,768)
      • disk I/O: มี overhead จาก OverlayFS และต้องไล่ดูหลายเลเยอร์
    • ใน VM 16GB เมื่อรัน workload จริง ขีดจำกัดที่ใช้งานได้คือ:
      • คอนเทนเนอร์ขนาดเล็ก (API, worker): 50-100 ตัว
      • คอนเทนเนอร์ขนาดกลาง (DB, cache): 10-30 ตัว
      • คอนเทนเนอร์ขนาดใหญ่ (โมเดล ML, แอป JVM): 5-10 ตัว

ความเข้ากันได้ของ Linux distribution

  • คำมั่นเรื่อง kernel ABI

    • Linux รักษา syscall interface ที่เสถียร
    • ไบนารีที่คอมไพล์สำหรับเคอร์เนลเก่ายังทำงานได้บนเคอร์เนลใหม่
    • ไบนารีของ Ubuntu 18.04 รันได้ปกติบนเคอร์เนล 6.5
  • กรณีที่ความเข้ากันได้พัง

    • ความต้องการฟีเจอร์ของเคอร์เนล: หากคอนเทนเนอร์ต้องการฟีเจอร์ที่เคอร์เนลไม่มี (เช่น io_uring ต้องการเคอร์เนล 5.1+)
    • การพึ่งพา kernel module: Wireguard ต้องใช้ wireguard kernel module, ส่วน NVIDIA container ต้องใช้ nvidia kernel driver
    • ข้อจำกัดของ seccomp/capability: หากโฮสต์บล็อก syscall ที่คอนเทนเนอร์ต้องใช้ (เช่น การใช้ ptrace ต้องใส่ --cap-add SYS_PTRACE)

แนวทางเลือก base image

Base image ขนาด ตัวจัดการแพ็กเกจ การใช้งาน
scratch 0 MB ไม่มี ไบนารี Go/Rust ที่ static compile แล้ว
alpine 7 MB apk คอนเทนเนอร์ขนาดเล็กมาก, ใช้ musl libc
distroless 20 MB ไม่มี เน้นความปลอดภัย, ไม่มีเชลล์และตัวจัดการแพ็กเกจ
debian-slim 80 MB apt สมดุลระหว่างขนาดและความเข้ากันได้
ubuntu 78 MB apt เป็นมิตรกับงานพัฒนา
fedora 180 MB dnf แพ็กเกจใหม่, SELinux
  • ควรใช้อิมเมจแต่ละแบบเมื่อไร

    • scratch: สำหรับไบนารีที่ static compile แล้ว โดยมีเพียงไบนารีและไม่มี OS เลย
    • alpine: อิมเมจขั้นต่ำที่ยังต้องการ shell access แต่ใช้ musl libc แทน glibc จึงอาจมีปัญหาความเข้ากันได้บางส่วน
    • distroless: อิมเมจ production ที่เน้นความปลอดภัย ไม่มี shell และ package manager ทำให้ดีบักยากกว่าแต่ปลอดภัยกว่า

เส้นแบ่งระหว่าง user space กับเคอร์เนล

  • สิ่งที่มาจาก base image (user space)

    • เชลล์ (/bin/bash, /bin/sh)
    • C library (glibc, musl)
    • ตัวจัดการแพ็กเกจ (apt, apk, yum)
    • ยูทิลิตีหลัก (ls, cat, grep)
    • การตั้งค่า init system (โดยมากไม่ใช่ systemd เต็มตัว)
    • ผู้ใช้และกลุ่มพื้นฐาน (/etc/passwd)
    • path ของไลบรารีและการตั้งค่าต่าง ๆ
  • สิ่งที่มาจากโฮสต์ (เคอร์เนล)

    • การจัดตารางโปรเซสและการจัดการหน่วยความจำ
    • network stack (TCP/IP, routing)
    • การทำงานของไฟล์ซิสเต็ม (อ่าน, เขียน, mount)
    • ฟีเจอร์ด้านความปลอดภัย (namespaces, cgroups, seccomp)
    • device driver (GPU, เครือข่าย, สตอเรจ)
    • การจัดการเวลาและนาฬิกา
    • การเข้ารหัสและการสร้างเลขสุ่ม
  • ภาพลวงที่สร้างโดย namespace

    • เคอร์เนลจัดเตรียม namespace เพื่อให้คอนเทนเนอร์รู้สึกเหมือนถูกแยกออกจากกัน
    • โปรเซสที่เห็นเป็น PID 1 ภายในคอนเทนเนอร์ อาจมี PID ที่สูงกว่ามากบนโฮสต์ (เช่น 45678)
    • เคอร์เนลคง mapping นี้ไว้: PID 1 ในคอนเทนเนอร์ → PID 45678 บนโฮสต์
    • นี่คือวิธีที่ การแยกทำงานได้โดยไม่ต้องมี virtualization

ความหมายในสภาพแวดล้อม production

  • 1. ช่องโหว่ของเคอร์เนลกระทบทุกคอนเทนเนอร์

    • หากเคอร์เนลของโฮสต์มีช่องโหว่ คอนเทนเนอร์ทั้งหมดก็เสี่ยงไปด้วย
    • จำเป็นต้องแพตช์โฮสต์อย่างสม่ำเสมอ
  • 2. เคอร์เนลของโฮสต์เป็นตัวจำกัดความสามารถของคอนเทนเนอร์

    • หากจะใช้ io_uring โฮสต์ต้องใช้เคอร์เนล 5.1+
    • ฟีเจอร์ eBPF ต้องใช้เคอร์เนล 4.15+ ที่เปิดออปชันบางอย่างไว้
  • 3. ความสำคัญของ glibc vs musl

    • Alpine ใช้ musl libc
    • ไบนารีบางตัวที่คอมไพล์มาสำหรับ glibc อาจรันไม่ได้
    • ตัวอย่างเช่น บน Alpine อาจเกิดข้อผิดพลาดว่าไม่พบ /lib/x86_64-linux-gnu/libc.so.6 เมื่อรัน glibc binary
  • 4. “OS” ของคอนเทนเนอร์เป็นเพียงแนวคิดเชิงการจัดระเบียบ

    • จากมุมมองของเคอร์เนล “Ubuntu container” กับ “Debian container” ไม่ต่างกันเลย
    • ทั้งหมดเป็นเพียงโปรเซสที่สร้าง syscall เท่านั้น

ความเข้าใจผิดที่พบบ่อย

  • ❌ “คอนเทนเนอร์คือ VM แบบเบา”: คอนเทนเนอร์คือโปรเซสที่มีการแยกระดับสูง ส่วน VM ทำ virtualization ของฮาร์ดแวร์และรันเคอร์เนลแยกต่างหาก
  • ❌ “แต่ละคอนเทนเนอร์มีเคอร์เนลของตัวเอง”: คอนเทนเนอร์ทั้งหมดแชร์เคอร์เนลของโฮสต์ และ “OS” ของคอนเทนเนอร์มีเพียงไฟล์ใน user space
  • ❌ “รัน Ubuntu container = รัน Ubuntu”: จริง ๆ คือการรันเครื่องมือของ Ubuntu บนเคอร์เนลของโฮสต์ หากโฮสต์เป็น Debian ก็เท่ากับกำลังรันบน Debian kernel
  • ❌ “base image มีระบบปฏิบัติการครบชุด”: base image มีเพียงเครื่องมือขั้นต่ำใน user space ไม่มีเคอร์เนล, bootloader หรือไดรเวอร์
  • ❌ “ยิ่งมีคอนเทนเนอร์มาก = ใช้หน่วยความจำมากขึ้นเสมอ”: ด้วยเลเยอร์ที่แชร์กันและ page cache ของเคอร์เนล คอนเทนเนอร์จึงมัก แชร์หน่วยความจำกันได้อย่างมีประสิทธิภาพ

สรุปประเด็นสำคัญ

  • Docker base image คือ สแนปช็อตของไฟล์ซิสเต็มที่มีองค์ประกอบใน user space ของ Linux distribution
    • ประกอบด้วยไบนารี, ไลบรารี และคอนฟิกที่ทำให้ Ubuntu ดูและใช้งานเหมือน Ubuntu
  • แต่ระบบปฏิบัติการจริงอย่าง เคอร์เนลนั้นแชร์กับโฮสต์
  • สถาปัตยกรรมนี้ทำให้เกิด:
    • เวลาเริ่มต้นระดับมิลลิวินาที (ไม่มีการบูตเคอร์เนล)
    • memory overhead ต่ำมาก (เคอร์เนลเดียว, แชร์ page ร่วมกัน)
    • ความหนาแน่นสูงในระดับใหญ่ (หลายร้อยคอนเทนเนอร์ต่อโฮสต์)
    • ประสิทธิภาพใกล้เคียง native (เรียก syscall ตรงสู่เคอร์เนล)
  • trade-off คือการแยกที่อ่อนกว่า VM เพราะคอนเทนเนอร์แชร์เคอร์เนลเดียวกัน จึงทำให้ kernel exploit กระทบทุกคอนเทนเนอร์ได้
  • สำหรับ workload ส่วนใหญ่ trade-off นี้คุ้มค่า

9 ความคิดเห็น

 
bbulbum 2026-01-22

เคอร์เนล + เครื่องมือ = ดิสทริบิวชัน
ถ้าอย่างนั้น แบบนี้ก็นับว่าเป็น Ubuntu เหมือนกันไม่ใช่หรือ..

 
sacredshine 2026-01-21

ก็เลยมีพวกบทสอนที่ให้ลองสร้าง Docker เองบน Linux โดยแยกไดเรกทอรีแล้วก็จัดการเรื่องผู้ใช้กับกลุ่มอะไรทำนองนั้นด้วยนะครับ

 
dongho42 2026-01-21

มีประโยชน์มาก

 
seunggi 2026-01-21

리눅스 네임스페이스, cgroups, 및 chroot를 사용하여 자체 Docker를 구축하세요.

ดังนั้นถ้าจะพูดแบบเวอร์ ๆ หน่อย ก็มองได้ประมาณว่า chroot + cgroup = docker

 
euphcat 2026-01-21

Actually แล้วมันค่อนข้างใกล้กับ systemd-nspawn มากกว่านิดหน่อยนะ ☝️🤓

 
hohemian 2026-01-22

จริงๆ แล้ว

 
euphcat 2026-01-22

ประชดประชัน / ถ่อมตัวแบบขำ ๆ

 
crawler 2026-01-21

น่าสนใจมากจริงๆ

แม้ว่าเนื้อหาหลักน่าจะอธิบายโดยอิงกับ LINUX เป็นหลัก แต่
ถ้ารันบน Windows ก็คงเป็นการแชร์ virtual kernel ที่สร้างขึ้นด้วย WSL2 แบบที่บทความอธิบายไว้ใช่ไหมครับ?

แล้วถ้าเกิดมีช่องโหว่ใน Docker จนสามารถแตะต้องเคอร์เนลได้ขึ้นมา อยากรู้เหมือนกันว่าควรมองว่า Windows ที่มีการทำ virtualization ซ้อนอีกชั้นหนึ่งจะปลอดภัยกว่า Linux หรือเปล่า

 
minsuchae 2026-01-22

ผมค่อนข้างตกใจกับปฏิกิริยาในคอมเมนต์ด้านบนเหมือนกันครับ
ผมนึกว่าทุกคนน่าจะรู้อยู่แล้วเลยใช้งานกันเสียอีก
ลินุกซ์เคอร์เนลใช้ของโฮสต์ ส่วนที่เหลือเป็นการนำเครื่องมือที่ใช้ในลินุกซ์ดิสทริบิวชันมาใช้งานครับ

เท่าที่ผมทราบ WSL2 รันแบบ virtualized บน Hyper-V ครับ
Windows - ลินุกซ์ใน virtual machine - แล้วค่อยมี Container อยู่ข้างในอีกที...

โดยพื้นฐานแล้ว root ภายใน Container ไม่ใช่ root ของทั้งระบบจริง ๆ เลยไม่สามารถไปจัดการเคอร์เนลได้ตามอำเภอใจครับ
แต่ถ้ามีช่องโหว่ขึ้นมาก็เป็นเรื่องใหญ่เหมือนกัน

ถ้ามองในแง่ประสิทธิภาพ ที่ Windows ช้ากว่านิดหน่อยก็เพราะต้องผ่าน virtualization เพิ่มอีกชั้นหนึ่งครับ