Mayıs 22 2026

HAProxy ile Yüksek Erişilebilirlik: Keepalived ve Health Check

Modern mikroservis mimarilerinde veya yüksek trafikli web uygulamalarında kesintisiz hizmet sunmak artık bir lüks değil, zorunluluk. Tam da bu noktada, haproxy ve keepalived ikilisi, linux sunucularınız üzerinde kurabileceğiniz, kurumsal sınıfta, maliyetsiz ve son derece güvenilir bir aktif-pasif ha (high availability) loadbalancer çözümü olarak imdadımıza yetişiyor. Peki ama bu iki canavarı prod ortamında birbirine küstürmeden, arkadaki servislerin sağlık durumlarına (health check) ve gelen isteğin içeriğine (ACL) göre nasıl kusursuzca dans ettireceğiz? Bu yazıda lafı hiç dolandırmadan, doğrudan production ortamında hırpalanmış senaryolardan süzülen pratik bir mimariyi ayağa kaldıracağız.

Neden Sadece HAProxy Yetmiyor? (SPOF Nedir?)

HAProxy, performansına ve stabilitesine şapka çıkardığımız harika bir load balancer. Ancak ne kadar güçlü olursa olsun, tek bir sunucu üzerinde çalıştığı sürece sisteminizde bir SPOF (Single Point of Failure) yani tek hata noktası oluşturur. HAProxy’nin üzerinde çalıştığı Linux sunucunun donanımı çökerse, network kartı yanarsa ya da kernel panik yaparsa tüm sisteminiz karanlığa gömülür.

İşte bu riski bertaraf etmek için Keepalived devreye giriyor. Keepalived, VRRP (Virtual Router Redundancy Protocol) protokolünü kullanarak iki farklı load balancer sunucusunun tek bir Sanal IP (Virtual IP – VIP) arkasında çalışmasını sağlar. Aktif olan sunucu çöktüğünde, pasif durumdaki yedek sunucu VIP’yi milisaniyeler içinde üzerine alır. Kullanıcılar bu failover sürecini ruhları bile duymadan atlatırlar.

Altyapı Hazırlığı ve Olmazsa Olmaz Kernel Parametresi

Kuruluma başlamadan önce topolojimizi netleştirelim. Elimizde iki adet Linux (Ubuntu/Debian tabanlı kabul ediyoruz) sunucu olduğunu varsayalım:

  • LB01 (Master): 192.168.1.10
  • LB02 (Backup): 192.168.1.11
  • Sanal IP (VIP): 192.168.1.100

Şimdi kıdemli bir sistemcinin asla atlamayacağı o kritik kernel ayarına gelelim: net.ipv4.ip_nonlocal_bind. Varsayılan olarak Linux, sunucu üzerinde fiziksel olarak tanımlanmamış bir IP adresine servislerin bind olmasını (yani o IP’yi dinlemesini) engeller. VIP, pasif sunucuda o an aktif olmadığı için backup sunucudaki HAProxy servisi başlatılamaz ve çöker. Bunun önüne geçmek için her iki sunucuda da şu komutu koşturuyoruz:

echo "net.ipv4.ip_nonlocal_bind=1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

Bu sihirli dokunuş sayesinde HAProxy, sunucuda henüz tanımlanmamış olan 192.168.1.100 IP’sini hiç çekinmeden dinlemeye başlayacaktır.

Keepalived ile Aktif-Pasif VIP Kurulumu

Her iki sunucuya da keepalived paketini kuralım:

sudo apt update && sudo apt install keepalived -y

Şimdi konfigürasyon zamanı. En kritik nokta, master sunucu çöktüğünde VIP’nin pasif sunucuya geçmesi, ancak master geri geldiğinde (eğer istemiyorsak) gereksiz yere VIP’yi geri alıp ufak da olsa bir kesinti yaratmamasıdır. Buna “non-preemptive” yapılandırma denir. Ancak biz bu örnekte klasik aktif-pasif öncelik yapısını kuracağız.

Master (LB01) Konfigürasyonu

/etc/keepalived/keepalived.conf dosyasını oluşturun ve aşağıdaki gibi düzenleyin:

vrrp_script check_haproxy {
    script "killall -0 haproxy" # HAProxy çalışıyor mu kontrol et
    interval 2                  # Her 2 saniyede bir çalıştır
    weight 2                    # Başarılıysa önceliğe (priority) 2 ekle
}

vrrp_instance VI_1 {
    state MASTER
    interface eth0              # Network arayüzünüzün adı (ip a ile kontrol edin)
    virtual_router_id 51        # İki sunucuda da AYNI olmalı
    priority 101                # Master daha yüksek önceliğe sahip
    advert_int 1

    authentication {
        auth_type PASS
        auth_pass GizliSifre123  # İki sunucuda da aynı olmalı
    }

    virtual_ipaddress {
        192.168.1.100/24         # Paylaşılan Sanal IP (VIP)
    }

    track_script {
        check_haproxy
    }
}

Backup (LB02) Konfigürasyonu

Backup sunucumuzda ise dosya neredeyse aynıdır, sadece state ve priority değerleri değişir:

vrrp_script check_haproxy {
    script "killall -0 haproxy"
    interval 2
    weight 2
}

vrrp_instance VI_1 {
    state BACKUP
    interface eth0
    virtual_router_id 51
    priority 100                # Master'dan daha düşük
    advert_int 1

    authentication {
        auth_type PASS
        auth_pass GizliSifre123
    }

    virtual_ipaddress {
        192.168.1.100/24
    }

    track_script {
        check_haproxy
    }
}

Konfigürasyonları kaydettikten sonra her iki sunucuda da servisi başlatalım:

sudo systemctl enable --now keepalived

Eğer her şey yolunda gittiyse, LB01 üzerinde ip a show eth0 komutunu verdiğinizde secondary IP olarak 192.168.1.100 adresini görmelisiniz. LB02’de ise bu IP görünmemelidir.

HAProxy Kurulumu ve Sağlık Kontrolü (Health Check) Sanatı

Sıra geldi yük dengeleme katmanımıza. HAProxy’yi kuralım:

sudo apt install haproxy -y

Bir load balancer’ı akıllı kılan şey, arkasındaki uygulama sunucularının (backend) gerçekten yaşayıp yaşamadığını bilmesidir. Sadece TCP portunun açık olması (Layer 4) uygulamanın sağlıklı çalıştığı anlamına gelmez. Uygulama veritabanına bağlanamadığı için HTTP 500 hatası veriyor olabilir ama TCP portu hala ayaktadır. Bu yüzden her zaman HTTP tabanlı (Layer 7) health check tercih etmelisiniz.

Gelin, hem gelişmiş health check mekanizmasını hem de VIP binding konseptini barındıran örnek bir /etc/haproxy/haproxy.cfg dosyası hazırlayalım:

global
    log /dev/log local0
    log /dev/log local1 notice
    chroot /var/lib/haproxy
    user haproxy
    group haproxy
    daemon

defaults
    log     global
    mode    http
    option  httplog
    option  dontlognull
    timeout connect 5000ms
    timeout client  50000ms
    timeout server  50000ms

frontend http_front
    bind 192.168.1.100:80 # Sadece VIP üzerinden gelen istekleri dinle
    mode http
    default_backend app_backend

backend app_backend
    mode http
    balance roundrobin
    
    # Layer 7 Health Check Yapılandırması
    option httpchk GET /healthz HTTP/1.1\r\nHost:\ myapp.local
    http-check expect status 200
    
    # Backend Sunucuları
    server app01 192.168.1.20:8080 check inter 3000 rise 2 fall 3
    server app02 192.168.1.21:8080 check inter 3000 rise 2 fall 3

Burada Neler Döndü? (Detaylı “Neden” Analizi)

  • bind 192.168.1.100:80: HAProxy’ye sadece VIP adresini dinlemesini söyledik. Bu, trafiğin kontrolsüz şekilde doğrudan nodeların kendi IP’leri üzerinden akmasını engeller.
  • option httpchk GET /healthz: HAProxy, backend sunucularına her 3 saniyede bir (inter 3000) HTTP GET isteği gönderir.
  • http-check expect status 200: Gelen yanıtın HTTP 200 OK olması durumunda sunucu “sağlıklı” kabul edilir.
  • rise 2 fall 3: Bir sunucu ardışık 3 kez başarısız health check yanıtı verirse (fall), trafik ona gönderilmez (out of rotation). Ne zaman ki ardışık 2 başarılı yanıt verir (rise), o zaman tekrar gruba dahil edilir.

