Bölüm 18:Sanal Makineler ve Container'lar
CPU'ya “Sen”i Katmak yazısının parçası: bilgisayarının programları nasıl çalıştırdığına doğru inen uzun bir teknik tavşan deliği.
Tüm bölümler
- Giriş
- Başlamadan Önce
- Temeller
- Kernel, User Mode ve Syscall
- Mimariler: x86, ARM ve Diğerleri
- Bellek Hiyerarşisi
- Cache Nasıl Çalışır
- Zamanı Dilimle
- İşlemciyi Hızlandıran Hileler
- Tahmin, Spekülasyon ve Spectre
- Bir Program Nasıl Çalıştırılır?
- Shell'den Kernel'e
- Bir ELF Ustasına Dönüşmek
- Bellek Aslında Sanal
- Adres Çevirisi ve TLB
- Bellek Güvenliği ve Sertleştirme
- Fork'lar ve COW'lar Hakkında Konuşalım
- Dosya Sistemi ve I/O
- Sanal Makineler ve Container'lar
- Son Söz
Buraya kadar hep tek bir bilgisayardan söz ettik: bir donanım, üstünde bir kernel, onun üstünde process’ler. Şimdi bu resmi bir kez daha katlayacağız.
Buluttan bir sunucu kiraladığında sana verilen şey fiziksel bir makine değil. Bir veri merkezinde duran çok daha büyük bir makinenin, senin için ayrılmış bir dilimi. O dilim kendini gerçek bir bilgisayar sanıyor: kendi kernel’ını açıyor, kendi diskini biçimlendiriyor, kendi ağ kartını görüyor. Hepsi doğru değil — ama hiçbiri de yalan sayılmaz.
Aynı işi yapmanın ikinci bir yolu daha var ve bugün daha yaygın olanı o: container. İkisi de sana “kendi makinen” hissi verir, ama sınırı bambaşka yerlerden çizerler. Bu bölümde ikisini de söküp bakacağız.
Güzel tarafı şu: yeni bir mekanizma öğrenmeyeceğiz. Kitap boyunca gördüğümüz üç fikir — ayrıcalık seviyeleri, adres çevirisi ve interrupt — bir kez daha, bu sefer bir kat yukarıda uygulanacak.
Bu bölümde neyi çözüyoruz?
- Bir işletim sisteminin başka bir işletim sisteminin içinde nasıl çalışabildiğini göreceğiz.
- Hypervisor’ın ne olduğunu ve belleğin neden iki kez çevrildiğini anlayacağız.
- Container’ın neden bir sanal makine olmadığını, kernel’ın hangi araçlarla bu izolasyonu kurduğunu çözeceğiz.
docker runyazdığında arkada sırayla ne olduğunu izleyeceğiz.
Temel Fikir: Kernel’a Kernel Olmadığını Söylememek
Bir işletim sistemi, donanımın tek sahibi olduğu varsayımıyla yazılır. Kendini kernel mode’da sanır, sayfa tablolarını kendi kurar, disk denetleyicisine doğrudan komut yazar.
Sanallaştırmanın bütün numarası şu: bu varsayımı bozmadan yalan söylemek.
Araya bir katman koyarsın. O katman fiziksel donanımın gerçek sahibidir. Üstünde çalışan işletim sistemi ayrıcalıklı bir iş yapmaya kalktığında — mesela sayfa tablosu register’ını değiştirmeye ya da diske komut yazmaya — işlem gerçekten donanıma gitmez. Araya giren katman onu yakalar, taklit eder ve sonucu sanki gerçekten olmuş gibi geri verir.
Bu araya giren katmanın adı hypervisor (ya da virtual machine monitor). Üstünde çalışan işletim sistemine guest, altındaki fiziksel makineye host denir.
Mekanizmanın çekirdeği 3. bölümde tanıştığımız fikrin ta kendisi. Orada user mode’daki bir programın ayrıcalıklı bir talimat çalıştırmaya kalkması durumunda CPU’nun bunu reddedip kernel’a atladığını görmüştük. Sanallaştırmada aynı şey bir kat yukarıda olur: guest kernel ayrıcalıklı bir talimat çalıştırır, CPU bunu yakalar ve kontrol hypervisor’a geçer. Hypervisor işi taklit eder, guest hiçbir şey fark etmez.
Bu düzene trap-and-emulate denir ve sanallaştırmanın tek cümlelik özetidir: yakala, taklit et, geri ver.
İki Tür Hypervisor
Hypervisor’ın nerede durduğuna göre iki aile var.
Tip 1 — doğrudan donanımın üstünde. Altında bir işletim sistemi yoktur; hypervisor’ın kendisi zaten bir çeşit kernel’dır. Sunucu ve bulut tarafında kullanılan budur: VMware ESXi, Microsoft Hyper-V, Xen. Aradan bir katman kalktığı için hem daha hızlıdır hem de saldırı yüzeyi daha dardır.
Tip 2 — normal bir işletim sisteminin üstünde. Kendi makinende çalıştırdığın VirtualBox, VMware Workstation ya da UTM bu gruptan. Guest’i sıradan bir uygulama gibi başlatırsın; host işletim sistemi çalışmaya devam eder.
Linux’un KVM’i ikisinin arasında duran ilginç bir örnek. KVM ayrı bir hypervisor değil, Linux kernel’ının kendisine eklenen bir modüldür. Yani Linux, modül yüklendiği anda tip 1 hypervisor’a dönüşür; ama aynı anda sıradan bir Linux olmaya da devam eder. Bulut sağlayıcılarının büyük kısmı bu modeli kullanır.
Windows’ta WSL2 ve Docker Desktop, macOS’ta Docker Desktop — hepsi arka planda sessizce bir sanal makine açar. Linux olmayan bir makinede “container çalıştırıyorum” dediğinde, aslında önce bir Linux sanal makinesi çalışıyor demektir.
x86’nın Sanallaştırmayla İmtihanı
Trap-and-emulate kulağa temiz geliyor ama bir şartı var: ayrıcalıklı her talimatın gerçekten trap etmesi gerekir.
x86 uzun süre bu şartı sağlamadı. Mimaride, user mode’da çalıştırıldığında hata vermek yerine sessizce farklı — ve yanlış — davranan talimatlar vardı. Guest kernel böyle bir talimatı çalıştırdığında hypervisor devreye giremiyor, guest de yanlış bir cevap alıp yoluna devam ediyordu. Yakalanamayan bir yalan, yalanın tamamını bozar.
Sektör bu duvarı iki farklı yoldan aştı:
- Binary translation. VMware’in yolu buydu: guest kernel’ın makine kodunu çalışmadan önce tarayıp sorunlu talimatları güvenli karşılıklarıyla değiştirmek. İşe yarıyordu ama karmaşık ve pahalıydı.
- Paravirtualization. Xen’in yolu: guest kernel’ı değiştirmek. Guest, sanallaştırıldığını bilir ve sorunlu talimatları hiç çalıştırmaz; onun yerine hypervisor’a doğrudan çağrı yapar. Hızlıdır ama guest’in kaynak kodunu değiştirebilmeyi gerektirir — kapalı kaynak bir işletim sistemi için işe yaramaz.
Kalıcı çözüm 2005-2006’da donanımdan geldi: Intel VT-x, AMD AMD-V. İkisi de CPU’ya yepyeni bir eksen ekledi.
Şöyle düşün: artık ring 0 ile ring 3 ayrımının yanına ikinci bir ayrım kondu. CPU ya root mode’dadır (hypervisor burada çalışır) ya da non-root mode’da (guest burada çalışır). Guest kendi içinde hâlâ ring 0 ve ring 3 kullanır — yani guest kernel gerçekten kendini ring 0’da görür, yalana gerek kalmaz. Ama tüm o non-root dünya, hypervisor’ın çizdiği sınırların içindedir.
Guest ayrıcalıklı bir iş yapmaya kalktığında CPU VM exit üretir: non-root’tan çıkılır, kontrol hypervisor’a geçer. Hypervisor işini bitirince VM entry ile geri döner. Adım adım izleyelim:
- Guest çalışıyor. Sanal makinenin içindeki kernel non-root mode'da yürüyor. Kendi içinde ring 0'dadır ve sanallaştırıldığının farkında değildir.
- Ayrıcalıklı bir iş isteniyor. Guest kernel gerçek donanıma dokunmaya kalkıyor — mesela bir cihaz register'ına yazmaya. Fiziksel makinede bu iş gerçekten yapılırdı.
- VM exit. CPU işlemi donanıma geçirmez. Guest'in bütün görünür durumunu bir kenara kaydeder, root mode'a çıkar ve kontrolü hypervisor'a verir.
- Hypervisor taklit eder. İsteği okur, kendi kurallarına göre karşılar ve sonucu guest'in göreceği yere yazar. Gerçek donanıma dokunup dokunmayacağına o karar verir.
- VM entry. Guest'in durumu geri yüklenir ve CPU non-root mode'a döner. Guest için hiçbir şey olmamıştır: istediği işin sonucu elindedir.
Bu iki geçiş, syscall’ın kernel/user geçişinin bir üst kattaki karşılığıdır — ve tıpkı onun gibi, sayısı arttıkça performansı yer.
DerinleşmeVM exit neden performansın anahtarı?
Sanal makine performansını konuşurken asıl ölçtüğün şey neredeyse her zaman VM exit sayısıdır.
Sebep şu: her exit’te CPU’nun guest’in bütün görünür durumunu bir kenara kaydedip hypervisor’ınkini yüklemesi gerekir. Register’lar, ayrıcalık durumu, adres çevirisi ayarları… 7. bölümde konuştuğumuz context switch’in daha ağır bir versiyonu.
Bu yüzden sanallaştırma tarihinin son yirmi yılı, büyük ölçüde “hangi işi exit üretmeden yapabiliriz?” sorusunun cevabıdır. Adres çevirisini donanıma taşımak, zamanlayıcı okumalarını guest içinde halletmek, ağ ve disk için exit toplayıp tek seferde işlemek — hepsi aynı hedefe hizmet eder.
Pratik sonucu şu: CPU-yoğun bir iş yükü sanal makinede fiziksel makineye çok yakın hızda koşar, çünkü hesap yaparken exit üretmez. I/O-yoğun bir iş yükü ise aradaki farkı acı biçimde hisseder.
Belleğin İki Kez Çevrilmesi
Şimdi işin en güzel kısmına geldik, çünkü doğrudan 14. bölümde öğrendiğimiz şeyin üstüne biniyor.
Bir sanal makinede iki ayrı adres çevirisi vardır:
- Guest’in içindeki bir program sanal adres kullanır. Guest kernel, kendi sayfa tablolarıyla bunu bir guest fiziksel adrese çevirir.
- Ama o “fiziksel” adres de gerçek değildir. Guest’in RAM sandığı şey, host’un bakış açısından sadece bir bellek bloğudur. Dolayısıyla ikinci bir çeviri daha gerekir: guest fiziksel adres → host fiziksel adres.
İlk yıllarda bu ikinci çeviriyi hypervisor yazılımla yapıyordu. Shadow page table denen yöntemde hypervisor, guest’in sayfa tablolarını sürekli izler ve ikisini birleştiren gizli bir tablo bakımı yapardı. Guest tablosuna her dokunduğunda bir VM exit; yani sürekli maliyet.
Donanım burada da imdada yetişti. EPT (Intel) ya da NPT (AMD) ile MMU artık iki seviyeli çeviriyi kendi başına yapabiliyor: guest’in tablosunu yürüyor, çıkan sonucu ikinci bir tabloda tekrar yürüyor, host fiziksel adresi buluyor. Hypervisor’ın araya girmesine gerek kalmıyor.
Bedeli ise artan yürüyüş maliyeti. Normalde dört seviyeli bir sayfa yürüyüşü, sanallaştırmada her seviyede ikinci bir yürüyüş daha tetikleyebilir. Kötü senaryoda tek bir çeviri için yirmiden fazla bellek erişimi. TLB’nin sanal makinelerde neden bu kadar kritik olduğunu da bu açıklıyor — TLB tuttuğu sürece bu yürüyüşün hiçbiri yaşanmıyor.
Büyük sayfaların (huge page) sanal makinelerde önerilmesinin sebebi tam olarak bu. Yürüyüş maliyeti iki katına çıkmışken, yürüyüş sayısını azaltmak iki kat değerli hâle geliyor.
Sahte Donanım: Cihazlar Nereden Geliyor?
Guest’in gördüğü disk ve ağ kartı da gerçek değil. Burada üç yaklaşım var ve üçü de farklı bir takas yapıyor.
Öykünme (emulation). Hypervisor, gerçekten var olan eski bir donanımın davranışını yazılımla taklit eder — mesela yıllardır üretilmeyen bir ağ kartını. Avantajı, guest işletim sisteminin sürücüsünün zaten elinde olması; hiçbir şey kurmana gerek kalmaz. Dezavantajı, her register erişiminin bir VM exit üretmesi. Yavaştır.
Paravirtualize cihazlar (virtio). Guest’e “bu gerçek bir donanım değil, o yüzden gerçek donanım gibi davranmayalım” denir. Guest’e virtio sürücüsü kurulur ve hypervisor ile guest, paylaşılan bellek üzerinde bir halka tampon üzerinden konuşur. 17. bölümde io_uring için anlattığım fikrin aynısı: isteği belleğe yaz, karşı taraf oradan alsın, syscall’a — burada exit’e — gerek kalmasın. Bugün bulutta gördüğün disk ve ağ neredeyse her zaman virtio’dur.
Doğrudan geçiş (passthrough). Fiziksel bir cihaz tamamen guest’e verilir; hypervisor aradan çıkar. En hızlı yol budur ve GPU ile yüksek performanslı ağ kartlarında kullanılır. Bedeli esneklik: o cihaz artık başka hiçbir guest tarafından kullanılamaz, makineler arası taşıma da zorlaşır.
Container: Donanımı Değil, Görüntüyü Taklit Etmek
Şimdi ikinci yola geçelim ve baştan net bir cümleyle başlayalım: container bir sanal makine değildir. Hatta daha da net bir şey söyleyeyim — kernel’da “container” diye bir özellik yoktur. Ne böyle bir syscall vardır, ne böyle bir veri yapısı.
Sanal makinede hypervisor donanımı taklit eder ve guest kendi kernel’ını açar. Bedeli, her sanal makinenin bir bilgisayarın bütün masrafını yeniden ödemesidir: kendi kernel’ı, kendi sürücüleri, kendi açılış süreci.
Container’da böyle bir şey yok. Aynı Linux makinesindeki bütün container’lar tek bir kernel’ı paylaşır. Kernel her birine “senin göreceğin process listesi bu, senin ağın bu, senin kök dizinin burası” der ve iş biter. Yeni bir kernel açılmadığı için başlatma maliyeti neredeyse sıfırdır; çalıştırdığın şey en baştan beri sıradan bir Linux process’iydi.
Bunu somutlaştıran şu deneyi hayal et: bir container’ın içinde ps çalıştırdığında iki üç process görürsün. Aynı anda host makinede ps çalıştırırsan, o container’ın process’lerini diğer bütün process’lerin arasında, sıradan process’ler olarak görürsün. Aynı process’ler, iki farklı pencereden bakıldığında iki farklı dünya. Container dediğimiz şey tam olarak o pencerenin kendisi.
Kernel Bu Pencereyi Nasıl Kuruyor?
Pencereyi kuran dört araç var ve dördü de container’lar icat edilmeden önce, bambaşka işler için yazılmıştı.
Namespace: Kim Neyi Görsün?
Namespace (ad alanı), kernel’ın aynı kaynağı farklı process gruplarına farklı görünür hâle getirmesidir. Linux’ta birkaç tür vardır ve her biri tek bir şeyi izole eder:
| Namespace | Ne izole eder? | Container içinde sonucu |
|---|---|---|
| PID | Process numaraları | İçerideki ilk process kendini PID 1 sanır |
| NET | Ağ arayüzleri ve portlar | Kendi eth0’ı ve kendi IP’si olur |
| MNT | Mount noktaları | Kendi kök dosya sistemini görür |
| UTS | Hostname | Kendi makine adını belirleyebilir |
| IPC | Process’ler arası iletişim | Paylaşılan bellek alanları ayrışır |
| USER | Kullanıcı ve grup numaraları | İçeride root görünen kullanıcı dışarıda sıradan bir kullanıcıdır |
Bir process yeni bir namespace’e clone ya da unshare ile girer. Yani container başlatmak, aslında 16. bölümde gördüğümüz process klonlamanın birkaç ek bayrakla yapılmış hâlidir.
Son satır özellikle önemli, çünkü modern container güvenliğinin dayandığı yer orası. USER namespace sayesinde container içindeki root, dışarıda hiçbir ayrıcalığı olmayan sıradan bir kullanıcıya eşlenebilir. İçeride whoami root der, dışarıda o process’in host üzerinde hiçbir yetkisi yoktur. “Rootless container” denen şey budur ve container’dan kaçmayı ciddi biçimde zorlaştırır.
cgroup: Kim Ne Kadar Tüketsin?
Namespace “kim ne görür”ü çözer ama “kim ne kadar tüketir”i çözmez. Görünürlüğü kısıtlanmış bir process hâlâ bütün RAM’i yiyebilir.
cgroup (control group) tam olarak bunu sınırlar: bir process grubunun kullanabileceği CPU zamanını, belleği, disk bant genişliğini ve hatta açabileceği process sayısını.
Arayüzü şaşırtıcı derecede sade, çünkü dosya sistemi kılığında sunulur. /sys/fs/cgroup altında bir dizin oluşturmak yeni bir grup açmak, o dizindeki bir dosyaya sayı yazmak da sınır koymak demektir. docker run --memory=512m yazdığında olan biten tam olarak budur: runtime bir cgroup açar ve memory.max dosyasına 536870912 yazar.
Buradaki
mkdiralıştığınmkdirdeğil./sys/fs/cgroupdiskte yer kaplamaz; kernel’ın kendi veri yapılarını dosya kılığında sunduğu sanal bir dosya sistemidir. Bir dizin oluşturduğunda kernel bellekte yeni bir cgroup nesnesi yaratır. Makine kapandığında hepsi buharlaşır — zaten hiçbiri diske yazılmıyordu.
Capability: Root Yetkisini Parçalara Ayırmak
Geleneksel Unix’te bir kullanıcı ya root’tur (her şeyi yapar) ya değildir (çok az şey yapar). Capability’ler bu ikiliği parçalar.
Mekanizma sandığından ince: capability’ler kullanıcıya değil, tek tek thread’lere iliştirilmiş bit kümeleridir. Kernel mount gibi ayrıcalıklı bir işlem istendiğinde “bu kullanıcı root mu?” diye bakmaz; “bu thread’in yetki kümesinde CAP_SYS_ADMIN var mı?” diye bakar. Root olmak, artık bu bitlerin hepsinin açık olmasının kısa adıdır.
Container runtime’ları bir container’ı başlatırken bu bitlerin çoğunu düşürür. Sonuç, içeride kendini root sanan ama sistem saatini değiştiremeyen, kernel modülü yükleyemeyen ve yeni bir dosya sistemi mount edemeyen bir process’tir.
seccomp: Hangi Kapılar Açık Kalsın?
Son katman en dar olanı. seccomp, bir process’in hangi syscall’ları yapabileceğini kısıtlar. Runtime, container’ı başlatmadan önce kernel’a bir filtre verir; o filtre bundan sonraki her syscall’dan önce çalışır ve tek bir karar döndürür: izin ver, hata döndür ya da process’i öldür.
İki özelliği tasarımın tamamını belirler. Tek yönlüdür — bir kez uygulanan filtre kaldırılamaz, yalnızca daraltılabilir; bu yüzden bir process kendi ayrıcalığını güvenle düşürebilir. Ve miras alınır — fork ve execve sonrası geçerli kalır; bu yüzden runtime filtreyi kurup ardından asıl uygulamayı exec ile başlatır, uygulama hiçbir şey yapmadan kısıtlanmış olarak doğar.
Kazanç saldırı yüzeyini daraltmaktır: uygulamanın kendisi ele geçirilse bile kernel’a ulaşabildiği kapı sayısı azalmıştır.
Container İmajı Nedir?
Günlük hayatta en çok karşılaştığın parçayı da yerine oturtalım: imaj.
Bir container imajı, bir programın çalışması için gereken her şeyi içeren dondurulmuş bir dosya sistemidir. Programın kendisi, bağlı olduğu kütüphaneler, yapılandırma dosyaları, hatta /etc ve /usr dizinlerinin tamamı. İçinde olmayan tek şey kernel’dır — çünkü onu host’tan ödünç alacak.
Bunu kurulum paketiyle karıştırmamak önemli. Bir .deb ya da .msi paketi sisteme dokunur: dosyaları sistemin dizinlerine dağıtır ve o andan sonra sistemin hâline bağımlı olur. İmaj ise sisteme hiç dokunmaz; kendi dosya sistemini yanında getirir. “Bende çalışıyordu ama sunucuda çalışmıyor” cümlesinin ortadan kalkmasının sebebi tam olarak bu.
İmajların ikinci özelliği katmanlı olmalarıdır. Bir imaj tek bir blok değil, üst üste bindirilmiş birkaç salt-okunur katmandan oluşur: en altta işletim sisteminin temel dosyaları, üstünde kurduğun kütüphaneler, en üstte kendi uygulaman. Bunun iki somut kazancı var:
- Paylaşım. Aynı temel katmanı kullanan yirmi imaj, o katmanı diskte bir kez tutar.
- Artımlı güncelleme. Uygulamanın tek satırını değiştirdiğinde yalnızca en üst katman yeniden üretilir; alttaki yüzlerce megabaytlık katmanlar olduğu gibi kalır ve ağdan tekrar indirilmez.
Katmanların üst üste nasıl bindirildiğini 17. bölümde OverlayFS başlığında görmüştük. Akılda tutulacak tek cümle şu: imaj salt okunurdur, container ise onun üstüne eklenmiş ince ve yazılabilir bir katmandır. Container’ı sildiğinde giden şey yalnızca o ince katmandır; imaj olduğu gibi kalır.
docker run Yazdığında Sırayla Ne Oluyor?
Artık bütün parçalar elimizde. Terminale docker run -it --memory=512m ubuntu bash yazdığın anda olan biteni baştan sona dizelim:
- İmaj bulunur. Runtime önce imajın yerelde var olup olmadığına bakar; yoksa registry’den katman katman indirir.
- Katmanlar birleştirilir. Salt-okunur katmanların üstüne yazılabilir ince bir katman eklenir ve hepsi tek bir dosya sistemi gibi sunulur.
- Yeni namespace’lerle bir process klonlanır.
clone, bayraklarına bakarak child’a yeni bir PID, ağ, mount ve hostname dünyası verir. - Kaynak sınırları yazılır. Runtime bu process için bir cgroup oluşturur ve
--memory=512mgibi bayrakları o cgroup’un dosyalarına yazar. - Kök dizin değiştirilir.
pivot_rootile process’in gördüğü/, imajdan açılan dosya sistemine taşınır. Host’un dosya sistemi artık onun için var değildir. - Yetkiler kırpılır. Gereksiz capability’ler düşürülür ve seccomp filtresi kurulur.
- Program çalıştırılır. Son adım
execve: process kimliğini değiştirir vebasholur. 10. bölümde adım adım izlediğimiz akışın aynısı çalışır.
Dikkat et: bu yedi adımın hiçbirinde “container oluştur” diye bir çağrı yok. Sıradan bir process başlatılıyor; sadece başlatılırken görebileceği dünya daraltılıyor.
Bu adımlar Docker’a da özgü değil. Tamamı OCI adı verilen ortak bir standartta tarif edilir; Docker, Podman ve containerd aynı standardı uygular. Bir imajı Docker ile üretip Kubernetes altında çalıştırabilmenin sebebi bu — ikisi de aynı tarifi okuyor.
DerinleşmeKubernetes pod’u tam olarak nedir?
Pod, çoğu zaman “birkaç container’ın bir arada durması” diye anlatılır. Daha kesin bir tanım vermek mümkün: pod, bir avuç container’ın hangi namespace’leri paylaşacağına dair bir karardır.
Aynı pod’daki container’lar network, IPC ve UTS namespace’lerini ortak kullanır. Birbirlerine localhost üzerinden ulaşabilmelerinin sebebi budur — kernel’ın gözünde aynı ağ dünyasındadırlar. Buna karşılık her biri kendi PID ve mount namespace’inde durur; yani birbirlerinin process’lerini ve dosya sistemlerini görmezler.
Namespace’lerin iç içe geçmediğine dikkat et. Burada olan şey iç içe geçme değil, paylaşma. Pod diye ayrı bir kernel nesnesi yoktur; pod, bu paylaşım kararının Kubernetes tarafındaki adıdır.
Sanal Makine mi, Container mı?
İki yolu da gördük. Karşılaştırma tek bir cümleye iniyor: sanal makine donanımı taklit eder, container işletim sisteminin görüntüsünü.
| Sanal Makine | Container | |
|---|---|---|
| İzolasyon sınırı | Donanım (hypervisor) | Kernel (namespace + cgroup) |
| Kernel | Her guest kendi kernel’ını çalıştırır | Hepsi aynı kernel’ı paylaşır |
| Başlangıç | Saniyeler (tam bir açılış) | Milisaniyeler |
| Ek yük | Guest kernel + guest servisleri | Neredeyse yok |
| Farklı işletim sistemi | Mümkün (Linux üstünde Windows) | Mümkün değil |
| Bir kernel açığının etkisi | Guest’te kalır | Bütün container’ları etkileyebilir |
Seçim de buradan çıkıyor. Farklı bir işletim sistemine ihtiyacın varsa ya da güvenmediğin kod çalıştırıyorsan sanal makine gerekir; çünkü sınır donanımdadır ve aşmak çok daha zordur. Aynı kernel’a güvenebiliyorsan container hem daha hızlı hem daha ucuzdur.
Son yıllarda ikisi birbirine yaklaşıyor. Firecracker gibi mikro-VM’ler, guest’e verilen sahte donanımı asgariye indirerek sanal makine açılışını milisaniyelere düşürdü; AWS Lambda’nın altında bu var. Ters yönde ise gVisor gibi projeler, container’ın gördüğü kernel arayüzünü araya giren bir katmanla karşılayarak paylaşılan kernel riskini azaltıyor. İkisi de aynı soruya farklı uçlardan yaklaşıyor: ne kadar izolasyona, ne kadar bedelle?
Özet
Peki, ne öğrendik?
- Sanallaştırma yeni bir mekanizma değil; ayrıcalık seviyeleri, adres çevirisi ve interrupt fikirlerinin bir kat yukarıda tekrar uygulanmasıdır.
- Hypervisor, guest’in ayrıcalıklı işlemlerini yakalayıp taklit eder. Tip 1 doğrudan donanım üstünde, tip 2 bir işletim sisteminin üstünde çalışır.
- x86 uzun süre trap-and-emulate için uygun değildi; VT-x / AMD-V ile CPU’ya root/non-root ayrımı eklenince sorun donanımda çözüldü.
- Bellek iki kez çevrilir. EPT/NPT bu ikinci çeviriyi donanıma taşıdı; bedeli, sayfa yürüyüşünün belirgin biçimde pahalılaşması.
- Container bir sanal makine değildir. Kernel’da container diye bir özellik yoktur; namespace, cgroup, capability ve seccomp bir araya gelince ortaya çıkan şeye o adı veriyoruz.
- Namespace görünürlüğü, cgroup tüketimi, capability ve seccomp ise yetkiyi kısıtlar.
- İmaj salt okunur ve katmanlıdır; container, onun üstüne eklenen ince ve yazılabilir bir katmandır.
- Fark izolasyon sınırının nerede çizildiğidir: donanımda mı, kernel’da mı.
Yolun sonuna geldik. Son bölümde bilgisayarın açılışından bir programın ilk talimatını yürüttüğü ana kadar olan zincirin tamamını tek parça hâlinde toparlayacağız.
19. bölüme devam et: Son Söz