00 Linux izolasyon modeli
Container teknolojisi tamamen yeni bir buluş değildir — Linux çekirdeğinde on yılı aşkın süredir var olan namespace ve cgroup mekanizmalarının üst seviyede paketlenmesidir.
Container = cgroup + namespace + chroot
Container (Docker, Podman, LXC...)
|
├── Namespaces (görünüm izolasyonu)
│ ├── PID ns → kendi PID 1'i var
│ ├── Net ns → kendi network stack'i
│ ├── Mount ns → kendi dosya sistemi görünümü
│ ├── UTS ns → kendi hostname'i
│ ├── IPC ns → kendi mesaj kuyruğu
│ └── User ns → UID/GID eşlemesi
│
├── cgroups v2 (kaynak sınırlama)
│ ├── memory.max → bellek limiti
│ ├── cpu.max → CPU kota
│ └── io.max → disk I/O limiti
│
└── chroot / pivot_root (rootfs izolasyonu)
└── /container/rootfs → /
Neden cgroup + namespace birlikte kullanılır?
Namespace yalnızca görünüm izolasyonu sağlar — bir process kendi PID namespace'inde PID 1'dir ama fiziksel CPU'yu sınırsız kullanabilir. cgroup ise kaynak kullanımını sınırlar ancak tek başına görünüm izolasyonu sağlamaz. İkisi birlikte gerçek container izolasyonu oluşturur.
| Mekanizma | Sağladığı | Kernel Sürümü |
|---|---|---|
| chroot | Dosya sistemi kökü izolasyonu | Unix v7 (1979) |
| cgroups v1 | Kaynak sınırlama (hiyerarşik) | Linux 2.6.24 (2008) |
| Namespaces | PID/Net/Mount/UTS/IPC/User izolasyonu | Linux 3.8 (2013) |
| cgroups v2 | Unified hiyerarşi, gelişmiş kontroller | Linux 4.5 (2016) |
| seccomp | Sistem çağrısı filtreleme | Linux 3.5 (2012) |
01 Namespace türleri
Linux 7 farklı namespace türüne sahiptir. Her biri process'e belirli bir sistem kaynağının izole görünümünü sunar.
Namespace türleri ve unshare kullanımı
unshare ile namespace oluşturma
# Yeni PID + mount namespace'de bash başlat
unshare --pid --mount --fork /bin/bash
# Tüm namespace türlerini izole et
unshare --pid --mount --net --uts --ipc --user \
--map-root-user --fork /bin/bash
# UTS namespace'de hostname değiştir (host etkilenmez)
unshare --uts /bin/bash
hostname container-01
hostname # container-01
# Başka terminal: hostname → değişmedi
# Mevcut process namespace'lerini listele
ls -la /proc/self/ns/
# lrwxrwxrwx 1 root root 0 ... cgroup -> cgroup:[4026531835]
# lrwxrwxrwx 1 root root 0 ... ipc -> ipc:[4026531839]
# lrwxrwxrwx 1 root root 0 ... mnt -> mnt:[4026531840]
# lrwxrwxrwx 1 root root 0 ... net -> net:[4026531840]
# lrwxrwxrwx 1 root root 0 ... pid -> pid:[4026531836]
# lrwxrwxrwx 1 root root 0 ... user -> user:[4026531837]
# lrwxrwxrwx 1 root root 0 ... uts -> uts:[4026531838]
nsenter ile namespace'e giriş
# Çalışan bir container'ın (PID 1234) namespace'ine gir
nsenter --target 1234 --pid --mount --net /bin/bash
# Sadece network namespace'e gir
nsenter --target 1234 --net ip addr show
# Docker container'ına nsenter ile giriş
CPID=$(docker inspect --format '{{.State.Pid}}' my-container)
nsenter --target $CPID --pid --mount --net --uts bash
02 cgroups v2 unified hierarchy
cgroups v2, tüm controllerları tek bir hiyerarşik ağaç altında birleştirerek v1'in çoklu hiyerarşi karmaşıklığını ortadan kaldırır.
v1 vs v2 karşılaştırması
| Özellik | cgroups v1 | cgroups v2 |
|---|---|---|
| Hiyerarşi | Controller başına ayrı ağaç | Tek unified ağaç |
| Bağlama noktası | /sys/fs/cgroup/memory/, /sys/fs/cgroup/cpu/ ... | /sys/fs/cgroup/ tek nokta |
| Thread desteği | Thread granülasyon yok | Thread mode controller |
| Pressure | Yok | PSI (Pressure Stall Information) |
| Delegation | Karmaşık | Temiz, güvenli delegation |
cgroup v2 filesystem yapısı
# cgroup v2 bağlı mı kontrol et
mount | grep cgroup
# cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,...)
# Root cgroup içeriği
ls /sys/fs/cgroup/
# cgroup.controllers cpu.stat io.stat memory.stat
# cgroup.procs cpu.weight io.weight memory.max
# cgroup.subtree_control ...
# Aktif controllerları gör
cat /sys/fs/cgroup/cgroup.controllers
# cpuset cpu io memory hugetlb pids rdma misc
# Alt gruplara hangi controller'lar delege edilecek
cat /sys/fs/cgroup/cgroup.subtree_control
# cpu io memory pids
Yeni cgroup oluşturma
# myapp cgroup oluştur
mkdir /sys/fs/cgroup/myapp
# Controller'ları etkinleştir
echo "+memory +cpu +io" > /sys/fs/cgroup/myapp/cgroup.subtree_control
# Process'i cgroup'a ekle
echo $$ > /sys/fs/cgroup/myapp/cgroup.procs
# Tüm process'leri listele
cat /sys/fs/cgroup/myapp/cgroup.procs
# Bir process'in cgroup'unu bul
cat /proc/$$/cgroup
# 0::/myapp
03 Memory controller
Memory controller, bir cgroup'un kullanabileceği fiziksel bellek ve swap miktarını sınırlar; OOM killer davranışını yapılandırır ve bellek baskısını izler.
Temel memory kontrolleri
CGRP=/sys/fs/cgroup/myapp
# Bellek limiti: 256 MB
echo $((256 * 1024 * 1024)) > $CGRP/memory.max
# Yüksek eşik (soft limit — baskı başlar): 200 MB
echo $((200 * 1024 * 1024)) > $CGRP/memory.high
# Swap limiti (bellek+swap toplamı)
echo $((512 * 1024 * 1024)) > $CGRP/memory.swap.max
# OOM killer davranışı
# 0 = normal OOM killer (cgroup içinde process seç)
# 1 = cgroup başarısız olursa kill (daha deterministik)
echo 1 > $CGRP/memory.oom.group
memory.stat ile izleme
cat /sys/fs/cgroup/myapp/memory.stat
# anon 41943040 ← anonim bellek (heap/stack)
# file 10485760 ← dosya cache
# slab 2097152 ← kernel slab allocator
# sock 0 ← socket buffer
# pgfault 10234 ← page fault sayısı
# pgmajfault 12 ← major (disk) fault sayısı
# inactive_anon 0
# active_anon 41943040
# inactive_file 5242880
# active_file 5242880
# Anlık kullanım
cat /sys/fs/cgroup/myapp/memory.current
# 52428800 → 50 MB kullanımda
PSI — Pressure Stall Information
# Bellek baskısı istatistikleri
cat /sys/fs/cgroup/myapp/memory.pressure
# some avg10=0.00 avg60=0.00 avg300=0.00 total=0
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0
# "some" = en az 1 task bekliyor
# "full" = tüm task'lar bellek için bekliyor
# avg10/60/300 = 10/60/300 saniye ortalaması (%)
memory.high yumuşak limit olarak çalışır: limit aşıldığında kernel bellekten geri kazanım (reclaim) yapar ve process yavaşlar. memory.max sert limittir ve aşıldığında OOM killer devreye girer. Önce memory.high ile uyarı almak, sonra memory.max ile sert limit koymak iyi bir pratiktir.
04 CPU controller
CPU controller, cgroup'ların CPU zamanını ağırlık (weight) veya kota (quota/period) bazlı olarak sınırlamasını sağlar.
cpu.weight — göreli ağırlık
CGRP=/sys/fs/cgroup/myapp
# CPU ağırlığı (1–10000, varsayılan 100)
# myapp'a 2x daha fazla CPU ver
echo 200 > $CGRP/cpu.weight
# İkinci cgroup oluştur
mkdir /sys/fs/cgroup/background
echo 50 > /sys/fs/cgroup/background/cpu.weight
# CPU kullanımı oranı: myapp:background = 200:50 = 4:1
# Yani myapp sistem yüklüyken 4x daha fazla CPU alır
cpu.max — mutlak kota (bandwidth control)
# FORMAT: quota period (microseconds)
# Her 100ms'de maksimum 50ms CPU kullanabilsin = %50 CPU
echo "50000 100000" > $CGRP/cpu.max
# %25 CPU limiti
echo "25000 100000" > $CGRP/cpu.max
# Sınırsız (default)
echo "max 100000" > $CGRP/cpu.max
# CPU kullanım istatistikleri
cat $CGRP/cpu.stat
# usage_usec 1234567 ← toplam CPU kullanımı (µs)
# user_usec 890000 ← user space
# system_usec 344567 ← kernel space
# nr_periods 1000 ← kota periyot sayısı
# nr_throttled 45 ← throttle olan periyot sayısı
# throttled_usec 450000 ← throttle süresi (µs)
cpuset — CPU core pinleme
# Yalnızca CPU 0 ve 1'i kullan
echo "0-1" > $CGRP/cpuset.cpus
# NUMA node 0'daki belleği kullan
echo "0" > $CGRP/cpuset.mems
# cpuset.cpus.effective — gerçekte kullanılan
cat $CGRP/cpuset.cpus.effective
05 IO controller
IO controller, blok cihazlarına erişimi ağırlık ve mutlak bant genişliği limitleriyle kısıtlar.
io.max ile mutlak limit
# Cihaz major:minor numarasını öğren
ls -la /dev/mmcblk0
# brw-rw---- 1 root disk 179, 0 /dev/mmcblk0
# major=179, minor=0
CGRP=/sys/fs/cgroup/myapp
DEV="179:0"
# Okuma: max 50 MB/s, Yazma: max 10 MB/s
echo "$DEV rbps=52428800 wbps=10485760" > $CGRP/io.max
# IOPS limiti
echo "$DEV riops=1000 wiops=100" > $CGRP/io.max
# Her ikisi birlikte
echo "$DEV rbps=52428800 wbps=10485760 riops=1000 wiops=100" > $CGRP/io.max
# Limiti sıfırla (sınırsız)
echo "$DEV rbps=max wbps=max riops=max wiops=max" > $CGRP/io.max
io.stat ile izleme
cat $CGRP/io.stat
# 179:0 rbytes=10485760 wbytes=2097152 rios=1024 wios=256 dbytes=0 dios=0
# rbytes = okunan toplam byte
# wbytes = yazılan toplam byte
# rios/wios = okuma/yazma işlem sayısı
# dbytes/dios = DISCARD istatistikleri
io.weight — göreli I/O ağırlığı
# Genel I/O ağırlığı (1–10000, varsayılan 100)
echo "default 200" > $CGRP/io.weight
# Belirli cihaz için ağırlık
echo "179:0 300" > $CGRP/io.weight
06 cgroup v2 ve systemd
systemd, cgroup v2'yi birinci sınıf olarak destekler ve her servis, slice ve scope için otomatik cgroup yönetimi yapar.
systemd cgroup hiyerarşisi
/sys/fs/cgroup/
├── system.slice/ ← sistem servisleri
│ ├── nginx.service/
│ ├── sshd.service/
│ └── myapp.service/
├── user.slice/ ← kullanıcı oturumları
│ └── user-1000.slice/
│ └── session-1.scope/
└── init.scope/ ← systemd kendisi (PID 1)
Servis kaynak limitleri (unit dosyası)
# /etc/systemd/system/myapp.service
[Unit]
Description=My Application
[Service]
ExecStart=/usr/bin/myapp
Restart=always
# CPU sınırlama
CPUWeight=200 # göreli ağırlık (varsayılan 100)
CPUQuota=50% # maksimum %50 CPU
# Bellek sınırlama
MemoryHigh=200M # soft limit
MemoryMax=256M # hard limit (OOM)
MemorySwapMax=0 # swap kullanma
# IO sınırlama
IOWeight=100
IOReadBandwidthMax=/dev/mmcblk0 50M
IOWriteBandwidthMax=/dev/mmcblk0 10M
# Görev sayısı limiti
TasksMax=100
[Install]
WantedBy=multi-user.target
systemd-cgtop ile izleme
# Gerçek zamanlı cgroup kaynak kullanımı
systemd-cgtop
# Sadece bellek sıralaması
systemd-cgtop --order=memory
# 5 saniye aralıkla
systemd-cgtop --delay=5
# Belirli servisi sorgula
systemctl show myapp.service --property=ControlGroup
# ControlGroup=/system.slice/myapp.service
# Cgroup'daki process'leri listele
systemd-cgls /system.slice/myapp.service
Geçici kaynak limiti (systemd-run)
# Tek seferlik kısıtlı komut çalıştır
systemd-run --scope \
-p MemoryMax=128M \
-p CPUQuota=25% \
-- /usr/bin/intensive-task arg1 arg2
# Transient service olarak
systemd-run --unit=my-transient \
-p MemoryMax=64M \
--remain-after-exit \
/bin/sleep 60
07 unshare ile sandbox oluşturma
unshare komutu yeni namespace'ler oluştururken, pivot_root ve capset ile güvenlik sınırlamaları eklenerek Docker benzeri bir sandbox kurulabilir.
Temel sandbox scripti
#!/bin/bash
# sandbox.sh — basit Linux sandbox
ROOTFS=${1:-/tmp/sandbox-rootfs}
CMD=${2:-/bin/sh}
# Minimal rootfs oluştur (yoksa)
if [ ! -d "$ROOTFS" ]; then
mkdir -p $ROOTFS/{bin,lib,lib64,proc,sys,dev,tmp}
# busybox'ı kopyala
cp /bin/busybox $ROOTFS/bin/
(cd $ROOTFS/bin && busybox --install .)
cp /lib/x86_64-linux-gnu/libc.so.6 $ROOTFS/lib/ 2>/dev/null || true
cp /lib64/ld-linux-x86-64.so.2 $ROOTFS/lib64/ 2>/dev/null || true
fi
# Yeni namespace'lerde çalıştır
unshare \
--mount \
--pid \
--uts \
--ipc \
--net \
--user \
--map-root-user \
--fork \
/bin/bash -c "
# Proc ve sys bağla
mount -t proc proc $ROOTFS/proc
mount -t sysfs sysfs $ROOTFS/sys
mount -t tmpfs tmpfs $ROOTFS/tmp
mount --bind /dev $ROOTFS/dev
# pivot_root ile yeni root'a geç
cd $ROOTFS
mkdir -p .old_root
pivot_root . .old_root
cd /
# Eski root'u unmount et
mount --make-rprivate /.old_root
umount -l /.old_root
rmdir /.old_root
# Hostname ayarla
hostname sandbox-\$\$
exec $CMD
"
Capabilities ile kısıtlama
#!/bin/bash
# capsh ile minimum capability ile çalıştır
# Sadece net_bind_service ve setuid bırak, diğerlerini kaldır
capsh \
--drop=cap_sys_admin,cap_sys_ptrace,cap_net_admin \
--caps="cap_net_bind_service+eip cap_setuid+eip" \
-- -c "/usr/bin/myserver"
# Çalışan process'in capability'lerini gör
cat /proc/self/status | grep Cap
# CapInh: 0000000000000000
# CapPrm: 0000000000000400
# CapEff: 0000000000000400
# CapBnd: 000000000000ffff
# CapAmb: 0000000000000000
# Decode et
capsh --decode=0000000000000400
# 0x0000000000000400=cap_net_bind_service
seccomp filtresi
# libseccomp ile tehlikeli syscall'ları engelle (C örneği)
#include <seccomp.h>
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
// ptrace'i engelle
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(ptrace), 0);
// mount'u engelle
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(mount), 0);
// kexec'i engelle
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(kexec_load), 0);
seccomp_load(ctx);
seccomp_release(ctx);
08 Pratik: Minimal container sıfırdan
Docker kullanmadan, yalnızca unshare + chroot + cgroup v2 ile çalışan minimal bir container kurulumu.
Adım 1: Alpine Linux rootfs hazırla
#!/bin/bash
# Minimal Alpine Linux rootfs indir
ROOTFS=/tmp/mycontainer
mkdir -p $ROOTFS
ALPINE_VERSION=3.19
ARCH=x86_64
# Alpine minirootfs tar'ını indir
wget -qO /tmp/alpine.tar.gz \
"https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VERSION}/releases/${ARCH}/alpine-minirootfs-${ALPINE_VERSION}.0-${ARCH}.tar.gz"
tar -xzf /tmp/alpine.tar.gz -C $ROOTFS
echo "Rootfs hazır: $(du -sh $ROOTFS | cut -f1)"
Adım 2: cgroup v2 hazırla
#!/bin/bash
CGROUP_NAME="mycontainer"
CGRP=/sys/fs/cgroup/$CGROUP_NAME
# cgroup oluştur
mkdir -p $CGRP
# Controller'ları etkinleştir
echo "+memory +cpu +pids" > /sys/fs/cgroup/cgroup.subtree_control
echo "+memory +cpu +pids" > $CGRP/../cgroup.subtree_control 2>/dev/null || true
# Limitler ayarla
echo $((128 * 1024 * 1024)) > $CGRP/memory.max # 128 MB
echo "50000 100000" > $CGRP/cpu.max # %50 CPU
echo "50" > $CGRP/pids.max # max 50 process
echo "cgroup hazır: $CGRP"
Adım 3: Container başlat
#!/bin/bash
# container-run.sh
ROOTFS=/tmp/mycontainer
CGRP=/sys/fs/cgroup/mycontainer
# Bu scriptin PID'ini cgroup'a ekle
echo $$ > $CGRP/cgroup.procs
# Namespace izolasyonu ile çalıştır
unshare \
--mount \
--pid \
--uts \
--ipc \
--net \
--fork \
/bin/bash -c "
# Mount bind'ları
mount --bind $ROOTFS $ROOTFS
mount --make-rprivate $ROOTFS
mkdir -p $ROOTFS/proc $ROOTFS/sys $ROOTFS/dev/pts
mount -t proc proc $ROOTFS/proc
mount -t sysfs sysfs $ROOTFS/sys
mount -t devpts devpts $ROOTFS/dev/pts
# Hostname
hostname mycontainer
# pivot_root
cd $ROOTFS
mkdir -p .oldroot
pivot_root . .oldroot
cd /
umount -l .oldroot
rmdir .oldroot
# DNS
echo 'nameserver 1.1.1.1' > /etc/resolv.conf
# Shell başlat
exec /bin/sh -l
"
# cgroup temizle
rmdir $CGRP 2>/dev/null || true
Adım 4: Doğrulama
# Container içinde:
# PID namespace testi
ps aux
# PID 1 = sh (kendi PID 1'imiz)
# Hostname testi
hostname
# mycontainer
# Bellek limiti testi
# Python ile 200MB ayırmayı dene
python3 -c "x = bytearray(200 * 1024 * 1024)"
# Killed (OOM — 128MB sınırını aştı)
# cgroup durumunu host'tan gör
cat /sys/fs/cgroup/mycontainer/memory.current
cat /sys/fs/cgroup/mycontainer/cpu.stat
cat /sys/fs/cgroup/mycontainer/pids.current
Network namespace'e veth çifti ekle
#!/bin/bash
# container-network.sh — veth çifti ile network bağla
CONTAINER_PID=$1
# veth çifti oluştur
ip link add veth0 type veth peer name veth1
# veth1'i container namespace'ine taşı
ip link set veth1 netns $CONTAINER_PID
# Host tarafını konfigüre et
ip addr add 10.200.0.1/24 dev veth0
ip link set veth0 up
# Container namespace içinde konfigüre et
nsenter --target $CONTAINER_PID --net -- \
ip addr add 10.200.0.2/24 dev veth1
nsenter --target $CONTAINER_PID --net -- \
ip link set veth1 up
nsenter --target $CONTAINER_PID --net -- \
ip route add default via 10.200.0.1
# NAT kur
iptables -t nat -A POSTROUTING -s 10.200.0.0/24 -j MASQUERADE
echo 1 > /proc/sys/net/ipv4/ip_forward
Container teknolojisi sihirli bir şey değildir — Linux çekirdeğinin namespace (görünüm izolasyonu) ve cgroup v2 (kaynak sınırlama) mekanizmalarının bir araya getirilmesidir. Docker ve Podman bu primitive'lerin üzerinde kullanıcı dostu bir API sağlar. Temel mekanizmaları anlamak, debug süreçlerini, güvenlik analizini ve özel sandbox gereksinimlerini çok daha kolay hale getirir.