Gelişmiş ACL (Access Control List) Tabanlı Routing

Prod ortamlarında genellikle tek bir domain arkasında birden fazla mikroservis barındırırız. Örneğin /api ile başlayan isteklerin API servislerine, statik dosyaların ise başka bir backende gitmesini isteriz. HAProxy’nin ACL mekanizması bu konuda tam bir canavardır.

Aşağıdaki konfigürasyon örneğinde, path ve domain bazlı yönlendirmeyi nasıl yapacağımızı görelim:

frontend http_front_advanced
    bind 192.168.1.100:80
    mode http

    # ACL Tanımları
    acl is_api path_beg -i /api
    acl is_static path_end -i .jpg .png .css .js
    acl host_admin hdr_beg(host) -i admin.

    # Yönlendirme Kuralları (Routing Rules)
    use_backend api_backend if is_api
    use_backend static_backend if is_static
    use_backend admin_backend if host_admin
    
    # Varsayılan Backend
    default_backend web_backend

backend web_backend
    server web01 192.168.1.30:80 check

backend api_backend
    server api01 192.168.1.40:8080 check

backend static_backend
    server storage01 192.168.1.50:80 check

backend admin_backend
    server admin01 192.168.1.60:80 check

Bu yapıyla birlikte, tek bir load balancer IP’si üzerinden gelen istekleri, url path’ine veya HTTP host header’ına göre ayıklayıp tamamen farklı sunucu gruplarına sıfır performans kaybıyla dağıtabiliyoruz.

Failover Testi: Fişi Çektiğimizde Ne Oluyor?

Kurulumumuzu tamamladık ve servislerimizi başlattık (sudo systemctl enable --now haproxy). Peki sistemimiz gerçekten yüksek erişilebilir mi? Bunu test etmenin en vahşi (ve keyifli) yolu canlı ortamda simülasyon yapmaktır.

Öncelikle master sunucumuzda (LB01) IP adresini izleyelim:

ip addr show eth0

Burada 192.168.1.100 IP’sini görmeliyiz. Şimdi master sunucu üzerinde HAProxy servisini durdurarak Keepalived’ın bu durumu fark etmesini sağlayalım:

sudo systemctl stop haproxy

Hemen ardından pasif sunucuya (LB02) geçip logları izleyelim:

tail -f /var/log/syslog | grep Keepalived

Loglarda şuna benzer bir satır görmelisiniz:

Keepalived_vrrp[1234]: VRRP_Instance(VI_1) Entering MASTER STATE

Ve ip addr show eth0 çalıştırdığınızda, VIP’nin saniyeden daha kısa bir sürede LB02’ye göç ettiğini göreceksiniz. Kullanıcılarınız, veritabanına veri yazmaya ve web sitenizde gezinmeye kesintisiz olarak devam edecektir. İşte gerçek HA kalitesi!

Sonuç ve Pro-Tip

HAProxy ve Keepalived ikilisi, karmaşık cloud mimarilerine veya pahalı donanımsal load balancer cihazlarına ihtiyaç duymadan, Linux’un gücüyle scale olabilen harika bir çözümdür.

Son bir prod tavsiyesi: Keepalived loglarını mutlaka merkezi bir izleme aracına (Elasticsearch, Loki vb.) yönlendirin ve VIP geçişlerinde (state transitions) ekibinize Slack veya Teams üzerinden alert atacak bir script tetikleyin (bunun için Keepalived’ın notify_master ve notify_backup direktiflerini araştırabilirsiniz). VIP’nin sessiz sedasız sürekli yer değiştirmesi, altyapınızda sinsi bir network dalgalanması olduğunun habercisi olabilir.

Category: Genel | LEAVE A COMMENT
Şubat 5 2026

Bash’te Hata Yönetimi: set -euo pipefail, Trap ve Bats ile Defansif Scripting

Gece saat 03:00. PagerDuty çalıyor. Sorun: Kubernetes cluster’ına yeni bir deployment çıkılırken pipeline yarıda kalmış ama deploy başarılıymış gibi davranıp eski stabil replicaları da temizlemiş. Loglara bakıyorsunuz; kritik bir environment variable tanımlanmadığı için script sessizce patlamış, fakat exit code 0 döndüğü için CI/CD pipeline’ı her şeyin yolunda olduğunu varsayarak devam etmiş. Hepimiz bu filmi en az bir kere izledik. Günümüz modern altyapılarında bash scripting ve linux tabanlı otomasyon süreçleri, devops mühendisliğinin görünmez omurgasını oluşturuyor. Ancak bu omurga genellikle oldukça kırılgan temeller üzerine kurulu.

Bu makalede, Bash betiklerinizi kurumsal seviyede, hata toleransı yüksek ve test edilebilir yapılara dönüştürmenin yollarını inceleyeceğiz. Temel kavramları bir kenara bırakıp, doğrudan production ortamlarında hayat kurtaran pratik yaklaşımlara odaklanacağız.

Sessiz Katilleri Durdurun: “set -euo pipefail” Anatomisi

Bash, varsayılan olarak aşırı iyimser bir kabuktur. Bir komut hata verse de, bir değişken tanımlanmamış olsa da “yola devam et” felsefesini benimser. Bu felsefe interaktif terminal kullanımı için harika olsa da, otomasyon scriptleri için tam bir felakettir. Bu felaketi önlemenin ilk adımı, script’lerin başına o meşhur üçlüyü eklemektir:

set -euo pipefail

Peki bu parametreler arka planda tam olarak ne yapar ve neden hayati önem taşırlar? Tek tek inceleyelim.

1. set -e (errexit)

Bu parametre, script içindeki herhangi bir komut non-zero (sıfır dışı) bir exit code ile sonuçlandığında script’in anında sonlandırılmasını sağlar. Varsayılan davranışta Bash, komut hata alsa bile bir sonraki satıra geçmeye çalışır.

Neden kritik? Aşağıdaki senaryoyu düşünün:

cd /tmp/non_existent_directory
rm -rf *

Eğer ilk satırdaki cd komutu dizin bulunamadığı için başarısız olursa ve set -e aktif değilse, script bir sonraki satıra geçer ve o an hangi dizinde bulunuyorsa (muhtemelen script’in çalıştığı root dizini) oradaki tüm dosyaları siler. set -e bu felaketi engeller.

2. set -u (nounset)

Eğer tanımlanmamış bir değişkeni okumaya çalışırsanız, Bash bunu boş bir string olarak kabul eder ve hata vermez. set -u, tanımlanmamış her değişkeni ölümcül bir hata (fatal error) olarak kabul eder ve script’i durdurur.

Neden kritik?

# set -u aktif değilse:
TARGET_DIR="" # ya da yanlışlıkla typo yapılmış bir değişken
rm -rf "${TARGET_DIR}/bin" # Sistem "rm -rf /bin" komutunu çalıştırır!

set -u aktif olduğunda, Bash bu satıra geldiğinde TARGET_DIR: unbound variable hatası fırlatacak ve execution’ı hemen durduracaktır.

3. set -o pipefail

Bash’te pipeline (boru) işlemlerinde varsayılan olarak sadece son komutun exit code’u dikkate alınır. Örneğin komut1 | komut2 | komut3 zincirinde komut1 patlasa bile, komut3 başarılı olursa tüm pipeline başarılı (exit 0) sayılır.

Neden kritik?

# pipefail aktif değilse:
curl -s https://invalid-url-destination.tar.gz | tar -xzf -
echo $? # Çıktı: 0

Yukarıdaki örnekte curl başarısız olacak ve hiçbir veri indiremeyecektir. Ancak tar komutu boş girdi aldığında (veya pipe kapandığında) hata üretse de, exit code bazen yanıltıcı olabilir. Daha da kötüsü, aradaki kritik log analiz araçlarında hatalar yutulur. set -o pipefail aktif edildiğinde, pipeline içindeki en sağdaki non-zero exit code tüm pipeline’ın dönüş kodu kabul edilir.

İstisnaları Yönetmek: set -e Altında Güvenli Hata Toleransı

set -e kullanmaya başladığınızda karşılaşacağınız ilk sorun, bazı komutların hata vermesinin aslında beklenen bir durum olmasıdır. Örneğin bir dizinin varlığını kontrol etmek veya grep ile bir log dosyasında spesifik bir kelimeyi aramak gibi durumlarda, exit code’un 1 gelmesi script’i patlatmamalıdır.

Hata Kodunu Güvenli Bir Şekilde Absorbe Etmek

Eğer bir komutun başarısız olabileceğini biliyor ve bunun script’i durdurmasını istemiyorsanız, OR (||) operatörünü kullanarak hata durumunu bypass edebilirsiniz:

