Systemd ile çalışırken en çok baş ağrıtan konulardan biri servis bağımlılıklarını doğru yapılandırmaktır. “Servisim neden başlamıyor?” sorusunun cevabı çoğu zaman yanlış yazılmış bir unit dosyasında gizlidir. Bu yazıda systemd unit dosyalarını derinlemesine inceleyecek, servis bağımlılıklarını gerçek dünya senaryolarıyla ele alacağız.
Systemd Unit Dosyası Temelleri
Systemd, her servisi bir “unit” olarak tanımlar. Unit dosyaları genellikle üç ana bölümden oluşur: [Unit], [Service] ve [Install]. Bu yapının her bölümü farklı bir amaca hizmet eder ve bağımlılık yönetimi ağırlıklı olarak [Unit] bölümünde gerçekleşir.
Bir unit dosyasının temel iskeletine bakalım:
[Unit]
Description=Örnek Servis
Documentation=https://example.com/docs
After=network.target
Requires=postgresql.service
[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/start.sh
ExecStop=/opt/myapp/bin/stop.sh
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
Bu yapıyı anlamak için her direktifi tek tek ele almak gerekiyor. Ancak asıl konumuz bağımlılıklar olduğu için [Unit] bölümüne odaklanacağız.
Bağımlılık Direktifleri: Requires, Wants, After, Before
Systemd bağımlılık sistemi ilk bakışta karmaşık görünebilir ama mantığı kavradıktan sonra oldukça güçlü bir araç haline gelir.
Requires ve Wants: Zorunlu ve Tercihli Bağımlılıklar
Requires: En sert bağımlılık türüdür. Eğer listelenen unit başlamazsa veya çalışma sırasında duruyorsa, bu direktifi kullanan servis de durur.
Wants: Daha yumuşak bir bağımlılıktır. Listelenen unit başlayamazsa bile bu servis çalışmaya devam eder. Genellikle “olsa iyi olur ama zorunlu değil” durumları için kullanılır.
[Unit]
Description=Web Uygulama Servisi
# Kesinlikle gerekli - PostgreSQL olmadan çalışamayız
Requires=postgresql.service
# Redis olsa iyi olur ama olmadan da ayakta kalabiliriz
Wants=redis.service
After ve Before: Sıralama Direktifleri
Pek çok sysadmin Requires ile After direktiflerini birbirine karıştırır. Çok önemli bir noktanın altını çizelim: Requires bağımlılığı tanımlar, After sıralamayı tanımlar.
Requires=postgresql.service yazarsanız ama After=postgresql.service yazmazsanız, her iki servis de aynı anda başlamaya çalışır. PostgreSQL henüz tam anlamıyla hazır olmadan uygulamanız başlangıç yapabilir ve bağlantı hatası alabilirsiniz.
[Unit]
Description=Web Uygulama Servisi
# PostgreSQL olmadan çalışamam
Requires=postgresql.service
# Ama aynı zamanda PostgreSQL'den SONRA başlamalıyım
After=postgresql.service
Before direktifi bunun tersidir. “Ben başlamadan önce şu servis başlamış olmalı” değil, “Ben başladıktan sonra şu servis başlayabilir” anlamına gelir. Yani A.service dosyasında Before=B.service yazmak, B.service dosyasında After=A.service yazmakla eşdeğerdir.
Gerçek Dünya Senaryosu 1: Üç Katmanlı Web Uygulaması
Diyelim ki şu mimaride bir uygulamanız var: PostgreSQL veritabanı, Redis önbellek, ve bunlara bağlı bir Python/Django uygulaması. Her birinin doğru sırada başlaması gerekiyor.
Önce PostgreSQL unit dosyasına bakalım (zaten sistemde kurulu olduğunu varsayıyoruz, bu sadece referans için):
# /etc/systemd/system/postgresql.service dosyasına bakış
# Genellikle paket yöneticisi tarafından kurulur
# systemctl cat postgresql ile görebilirsiniz
Şimdi Django uygulamamızın unit dosyasını yazalım:
# /etc/systemd/system/django-app.service
[Unit]
Description=Django Web Uygulaması
Documentation=https://internal.wiki/django-app
# Ağ hazır olmalı
After=network.target
# Veritabanı ve önbellek hazır olmalı ve çalışıyor olmalı
Requires=postgresql.service redis.service
After=postgresql.service redis.service
[Service]
Type=notify
User=djangouser
Group=djangouser
WorkingDirectory=/opt/django-app
EnvironmentFile=/etc/django-app/env
ExecStartPre=/opt/django-app/venv/bin/python manage.py migrate --noinput
ExecStart=/opt/django-app/venv/bin/gunicorn
--workers 4
--bind 0.0.0.0:8000
--pid /run/django-app/gunicorn.pid
myapp.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
RuntimeDirectory=django-app
Restart=on-failure
RestartSec=10s
TimeoutStartSec=60s
[Install]
WantedBy=multi-user.target
Bu dosyada dikkat edilmesi gereken birkaç nokta var. ExecStartPre ile migration’ları uygulama başlamadan önce çalıştırıyoruz. Eğer migration başarısız olursa, ana servis hiç başlamaz. Bu davranış bazı durumlarda istenirken bazı durumlarda sorun çıkarabilir.
BindsTo ve PartOf: Daha Güçlü Bağlar
Requires‘dan bile daha sıkı bir bağımlılık istiyorsanız BindsTo direktifini kullanabilirsiniz.
BindsTo: Requires gibi çalışır ama fark şudur: Bağlı olunan unit aktif durumdan çıkarsa (sadece başarısız olsa değil, systemctl stop ile durdurulsa bile), bu servis de otomatik olarak durur.
PartOf: Bir servisin başka bir servis veya grubun parçası olduğunu belirtir. Üst birim durdurulduğunda veya yeniden başlatıldığında, PartOf direktifini kullanan servis de aynı işleme tabi tutulur. Ancak üst birim başlatıldığında alt birimi otomatik başlatmaz.
[Unit]
Description=Uygulama Metrik Toplayıcı
# Ana uygulama durursa biz de durmalıyız
BindsTo=main-application.service
# Ana uygulama grubunun parçasıyız
PartOf=app-stack.target
After=main-application.service
Custom Target Oluşturma: Servis Gruplarını Yönetmek
Birden fazla servisi bir grup olarak yönetmek istediğinizde custom target’lar son derece işe yarar. Örneğin tüm uygulama stack’inizi tek bir komutla başlatıp durdurmak isteyebilirsiniz.
# /etc/systemd/system/app-stack.target
[Unit]
Description=Uygulama Stack Hedefi
Requires=postgresql.service redis.service nginx.service django-app.service
After=postgresql.service redis.service nginx.service django-app.service
[Install]
WantedBy=multi-user.target
Bu target’ı etkinleştirdikten sonra:
sudo systemctl enable app-stack.target
sudo systemctl start app-stack.target
sudo systemctl status app-stack.target
Tek komutla tüm stack’i yönetebilirsiniz. Üretim ortamlarında bu yaklaşım özellikle deployment süreçlerini basitleştirir.
Gerçek Dünya Senaryosu 2: Ağ Bağımlılıkları ve Sorunları
Ağ bağımlılıklarında sıkça karşılaşılan bir sorun var: network.target ile network-online.target arasındaki fark. Bu iki direktif çok farklı anlara işaret eder.
network.target: Ağ stack’i başlatılmış ama arayüzler henüz tam olarak yapılandırılmamış olabilir.
network-online.target: Tüm ağ arayüzleri yapılandırılmış ve çevrimiçi. Bu target, NetworkManager veya systemd-networkd’nin işini bitirdiğini bildirmesini bekler.
# /etc/systemd/system/remote-backup.service
[Unit]
Description=Uzak Sunucu Yedekleme Servisi
# Gerçekten ağa ihtiyacımız var, sadece stack başlamış olması yetmez
After=network-online.target
Wants=network-online.target
# NFS bağlantımız varsa
After=remote-fs.target
[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/remote-backup.sh
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
network-online.target için Wants kullanıyoruz çünkü bazı sistemlerde bu target aktif edilmemiş olabilir ve Requires ile sertleştirirsek servisimiz hiç başlamayabilir.
Conflict ve Condition Direktifleri
Bağımlılıklar sadece “şu servis çalışsın” değil, “şu servis çalışmasın” da olabilir.
Conflicts: Belirtilen unit çalışıyorsa bu servis başlamaz. Ve tam tersi: Bu servis başlarsa belirtilen unit durdurulur.
[Unit]
Description=Birincil Veritabanı Servisi
# Failover servisiyle aynı anda çalışamayız
Conflicts=db-failover.service
Condition direktifleri ise biraz farklı çalışır. Eğer koşul sağlanmazsa, servis başlatılmaz ama bu bir hata olarak değil, başarılı bir atlama olarak değerlendirilir.
# /etc/systemd/system/conditional-service.service
[Unit]
Description=Koşullu Servis
# Sadece x86_64 mimaride çalış
ConditionArchitecture=x86-64
# Sadece bu dosya varsa çalış
ConditionPathExists=/etc/myapp/config.json
# Sadece bu dosya yoksa çalış (ünlem işareti tersini alır)
ConditionPathExists=!/var/lock/myapp.lock
# Sadece bu sanal makine değilse çalış
ConditionVirtualization=false
[Service]
Type=simple
ExecStart=/usr/local/bin/myapp
Systemd ve Socket Activation
Socket activation, bir servisin her zaman çalışmak zorunda olmadığı ama bir bağlantı geldiğinde otomatik başlaması gereken durumlar için mükemmel bir çözümdür.
Önce socket unit dosyasını oluşturalım:
# /etc/systemd/system/myapp.socket
[Unit]
Description=MyApp Socket
[Socket]
ListenStream=0.0.0.0:8080
Accept=false
# Bağlantı gelince ilgili servisi başlat
Service=myapp.service
[Install]
WantedBy=sockets.target
Şimdi bu socket’i kullanacak servis:
# /etc/systemd/system/myapp.socket ile eşleşen servis
# /etc/systemd/system/myapp.service
[Unit]
Description=MyApp Servisi (Socket Activated)
# Socket hazır olana kadar bekleme
Requires=myapp.socket
After=myapp.socket
[Service]
Type=simple
User=appuser
ExecStart=/opt/myapp/bin/myapp --socket-fd=SD_LISTEN_FDS_START
StandardInput=socket
[Install]
WantedBy=multi-user.target
Bu yaklaşımla servis, birisi bağlanmaya çalışana kadar RAM kullanmaz. Özellikle çok sayıda mikro servis çalıştıran sistemlerde kaynak tasarrufu sağlar.
Troubleshooting: Bağımlılık Sorunlarını Çözmek
Bağımlılık sorunlarını debug ederken kullandığım birkaç komut var:
# Servisin bağımlılık ağacını göster
systemctl list-dependencies django-app.service
# Tüm başarısız servisleri listele
systemctl --failed
# Servis başlangıç detaylarını göster (son 50 satır)
journalctl -u django-app.service -n 50 --no-pager
# Belirli bir zaman dilimindeki logları göster
journalctl -u django-app.service --since "2024-01-15 10:00:00" --until "2024-01-15 11:00:00"
# Systemd'nin boot sürecinde nelerin ne zaman başladığını göster
systemd-analyze plot > boot-analysis.svg
# Hangi servisin en çok boot süresine yol açtığını bul
systemd-analyze blame | head -20
# Servisler arası bağımlılık çakışmalarını kontrol et
systemd-analyze verify /etc/systemd/system/django-app.service
systemd-analyze verify komutu özellikle yeni yazdığınız unit dosyalarını production’a almadan önce kontrol etmek için çok değerlidir. Syntax hatalarını ve mantıksal sorunları erkenden yakalar.
Drop-in Dosyaları ile Mevcut Unit’leri Genişletmek
Paket yöneticisi tarafından kurulan servislerin unit dosyalarını doğrudan düzenlemeniz önerilmez çünkü paket güncellemelerinde değişiklikleriniz kaybolabilir. Bunun yerine drop-in dosyaları kullanın.
# Önce drop-in dizinini oluştur
sudo mkdir -p /etc/systemd/system/postgresql.service.d/
# Sonra override dosyasını oluştur
sudo nano /etc/systemd/system/postgresql.service.d/custom-deps.conf
# /etc/systemd/system/postgresql.service.d/custom-deps.conf
[Unit]
# Mevcut bağımlılıklara ek olarak storage servisinin başlamasını bekle
After=storage-setup.service
Wants=storage-setup.service
[Service]
# Ortam değişkenleri ekle
Environment="PGDATA=/custom/path/pgdata"
# Bellek limitini artır
MemoryLimit=4G
Drop-in dosyaları orijinal unit dosyasının üzerine eklenir, onu silmez. Bu sayede paket güncellemelerinden etkilenmesiniz.
# Değişiklikleri yükle
sudo systemctl daemon-reload
# Drop-in'in uygulandığını doğrula
systemctl cat postgresql.service
Gerçek Dünya Senaryosu 3: Microservice Ortamında Bağımlılık Yönetimi
Bir microservice ortamında 10-15 servis çalıştırdığınızı hayal edin. Her birinin birbirinden farklı bağımlılıkları var. Bu karmaşıklığı yönetmek için template unit dosyaları kullanabilirsiniz.
# /etc/systemd/system/[email protected]
# @ işareti bu dosyanın bir template olduğunu belirtir
[Unit]
Description=Microservice: %i
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service
# %i instance adını temsil eder
[Service]
Type=notify
User=microservice
Group=microservice
WorkingDirectory=/opt/microservices/%i
EnvironmentFile=/etc/microservices/%i.env
ExecStartPre=/opt/microservices/%i/bin/healthcheck.sh
ExecStart=/opt/microservices/%i/bin/start
ExecStop=/opt/microservices/%i/bin/stop
Restart=on-failure
RestartSec=5s
# Log tanımlamasını servis adına göre yap
SyslogIdentifier=microservice-%i
[Install]
WantedBy=multi-user.target
Bu template ile her microservice için ayrı unit dosyası yazmak zorunda kalmaksızın:
# user-service instance'ını başlat
sudo systemctl start [email protected]
# payment-service instance'ını başlat
sudo systemctl start [email protected]
# Tüm instance'ları göster
systemctl list-units 'microservice@*'
Her instance kendi log dosyasına, kendi environment dosyasına ve kendi çalışma dizinine sahip olur. Bağımlılıklar ise template seviyesinde tanımlandığı için merkezi olarak yönetilir.
Restart Politikaları ve Bağımlılıkların Etkileşimi
Restart politikalarını bağımlılıklarla birlikte doğru yapılandırmak kritiktir. Yanlış yapılandırma durumunda bir servis sürekli restart döngüsüne girebilir ve bağımlı servisleri de etkiler.
- Restart=no: Hiçbir koşulda yeniden başlatma
- Restart=on-success: Sadece başarıyla çıkınca yeniden başlat
- Restart=on-failure: Hata durumunda yeniden başlat (en çok kullanılan)
- Restart=on-abnormal: Anormal çıkış, zaman aşımı veya watchdog hatalarında başlat
- Restart=always: Her durumda yeniden başlat
[Service]
Restart=on-failure
RestartSec=5s
# Kaç restart denemesinden sonra pes et
StartLimitIntervalSec=60s
StartLimitBurst=3
StartLimitBurst=3 ve StartLimitIntervalSec=60s kombinasyonu, servisin 60 saniye içinde 3 kezden fazla restart denemesini engeller. Bu noktadan sonra servis failed durumuna geçer ve manuel müdahale gerekir. Bu sayede hatalı bir servisin bağımlı servisleri sürekli restart döngüsüne sokması engellenir.
Unit Dosyası Yazarken Dikkat Edilmesi Gereken Noktalar
Yıllar içinde öğrendiğim bazı önemli pratik bilgileri paylaşayım:
AfterileRequires‘ı birlikte kullanın: Bağımlılığı belirtmek içinRequires, sıralamayı belirtmek içinAftergereklidir. Biri olmadan diğeri eksik kalır.
network.targetilenetwork-online.targetfarkını bilin: Ağa gerçekten ihtiyaç duyuyorsanıznetwork-online.targetkullanın.
- Döngüsel bağımlılıklardan kaçının: A servisi B’ye, B servisi A’ya bağımlıysa systemd bu durumu hata olarak raporlar.
systemd-analyze verifyile erken yakalayın.
systemctl daemon-reload‘u unutmayın: Unit dosyasında her değişiklik sonrası mutlaka çalıştırın, yoksa değişiklikler uygulanmaz.
- Drop-in dosyalarını tercih edin: Sistem paketlerinin unit dosyalarını doğrudan düzenlemek yerine drop-in kullanın.
Type=direktifini doğru seçin:Type=simple,Type=forking,Type=notifyveType=oneshotfarklı davranışlara sahiptir. Özellikle daemon olarak fork eden servisler içinType=forkingvePIDFile=belirtmeniz gerekir.
ExecStartPrebaşarısızlığı ana servisi durdurur: Bu genellikle istenen davranıştır ama bazen-ön ekiyle başarısızlığı görmezden gelebilirsiniz:ExecStartPre=-/opt/app/optional-check.sh
Sonuç
Systemd unit dosyaları ve bağımlılık yönetimi, Linux sistem yönetiminin belki de en çok gözden kaçan ama en kritik konularından biridir. Requires ile After‘ın ayrı görevleri olduğunu anlamak, network.target ile network-online.target farkını kavramak, drop-in dosyalarının gücünü kullanmak, bunların hepsi üretim ortamlarında karşılaşılan gerçek sorunları çözer.
En sık yaptığım hatalardan biri, yeni bir servis yazarken sadece Requires yazmak ve After‘ı unutmaktır. Servis bazen çalışır bazen çalışmaz, ve bu intermittent sorunlar debug etmesi en zor sorunlardır. systemd-analyze verify ve systemctl list-dependencies komutlarını günlük iş akışınıza entegre edin.
Karmaşık bağımlılık zincirlerine sahip ortamlarda custom target dosyaları ve template unit’ler hayat kurtarır. Tek tek servis yönetmek yerine bir grubun tamamını yönetmek hem zaman kazandırır hem de tutarlılık sağlar. Systemd’yi öğrenmek biraz zaman alsa da sunduğu kontrol ve güç, bu yatırımı fazlasıyla karşılıyor.