Yeni yazı: Kümenin Kapağını Açalım: Kubernetes Bileşenleri Birbirini Neden Hiç Aramaz?
Kümenin Kapağını Açalım: Kubernetes Bileşenleri Birbirini Neden Hiç Aramaz?

Kümenin Kapağını Açalım: Kubernetes Bileşenleri Birbirini Neden Hiç Aramaz?

İç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-w01 ve argo-w02 adlı 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:

  1. Filtreleme. Pod’un çalışamayacağı düğümler elenir: kaynağı yetmeyenler, nodeSelector‘a uymayanlar, tolere edilmemiş taint taşıyanlar, istenen portu dolu olanlar.
  2. 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 build ile ürettiğiniz imaj containerd ü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-dockerd adında ayrı bir CRI adaptörü sürdürüyor.
  • Değişen şey düğüm üzerindeki araç setidir. docker ps yerine crictl ps, docker logs yerine kubectl logs veya crictl logs. Düğüme SSH atıp docker ps yazma 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ğrulama get ve describe ile 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 + FailedScheduling olayı “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 Ready olmaz 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.
Paylaş :

İlgili Yazılar

Kubernetes Öğrenme Yolculuğu

Kubernetes Öğrenme Yolculuğu

Kubernetes öğrenmeye çalışan çoğu kişi aynı yerden başlıyor: internetten bir YAML dosyası kopyalıyor, kubectl apply diyor, çalışınca seviniyor, çalışmayınca ne olduğunu anlamıyor. Ben de öyle başladım. Sorun şu ki Kubernetes bir komut seti değil, bir model; o modeli anlamadan ezberlenen komutlar ilk arızada işe yaramıyor.

Longhorn: Kubernetes Ortamları İçin Devrimsel Depolama Çözümü

Longhorn: Kubernetes Ortamları İçin Devrimsel Depolama Çözümü

Bu yazı Kubernetes Öğrenme Yolculuğu serisinin bir parçasıdır. Günümüzde, konteyner teknolojileri ve mikro hizmet mimarileri, yazılım geliştirme ve dağıtım süreçlerini dönüştürmekte. Bu dönüşüm, depolama çözümlerine yönelik ihtiyaçları da yeniden şekillendiriyor. Kubernetes ekosistemi içinde yer alan Longhorn, bu yeni ihtiyaçlara cevap veren açık kaynaklı bir projedir. Bu yazıda, Longhorn’un ne olduğunu, ne işe yaradığını ve sunduğu faydaları detaylıca ele alacağız.

Monitoring Nedir ve Neden Önemlidir?

Monitoring Nedir ve Neden Önemlidir?

Bir sunucunun diski dolduğunda bunu iki şekilde öğrenirsiniz: ya sistem size söyler, ya da müşteri arar. Aradaki fark teknik bir yetenek değil, bir tercihtir — o sunucuyu izliyor muydunuz, izlemiyor muydunuz?