Kümenin Kapağını Açalım: Kubernetes Bileşenleri Birbirini Neden Hiç Aramaz?
- Murat Akpınar
- Kubernetes öğrenme yolculuğu
- 13 Ağustos 2026
İçindekiler
Önceki yazıda Kubernetes’in emir değil istenen durum aldığını, uzlaşma döngüsünün farkı sürekli kapattığını görmüştük. Orada bir söz vermiştik: kümenin kapağını açıp o döngüyü fiilen kimin çevirdiğine bakacaktık.
Kapağı açınca çoğu kişinin beklediği şey çıkmaz. Ortada merkezî bir yönetici süreç, bileşenlerin birbirine iş havale ettiği bir çağrı zinciri yoktur. Tam tersi geçerlidir: Kubernetes’te hiçbir bileşen bir diğerini aramaz. Scheduler kubelet’i tanımaz, controller-manager scheduler’a haber vermez, kubelet kimseden emir almaz. Hepsi yalnızca tek bir yere bakar — API sunucusuna — ve orada gördükleri değişikliğe tepki verir.
Bu yazıda önce o tek kuralı ve neden böyle kurulduğunu göreceğiz. Sonra control plane ve worker düğüm bileşenlerini tek tek açacak, kubectl apply dediğinizde isteğin geçtiği durakları sırayla izleyecek, ardından bileşenleri canlı kümede tek tek durdurup neyin bozulup neyin ayakta kaldığını ölçeceğiz. Sonunda da dockershim’in neden kaldırıldığına ve CRI, CNI, CSI arayüzlerinin kümenin tam olarak neresine takıldığına bakacağız.
Aşağıdaki çıktılar
argo-cp01,argo-w01veargo-w02adlı kurgusal bir üç düğümlü kubeadm kümesinden (v1.33.4) alınmış örnek değerlerdir; isimler, IP’ler, pod adları ve sürüm numaraları sizin kümenizde farklı olacaktır. Uzun çıktılarda sütunlar okunabilirlik için kısaltılmıştır. Komutlar ve davranışlar gerçektir.
Yazı boyunca geçen terimlerin kısa karşılıkları için sondaki terimler sözlüğüne bakabilirsiniz. Bu kümeyi nasıl kuracağımız serinin bir sonraki başlığı; burada henüz kurmuyoruz, kurulmuş bir kümenin içine bakıyoruz.
Tek Kural: Herkes Yalnızca API Sunucusuyla Konuşur
Kubernetes mimarisini tek cümlede özetlemek gerekseydi şu olurdu: bileşenler birbirini değil, ortak bir veritabanını dinler.
O veritabanı etcd, ona erişen tek kapı kube-apiserver. Scheduler bir pod’u bir düğüme yerleştirdiğinde kubelet’e telefon açmaz; API sunucusundaki pod kaydına düğümün adını yazar. Kubelet de scheduler’ı beklemez; API sunucusuna “bana atanmış pod’lar” diye kalıcı bir izleme (watch) açmıştır ve kayıt değişince haberi olur.
flowchart TB
K["kubectl"] --> API
subgraph CP["Control plane · argo-cp01"]
API["kube-apiserver<br/>:6443"]
ETCD[("etcd<br/>:2379")]
SCH["kube-scheduler"]
CM["kube-controller-manager"]
end
subgraph WN["Worker · argo-w01"]
KL["kubelet<br/>:10250"]
KP["kube-proxy"]
CR["containerd"]
end
API <--> ETCD
SCH <--> API
CM <--> API
KL <--> API
KP <--> API
KL --> CR
classDef kapi fill:#fef3c7,stroke:#d97706,color:#78350f
classDef hafiza fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef bilesen fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
class API kapi
class ETCD hafiza
class SCH,CM,KL,KP,CR,K bilesen
Şemadaki her okun bir ucunda kube-apiserver var; tek istisna kubelet’in yerel olarak konteyner çalışma zamanını çağırması. etcd’ye dokunan tek bileşen API sunucusudur — scheduler’ın da kubelet’in de etcd adresini bilmesine gerek yoktur, zaten erişimleri de yoktur.
Bu tasarımın üç somut sonucu var:
- Bileşenler durumsuzdur. Scheduler’ı yeniden başlatırsanız hiçbir şey kaybolmaz; hafıza etcd’de duruyor. Kaldığı yerden değil, o anki duruma bakarak devam eder.
- Sıra bağımlılığı yoktur. Controller-manager, scheduler’dan önce veya sonra açılabilir. Kimse kimseyi beklemediği için açılış sırası önemsizdir.
- Yetkilendirme tek noktadan yapılır. Her bileşen API sunucusuna kendi kimliğiyle bağlandığı için, “kubelet neye erişebilir” sorusunun cevabı tek bir yerde, RBAC kurallarındadır.
Bir uç durum dürüstlük gerektiriyor: API sunucusu kubelet’i arayan tek bileşendir. kubectl logs, kubectl exec ve kubectl port-forward çalışırken API sunucusu ilgili düğümün kubelet’ine 10250 portundan bağlanır. Yani kural “hiç kimse kimseyi aramaz” değil, **“denetleyiciler birbirini aramaz”**dır.
Control Plane: Kararı Veren Dört Bileşen
kubeadm ile kurulmuş bir kümede control plane bileşenleri statik pod olarak çalışır: düğümdeki kubelet, /etc/kubernetes/manifests/ dizinini izler ve oradaki YAML dosyalarını API sunucusuna hiç sormadan başlatır. Bu bir tavuk-yumurta çözümüdür — API sunucusunu başlatacak bileşen API sunucusuna bağımlı olamaz.
[root@argo-cp01 ~]# ls /etc/kubernetes/manifests/
etcd.yaml kube-apiserver.yaml kube-controller-manager.yaml kube-scheduler.yaml
[root@argo-cp01 ~]# kubectl get pods -n kube-system -o wide
NAME READY STATUS RESTARTS AGE NODE
etcd-argo-cp01 1/1 Running 0 9d argo-cp01
kube-apiserver-argo-cp01 1/1 Running 0 9d argo-cp01
kube-controller-manager-argo-cp01 1/1 Running 1 9d argo-cp01
kube-scheduler-argo-cp01 1/1 Running 1 9d argo-cp01
kube-proxy-8n4tz 1/1 Running 0 9d argo-w01
kube-proxy-mq7vd 1/1 Running 0 9d argo-w02
kube-proxy-zc2xk 1/1 Running 0 9d argo-cp01
coredns-6f9b8d5c4b-4jptn 1/1 Running 0 9d argo-w01
coredns-6f9b8d5c4b-h2mrx 1/1 Running 0 9d argo-w02
kube-apiserver: Kümenin Tek Kapısı ve Tek Darboğazı
API sunucusu bir REST arayüzüdür ve yaptığı iş sanıldığından dardır: gelen isteği doğrular, yetkilendirir, biçimini denetler, etcd’ye yazar, değişikliği izleyenlere duyurur. Hiçbir iş mantığı içermez — bir Deployment’ın ne anlama geldiğini bilmez, replicas: 3 yazdığınızda pod oluşturmaz. Onun için Deployment yalnızca şeması belli bir kayıttır.
Yatay ölçeklenebilir olması da buradan gelir: durum tutmadığı için önüne yük dengeleyici koyup üç kopya çalıştırabilirsiniz. Ama bu, kümedeki tek gerçek darboğazın da o olduğu anlamına gelir; API sunucusu yavaşladığında küme genelinde her şey yavaşlar, çünkü herkes ona bağlıdır.
etcd: Kümenin Tek Hafızası
etcd, Raft uzlaşı algoritmasıyla çalışan dağıtık bir anahtar-değer deposudur. Kümedeki her şey buradadır: nesneler, durumları, secret’lar, servis hesapları.
Kritik nokta çoğunluk (quorum) kuralıdır: bir yazmanın kabul edilmesi için üyelerin yarıdan fazlasının onaylaması gerekir.
| Üye sayısı | Quorum | Tolere edilen kayıp |
|---|---|---|
| 1 | 1 | 0 |
| 3 | 2 | 1 |
| 4 | 3 | 1 |
| 5 | 3 | 2 |
Tablodaki 4 satırı önemlidir: dört üye, üç üyeden daha dayanıklı değildir — ikisi de yalnızca bir kayıp tolere eder, ama dördüncü üye her yazmanın gecikmesini artırır. Bu yüzden etcd kümeleri her zaman tek sayıda kurulur.
Pratik sonucu şudur: kümenizin yedeği, etcd’nin yedeğidir. Manifest dosyalarınızı Git’te tutmanız iyidir ama yeterli değildir; Git’te olmayan her şey (dinamik olarak oluşturulmuş secret’lar, servis hesabı token’ları, operatörlerin yazdığı durum kayıtları) yalnızca etcd’dedir. Anlık görüntü almanın yolu:
[root@argo-cp01 ~]# ETCDCTL_API=3 etcdctl \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
--key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
snapshot save /var/backups/etcd-snapshot.db
{"level":"info","msg":"saved","path":"/var/backups/etcd-snapshot.db"}
Snapshot saved at /var/backups/etcd-snapshot.db
Ön koşul: etcdctl ikilisi kubeadm ile gelmez, ayrıca kurmanız gerekir; sertifika yolları da kubeadm varsayılanlarıdır. Yedeğin gerçekten işe yaradığını geri yükleme tatbikatı yapmadan bilemezsiniz — bu, serinin yedekleme başlığında ayrıca ele alınacak bir konu.
kube-scheduler: Tek Bir Alan Yazan Bileşen
Scheduler’ın işi kulağa karmaşık gelir, çıktısı ise tek bir alandır: pod’un spec.nodeName değeri. Süreç iki aşamalıdır:
- Filtreleme. Pod’un çalışamayacağı düğümler elenir: kaynağı yetmeyenler,
nodeSelector‘a uymayanlar, tolere edilmemiş taint taşıyanlar, istenen portu dolu olanlar. - Puanlama. Kalan düğümler puanlanır (boş kaynağı fazla olan, imajı zaten indirilmiş olan, aynı servisin diğer pod’larından uzak duran düğümler öne çıkar) ve en yüksek puanlı seçilir.
Sonra scheduler bir Binding nesnesi oluşturur; API sunucusu bunu pod kaydına işler. Scheduler konteyner başlatmaz, kubelet’e haber vermez, düğüme hiç bağlanmaz. Yaptığı tek şey bir alan doldurmaktır.
Bunun doğrudan bir sonucu var: spec.nodeName alanını manifest’e elle yazarsanız scheduler devre dışı kalır. Pod, düğümün kaynağı yetmese bile o düğüme atanmış sayılır ve kubelet başlatmaya çalışıp başarısız olur. Öğrenirken faydalı, üretimde tehlikeli bir kaçamaktır.
kube-controller-manager: Uzlaşma Döngüsünü Fiilen Çeviren Yer
Önceki yazıda anlattığımız uzlaşma döngüsü işte burada dönüyor. Tek bir denetleyici değil, tek süreçte paketlenmiş onlarca denetleyici var; her biri kendi nesne türünü izler ve kendi farkını kapatır.
| Denetleyici | İstenen durum | Fark görünce ne yapar |
|---|---|---|
| Deployment | Belirtilen sürümde replicas kadar pod |
Yeni bir ReplicaSet oluşturur, eskisini küçültür |
| ReplicaSet | Etiketiyle eşleşen replicas kadar pod |
Eksikse pod oluşturur, fazlaysa siler |
| Node | Her düğüm düzenli haber veriyor | Sessiz kalanı NotReady işaretler, taint basar |
| Job | Belirtilen sayıda başarılı tamamlanma | Başarısız pod’u yeniden dener |
| ServiceAccount | Her namespace’te varsayılan hesap | Eksikse oluşturur |
Denetleyicilerin birbirini çağırmadığına dikkat edin. Deployment denetleyicisi pod oluşturmaz — ReplicaSet oluşturur. Pod’ları oluşturan ReplicaSet denetleyicisidir ve o da Deployment’ın varlığından habersizdir; yalnızca “benim etiketimle eşleşen kaç pod var” sorusuna bakar. Zincir, çağrılarla değil, ortak veritabanındaki kayıtlar üzerinden kuruluyor.
Bulut ortamlarında bir beşinci bileşen daha vardır: cloud-controller-manager. Yük dengeleyici oluşturmak, düğüme bulut sağlayıcısının kimliğini işlemek, silinmiş sanal makineyi kümeden düşürmek gibi sağlayıcıya özgü işleri o üstlenir. Kendi donanımınızda kurduğunuz kümede bu bileşen yoktur — type: LoadBalancer bir servisin neden sonsuza kadar <pending> kaldığının cevabı da çoğu zaman budur.
Worker Düğüm: İşi Fiilen Yapan Üç Bileşen
Control plane karar verir, iş worker düğümlerde yapılır. Orada üç şey çalışır.
kubelet: Düğümün Tek Sorumlusu
kubelet, düğümdeki tek Kubernetes ajanıdır ve tek bir soruyu cevaplar: bu düğüme atanmış pod’lar çalışıyor mu? API sunucusuna spec.nodeName kendi adı olan pod’lar için bir watch açar, listedeki her pod için konteyner çalışma zamanını çağırır, sonuçları düzenli olarak API sunucusuna bildirir.
Kritik ayrım: kubelet konteynerlerin ebeveyni değildir. Konteynerleri containerd başlatır, süreç ağacında da onun altında dururlar. kubelet’i durdurduğunuzda çalışan konteynerler çalışmaya devam eder — az sonra bunu ölçeceğiz.
kubelet ayrıca /etc/kubernetes/manifests/ dizinini izler ve oradaki dosyaları API sunucusundan bağımsız olarak başlatır. Bunlara statik pod denir; API sunucusunda görünen kopyaları yalnızca birer aynadır (mirror pod). Bu ayrımın pratikte canınızı yakacağı yer şurası:
[root@argo-cp01 ~]# kubectl delete pod kube-apiserver-argo-cp01 -n kube-system
pod "kube-apiserver-argo-cp01" deleted
[root@argo-cp01 ~]# kubectl get pod kube-apiserver-argo-cp01 -n kube-system
NAME READY STATUS RESTARTS AGE
kube-apiserver-argo-cp01 1/1 Running 0 12s
Pod silinmedi, yalnızca aynası silindi; kubelet manifest dosyasını hâlâ gördüğü için saniyeler içinde yeniden oluşturdu. Statik bir pod’u gerçekten durdurmanın yolu manifest dosyasını dizinden çıkarmaktır, kubectl delete değil.
Konteyner Çalışma Zamanı: containerd, CRI-O ve Aradaki Arayüz
kubelet konteyner başlatmayı bilmez; bunu bir çalışma zamanına yaptırır ve arada CRI (Container Runtime Interface) adlı gRPC arayüzü vardır. Bugün yaygın iki gerçekleme containerd ve CRI-O.
[root@argo-cp01 ~]# kubectl get nodes -o wide
NAME STATUS ROLES VERSION INTERNAL-IP CONTAINER-RUNTIME
argo-cp01 Ready control-plane v1.33.4 192.168.30.21 containerd://1.7.24
argo-w01 Ready <none> v1.33.4 192.168.30.22 containerd://1.7.24
argo-w02 Ready <none> v1.33.4 192.168.30.23 containerd://1.7.24
Düğümün üstünde docker komutu yoktur; konteynerleri görmek için crictl kullanılır. Çıktıda yalnızca uygulama pod’ları değil, o düğümde çalışan sistem pod’ları da görünür:
[root@argo-w01 ~]# crictl --runtime-endpoint unix:///run/containerd/containerd.sock ps
CONTAINER IMAGE STATE NAME POD
9c4e1b7a3d052 ornek/api:1.4.2 Running api api-7d9c4b8f6c-x2ktp
f0a83d6b5c197 ornek/api:1.4.2 Running api api-7d9c4b8f6c-qn6zw
2b7d5e9c1a648 coredns Running coredns coredns-6f9b8d5c4b-4jptn
c31f8a05d7e29 kube-proxy Running kube-proxy kube-proxy-8n4tz
crictl, docker‘ın yerine geçen bir kullanıcı aracı değil, CRI soketine konuşan bir hata ayıklama aracıdır. Konteyner oluşturmak için kullanmayın: kubelet’in bilmediği bir konteyner oluşturursanız kimse onu yönetmez, uzlaşma döngüsünün dışında kalır.
kube-proxy: Service IP’sini Çekirdek Kuralına Çeviren Katman
Service nesnesinin bir ClusterIP’si vardır ama o IP hiçbir arayüze bağlı değildir — ping atarsanız cevap alamazsınız. Onu gerçeğe çeviren şey, her düğümde çalışan kube-proxy’nin çekirdeğe yazdığı yönlendirme kurallarıdır.
kube-proxy da API sunucusunu izler: Service ve EndpointSlice nesneleri değiştikçe kuralları günceller. Varsayılan mod hâlâ iptables‘tır; büyük kümeler için ipvs modu vardır ve 1.29’da alpha olarak gelen nftables arka ucu 1.33’te kararlı ilan edilmiştir. Kendi kümenizde hangisinin çalıştığını doğrulamadan varsaymayın:
[root@argo-cp01 ~]# kubectl logs -n kube-system kube-proxy-8n4tz | head -3
I0813 09:12:04.881204 1 server_linux.go:66] "Using iptables proxy"
I0813 09:12:04.913772 1 server.go:677] "Successfully retrieved node IP(s)" IPs=["192.168.30.22"]
I0813 09:12:04.986110 1 server_linux.go:170] "Using iptables Proxier"
Buradan çıkan önemli bir sonuç var: kurallar çekirdekte durduğu için, kube-proxy çökse bile mevcut Service trafiği akmaya devam eder. Bozulan şey trafik değil, kuralların güncellenmesidir — yeni pod’lar yük dağıtımına girmez, ölenler listeden çıkmaz.
kubectl apply Dediğinizde İstek Hangi Duraklardan Geçer?
Şimdi bütün bu bileşenleri tek bir olayda birleştirelim. Kümede dokuz gündür çalışan api Deployment’ına dönüp, dokuz gün önce onu ilk uyguladığımızda ne olduğunu adım adım izleyelim:
[root@argo-cp01 ~]# kubectl apply -f api-deployment.yaml
deployment.apps/api created
Bu satır ekrana düştüğünde henüz tek bir konteyner bile başlamamıştır. Olan biten şudur:
sequenceDiagram
participant U as kubectl
participant A as kube-apiserver
participant E as etcd
participant C as controller-manager
participant S as kube-scheduler
participant K as kubelet argo-w01
U->>A: POST .../deployments
A->>A: authn · authz · admission
A->>E: Deployment kaydını yaz
E-->>A: onay
A-->>U: deployment.apps/api created
Note over U,A: İstek burada bitti. Gerisi asenkron.
A-->>C: watch olayı: yeni Deployment
C->>A: ReplicaSet oluştur
A-->>C: watch olayı: yeni ReplicaSet
C->>A: 3 Pod oluştur (nodeName boş)
A-->>S: watch olayı: atanmamış Pod
S->>A: Binding · nodeName=argo-w01
A-->>K: watch olayı: bana atanmış Pod
K->>K: CRI ile konteyneri başlat
K->>A: status: Running
Durakları tek tek açalım.
| Durak | Yapan | Ne olur |
|---|---|---|
| 1. Kimlik doğrulama | apiserver | Sertifika, token veya OIDC ile kimsiniz sorusu cevaplanır |
| 2. Yetkilendirme | apiserver | RBAC: bu kimlik bu nesneye bu işlemi yapabilir mi |
| 3. Değiştiren kabul | admission webhook’ları | Nesne yazılmadan önce değiştirilebilir (sidecar enjeksiyonu, varsayılan atama) |
| 4. Şema denetimi | apiserver | Alan adları ve türleri OpenAPI şemasına uyuyor mu |
| 5. Doğrulayan kabul | admission webhook’ları | Nesne reddedilebilir, ama artık değiştirilemez |
| 6. Kalıcılaştırma | apiserver → etcd | Kayıt yazılır, sürüm (resourceVersion) artar |
| 7. Duyuru | apiserver | İzleyen bileşenlere watch olayı gider |
Sıra tesadüfi değildir: değiştiren kabul, doğrulayandan önce çalışır. Sebebi mantıklı — bir webhook nesneyi değiştirdikten sonra tekrar doğrulanması gerekir, tersi olsaydı doğrulanmış nesne doğrulama sonrası değiştirilebilirdi. Kendi kurallarınızı webhook yazmadan tanımlamak isterseniz, 1.30’da kararlı hâle gelen ValidatingAdmissionPolicy ile CEL ifadeleri kullanabilirsiniz; ayrı bir servis işletme yükünü ortadan kaldırır.
Bu akışın en çok yanlış anlaşılan tarafı 6. duraktır: kubectl apply başarılı dönmesi, uygulamanızın çalıştığı anlamına gelmez. Yalnızca “isteğiniz kaydedildi” demektir. İmaj çekilemiyorsa, düğümlerde yer yoksa, bir taint tolere edilmemişse hata mesajı apply çıktısında görünmez; nesnenin status alanında ve olaylarda birikir:
Kümenin kapasitesini zorlayıp bunu görelim. Dördüncü bir kopya isteyelim:
[root@argo-cp01 ~]# kubectl scale deployment api --replicas=4
deployment.apps/api scaled
[root@argo-cp01 ~]# kubectl get deployment api
NAME READY UP-TO-DATE AVAILABLE AGE
api 3/4 4 3 9d
[root@argo-cp01 ~]# kubectl get events --field-selector reason=FailedScheduling
LAST SEEN TYPE REASON OBJECT MESSAGE
38s Warning FailedScheduling pod/api-7d9c4b8f6c-p4wlz 0/3 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }, 2 Insufficient memory.
Komut başarıyla döndü, nesne güncellendi, ama dördüncü kopya hiçbir zaman çalışmayacak: iki worker’ın da belleği yetmiyor, control plane düğümü ise taint’li. Bu bilgi scale çıktısında değil, olaylarda. Eski hâline dönelim:
[root@argo-cp01 ~]# kubectl scale deployment api --replicas=3
deployment.apps/api scaled
Alışkanlık olarak edinilecek kural: apply‘dan sonra kubectl get ile istenen sayı ile hazır sayının eşitlendiğini görün. Yeşil bir created satırı bir doğrulama değildir.
Yanlış Yapılırsa: Bileşenleri Tek Tek Durdurunca Ne Bozulur?
Buraya kadarki anlatının doğru olup olmadığını varsaymayalım, ölçelim. “Bileşenler birbirinden bağımsızdır” iddiası doğruysa, bir bileşeni durdurduğumuzda yalnızca onun işi durmalı, kümenin geri kalanı ayakta kalmalıdır.
Kubeadm kümesinde control plane bileşenlerini durdurmanın yolu manifest dosyasını dizinden çıkarmaktır:
[root@argo-cp01 ~]# mv /etc/kubernetes/manifests/kube-scheduler.yaml /root/
[root@argo-cp01 ~]# kubectl get pods -n kube-system | grep scheduler
[root@argo-cp01 ~]#
Scheduler yok. Şimdi biraz önce çalıştırdığımız komutun aynısını verelim — dördüncü kopyayı yine isteyelim:
[root@argo-cp01 ~]# kubectl scale deployment api --replicas=4
deployment.apps/api scaled
[root@argo-cp01 ~]# kubectl get pods -l app=api
NAME READY STATUS RESTARTS AGE
api-7d9c4b8f6c-x2ktp 1/1 Running 0 9d
api-7d9c4b8f6c-qn6zw 1/1 Running 0 9d
api-7d9c4b8f6c-t8vhd 1/1 Running 0 9d
api-7d9c4b8f6c-k5rn2 0/1 Pending 0 15s
Dikkat edilecek iki şey: pod oluşturuldu (demek ki controller-manager çalışıyor) ama Pending durumunda takılı (demek ki onu bir düğüme yerleştirecek kimse yok). Çalışan üç pod’a hiçbir şey olmadı. describe çıktısında olay bile yok — FailedScheduling uyarısını yazan da scheduler’dır, o da yoktur:
[root@argo-cp01 ~]# kubectl describe pod api-7d9c4b8f6c-k5rn2 | grep -A2 "^Events:"
Events: <none>
Bu ayrım tanı koyarken hayat kurtarır. Aynı komutu iki kez çalıştırdık ve iki kez de Pending bir pod aldık, ama imzaları farklı: scheduler ayaktayken FailedScheduling uyarısı geldi — baktı, yer bulamadı. Scheduler yokken hiçbir olay üretilmedi — kimse bakmadı bile. Pending durumunu görünce sorulacak ilk soru “kaynak mı yetmedi” değil, “olaylarda bir şey var mı” olmalıdır.
Aynı testi diğer bileşenler için tekrarladığımızda tablo şöyle çıkıyor:
| Durdurulan | Anında bozulan | Ayakta kalan |
|---|---|---|
kube-scheduler |
Yeni pod’lar Pending, olay üretilmiyor |
Çalışan pod’lar, Service trafiği, kubectl |
kube-controller-manager |
Silinen pod yerine yenisi gelmiyor, NotReady düğüm işaretlenmiyor |
Çalışan pod’lar, elle oluşturulan pod’ların yerleştirilmesi |
kube-apiserver |
kubectl, tüm denetleyiciler, tüm durum güncellemeleri |
Çalışan konteynerler, Service trafiği, DNS |
etcd |
API sunucusu (dolayısıyla yukarıdakilerin hepsi) | Çalışan konteynerler, Service trafiği |
Bir düğümde kubelet |
O düğümde pod başlatma/durdurma, durum bildirimi | O düğümde çalışmakta olan konteynerler |
Bir düğümde kube-proxy |
Yeni Service ve endpoint kurallarının güncellenmesi | Mevcut Service trafiği (kurallar çekirdekte) |
Sağ sütun bu yazının en önemli çıkarımıdır: control plane komple çökse bile uygulamalarınız çalışmaya devam eder. Konteynerleri containerd çalıştırıyor, trafiği çekirdekteki kurallar taşıyor; ikisinin de control plane’e anlık bağımlılığı yok. Kaybettiğiniz şey uygulamalar değil, değişiklik yapabilme yeteneğinizdir — ölçekleyemez, güncelleyemez, arızayı toparlayamazsınız.
Bunun tersi de doğrudur ve daha az bilinir: kubelet durdurulmuş bir düğümde konteynerler çalışmaya devam ettiği için, düğüm NotReady görünürken uygulama hâlâ hizmet veriyor olabilir. Testi tamamlarken bunu da doğrulayalım — argo-w01‘de iki api kopyası çalışıyordu:
[root@argo-w01 ~]# systemctl stop kubelet
[root@argo-w01 ~]# crictl --runtime-endpoint unix:///run/containerd/containerd.sock ps | grep -c api
2
Konteynerler yerinde. Arızanın gerçekten düğüme özel olduğunu görmek için diğer tarafı da kontrol edelim; argo-w02 hiç etkilenmedi ve durumunu bildirmeye devam ediyor:
[root@argo-cp01 ~]# kubectl get nodes
NAME STATUS ROLES AGE VERSION
argo-cp01 Ready control-plane 9d v1.33.4
argo-w01 NotReady <none> 9d v1.33.4
argo-w02 Ready <none> 9d v1.33.4
Denemeyi bitirdikten sonra kubelet’i ve manifest dosyasını geri koymayı unutmayın:
[root@argo-w01 ~]# systemctl start kubelet
[root@argo-cp01 ~]# mv /root/kube-scheduler.yaml /etc/kubernetes/manifests/
Dördüncü kopyayı şimdilik Pending bırakıyoruz — bir sonraki bölümde ona bakacağız.
Bu testleri yalnızca öğrenme kümenizde yapın; üretimde kube-controller-manager‘ı durdurmak, o sırada ölen her pod’un yerine yenisinin gelmemesi demektir.
Tek control plane düğümünün bu kadar belirleyici olması bir tasarım kusuru değil, bizim kurulum tercihimiz. Kubernetes’in bu risk için resmî cevabı yüksek erişilebilir control plane‘dir: tek sayıda (genellikle üç) control plane düğümü, önlerinde bir yük dengeleyici ve her düğümde bir etcd üyesi. Scheduler ile controller-manager bu kurulumda hepsi birden çalışmaz; aralarında bir lider seçimi yapar, yalnızca lider iş görür, lider düşerse bir diğeri devralır. Sınırını da söylemek gerekir: bu kurulum control plane kaybını tolere eder, etcd çoğunluğunun kaybını tolere etmez — üç üyeli bir kümede iki üye birden düşerse küme salt okunur hâle gelir ve geri dönüş yolu yedekten geri yüklemedir.
dockershim’den CRI’ye: 1.24’te Tam Olarak Ne Kaldırıldı?
Kubernetes başlangıçta konteynerleri doğrudan Docker Engine ile başlatıyordu ve bu bağlantı kubelet’in içine gömülüydü — adı dockershim‘di. CRI standardı geldikten sonra bu gömülü kod bir tuhaflığa dönüştü: kubelet, Docker Engine’i çağırıyor, o da altta zaten containerd‘ye devrediyordu. Yani araya gereksiz bir katman girmiş oluyordu.
Kubernetes 1.20’de kullanımdan kaldırma duyurusu yapıldı, 1.24 sürümünde dockershim kubelet’ten çıkarıldı. Bu duyuru o dönemde geniş bir yanlış anlaşılmaya yol açtı, düzeltelim:
- Docker ile ürettiğiniz imajlar çalışmaya devam ediyor. İmaj biçimi OCI standardıdır, Docker’a özgü değildir.
docker buildile ürettiğiniz imajcontainerdüzerinde sorunsuz çalışır. - Kaldırılan şey Docker değil, kubelet’in içindeki Docker’a özel koddu. Docker Engine’i çalışma zamanı olarak kullanmayı sürdürmek isteyenler için Mirantis
cri-dockerdadında ayrı bir CRI adaptörü sürdürüyor. - Değişen şey düğüm üzerindeki araç setidir.
docker psyerinecrictl ps,docker logsyerinekubectl logsveyacrictl logs. Düğüme SSH atıpdocker psyazma alışkanlığınız varsa, kırılan tek şey odur.
Kendi kümenizde neyin çalıştığını kubectl get nodes -o wide çıktısındaki CONTAINER-RUNTIME sütunundan okuyabilirsiniz — yukarıdaki örnekte containerd://1.7.24 yazıyordu.
Genişletme Arayüzleri: CRI, CNI ve CSI Kümenin Neresine Takılır?
dockershim hikâyesinin asıl dersi tek bir bileşenle ilgili değil, bir desenle ilgili: Kubernetes belirli bir teknolojiyi gömmek yerine arayüz tanımlar, gerçeklemeyi eklentilere bırakır. Üç büyük arayüz şunlardır:
| Arayüz | Neyi soyutlar | Kimin çağırdığı | Örnek gerçeklemeler |
|---|---|---|---|
| CRI | Konteyner çalıştırma | kubelet | containerd, CRI-O |
| CNI | Pod ağı | Konteyner çalışma zamanı | Calico, Cilium, Flannel |
| CSI | Kalıcı depolama | kubelet + control plane sidecar’ları | Longhorn, Ceph RBD, bulut disk sürücüleri |
Üçüncü sütun en sık yanlış bilinen kısımdır: CNI eklentisini kubelet çağırmaz. Pod’un ağ ad alanını kuran, sonra CNI eklentisini çalıştırıp arayüzü ve IP’yi ekleyen taraf konteyner çalışma zamanıdır (containerd, /etc/cni/net.d/ altındaki yapılandırmayı okur). Bu ayrım tanı koyarken doğrudan işe yarar: pod ContainerCreating durumunda takılıyorsa ve olaylarda ağ eklentisi hatası varsa, bakılacak yer kubelet günlüğünden önce çalışma zamanının günlüğü ve CNI yapılandırmasıdır.
Ayrıca CNI kurulmamış bir kümede düğümler NotReady kalır — kubelet ağ eklentisi hazır olmadan düğümü hazır ilan etmez. Yeni kurulan kümede herkesin ilk karşılaştığı arıza budur ve sebebi eksik kurulum değil, eksik bırakılmış bir tercihtir: Kubernetes bilerek varsayılan bir ağ eklentisiyle gelmez.
CSI tarafında iş ikiye bölünür: kalıcı diski bulup düğüme bağlayan node bileşeni kubelet ile konuşur, diski oluşturan controller bileşeni ise control plane’de sidecar olarak çalışır ve API sunucusunu izler. Depolama tarafını serinin 7. başlığında ayrıntılı ele alacağız; kümede kullanılan bir örnek olarak Longhorn yazısına bakabilirsiniz.
Deklaratif Modelin Asıl Kazancı: Kaçırılan Olay Diye Bir Şey Yok
Son bir kavram, çünkü mimarinin neden bu şekilde kurulduğunu açıklıyor. Denetleyiciler olayları değil, durumu izler.
Olay tabanlı (edge-triggered) bir sistemde “pod silindi” mesajını kaçırırsanız o pod bir daha oluşturulmaz; mesaj geçmiştir. Kubernetes ise durum tabanlıdır (level-triggered): denetleyici periyodik olarak “kaç pod olmalıydı, kaç tane var” diye sorar. Bu yüzden kaçırılan bir olay kalıcı hasar bırakmaz — bir sonraki turda fark yine görülür.
Watch mekanizması bir hızlandırmadır, doğruluğun kaynağı değil. Bağlantı koptuğunda denetleyici yeniden listeler ve kaldığı yeri değil, o anki gerçeği alır.
Az önceki testin son adımı bunun kanıtı. Scheduler’ı geri koyduğumuzda, yokluğunda oluşturulmuş k5rn2 pod’unu “kaçırılmış bir olay” saymadı; listede buldu ve değerlendirdi. Değerlendirmenin sonucu yine olumsuz — kümede hâlâ yer yok — ama bu sefer pod sessizce beklemiyor:
[root@argo-cp01 ~]# kubectl describe pod api-7d9c4b8f6c-k5rn2 | grep -A2 "^Events:"
Events:
Type Reason Age From Message
Warning FailedScheduling 11s default-scheduler 0/3 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }, 2 Insufficient memory.
Aynı pod, aynı sonuç, farklı imza: olayın geri gelmesi scheduler’ın döndüğünün kanıtıdır. Olay tabanlı bir sistemde bu pod sonsuza kadar unutulmuş olurdu. Denemeyi kapatalım:
[root@argo-cp01 ~]# kubectl scale deployment api --replicas=3
deployment.apps/api scaled
Sonuç
- Kubernetes bileşenleri birbirini çağırmaz; hepsi yalnızca API sunucusunu okur ve yazar. Bileşenlerin durumsuz, sırasız ve tek tek yeniden başlatılabilir olmasının sebebi budur — hafıza bileşenlerde değil, etcd’dedir.
kubectl apply‘ın başarılı dönmesi yalnızca isteğin etcd’ye yazıldığını kanıtlar. Konteyner başlatma, yerleştirme ve imaj çekme bu cevaptan sonra asenkron olarak yürür; doğrulamagetvedescribeile yapılır.- Control plane çökse bile uygulamalarınız çalışmaya devam eder. Konteynerleri
containerd, trafiği çekirdekteki proxy kuralları taşır; kaybettiğiniz şey hizmet değil, kümeyi değiştirebilme yeteneğinizdir. - Aynı belirtinin farklı sebepleri farklı bileşenleri işaret eder.
Pending+FailedSchedulingolayı “scheduler baktı, yer bulamadı” demektir;Pending+ hiç olay yok ise “scheduler hiç bakmadı” demektir. - Kubernetes ağ ve depolamayı bilerek boş bırakır. CNI kurulmadan düğümler
Readyolmaz ve CNI’yı çağıran kubelet değil, konteyner çalışma zamanıdır — arıza ararken bakılacak günlük buna göre değişir.
Sıradaki yazıda konuşmayı bırakıp kümeyi fiilen kuruyoruz: kind ile dakikalar içinde tek düğümlü bir öğrenme kümesi, ardından bu yazıdaki argo-cp01 ve worker’ların gerçek karşılığı. Serinin tamamı için Kubernetes Öğrenme Yolculuğu sayfasına bakabilirsiniz.
Ek: Terimler Sözlüğü
| Terim | Karşılığı |
|---|---|
| Control plane | Kümenin kararlarını veren bileşenlerin bütünü: apiserver, etcd, scheduler, controller-manager. |
| kube-apiserver | Kümenin tek REST arayüzü. İş mantığı içermez; doğrular, yetkilendirir, etcd’ye yazar, değişikliği duyurur. |
| etcd | Raft tabanlı dağıtık anahtar-değer deposu. Kümenin tüm durumu burada; yazma için çoğunluk gerekir. |
| kube-scheduler | Atanmamış pod’lara düğüm seçer. Tek çıktısı pod kaydındaki spec.nodeName alanıdır. |
| kube-controller-manager | Onlarca denetleyiciyi tek süreçte çalıştırır; istenen durum ile gözlenen durum arasındaki farkı kapatır. |
| kubelet | Düğümdeki tek Kubernetes ajanı. Kendisine atanmış pod’ları çalışma zamanına başlatır, durumu bildirir. |
| kube-proxy | Service ClusterIP’lerini çekirdek yönlendirme kurallarına çevirir (iptables, ipvs veya nftables). |
| Statik pod | kubelet’in /etc/kubernetes/manifests/ dizininden, API sunucusuna sormadan başlattığı pod. API’de yalnızca aynası görünür. |
| Watch | Bir istemcinin API sunucusunda açtığı kalıcı bağlantı; nesne değiştikçe olay akar. Doğruluğun kaynağı değil, hızlandırmadır. |
| Admission | Nesne etcd’ye yazılmadan önce çalışan denetim katmanı. Değiştiren (mutating) aşama doğrulayandan (validating) önce gelir. |
| CRI / CNI / CSI | Sırasıyla konteyner çalıştırma, pod ağı ve kalıcı depolama arayüzleri. Kubernetes gerçeklemeyi eklentilere bırakır. |
| dockershim | kubelet’in içine gömülü, Docker Engine’e özel eski bağlantı kodu. 1.20’de kullanımdan kaldırıldı, 1.24’te silindi. |
| Level-triggered | Olayı değil o anki durumu izleyen denetim biçimi. Kaçırılan olay kalıcı hasar bırakmaz. |