# grep satır bulamazsa normalde exit 1 verir ve script set -e yüzünden durur.
# || true kullanarak bunu engelliyoruz.
result=$(grep "ERROR" /var/log/app.log || true)

# Veya alternatif olarak lokal bir hata değişkenine atayabilirsiniz:
grep "CRITICAL" /var/log/app.log || local_exit=$?
if [ ${local_exit:-0} -ne 0 ]; then
    echo "Kritik log bulunamadı, ama yola devam ediyoruz."
fi

Bir diğer profesyonel yaklaşım ise conditional block’lar kullanmaktır. if, elif, while veya until ifadelerinin içinde çalıştırılan komutlar, hata verseler dahi set -e engeline takılmazlar:

# Bu blok set -e aktif olsa bile güvenle çalışır
if ! ping -c 1 8.8.8.8 > /dev/null 2>&1; then
    echo "İnternet bağlantısı yok, ancak script devam ediyor."
fi

Trap Mekanizması: Kaynakları Temizlemek ve Graceful Shutdown

Script’iniz ister başarıyla tamamlansın, ister yarıda hata alıp dursun; arkasında çöp bırakmamalıdır. Geçici dosyalar (temp files), oluşturulan socket’ler, kilit dosyaları (lock files) veya aktif edilen SSH tünelleri her durumda temizlenmelidir. İşte bu noktada Linux kernel sinyallerini yakalayan trap devreye girer.

Aşağıdaki boilerplate şablonu, production seviyesindeki script’lerinizde güvenle kullanabilirsiniz:

#!/usr/bin/env bash

set -euo pipefail

# Geçici dizin oluşturuluyor
TMP_DIR=$(mktemp -d -t my-app-XXXXXX)
echo "Geçici çalışma alanı: ${TMP_DIR}"

# Temizlik fonksiyonu
cleanup() {
    local exit_code=$?
    echo "Temizlik işlemi başlatılıyor... Son exit code: ${exit_code}"
    rm -rf "${TMP_DIR}"
    
    # Eğer script bir hata yüzünden durduysa ek bildirimler buraya yazılabilir (Slack webhook vb.)
    if [ ${exit_code} -ne 0 ]; then
        echo "Script beklenmedik bir şekilde sonlandı!" >&2
    fi
}

# EXIT sinyalini yakala. Script nasıl biterse bitsin cleanup çalışacak.
trap cleanup EXIT

Neden EXIT Sinyali?

Birçok junior geliştirici trap için sadece ERR veya INT sinyallerini tanımlar. Ancak EXIT pseudosignal’i, script normal yollarla bittiğinde de, set -e yüzünden yarıda kesildiğinde de veya bir syntax hatası alındığında da tetiklenir. Bu yüzden tek bir merkezden temizlik yönetimi için en güvenli limandır.

Exit Code Standartları: Kurumsal Entegrasyon

Yazdığınız script’lerin diğer otomasyon araçları (Ansible, Jenkins, ArgoCD vb.) tarafından doğru yorumlanabilmesi için POSIX standartlarına uygun exit code’lar dönmesi gerekir. Sadece exit 1 yazıp geçmek, hata analizi yaparken size zaman kaybettirir.

Exit Code Anlamı Kullanım Senaryosu
0 Success Script görevini başarıyla tamamladı.
1 Catchall for general errors Genel hatalar (izin yetersizliği, parse hataları vb.)
2 Misuse of shell builtins Eksik veya hatalı argüman kullanımı.
126 Command invoked cannot execute Çalıştırılmak istenen dosya execute yetkisine sahip değil.
127 Command not found Script içindeki bir binary sistemde yüklü değil.

Scriptlerinizde custom hata kodları tanımlarken 64-113 (BSD standardı) veya 128’den büyük olmayan serbest kodları (örneğin 10, 20, 30 gibi) kullanmanız, sistem hata kodlarıyla çakışmayı önler:

readonly ERR_INVALID_ARG=2
readonly ERR_DB_CONNECTION=10
readonly ERR_DISK_FULL=11

