Bir sabah uyandığında production sunucunun ayakta olmadığını görmen kadar kötü bir şey yoktur. Logları incelemeye başlarsın, servislerin birbirini beklediğini, yanlış sırayla başladığını ya da dependency zincirinin bir yerinde kopukluk olduğunu fark edersin. Systemd dünyasında servis başlatma sırası sorunları, her sysadmin’in er ya da geç karşılaştığı klasik problemlerden biridir. Bu yazıda bu tür sorunları nasıl tespit edeceğini, kökten nasıl çözeceğini ve tekrar yaşamamak için neler yapabileceğini adım adım ele alacağız.
Systemd’nin Başlatma Mantığını Anlamak
Systemd, eski SysVinit’in aksine paralel servis başlatma mantığıyla çalışır. Bu, boot süresini ciddi ölçüde kısaltır ama aynı zamanda “A servisi B’den önce başladı, B henüz hazır değildi” gibi sorunların kapısını aralar. Systemd’nin dependency sistemi şu kavramlar üzerine kurulur:
- Wants: Gevşek bağımlılık. A servisi B’yi ister ama B başlamazsa A yine de çalışır.
- Requires: Sıkı bağımlılık. B başlamazsa A da başlamaz.
- After: Sıralama direktifi. A, B’den sonra başlar. Ancak tek başına bağımlılık oluşturmaz.
- Before: Sıralama direktifi. A, B’den önce başlar.
- BindsTo: Requires’dan daha güçlü. B durursa A da durur.
- PartOf: A, B’nin bir parçasıdır. B yeniden başlatılınca A da yeniden başlatılır.
Bu kavramların farkını bilmeden bir servis unit dosyası yazmak, ilerleyen zamanlarda büyük baş ağrısına yol açar.
Sorunun Kaynağını Tespit Etmek
journalctl ile Başlangıç Loglarını İncelemek
İlk adım her zaman loglara bakmaktır. Systemd, boot sürecindeki tüm olayları journald aracılığıyla kaydeder.
# Son boot'taki tüm logları gör
journalctl -b
# Önceki boot'un loglarını gör (bir önceki crash için ideal)
journalctl -b -1
# Sadece belirli bir servisin boot loglarını gör
journalctl -b -u postgresql.service
# Hata seviyesindeki logları filtrele
journalctl -b -p err
Bir örnek senaryo düşün: PostgreSQL bağlı olan bir Django uygulaması başlarken “connection refused” hatası veriyorsa, ilk bakacağın yer burası olmalı. journalctl çıktısında timestamp’lere dikkat et. Eğer Django servisi PostgreSQL’den önce başlamışsa sorunun kaynağını bulmuş demeksin.
systemd-analyze ile Boot Analizi
Systemd’nin kendi analiz araçları bu konuda inanılmaz derecede yardımcı olur.
# Boot süresinin genel özeti
systemd-analyze
# Her servisin başlama süresini göster
systemd-analyze blame
# Kritik path'i göster (en uzun zinciri)
systemd-analyze critical-chain
# Belirli bir servisin kritik zincirini göster
systemd-analyze critical-chain nginx.service
systemd-analyze blame çıktısında şöyle bir şey görebilirsin:
# Örnek çıktı yorumu
15.234s NetworkManager-wait-online.service
8.901s postgresql.service
4.123s docker.service
2.456s myapp.service
Bu çıktıda NetworkManager-wait-online.service‘in 15 saniye sürmesi dikkat çekici. Bu servis, network’ün tamamen hazır olmasını bekler ve çoğu zaman gereksiz yere diğer servisleri bloke eder.
Dependency Zincirini Görselleştirmek
# Bir servisin tüm bağımlılıklarını listele
systemctl list-dependencies nginx.service
# Tersine bağımlılıkları göster (bu servisi kim bekliyor)
systemctl list-dependencies --reverse nginx.service
# Sadece başarısız servisleri listele
systemctl list-units --failed
Gerçek Dünya Senaryoları ve Çözümleri
Senaryo 1: Veritabanı Hazır Olmadan Uygulama Başlıyor
Bu, en sık karşılaşılan problem türüdür. PostgreSQL veya MySQL henüz bağlantı kabul edecek duruma gelmeden uygulama servisi başlamaya çalışır.
Önce mevcut durumu kontrol edelim:
# Servis durumlarına bak
systemctl status myapp.service
systemctl status postgresql.service
# Servis unit dosyasını incele
systemctl cat myapp.service
Sorunlu bir unit dosyası şöyle görünebilir:
[Unit]
Description=My Web Application
After=network.target
[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/start.sh
Restart=always
[Install]
WantedBy=multi-user.target
Bu dosyada sorun açık: PostgreSQL’e hiçbir bağımlılık tanımlanmamış. Düzeltilmiş versiyonu:
[Unit]
Description=My Web Application
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/start.sh
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Ancak dikkat et: Requires=postgresql.service eklemek, PostgreSQL servisinin unit olarak aktif olduğunu garanti eder, ama PostgreSQL’in bağlantı kabul etmeye hazır olduğunu garanti etmez. Bu ince fark çok önemli.
Senaryo 2: Servis Başladı Ama Uygulama Henüz Hazır Değil
Systemd açısından bir servis “active” durumuna geçtiğinde, arkadaki process her zaman tamamen hazır olmayabilir. Özellikle Type=simple olan servislerde bu durum sık yaşanır. Bu noktada Type=notify veya ExecStartPost ile sağlık kontrolü yapmak işe yarar.
# PostgreSQL'in gerçekten hazır olup olmadığını kontrol eden script
cat /opt/scripts/wait-for-postgres.sh
#!/bin/bash
# wait-for-postgres.sh
TIMEOUT=30
COUNTER=0
until pg_isready -h localhost -p 5432 -U postgres > /dev/null 2>&1; do
COUNTER=$((COUNTER + 1))
if [ $COUNTER -ge $TIMEOUT ]; then
echo "PostgreSQL $TIMEOUT saniye içinde hazır olmadı, çıkılıyor."
exit 1
fi
echo "PostgreSQL bekleniyor... ($COUNTER/$TIMEOUT)"
sleep 1
done
echo "PostgreSQL hazır!"
exec "$@"
Bu script’i kullanacak şekilde unit dosyasını güncelle:
[Unit]
Description=My Web Application
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/myapp
ExecStartPre=/opt/scripts/wait-for-postgres.sh
ExecStart=/opt/myapp/bin/start.sh
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=3
[Install]
WantedBy=multi-user.target
ExecStartPre direktifi, asıl servisi başlatmadan önce çalıştırılır. Script başarısız olursa servis başlamaz ve systemd bunu loglar.
Senaryo 3: Ağ Bağlantısı Gerektiren Servisler
NetworkManager-wait-online.service veya network.target beklemesi çoğu zaman yanlış yapılandırılmış olur. Ağa bağlı olan bir servis için doğru yaklaşım:
# Network target'ların farkını anlamak için
systemctl cat network.target
systemctl cat network-online.target
[Unit]
Description=Ağa Bağlı Servisim
# network.target: network stack başladı ama online olmayabilir
# network-online.target: network gerçekten kullanılabilir
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/mynetworkservice
Restart=on-failure
[Install]
WantedBy=multi-user.target
Wants=network-online.target kullanmak, bu target’ın aktif edilmesini tetikler. After ile sıralama sağlanır. Sadece After yazmak yetmez, çünkü systemd o target’ı başlatmaya çalışmaz bile.
Senaryo 4: Docker Servislerinde Başlatma Sırası
Docker container’larını systemd ile yönetiyorsan, container başlatma sırası kritik hale gelir.
[Unit]
Description=Redis Container
After=docker.service
Requires=docker.service
[Service]
TimeoutStartSec=0
Restart=always
ExecStartPre=-/usr/bin/docker stop redis-cache
ExecStartPre=-/usr/bin/docker rm redis-cache
ExecStart=/usr/bin/docker run
--name redis-cache
--rm
-p 6379:6379
redis:7-alpine
ExecStop=/usr/bin/docker stop redis-cache
[Install]
WantedBy=multi-user.target
Buradaki - işaretine dikkat et. ExecStartPre=- ifadesindeki tire, bu komutun başarısız olması durumunda systemd’nin devam etmesini sağlar. Container daha önce yoksa docker stop hata verir ama servis yine de başlar.
Ordering Cycle ve Circular Dependency Sorunları
Zaman zaman “ordering cycle” hataları alırsın. Bu, A servisi B’den sonra başlamak isterken, B’nin de A’dan sonra başlamak istemesiyle oluşan kısır döngüdür.
# Circular dependency var mı kontrol et
systemd-analyze verify /etc/systemd/system/myservice.service
# Boot sırasında cycle hataları için
journalctl -b | grep -i "cycle|loop|dependency"
Bu tür sorunları çözmek için önce hangi servisin gerçekten önce başlaması gerektiğini belirle ve gereksiz After direktiflerini temizle. Bazen DefaultDependencies=no kullanmak da çözüm olabilir ama bu direktif dikkatli kullanılmalı, çünkü shutdown sırasında da bağımlılıkları etkiler.
Timeout Sorunları ve Yönetimi
Servisler bazen başlıyor ama çok uzun süre alıyor ve systemd timeout veriyor.
# Servisin timeout nedeniyle başarısız olduğunu gör
journalctl -u myservice.service | grep -i "timeout|timed out"
[Unit]
Description=Yavaş Başlayan Servisim
[Service]
Type=forking
# Varsayılan 90 saniye yerine 5 dakika bekle
TimeoutStartSec=300
# Durdurma için de süre ayarla
TimeoutStopSec=120
ExecStart=/usr/local/bin/slowservice
[Install]
WantedBy=multi-user.target
Ancak timeout’u uzatmak her zaman doğru çözüm değildir. Servisin neden bu kadar uzun sürdüğünü araştır. Bazen bir ağ bağlantısı bekleniyor olabilir, bazen disk I/O problemi vardır.
Override Dosyaları ile Çakışmalar
Production’da sık yaşanan bir durum: paket yöneticisi bir servis güncellemesi yapar ve senin özelleştirmelerini ezer. Bunu önlemenin yolu systemctl edit kullanmak ve override dosyaları oluşturmaktır.
# Servis için override dosyası oluştur (doğru yol)
systemctl edit myapp.service
# Bu komut şu dosyayı oluşturur/düzenler:
# /etc/systemd/system/myapp.service.d/override.conf
# override.conf - sadece değiştirmek istediğin kısmı yaz
[Unit]
After=network.target postgresql.service redis.service
Requires=postgresql.service
[Service]
TimeoutStartSec=120
Restart=on-failure
RestartSec=10
Bu yöntemle orijinal unit dosyasına dokunmadan ayarları üst üste yazabilirsin. Paket güncellemesi geldiğinde orijinal dosya güncellenir ama senin override dosyan yerinde kalır.
# Override'ları gör
systemctl cat myapp.service
# Çıktıda hem orijinal hem override görünür
# Değişiklikleri uygula
systemctl daemon-reload
systemctl restart myapp.service
systemctl mask ve Servis Çakışmalarını Önlemek
Bazen iki servis aynı port için yarışır ya da birbirinin dosyalarına yazar. Bunu conflict direktifiyle yönetebilirsin.
# Servisleri tamamen devre dışı bırak (enable/start ile de açılamasın)
systemctl mask apache2.service
# Maskelemeyi kaldır
systemctl unmask apache2.service
# Mevcut maskeleme durumunu gör
systemctl status apache2.service | grep -i masked
Unit dosyasında çakışma tanımlama:
[Unit]
Description=Nginx Web Server
# apache2 ile çakışıyor, ikisi aynı anda çalışamaz
Conflicts=apache2.service
[Service]
Type=forking
ExecStart=/usr/sbin/nginx
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true
[Install]
WantedBy=multi-user.target
Debugging İçin İleri Düzey Teknikler
Servis Başlatmayı Manuel Simüle Etmek
# Servisin çalışacağı ortamı simüle et
systemd-run --unit=test-myapp --same-dir
--setenv=APP_ENV=production
--uid=appuser
/opt/myapp/bin/start.sh
# Geçici servisin loglarını izle
journalctl -u test-myapp -f
strace ile Servis Davranışını İzlemek
# Servisin yaptığı system call'ları izle
systemctl edit --force myapp.service
# Override dosyasına ekle:
# [Service]
# ExecStartPre=/bin/bash -c 'strace -f -o /tmp/myapp-strace.log /opt/myapp/bin/start.sh'
Boot Debug Modu
Çok ciddi boot problemlerinde sistemin kernel parametrelerini değiştirip debug modda başlatabilirsin:
# GRUB'dan geçici olarak systemd.log_level=debug ekle
# /etc/default/grub içinde:
# GRUB_CMDLINE_LINUX="systemd.log_level=debug systemd.log_target=kmsg"
# Değişikliği uygula
update-grub
# Boot sonrası logları incele
journalctl -b -p debug | grep -i "starting|started|failed"
Servis Bağımlılık Grafiği Oluşturmak
Karmaşık sistemlerde görsel analiz çok yardımcı olur. Systemd, bağımlılık grafiği SVG olarak çıkarabilir:
# Tüm sistemin bağımlılık grafiğini oluştur
systemd-analyze dot | dot -Tsvg > /tmp/systemd-deps.svg
# Sadece belirli bir servisin grafiğini çıkar
systemd-analyze dot myapp.service | dot -Tsvg > /tmp/myapp-deps.svg
# dot aracı graphviz paketinde gelir
apt install graphviz # Debian/Ubuntu
dnf install graphviz # RHEL/Fedora
Bu SVG dosyasını tarayıcıda açarak hangi servisin neye bağlı olduğunu görsel olarak görebilirsin. Büyük sistemlerde bu görsel analiz, metin bazlı incelemeden çok daha hızlı sonuç verir.
Proaktif Önlemler: Sorun Çıkmadan Önlem Al
Yaşadığın sorunları tekrar yaşamamak için birkaç alışkanlık edinmek gerekiyor.
Öncelikle her yeni servis eklendiğinde bağımlılıklarını belgele. Unit dosyasının [Unit] bölümüne yorum satırları ekle, neden o bağımlılığın gerekli olduğunu açıkla. Altı ay sonra o dosyaya bakan kişi sen olabilirsin ve o an için teşekkür edersin.
# Mevcut servislerin durumunu düzenli kontrol et
systemctl list-units --state=failed --no-pager
# Cron ile günlük rapor al
echo "0 8 * * * root systemctl list-units --state=failed --no-pager | mail -s 'Başarısız Servisler' [email protected]" >> /etc/cron.d/systemd-check
Yeni bir servis unit dosyası yazmadan önce şu kontrol listesini kullan:
- Servis hangi resource’lara ihtiyaç duyuyor? Veritabanı, ağ, dosya sistemi?
- Bu resource’lar ne zaman hazır hale geliyor?
- Servis başlama süresi nedir? Timeout yeterli mi?
- Başarısız olursa ne yapmalı? Restart politikası doğru mu?
- Başka hangi servisleri bloke ediyor?
Sonuç
Systemd servis başlatma sırası sorunları, görünüşte gizemli ama aslında sistematik yaklaşımla tamamen çözülebilir problemlerdir. Temel kurallar şunlar: önce journalctl ile logları oku, sonra systemd-analyze ile boot zincirini incele, ardından unit dosyalarındaki After, Requires ve Wants direktiflerini doğrula.
After direktifinin tek başına sıralama garantisi verdiğini ama bağımlılık oluşturmadığını, Requires ile birlikte kullanılması gerektiğini unutma. Servis “başlamış” olmak ile “kullanıma hazır” olmak arasındaki farkı her zaman göz önünde bulundur; bu ayrım için ExecStartPre script’leri veya Type=notify kullan.
Override dosyaları kullan, orijinal unit dosyalarını düzenleme. Değişikliklerini paket güncellemelerine karşı korumak için systemctl edit alışkanlığı edin. Son olarak, sorun çıkmadan önce systemd-analyze verify ile unit dosyalarını test et. Bu araçlar ve teknikler, gece 3’te uyanıp “neden başlamıyor bu servis?” diye saçlarını yolma ihtimalini ciddi ölçüde azaltacak.