if [ $# -lt 1 ]; then
    echo "Hata: En az bir argüman girmelisiniz." >&2
    exit ${ERR_INVALID_ARG}
fi

Bats (Bash Automated Testing System) ile Unit Test Yazımı

“Bash script’inin testi mi olur?” demeyin. Eğer yazdığınız script kritik bir deploy sürecini yönetiyorsa, her değişiklikten sonra manuel test yapmak hem riskli hem de zaman alıcıdır. Bats, Bash script’lerinizi gerçek birer yazılım projesi gibi test etmenizi sağlar.

Öncelikle test edeceğimiz basit ama fonksiyonel bir script yazalım (isim: deploy.sh):

#!/usr/bin/env bash
set -euo pipefail

deploy_artifact() {
    local env=$1
    if [ -z "${env}" ]; then
        echo "Hata: Environment belirtilmedi." >&2
        return 2
    fi
    
    if [ "${env}" == "prod" ]; then
        echo "Production deployment başarılı."
        return 0
    else
        echo "Bilinmeyen environment: ${env}" >&2
        return 10
    fi
}

# Script doğrudan çalıştırıldıysa (test import etmediyse) main'i çalıştır
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    deploy_artifact "${1:-}"
fi

Şimdi bu script için yazacağımız unit test dosyasına bakalım (isim: deploy.bats):

#!/usr/bin/env bats

# Test öncesi script'i load et
setup() {
    source ./deploy.sh
}

@test "Parametre verilmediğinde script exit code 2 ile patlamalı" {
    run deploy_artifact ""
    [ "$status" -eq 2 ]
    [ "$output" = "Hata: Environment belirtilmedi." ]
}

@test "Prod environment sağlandığında başarılı dönmeli" {
    run deploy_artifact "prod"
    [ "$status" -eq 0 ]
    [ "$output" = "Production deployment başarılı." ]
}

@test "Bilinmeyen env verildiğinde exit code 10 dönmeli" {
    run deploy_artifact "staging"
    [ "$status" -eq 10 ]
}

Bu testleri lokalinizde veya CI/CD pipeline’ınızda çalıştırmak için tek yapmanız gereken bats binary’sini çağırmaktır:

bats deploy.bats

Sonuçlar yeşil yandığında, refactoring yaparken hiçbir şeyi bozmadığınızdan emin olarak production’a güvenle push edebilirsiniz.

Sonuç: Defansif Scripting’i Alışkanlık Haline Getirin

Bash scripting, küçümsenen ama sistem mühendisliğinin en kritik katmanlarından biridir. Script yazarken kodun sadece “mutlu senaryoda” (happy path) nasıl çalışacağını değil, nerede ve nasıl patlayabileceğini tasarlamak gerekir. set -euo pipefail ile sessiz hataların önüne geçmek, trap ile sistem kaynaklarını her durumda temiz bırakmak ve bats ile bu süreçleri test edilebilir kılmak, sizi sıradan bir script yazıcısından profesyonel bir sistem mimarına dönüştürür.

Category: Genel | LEAVE A COMMENT
Ekim 11 2024

Linux Namespace ve Cgroup: Container’ların Altındaki Mekanizma

Eğer bir teknik mülakatta ya da kahve molasında “Container nedir?” sorusuna “Hizmetleri izole eden hafifletilmiş sanal makinelerdir (VM)” yanıtını verdiyseniz, bugün bu ezberi bozuyoruz. Çünkü aslında işletim sistemi seviyesinde “container” diye bir nesne veya teknoloji bulunmuyor. Bizim docker, containerd ya da podman adını verdiğimiz araçların yaptığı her şey, linux çekirdeğinin (kernel) sunduğu iki temel özelliğin etrafına örülmüş şık birer kullanıcı arayüzü (wrapper) olmaktan ibaret: namespace ve cgroup.

Bu makalede, işin kolayına kaçıp hazır CLI araçları kullanmak yerine, modern bir container runtime’ın (örneğin containerd) yaptığı işi tamamen elle (manual) yapacağız. Sıfırdan izole bir root filesystem (rootfs) hazırlayacak, kendi ağ geçidimizi kuracak, prosesleri izole edecek ve cgroup v2 ile kaynak sınırları koyacağız. Arkanıza yaslanın, terminalinizi açın ve container dünyasının arka bahçesine hoş geldiniz.

1. Hazırlık: Minimal Bir Root Filesystem (rootfs) Oluşturmak

Bir container’ın kendi dünyasında yaşayabilmesi için öncelikle bağımsız bir dosya sistemine ihtiyacı vardır. Docker imajları aslında katmanlaştırılmış (layered) tar dosyalarından başka bir şey değildir. Biz de işe minimal bir Alpine Linux rootfs indirerek başlayacağız.

# Çalışma dizinimizi oluşturalım
mkdir -p /tmp/kerten-container/rootfs
cd /tmp/kerten-container

# Alpine rootfs indiriyoruz
curl -sSL https://dl-cdn.alpinelinux.org/alpine/v3.18/releases/x86_64/alpine-minirootfs-3.18.4-x86_64.tar.gz -o alpine.tar.gz
tar -xzf alpine.tar.gz -C rootfs/
rm alpine.tar.gz

Artık elimizde izole bir işletim sisteminin sahip olması gereken tüm temel dizin yapısı (bin, sbin, etc, lib, proc…) mevcut. Sıradaki adım, bu dizini yeni dünyamızın kök dizini (root) haline getirmek.

2. Chroot Değil, Pivot Root: Güvenli Mount Namespace

Dosya sistemi izolasyonu denince akla gelen ilk syscall (sistem çağrısı) genellikle chroot olur. Ancak chroot esnektir ve root yetkilerine sahip bir proses kolayca bu hapishaneden kaçabilir (jailbreak). Modern container dünyası bunun yerine çok daha güvenli olan pivot_root sistem çağrısını kullanır.

pivot_root, mevcut mount namespace’in root mount noktasını yeni bir dizine taşır ve eski root’u başka bir dizine bind eder. Bunu elle simüle etmek için öncelikle yeni bir mount namespace oluşturmamız gerekir.

İşte sihirli komutumuz: unshare. Bu komut, belirtilen namespace türlerini sıfırdan oluşturarak yeni bir proses başlatır.

# Mount, UTS (hostname) ve IPC namespace'lerini izole ederek yeni bir bash oturumu açıyoruz
unshare --mount --uts --ipc --fork /bin/bash

Şu andan itibaren açılan bu yeni shell oturumunda yaptığımız mount işlemleri host sistemimizi etkilemeyecek. Şimdi rootfs dizinimizi bir mount noktasına dönüştürelim ve pivot_root için hazırlayalım:

# rootfs dizinimizi bir bind mount olarak işaretliyoruz (pivot_root bunu şart koşar)
mount --bind /tmp/kerten-container/rootfs /tmp/kerten-container/rootfs

# Eski root dizinini koyacağımız geçici bir klasör oluşturuyoruz
mkdir -p /tmp/kerten-container/rootfs/put_old

# pivot_root komutunu çalıştırıyoruz: [yeni_root] [eski_root_un_duracagi_yer]
cd /tmp/kerten-container/rootfs
pivot_root . put_old

# Artık yeni root içerisindeyiz. Eski sisteme ait mount noktalarını temizleyelim
cd /
umount -l /put_old
rmdir /put_old

# Hostname'i değiştirelim (UTS namespace sayesinde host etkilenmez)
hostname kerten-container

Harika! Şu an sadece kendi hazırladığımız rootfs içerisindeki ikili dosyaları (binaries) görebilen, izole edilmiş bir dosya sistemindeyiz. Ancak eksik bir şeyler var: Prosesler nerede?

3. PID Namespace: “Ben Kimim?” Sorusu

Eğer az önce oluşturduğumuz ortamda ps aux çalıştırmayı denerseniz, hata alırsınız. Çünkü proseslerin yönetildiği ve kernel bilgilerinin okunduğu sanal dosya sistemi olan /proc henüz mount edilmedi. Daha da önemlisi, hala host sistemin PID (Process ID) uzayını paylaşıyoruz.

Gelin, yeni bir PID namespace oluşturalım. Bunun için yeni bir terminal penceresi açıp host üzerinde çalışmaya devam edeceğiz. Bu sayede hem host hem de container arasındaki ilişkiyi daha net görebiliriz.

Bu sefer PID namespace’i de işin içine katarak unshare ile yeni bir container başlatalım (dosya sistemi adımlarını bu yeni namespace içinde hızlıca tekrarladığımızı varsayalım veya doğrudan aşağıdaki komutla temiz bir başlangıç yapalım):

# PID ve Mount namespace izole edilmiş şekilde başlatıyoruz
unshare --mount --pid --fork --mount-proc /bin/bash

--mount-proc parametresi, bizim için otomatik olarak yepyeni ve izole bir /proc dosya sistemi mount eder. Şimdi bu oturumda prosesleri listeleyelim:

ps aux

Çıktıya dikkat edin:

PID   USER     TIME  COMMAND
    1 root      0:00 /bin/bash
    2 root      0:00 ps aux

İşte container dünyasının kutsal kasesi! Host üzerinde binlerce çalışan proses varken, bizim container’ımız kendisini dünyadaki tek proses (PID 1) olarak görüyor. PID 1 olmak büyük bir sorumluluktur; eğer bu proses ölürse, kernel tüm namespace’i sonlandırır.

4. Network Namespace: Kablolama İşlemleri

Şu ana kadar dosya sistemini ve prosesleri izole ettik ancak container’ımızın dış dünya ile bağlantısı yok (loopback arayüzü bile kapalı). Docker arkada bu işi sanal bir switch (docker0 bridge) ve veth pair (sanal ethernet kablosu) kullanarak çözer.

Gelin bu kablolamayı host üzerinde elle yapalım. Bu senaryo için host üzerinde root yetkileriyle yeni bir terminal açın.

Öncelikle container’ımızın namespace’ini kalıcı hale getirmemiz gerekir ki host üzerinden oraya erişebilelim. Linux’ta her namespace /proc/[PID]/ns/ altında bir dosya olarak temsil edilir.

# Container'ımızın PID'sini host üzerinde bulalım (örneğin 12345 olsun)
# Host üzerinde:
ip netns attach kerten-netns 12345

Şimdi sanal ethernet çiftimizi oluşturalım. Bu işlem, bir ucu hostta, diğer ucu container içinde olan sanal bir kablo yaratacaktır:

# veth çiftini oluştur
ip link add veth-host type veth peer name veth-container

# Kablonun container ucunu container'ın network namespace'ine taşıyalım
ip link set veth-container netns kerten-netns

# Host tarafındaki uca IP verelim ve ayağa kaldıralım
ip addr add 10.200.0.1/24 dev veth-host
ip link set veth-host up

# Container tarafındaki uca IP verelim ve ayağa kaldıralım
ip netns exec kerten-netns ip addr add 10.200.0.2/24 dev veth-container
ip netns exec kerten-netns ip link set veth-container up
ip netns exec kerten-netns ip link set lo up # Loopback'i unutmayalım

# Container için default gateway tanımlayalım
ip netns exec kerten-netns ip route add default via 10.200.0.1

Artık container içinden host tarafındaki 10.200.0.1 IP adresine ping atabilirsiniz. Container runtime’ların her container için saniyeler içinde yaptığı o karmaşık ağ konfigürasyonunun temel mekanizması tam olarak budur.

5. Cgroup v2 Sihri: Kaynakları Sınırlandırmak

Güzel, izole bir ortamımız ve ağımız var. Peki ya bu container içerisindeki bir proses çıldırır ve host sistemin tüm CPU ve RAM kaynaklarını tüketmeye çalışırsa? İşte burada devreye cgroup (Control Groups) giriyor.

Modern Linux dağıtımları artık varsayılan olarak cgroup v2 kullanıyor. Cgroup v2, v1’deki dağınık yapıyı tek bir hiyerarşik ağaç altında birleştirerek işleri inanılmaz derecede kolaylaştırdı. Cgroup v2 hiyerarşisi varsayılan olarak /sys/fs/cgroup dizininde yaşar.

Şimdi elle “kerten-limit” adında bir kontrol grubu oluşturalım ve container’ımızın bellek kullanımını 100 MB ile sınırlayalım:

# Host üzerinde cgroup dizinine gidelim
cd /sys/fs/cgroup

# Yeni bir grup oluşturmak sadece bir dizin oluşturmaktan ibarettir!
mkdir kerten-limit
cd kerten-limit

# Kernel, bu dizini oluşturduğumuz an içine kontrol dosyalarını otomatik olarak yerleştirir.
ls -la

Şimdi bellek sınırımızı (memory limit) 100MB (104857600 bytes) olarak ayarlayalım:

echo "104857600" > memory.max

Peki bu sınırı container prosesimize nasıl uygulayacağız? Çok basit: Container prosesimizin host üzerindeki PID’sini (örneğin 12345) cgroup altındaki cgroup.procs dosyasına yazmamız yeterli:

echo "12345" > cgroup.procs

Artık bu proses veya bu prosesten türeyecek (fork) olan tüm alt süreçler toplamda 100 MB bellek sınırını aşamazlar. Sınırı aşmaya çalıştıkları anda kernel’ın meşhur OOM-Killer (Out of Memory Killer) mekanizması devreye girecek ve o prosesi acımasızca sonlandıracaktır. Tıpkı Kubernetes ortamında aldığınız o meşhur OOMKilled hatası gibi!

Özet: Container Aslında Bir İllüzyondur

Gördüğünüz gibi, arka planda çalışan gizemli hipervizörler, sanal donanımlar veya ağır sanallaştırma katmanları yok. Yaptığımız her şey, Linux kernel’ına “Bu prosese sadece şu dizini göster (mount namespace), sadece şu prosesleri görmesine izin ver (PID namespace), ağ trafiğini şu sanal kabloya yönlendir (net namespace) ve şu kadar kaynak tüketmesine izin ver (cgroups)” demekten ibaretti.

Docker ve containerd gibi araçlar, bu karmaşık syscall ve CLI yönetimini otomatize ederek bize pratik birer imaj paketleme ve dağıtım standardı sunar. Bu temel mekanizmayı kavramak, Kubernetes ortamlarında veya büyük ölçekli altyapılarda karşılaştığınız ağ, performans ve izolasyon sorunlarını (troubleshooting) çok daha hızlı ve profesyonelce çözmenizi sağlayacaktır.

Bir sonraki derin dalış makalemizde görüşmek üzere, sistemleriniz ayakta, container’larınız hafif kalsın!

Category: Genel | LEAVE A COMMENT
Ekim 4 2024

Cron İşlerini Bash ile Monitör Etme ve Alerting

Modern altyapılarda Kubernetes cronjob’ları, serverless scheduler’lar havada uçuşsa da, günün sonunda hepimizin bir yerlerde tıkır tıkır çalışan (ya da çalıştığını umduğu) emektar bir linux sunucusu ve içinde barınan klasik cron işleri vardır. Ancak cron’un en büyük problemi “sessiz ölüm” (silent failure) dediğimiz durumdur. Bir backup script’i patladığında, disk dolduğunda veya API key’in süresi geçtiğinde cron bunu size söylemez. Bu yazıda, kendi yazacağımız esnek bir bash wrapper script ile tüm cron işlerinizi merkezi olarak monitoring dünyasına entegre edecek, hata durumlarında anında alert üretecek ve üçüncü parti servislerle nasıl haberleştireceğimizi göreceğiz.

Klasik Cron Yaklaşımlarının Sorunu Nedir?

Hepimiz o yollardan geçtik. Bir cron işinin çıktısını almak için satırın sonuna efsanevi >> /var/log/myjob.log 2>&1 yönlendirmesini yazarız. Sonra ne mi olur? O log dosyası gigabaytlarca büyür, diski doldurur ve bir gün script hata verdiğinde kimsenin haberi olmaz. Çünkü logları aktif olarak okuyan kimse yoktur.

Eski usul Linux yöneticileri size “MTA (Mail Transfer Agent) kurun, cron hata aldığında lokal mail atsın” diyecektir. Yıl 2024+. ephemeral VM’lerin, cloud instance’larının cirit attığı bu çağda localhost mail kuyruklarıyla uğraşmak, postfix yapılandırmak ve spame düşen lokal mailleri takip etmek tam bir operasyonel işkencedir. Bizim daha proaktif, modern ve DevOps pratiklerine uygun bir çözüme ihtiyacımız var.

Wrapper Script Pattern Neden En İyi Çözümdür?

Elinizde 20 farklı cron script’i olduğunu düşünün. Her birinin içine tek tek Slack webhook entegrasyonu, hata yakalama (error handling) ve çalışma süresi ölçümü eklemek tam bir anti-pattern’dir. Kod tekrarı yaratır ve yarın öbür gün Slack yerine Teams’e geçildiğinde 20 farklı script’i update etmeniz gerekir.

Wrapper Script Pattern, çalıştırılacak asıl komutu sarmalayan, onun exit code’unu yakalayan, standart output (stdout) ve standart error (stderr) akışlarını yöneten merkezi bir aracıdır. Crontab içerisindeki tanımımız şuna benzer:

0 4 * * * /usr/local/bin/cron-wrapper.sh --job "db-backup" --cmd "/opt/scripts/backup.sh --force"

Bu yaklaşım sayesinde, asıl işi yapan script’in monitoring mantığından tamamen bağımsız (decoupled) kalmasını sağlarız. Script sadece işini yapar ve exit code döner; wrapper ise raporlamayı üstlenir.

Adım 1: Savaşçı Ruhlu Bash Wrapper Script

Lafı uzatmadan production ortamında güvenle kullanabileceğiniz, hata durumunda exit code’u ve logları yakalayan, zaman aşımı (timeout) kontrolü yapabilen wrapper script’imizi yazalım. Bu script’i /usr/local/bin/cron-wrapper.sh olarak kaydedebilirsiniz.

#!/usr/bin/env bash

# Robust Bash Ayarları
set -o nounset
set -o pipefail

# Değişken Tanımları
JOB_NAME=""
COMMAND=""
SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL:-""}"
HEALTHCHECK_URL="${HEALTHCHECK_URL:-""}"

# Kullanım Yardımcısı
usage() {
    echo "Kullanım: $0 --job [JOB_NAME] --cmd [COMMAND]"
    exit 1
}

# Parametreleri Parse Etme
while [[ $# -gt 0 ]]; do
    case "$1" in
        --job)
            JOB_NAME="$2"
            shift 2
            ;;
        --cmd)
            COMMAND="$2"
            shift 2
            ;;
        *)
            usage
            ;;
    esac
done

if [[ -z "$JOB_NAME" || -z "$COMMAND" ]]; then
    usage
fi

# Geçici Log Dosyası Oluşturma (Güvenli yöntem)
LOG_FILE=$(mktemp /tmp/cron_wrapper_${JOB_NAME}.XXXXXX)

# Her durumda temizlik yapılması için trap kullanımı
cleanup() {
    rm -f "$LOG_FILE"
}
trap cleanup EXIT

# Zamanı Ölçmeye Başla
START_TIME=$(date +%s)

echo "[$(date '+%Y-%m-%d %H:%M:%S')] Job '$JOB_NAME' başlatılıyor..." >> "$LOG_FILE"
echo "Komut: $COMMAND" >> "$LOG_FILE"
echo "--------------------------------------------------" >> "$LOG_FILE"

# Komutu çalıştır ve tüm çıktıyı log dosyasına yönlendir
# set +e kullanıyoruz çünkü komut hata verirse wrapper script'in hemen çökmesini istemiyoruz,
# hatayı yakalayıp alert üreteceğiz.
set +e
eval "$COMMAND" >> "$LOG_FILE" 2>&1
EXIT_CODE=$?
set -e

# Zamanı Hesapla
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))

echo "--------------------------------------------------" >> "$LOG_FILE"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Job bitti. Süre: ${DURATION}sn, Exit Code: $EXIT_CODE" >> "$LOG_FILE"

# Sonuçları Değerlendir
if [ $EXIT_CODE -eq 0 ]; then
    echo "Başarılı: $JOB_NAME sorunsuz tamamlandı."
    # Eğer tanımlıysa Healthcheck ping'i gönder (Mavi Tik)
    if [ -n "$HEALTHCHECK_URL" ]; then
        curl -fsS --retry 3 "$HEALTHCHECK_URL" > /dev/null 2>&1 || true
    fi
else
    echo "HATA: $JOB_NAME başarısız oldu! Exit code: $EXIT_CODE" >&2
    
    # Slack Alert Tetikleme fonksiyonunu çağır
    if [ -n "$SLACK_WEBHOOK_URL" ]; then
        # Log dosyasının son 20 satırını alalım ki Slack mesajı devasa olmasın
        LAST_LOGS=$(tail -n 20 "$LOG_FILE" | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g')
        
        PAYLOAD=$(cat <<EOF
{
  "text": "🚨 *Cron Job Hatası!*",
  "attachments": [
    {
      "color": "danger",
      "fields": [
        { "title": "Job Adı", "value": "${JOB_NAME}", "short": true },
        { "title": "Exit Code", "value": "${EXIT_CODE}", "short": true },
        { "title": "Süre", "value": "${DURATION} saniye", "short": true },
        { "title": "Sunucu", "value": "$(hostname)", "short": true }
      ],
      "text": "*Son Loglar:*\n\`\`\`${LAST_LOGS}\`\`\`"
    }
  ]
}
EOF
)
        curl -X POST -H 'Content-type: application/json' --data "$PAYLOAD" "$SLACK_WEBHOOK_URL" > /dev/null 2>&1 || true
    fi
    
    # Wrapper kendisi de hata koduyla çıksın ki pipeline'lar veya üst sistemler anlasın
    exit $EXIT_CODE
fi

Neden “set +e” ve “set -o pipefail” Kullandık?

Bash’te hata yönetimi hassas bir konudur. set -o pipefail komutu, bir pipe zincirindeki herhangi bir komut hata verirse tüm satırın hata kodu dönmesini sağlar. Wrapper script’in başında bunu aktif ettik. Ancak, sarmaladığımız asıl komut hata verdiğinde script’imizin patlayıp Slack alert’ini gönderemeden sonlanmasını engellemek için, çalıştırma aşamasında geçici olarak set +e ile esneklik sağladık, exit code’u güvenli bir şekilde $? ile lokal değişkene aldık ve ardından set -e ile güvenli modumuza geri döndük.

Ayrıca mktemp kullanımı, paralel çalışan cron işlerinin aynı log dosyası üzerine yazarak yarış durumuna (race condition) girmesini engeller.

Adım 2: “Dead Man’s Snitch” Mantığı ile Healthcheck Entegrasyonu

Peki ya sunucunun kendisi çökerse? Ya da cron daemon durursa? Bu durumlarda “hata aldığımda Slack’e yaz” mantığı tamamen devre dışı kalır. Çünkü çalışmayan bir sistem hata mesajı da gönderemez. Buna monitoring dünyasında silent failure denir.

Çözüm: Inverted Monitoring (Ters İzleme) ya da bilinen adıyla Heartbeat / Dead Man’s Snitch pattern. Sistem çalışmayı durdurduğunda alarm çalmasını istiyorsak, sistemin düzenli aralıklarla “Ben hayattayım!” sinyali göndermesi gerekir. Eğer sinyal gelmezse, monitoring sunucumuz bizi uyarır.

Bu iş için healthchecks.io veya kendi host ettiğiniz open-source alternatiflerini (örneğin Uptime Kuma) kullanabilirsiniz. Script’imizdeki HEALTHCHECK_URL tam olarak bunu yapar. Cron başarılı biterse ilgili adrese bir curl isteği atar. Eğer o istek zamanında gitmezse, dış dünya sunucunun patladığını anlar.

Adım 3: Slack Webhook ile Gerçek Zamanlı Alerting

Yukarıdaki script’imizde Slack için JSON payload’unu dinamik olarak oluşturduk. Bash içinde JSON oluştururken kaçış karakterleri (escaping) her zaman baş ağrıtır. Bu yüzden log dosyasından okuduğumuz son 20 satırı (LAST_LOGS) temizlemek için sed ile yeni satır karakterlerini (\n) ve çift tırnakları escape ettik.

Slack’te görünecek mesaj, hata anında debug yapmanızı inanılmaz kolaylaştıracaktır. Hangi sunucuda, ne kadar sürede patladığını ve hatanın son satırlarını terminale girmeden telefonunuzdan görebilirsiniz:

Slack Notification Alerting

Adım 4: Crontab Yapılandırması

Şimdi yazdığımız bu sistemi gerçek bir senaryoda test edelim. Sunucumuzdaki env variable’ları crontab ortamına geçirmek bazen can sıkıcı olabilir. En temiz yöntem webhook url gibi hassas verileri wrapper script içine hardcode etmek yerine wrapper’ı çağırırken bir konfigürasyondan beslemek veya crontab’ın üst kısmında tanımlamaktır.

# /etc/cron.d/my-monitored-crons veya crontab -e

SLACK_WEBHOOK_URL="https://hooks.slack.com/services/T00/B00/X00"
HEALTHCHECK_URL="https://hc-ping.com/your-uuid-here"

# Her gece 03:00'te yedek al, hata olursa Slack'e bildir, başarılı olursa ping at
0 3 * * * root /usr/local/bin/cron-wrapper.sh --job "database-backup" --cmd "/opt/backups/pg_dump.sh"

Dikkat Edilmesi Gereken İpuçları

  • Mutlak Yol (Absolute Paths) Kullanın: Cron ortamında $PATH değişkeni kısıtlıdır. Hem wrapper script’in içinde çağırdığınız tool’ların (curl, sed vb.) hem de crontab içindeki komutların tam yollarını yazmaya özen gösterin.
  • Yetkilendirme: Wrapper script’in çalıştırılabilir olduğundan emin olun: chmod +x /usr/local/bin/cron-wrapper.sh
  • Log Rotasyonu: Biz script içinde trap kullanarak geçici logları işlem sonunda siliyoruz. Ancak işlem yarıda kesilirse (örneğin kill -9) loglar kalabilir. Periyodik olarak /tmp dizininin temizlendiğinden emin olun (çoğu modern Linux dağıtımı bunu otomatik yapar).

Özet

Cron işlerini kendi haline bırakmak, prod ortamında saatli bir bomba üzerinde oturmak gibidir. Bu makalede yazdığımız esnek bash wrapper script sayesinde, mevcut hiçbir altyapınızı bozmadan, tek bir merkezi mekanizma ile tüm linux cron işlerinizi modern birer microservice gibi monitoring ve alert süreçlerine dahil ettik. Unutmayın, en iyi sistem tıkır tıkır çalışan değil, bozulduğunda neresinin bozulduğunu size ilk söyleyen sistemdir.

Category: Genel | LEAVE A COMMENT
Eylül 27 2024

Linux Sistem Performans Analizi: perf, strace ve eBPF Araçları

Bir sabah Slack’ten gelen o korkunç alarm sesiyle uyandınız: “Production veritabanı sunucusunda latency uçtu, CPU %99!” Hemen sunucuya SSH ile bağlandınız, top komutunu çalıştırdınız ve evet, CPU gerçekten can çekişiyor. Ancak top veya htop gibi geleneksel araçlar size sadece “yangın olduğunu” söyler; yangının hangi odada, hangi kibritle başladığını göstermez. İşte bu noktada modern bir SRE (Site Reliability Engineer) gibi düşünmeli ve linux çekirdeğinin (kernel) derinliklerine inmeliyiz. Bu yazıda, modern altyapılarda performance sorunlarını iğne deliğinden geçirir gibi analiz etmenizi sağlayacak üç silahşörü inceleyeceğiz: perf, strace ve devrim niteliğindeki ebpf tabanlı bpftrace.

1. Donanımın Nabzını Tutmak: perf (CPU & Hardware Profiling)

perf, Linux çekirdeği ile doğrudan entegre çalışan inanılmaz güçlü bir profil çıkarma (profiling) aracıdır. CPU döngülerini (cycles), cache miss (önbellek kaçırma) oranlarını ve CPU talimatlarını (instructions) donanımsal sayaçları (PMU – Performance Monitoring Unit) kullanarak analiz eder.

Neden Sadece CPU Yüzdesine Bakmak Yanıltıcıdır?

Çoğu mühendis CPU %100 olduğunda uygulamanın çok yoğun hesaplama yaptığını düşünür. Ancak durum her zaman bu değildir. CPU, RAM’den veri beklerken de (stall cycles) %100 meşgul görünebilir. Biz buna Memory-Bound (bellek sınırlandırılmış) deriz. Eğer CPU gerçekten matematiksel işlemler yapıyorsa buna da Compute-Bound denir.

Bu ayrımı görmek için ilk yapmamız gereken şey perf stat komutunu çalıştırmaktır:

# Belirli bir PID'yi 5 saniye boyunca analiz edelim
perf stat -p 1234 sleep 5

Karşımıza şöyle bir çıktı gelecektir:

 Performance counter stats for process id '1234':

       2001.45 msec task-clock                #    0.400 CPUs utilized          
              1234  context-switches          #    0.617 K/sec                  
                45  cpu-migrations            #    0.022 K/sec                  
               105  page-faults               #    0.052 K/sec                  
        4002938102  cycles                    #    2.000 GHz                    
        2001467291  instructions              #    0.50  insn per cycle         
         120495839  branches                  #   60.204 M/sec                  
           4958102  branch-misses             #    4.11% of all branches        

       5.002345123 seconds time elapsed

Burada odaklanmamız gereken en kritik metrik insn per cycle (IPC) değeridir. IPC (Instructions Per Cycle), CPU’nun her döngüde kaç talimat çalıştırdığını gösterir.

  • IPC < 1.0 ise: Uygulamanız muhtemelen I/O veya memory erişimi bekliyor (Memory-Bound). CPU boşta (stall) bekliyor demektir.
  • IPC > 1.5 ise: CPU gerçekten yoğun şekilde kodunuzu çalıştırıyor demektir (Compute-Bound).

Sıcak Noktaları Bulmak: perf record ve perf report

Uygulamanın tam olarak hangi fonksiyonunun CPU’yu sömürdüğünü bulmak için örnekleme (sampling) yapmamız gerekir. Bu işlem production ortamlarında genellikle %1 ila %5 arasında çok düşük bir overhead (ek yük) ile yapılabilir.

# Saniyede 99 frekansla (overkill olmaması için idealdir) 10 saniye boyunca kayıt alalım
# -g parametresi call-graph (çağrı ağacı) kaydetmesini sağlar
perf record -F 99 -p 1234 -g -- sleep 10

Bu komut geçerli dizinde perf.data adında bir dosya oluşturur. Bu dosyayı analiz etmek için terminalden şu komutu veririz:

perf report -n --stdio

Karşınıza çıkan interaktif arayüzde hangi fonksiyonun (sembolün) CPU döngülerinin yüzde kaçını harcadığını hiyerarşik bir şekilde görebilirsiniz. Eğer fonksiyon isimleri yerine [hexadecimal] adresler görüyorsanız, uygulamanızın “debug symbols” (hata ayıklama sembolleri) eksiktir demektir. Go kullanıyorsanız binary’nizi strip etmeyin, C++/Rust kullanıyorsanız -g flag’i ile derlediğinizden emin olun.

2. Karanlıkta Kalan Sistem Çağrıları: strace

Uygulamanız çalışıyor ama hiçbir şey yapmıyor gibi mi görünüyor? Dosya okumaya çalışırken kilitlenmiş olabilir mi? Yoksa DNS çözümlemesi yaparken timeout mu yaşıyor? Bu gibi durumlarda, uygulamanın kernel ile olan iletişimini yani System Calls (syscalls) trafiğini izlememiz gerekir.

İşte strace bu işin mutfağıdır.

Production Uyarısı: strace’i Dikkatli Kullanın!

Geliştirme ortamında strace my_app yazıp geçmek harikadır ancak bunu canlı production ortamında sakın yapmayın! strace, izlediği process’in her syscall yaptığında durdurulmasını (ptrace ile) sağlar. Bu durum, uygulamanın performansını %100 ila %1000 oranında düşürebilir.

Bunun yerine, production dostu parametrelerle nokta atışı yapmalıyız:

# Uygulamanın en çok hangi sistem çağrısında ne kadar zaman harcadığını özetleyelim
# Bu işlem ham log akıtmaya göre çok daha az overhead yaratır
strace -c -p 1234

Çıktı bize muhteşem bir özet sunacaktır:

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 89.45    1.204958         120     10041           read
 10.12    0.136120          13     10200      1050 openat
  0.43    0.005789          11       500           write
------ ----------- ----------- --------- --------- ----------------
100.00    1.346867                 20741      1050 total

Yukarıdaki tabloda net bir şekilde görüyoruz ki, uygulama zamanının %89’unu read sistem çağrısında harcıyor ve ciddi miktarda openat (dosya açma hatası) alıyor. Hangi dosyayı açamadığını bulmak için sadece hatalı çağrıları filtreleyebiliriz:

# Sadece başarısız olan (errors) openat çağrılarını göster
strace -e trace=openat -Z -p 1234

3. Modern Çağın Sihirli Değneği: eBPF ve bpftrace

Geleneksel araçların kısıtlamalarından sıkıldıysanız, sahneye eBPF (Extended Berkeley Packet Filter) çıkıyor. eBPF, Linux çekirdeğinin kodunu değiştirmeden veya kernel modülü yüklemeden, güvenli sandbox’lar içinde doğrudan çekirdekte kod çalıştırmamızı sağlar.

bpftrace ise eBPF dünyasının “awk” dilidir. Son derece hafif, pratik ve neredeyse sıfır overhead ile çalışan tek satırlık (one-liners) script’ler yazmamıza olanak tanır.

Örnek 1: Disk I/O Latency Analizi (Biolatency)

Diskinizin yavaş olduğunu düşünüyorsunuz ama hangi process’in ne kadarlık bir gecikmeye sebep olduğunu bulamıyorsunuz. Geleneksel iostat size sadece ortalama değerler verir. bpftrace ile gerçek zamanlı bir histogram çizelim:

# Disk I/O tamamlanma sürelerini mikrosaniye cinsinden histogram olarak gösterir
bpftrace -e '
kprobe:vfs_read { @start[tid] = nsecs; } 
kretprobe:vfs_read /@start[tid]/ { 
    @latency = hist((nsecs - @start[tid]) / 1000); 
    delete(@start[tid]); 
}'

Bu script, her vfs_read (sanal dosya sistemi okuma) başladığında bir zaman damgası alır ve bittiğinde aradaki farkı hesaplayıp logaritmik bir grafik çizer:

@latency: 
[2, 4)                12 |@@@@                                        |
[4, 8)                85 |@@@@@@@@@@@@@@@@@@@@@@@@                    |
[8, 16)              142 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[16, 32)              45 |@@@@@@@@@@@@                                |
[32, 64)               8 |@@                                          |

Bu grafiğe bakarak, okuma işlemlerinin büyük çoğunluğunun 8-16 mikrosaniye arasında tamamlandığını, yani diskimizin sağlıklı çalıştığını saniyeler içinde anlayabiliriz.

Örnek 2: Bellek Sızıntısı (Memory Leak) Avı

Sisteminizde gizemli bir bellek şişmesi var. Hangi uygulamanın sürekli malloc çağırıp free etmediğini bulmak istiyorsunuz. eBPF ile kullanıcı alanındaki (user-space) bellek allocation çağrılarını takip edebiliriz:

# libc içindeki malloc çağrılarının byte boyutlarına göre dağılımını izleyelim
bpftrace -e 'usdt:/lib/x86_64-linux-gnu/libc.so.6:libc:memory_malloc_retry { @[arg0] = count(); }'

Bu sayede production ortamında çalışan uygulamanıza hiçbir kütüphane enjekte etmeden veya onu durdurmadan bellek tüketim kalıplarını yakalayabilirsiniz.

Özet ve Doğru Aracı Seçme Rehberi

Sistem tıkandığında hangi aracı ne zaman kullanacağınıza karar vermek, tecrübeli bir mühendisi amatörden ayıran en önemli özelliktir. İşte size pratik bir başucu tablosu:

Senaryo / Belirti Kullanılacak Araç Neden?
Yüksek CPU kullanımı var, hangi kod satırının yavaş olduğunu bulmak istiyorum. perf record / report Düşük overhead ile CPU çağrı ağacını (call graph) çıkarır.
Uygulama kilitlendi (stuck), log üretmiyor, ne yaptığını göremiyorum. strace -p <PID> Çekirdeğe gönderdiği sistem çağrılarını (network, dosya erişimi) anlık listeler.
Disk performansından şüpheleniyorum ama genel istatistikler yetersiz kalıyor. bpftrace (eBPF) Kernel seviyesinde I/O kuyruk gecikmesini histogram olarak gösterir.
Sistemin donanımsal düzeyde (L1/L2 Cache, CPU Cycles) analizine ihtiyacım var. perf stat İşlemcinin donanımsal sayaçlarına doğrudan erişim sağlar.

Performans analizi yaparken her zaman en az invaziv (sisteme en az müdahale eden) yöntemden başlayın. Önce perf stat ve bpftrace ile genel resmi görün, gerekirse ve güvenliyse strace ile derinlemesine inceleme yapın. Unutmayın, iyi bir SRE sadece sorunu çözen değil, sorunu çözerken production sistemini ayakta tutabilen kişidir!

Category: Genel | LEAVE A COMMENT
Ağustos 23 2024

Bash Script’lerinde Kurşun Geçirmez Hata Yönetimi: set -euo pipefail ve Ötesi

Hepimiz oradaydık: Gece yarısı gelen bir PagerDuty alarmı, yarıda kesilmiş bir CI/CD pipeline’ı ve sessizce hata fırlatıp “başarılı” (exit 0) olarak sonlanan bir bash script’i. Linux sistemlerde scripting yaparken, özellikle modern devops ve otomasyon süreçlerinde, hata yönetimi lüks değil bir zorunluluktur. Varsayılan olarak Bash, adeta bir nihilist gibi davranır: Adımlardan biri başarısız olsa bile, hiçbir şey olmamış gibi bir sonraki satıra geçer. Bu makalede, bu vurdumduymazlığı nasıl dizginleyeceğimizi ve prod-ready scriptler yazacağımızı inceleyeceğiz.

5+ yıllık deneyime sahip bir mühendis olarak muhtemelen set -e komutunu duymuş, hatta şablonlarınıza eklemişsinizdir. Ancak bu buzdağının sadece görünen kısmı. Gelin, işi profesyonel seviyeye taşıyalım.

Sihirli Üçlü: set -euo pipefail

Bash script’lerinizin başına ekleyeceğiniz bu sihirli satır, hata yönetiminizin temel taşıdır. Peki ama neden bu üç parametre birlikte kullanılmalı? Her birinin arka planda çözdüğü spesifik problemi inceleyelim.

#!/usr/bin/env bash
set -euo pipefail

1. set -e (errexit)

Herhangi bir komut sıfırdan farklı bir exit code ile dönerse, script’in çalışmasını anında durdurur.

Neden gerekli? Varsayılan senaryoda, diskte yer kalmadığı için başarısız olan bir tar komutundan sonra script çalışmaya devam eder ve bir sonraki adımdaki silme işlemini tetikleyebilir. set -e bunu engeller. Ancak alt kabuklarda (subshell) veya mantıksal operatörlerde (&&, ||) her zaman beklediğiniz gibi davranmaz. Bu yüzden tek başına kurtarıcı değildir.

2. set -u (nounset)

Tanımlanmamış bir değişken kullanılmaya çalışıldığında script’i hata vererek durdurur.

Neden gerekli? Şu felaket senaryosunu düşünün:

# set -u OLMADAN
TARGET_DIR="" # Bir hata sonucu boş kaldı
rm -rf "$TARGET_DIR/*" # Tebrikler, kök dizini (root) sildiniz.

Eğer set -u aktif olsaydı, Bash daha rm komutuna geçmeden TARGET_DIR is not set hatasıyla işlemi sonlandıracaktı.

3. set -o pipefail

Pipeline (boru hattı) içerisindeki herhangi bir komut hata aldığında, tüm pipeline’ın exit code’unu en son hata alan komutun kodu olarak belirler.

Neden gerekli? Bash varsayılan olarak sadece pipeline’ın en sonundaki komutun exit code’una bakar.

# set -o pipefail OLMADAN
non_existent_command | echo "Merhaba"
echo $? # Çıktı: 0 (Çünkü echo başarılı oldu!)

Yukarıdaki örnekte ilk komut crash olsa bile pipeline başarılı sayılır. set -o pipefail ile bu durum engellenir ve ilk komutun hatası tüm hattı başarısız kılar.

İstisnaları Yönetmek: “Peki ya hata almasını bekliyorsam?”

set -e kullandığınızda, bazen hata vermesi normal olan komutlar da script’inizi sonlandırır. Örneğin, bir portun açık olup olmadığını nc ile kontrol etmek istiyorsunuz:

#!/usr/bin/env bash
set -euo pipefail

# nc başarısız olursa script burada ölecektir.
nc -z -w3 10.0.0.5 80

echo "Bu satıra asla ulaşılamayacak."

Bunu aşmanın “temiz” yolu, komutun sonuna mantıksal || true eklemek veya hata durumunu inline olarak handle etmektir:

# Güvenli yaklaşım 1: || true ile bypass etmek
nc -z -w3 10.0.0.5 80 || true

# Güvenli yaklaşım 2: Hata durumunda alternatif aksiyon almak
if ! nc -z -w3 10.0.0.5 80; then
    echo "Port kapalı, alternatif akışa geçiliyor..."
    # Burası script'i durdurmaz çünkü if bloğu içindeyiz
fi

Trap Mekanizması ile Graceful Degradation ve Cleanup

Script’iniz yarıda kesildiğinde (ister hata sebebiyle, ister kullanıcı CTRL+C’ye bastığında), arkasında çöp bırakmamalıdır. Geçici dosyalar (temporary files), kilit mekanizmaları (lock files) veya açık SSH tünelleri mutlaka temizlenmelidir. İşte burada trap devreye girer.

Bash’teki sinyalleri (signals) yakalayarak, script sonlanırken mutlaka çalıştırılacak bir cleanup fonksiyonu tanımlayabiliriz:

#!/usr/bin/env bash
set -euo pipefail

# Geçici bir dizin oluşturalım
TMP_DIR=$(mktemp -d)
echo "Geçici dizin oluşturuldu: ${TMP_DIR}"

# Cleanup fonksiyonumuz
cleanup() {
    local exit_code=$?
    echo "Temizlik yapılıyor: ${TMP_DIR} siliniyor..."
    rm -rf "${TMP_DIR}"
    
    if [ $exit_code -ne 0 ]; then
        echo "Script HATA ile sonlandı! Exit code: ${exit_code}"
    else
        echo "Script başarıyla tamamlandı."
    fi
    exit $exit_code
}

# EXIT sinyalini yakala (Hata olsun ya da olmasın, script çıkarken çalışır)
trap cleanup EXIT

# İş yükü simülasyonu
echo "İşlem yapılıyor..."
touch "${TMP_DIR}/important_data.tmp"

# Hata simülasyonu (Burada script patlayacak ama trap çalışacak!)
grep "olmayan_pattern" "${TMP_DIR}/important_data.tmp"

Standartlara Uygun Exit Code Kullanımı

Gelişigüzel exit 1 yazıp geçmek, üst seviye bir otomasyonda kabul edilemez. Script’inizin çağıran sisteme (örneğin Jenkins, GitHub Actions veya bir cron job) neyin yanlış gittiğini net bir şekilde söylemesi gerekir.

POSIX standartlarına ve /usr/include/sysexits.h yapısına uygun olarak şu kodları tercih edin:

  • 0: Başarılı operasyon.
  • 64 (EX_USAGE): Yanlış CLI argüman kullanımı.
  • 69 (EX_UNAVAILABLE): Servis veya kaynak ulaşılamaz durumda.
  • 70 (EX_SOFTWARE): İçsel yazılım hatası (Internal error).
  • 74 (EX_IOERR): Giriş/Çıkış (I/O) hatası (Disk dolu, dosya yazılamadı).
# Örnek kullanım
if [[ $# -lt 2 ]]; then
    echo "Hata: Eksik parametre. Kullanım: $0  " >&2
    exit 64
fi

Script’lerinizi Test Edin: BATS (Bash Automated Testing System)

SRE dünyasında “test edilmeyen kod çalışmıyordur” kuralı geçerlidir. Bash script’lerinizi manuel test etmek yerine, declarative testler yazmak için bats-core kullanabilirsiniz.

Önce basit bir script’imiz olsun (deploy.sh):

#!/usr/bin/env bash
# deploy.sh
set -euo pipefail

check_env() {
    if [[ -z "${APP_ENV:-}" ]]; then
        echo "Hata: APP_ENV tanımlı değil!" >&2
        return 64
    fi
}

check_env
echo "Deploying to ${APP_ENV}"

Şimdi bu script için yazacağımız BATS test dosyası (deploy.bats):

#!/usr/bin/env bats

# Test 1: APP_ENV tanımlı değilse script 64 koduyla hata vermeli
@test "APP_ENV set edilmediğinde script hata fırlatmalı" {
  run ./deploy.sh
  [ "$status" -eq 64 ]
  [[ "$output" =~ "Hata: APP_ENV tanımlı değil!" ]]
}

# Test 2: APP_ENV tanımlı olduğunda başarılı olmalı
@test "APP_ENV set edildiğinde script başarılı olmalı" {
  export APP_ENV="production"
  run ./deploy.sh
  [ "$status" -eq 0 ]
  [[ "$output" =~ "Deploying to production" ]]
}

Bu testleri CI pipeline’ınızda bats deploy.bats komutuyla çalıştırarak, script’lerinizin gelecekteki değişikliklerde kırılmasını engelleyebilirsiniz.

Özet

Bash’te hata yönetimi, sadece set -e yazıp şansımıza güvenmekten ibaret değildir. Profesyonel otomasyon süreçlerinde, pipefail ile boru hatlarını güvenceye almak, trap ile arkamızı temizlemek, standart exit code’lar ile çağıran sistemlere anlamlı yanıtlar dönmek ve nihayetinde BATS ile bu davranışları test etmek gerekir. Unutmayın, iyi bir DevOps mühendisi sadece çalışan kod değil, güvenle patlayan kod yazar.

Category: Genel | LEAVE A COMMENT