systemd servisi neden otomatik yeniden başlatmıyor?
Restart parametresi yalnızca ana süreci izler. cgroup içindeki alt süreçlerin çökmesi systemd tarafından algılanmaz. Type ayarları ve hata limitleri ile çözüm yollarını inceleyin.
Kısa cevap: systemd yeniden başlatma politikaları tek bir süreci izler
systemd yeniden başlatma politikaları, birim başına yalnızca bir süreci, yani ana süreci izler. Restart= yalnızca bu tek sürecin çıkış durumunu okur, başka hiçbir şeye bakmaz. Bir birimin kontrol grubu yirmi süreç barındırabilir; bunlardan biri ölebilir ancak ana süreç hala orada olduğu için birim active (running) durumunda kalır. systemd açısından başarısız olan hiçbir şey yoktur, bu nedenle hiçbir şey yeniden başlatılmaz.
systemd diğer süreçlerden haberdardır. Birim durdurulduğunda onları sonlandırır, bellek sınırlarını birimin toplam limitlerine dahil eder, birimin CPU kotasını onlara uygular ve systemctl status içinde listeler. Ancak bu süreçlerin çıkış durumlarını asla okumaz. Yeniden başlatma mantığı ile cgroup birbirinden farklı iki yapıdır ve bu kılavuzun büyük bir kısmı bu ikisi arasındaki boşluk hakkındadır.
cgroup'un içeriği ve yeniden başlatma mantığının okuduğu veriler
Bir cgroup (kontrol grubu), bir dizi sürece sahip olan bir çekirdek nesnesidir. Her servis birimi, adını birimden alan bir cgroup'a sahip olur. Bir süreç bu gruptan ayrılamaz. Alt süreçler, ebeveynlerinin cgroup'unu devralır ve yetkisiz bir süreç kendini başka bir yere taşıyamaz. systemd'nin, eski init betiklerinin güvenilir bir şekilde yapamadığı, iki kez fork yapan bir daemon'ı temizleyebilmesinin nedeni budur.
Her iki gerçeği yan yana inceleyin:
systemd-cgls --unit myapp.service
systemctl show -p MainPID -p NRestarts -p Restart -p RestartUSec myapp.servicesystemd-cgls, birimdeki her süreci listeler. MainPID, yeniden başlatma politikasının okuduğu tek sayıdır. Bu ikisi sizin zihinsel modelinizle uyuşmadığında, hata bu uyumsuzluktan kaynaklanır. MainPID=0, yanlış bir PID'den daha kötüdür: bu, systemd'nin hiçbir şeyi takip etmediği anlamına gelir, dolayısıyla hiçbir Restart= değeri tetiklenemez.
Ana süreç kuralının tek bir gerçek istisnası vardır. Çekirdek bellek yöneticisi (OOM killer), birimin cgroup'u içindeki herhangi bir süreci sonlandırırsa, systemd bunu görür çünkü cgroup'un memory.events dosyasını izler. OOMPolicy=, bir sonraki adımda ne olacağına karar verir ve varsayılan değer stop'dır: birimin tamamı durdurulur, sonuç oom-kill olarak kaydedilir ve bu bir hata olarak sayılır, dolayısıyla Restart=on-failure tetiklenir. Günlük kayıtları bunu açıkça belirtir.
myapp.service: A process of this unit has been killed by the OOM killer.
myapp.service: Failed with result 'oom-kill'.Bu nedenle, bellek nedeniyle sonlandırılan bir alt süreç birimi durdururken, aynı alt sürecin segmentation fault nedeniyle ölmesi birimi durdurmaz. Bir birim üzerinde bellek sınırları belirliyorsanız, yeniden başlatma politikasını ayarlamadan önce MemoryMax ve CPUQuota'nın bir birimin cgroup'una nasıl uygulandığını okuyun; çünkü bu iki özellik yalnızca burada bir araya gelir.
Type= ana süreci nasıl belirler
Type= içindeki [Service] bölümü yalnızca başlatma sırasıyla ilgili değildir. Hangi PID'nin (işlem kimliği) MainPID olacağına karar veren kuraldır; bu da Restart='nin neyi görebileceğini belirlemekle aynı şeydir.
Type=simplevarsayılan değerdir. systemd'ninExecStart=üzerinden çatalladığı (fork) süreç ana süreçtir. systemd,exec'in çalışıp çalışmadığını bilmeden birimi hemen başlatılmış olarak işaretler. İkili dosya yolundaki bir yazım hatası, başarılı görünen ancak bir an sonraMain process exited, code=exited, status=203/EXECdurumuna düşen bir başlatma işine neden olur.Type=exec,simplegibi davranır; ancak başlatma işiexecbaşarılı olana kadar bekler. Bu, yukarıdaki yazım hatasını dürüst bir başlatma başarısızlığına dönüştürür. Bu, desteklenen tüm dağıtımlarda bulunan systemd 240 veya daha yeni bir sürüm gerektirir.simpleyerine bunu tercih edin.Type=forking,ExecStart='den gelen sürecin bir arka plan daemon'ı çatallamasını ve ardından sonlanmasını bekler. systemd, ana sürecin sonlanmasını bekler ve ardından gerçek daemon'ı arar. OnaPIDFile=verin. Bu olmadan,GuessMainPID=(varsayılan olarak açıktır) yalnızca cgroup içinde tam olarak bir süreç kaldığında çalışır. Arkada iki süreç bırakırsanızMainPID,0durumunda kalır.Type=notify, servisinsd_notify(3)'i çağırdığı ve trafik karşılamaya hazır olduğundaREADY=1gönderdiği anlamına gelir. Ayrıca systemd'ye izlemesi için farklı bir süreç vermek üzereMAINPID=gönderebilir.NotifyAccess=varsayılan olarakmain'dir, bu nedenle bir alt süreç tarafından gönderilen bildirim yoksayılır ve günlük kaydı, bildirimin geldiği PID'yi isimlendirir.Type=oneshotkalıcı bir ana sürece sahip değildir.RemainAfterExit=yesayarlanmadığı sürece,ExecStart=biter bitmez birim inaktif duruma geçer.Restart=alwaysveRestart=on-successburada reddedilir veService has Restart= set to either always or on-success, which isn't allowed for Type=oneshot services. Refusing.mesajı verilir.on-failuredahil diğer değerler kabul edilir.
İki Type=forking hatasını ezberlemekte fayda vardır; çünkü her biri, görünürde hiçbir neden yokken bozuk görünen bir birimle sizi baş başa bırakır:
myapp.service: Can't open PID file /run/myapp.pid (yet?) after start: No such file or directory
myapp.service: New main PID 4711 does not belong to service, and PID file is not owned by root. Refusing.Birincisi, daemon'ın PID dosyasını başka bir yere yazdığı veya systemd'nin baktığı zamandan daha geç yazdığı anlamına gelir. İkincisi, PID dosyasının birimin cgroup'u dışındaki bir süreci işaret ettiği anlamına gelir; systemd bunu sahiplenmeyi reddeder, çünkü yazılabilir bir PID dosyası aksi takdirde systemd'nin sunucudaki herhangi bir sürece sinyal göndermesini sağlamanın bir yolu haline gelebilir.
Bir sarmalayıcı betiğin alt süreçlerinin ölümünü neden gizlediği
Başlıktaki soruyu doğuran yapı aşağıdadır.
#!/bin/bash
/usr/local/bin/myapp-web &
/usr/local/bin/myapp-worker &
waitBirim Type=simple olduğundan, ana süreç kabuktur (shell). Argümansız wait, yalnızca her alt süreç sonlandıktan sonra döner. Çalışan süreci (worker) öldürdüğünüzde kabuk, web sürecini beklemeye devam eder; dolayısıyla kabuk sonlanmaz, bu yüzden MainPID sonlanmaz ve Restart= hiçbir zaman sorgulanmaz. Cgroup artık bir eksik süreç tutar, systemctl status daha kısa ağacı yazdırır ve birim hala active (running) durumundadır. systemd içinde o ağacı değişiklikler için izleyen hiçbir mekanizma yoktur.
Aynı hatanın ikinci bir versiyonu daha sessizdir:
ExecStart=/bin/sh -c 'export APP_ENV=production; /usr/local/bin/myapp'Ana süreç myapp değil, kabuktur. systemctl stop üzerinde systemd, ana sürece SIGTERM sinyali gönderir; ön plandaki bir alt süreci bekleyen kabuk ise bu sinyali iletmez. Durdurma işlemi varsayılan olarak 90 saniye süren tam TimeoutStopSec süresini alır ve şu şekilde sonuçlanır:
myapp.service: State 'stop-sigterm' timed out. Killing.
myapp.service: Killing process 4711 (myapp) with signal SIGKILL.Çözüm exec kullanmaktır. exec /usr/local/bin/myapp yazıldığında kabuk, program tarafından değiştirilir; böylece MainPID programın kendisi olur ve sinyaller doğrudan ona ulaşır. Daha da iyisi, kabuğu kaldırıp birim dosyasında Environment= veya EnvironmentFile= kullanmaktır. Bu hatanın, -c dizgisi tek bir komut içerdiğinde kendini gizlediğine dikkat edin; çünkü hem bash hem de dash bu durumu doğrudan bir exec işlemine optimize eder. Dizgiye ikinci bir komut eklendiğinde kabuk, programınızın önünde canlı kalmaya devam eder.
İki dakika içinde bir test VPS üzerinde yeniden oluşturun
Yukarıdaki sarmalayıcıyı /usr/local/bin/two-children.sh olarak kaydedin, chmod +x ile çalıştırılabilir yapın ve iki program yolunu sleep 3600 ile değiştirin. Type=simple ve Restart=on-failure ile bir birimi buna yönlendirin, ardından systemctl daemon-reload komutunu çalıştırıp başlatın. systemd-cgls --unit two-children.service komutunu çalıştırın ve üç PID değerini not edin: kabuk ve iki alt süreci. Bir alt süreci sudo kill <pid> ile öldürün. Birimi tekrar kontrol edin. Ağaç bir süreç daha kısadır, durum hala active (running)'tir ve günlükte yeni bir kayıt yoktur. Şimdi bunun yerine sudo kill -9 <shell pid> komutunu çalıştırın. Birim başarısız olur, hayatta kalan alt süreç KillMode=control-group varsayılan olduğu için temizlenir ve günlükte Scheduled restart job, restart counter is at 1. görünür.
Kapsamlı Restart= sözlüğü ve on-failure'ın always'e tercih edildiği durumlar
Restart= yedi farklı değerden birini alır; bu değerleri birbirinden ayıran temel ölçüt, neyin temiz çıkış (clean exit) sayıldığıdır. systemd; 0 çıkış kodunu, SuccessExitStatus= içinde listelenen tüm kodları ve SIGHUP, SIGINT, SIGTERM ile SIGPIPE sinyallerini temiz çıkış olarak kabul eder. SIGKILL ve SIGSEGV dahil diğer tüm durumlar kirli (unclean) kabul edilir.
novarsayılan değerdir. Birim kendi kendini asla yeniden başlatmaz; bu nedenleRestart=satırı olmayan bir birim, ilk çöküşte durur ve öyle kalır.on-successyalnızca temiz bir çıkıştan sonra yeniden başlatır.on-failuresıfır olmayan bir çıkış kodunda, kirli bir sinyalde, başlatma veya durdurma zaman aşımında ya da watchdog süresi dolduğunda yeniden başlatır.on-abnormalkirli bir sinyalde, zaman aşımında veya watchdog süresi dolduğunda yeniden başlatır; ancak sıfır olmayan normal bir çıkış kodunda asla yeniden başlatmaz.on-abortyalnızca kirli bir sinyalde, yani bir çöküş durumunda yeniden başlatır.on-watchdogyalnızcaWatchdogSec=süresi dolduğunda yeniden başlatır.alwaysyukarıdaki tüm durumlarda, 0 durumuyla yapılan temiz çıkışlar dahil, yeniden başlatır.
on-failure, uzun süre çalışan bir daemon için doğru varsayılandır. Bir çöküşü düzeltir ve kasıtlı bir exit 0 durumuna müdahale etmez. always, kontrolü dışındaki nedenlerle temiz bir şekilde çıkan programlar içindir; örneğin karşı taraf bağlantıyı kestiğinde 0 dönen bir tünel istemcisi gibi. always kullanmanın maliyeti, hataları gizlemesidir: başlayan, bozuk bir yapılandırma dosyasını okuyan, hatayı günlüğe kaydeden ve 0 koduyla çıkan bir servis sonsuz döngüye girer; tek belirti ise artan yeniden başlatma sayacıdır.
SuccessExitStatus=, temiz ve kirli arasındaki sınırı değiştirir. Borg, uyarılar için 1 ve hatalar için 2 koduyla çıkar; bu nedenle SuccessExitStatus=1 içermeyen bir yedekleme birimi, okunamayan tek bir dosyayı atladığında bile başarısız olarak işaretlenir. RestartPreventExitStatus=, always altında bile yeniden başlatmayı engelleyen kodları listeler; bu, bir programın geri gelmemesi gerektiğini belirtmesinin temiz yoludur. RestartForceExitStatus= ise tam tersini yapar. Bir yedekleme işi, yeniden başlatma döngüsü yerine bir zamanlayıcı tarafından yönetilen Type=oneshot biriminde yer almalıdır; bir işi belirli bir takvimde çalıştıran servis ve zamanlayıcı çifti, bu tür durumlar için kopyalanması gereken yapıdır.
Test etme konusunda bir uyarı: Servisinizi normal kill <pid> ile sonlandırmak, temiz listede yer alan SIGTERM sinyalini gönderir; bu nedenle Restart=on-failure doğru bir şekilde hiçbir işlem yapmaz ve yapılandırmanızın bozuk olduğu sonucuna varırsınız. Bunun yerine kill -9 <pid> veya systemctl kill -s SIGKILL myapp.service kullanın. Ayrıca, Restart= değerlerinden hiçbirinin systemctl stop sonrasında veya birim, BindsTo= ya da PartOf= bağımlılığı ortadan kalktığı için durdurulduğunda tetiklenmediğini unutmayın. Bir durdurma işi, bir başarısızlık değildir.
RestartSec ve 100 milisaniyelik varsayılan değer
RestartSec=, birimin durması ile systemd tarafından tekrar başlatılması arasındaki bekleme süresidir ve varsayılan değer 100 milisaniyedir. Biriminizin gerçekte neyi yüklediğini kontrol edin:
systemctl show -p RestartUSec -p StartLimitIntervalUSec -p StartLimitBurst myapp.serviceAyarlanmamış bir birim RestartUSec=100ms çıktısını verir. Bu varsayılan değer, bir kez çöküp tekrar ayağa kalkan bir servis için uygundur. Hiç başlatılamayan bir servis için ise yanlıştır; çünkü yarım saniye içinde beş yeniden başlatma gerçekleşir ve bu durum, bir sonraki bölümde açıklanan hız sınırına (rate limit) takılmasına neden olur. Bir veritabanını, mount işlemini veya ağ yolunu bekleyen her türlü servis için RestartSec=5s veya daha yüksek bir değer ayarlayın.
Ağustos 2026 itibarıyla, systemd 254 ve daha yeni sürümler, gecikmeyi RestartSec= değerinden başlayarak belirtilen deneme sayısı boyunca bir üst sınıra kadar artıran RestartSteps= ve RestartMaxDelaySec= seçeneklerini sunmaktadır. Ubuntu 24.04, systemd 255 ile gelir ve bu özelliklere sahiptir. Debian 12 ise systemd 252 ile gelir ve bu özelliklere sahip değildir. Bağımlı olunan servisin uzun süre kapalı kalabileceği durumlarda, artan gecikme süreleri en doğru çözümdür.
"Start request repeated too quickly" hatasının gerçek anlamı
Bu durum, kullanıcıların systemd'nin keyfi olarak pes ettiğini düşündüğü noktadır. Bu bir sayaçtır. Kural şudur: Bir birim StartLimitIntervalSec= süresi içinde StartLimitBurst= defadan fazla başlatılırsa, systemd onu tekrar başlatmayı reddeder ve başarısız (failed) durumuna getirir. Varsayılan değerler 10 saniye içinde 5 başlatmadır.
Journal şu diziyi gösterir:
myapp.service: Scheduled restart job, restart counter is at 5.
myapp.service: Start request repeated too quickly.
myapp.service: Failed with result 'start-limit-hit'.
Failed to start myapp.service - My application.ve systemctl start, çözümün halihazırda yazılı olduğu şu yanıtı verir:
Job for myapp.service failed because start of the service was attempted too often. See "systemctl status myapp.service" and "journalctl -xeu myapp.service" for details. To force a start use "systemctl reset-failed myapp.service" followed by "systemctl start myapp.service" again.systemctl reset-failed myapp.service, sayacı ve başarısız durumunu temizler. Başka hiçbir işlem bunu yapmaz, bu nedenle siz çalıştırana kadar düz bir systemctl start komutu reddedilmeye devam eder. Manuel başlatmalar da sınıra dahil edilir; bu nedenle bir yapılandırma dosyasını düzenlerken birkaç sabırsız systemctl restart çalıştırması, herhangi bir çökme olmasa bile bu hatayı tetikleyebilir.
İnsanları yanıltan kısım şudur: start-limit-hit, servisin neden başarısız olduğunu asla söylemez. Sadece hızlı ve tekrarlı bir şekilde başarısız olduğunu belirtir. Asıl neden, üzerindeki journal satırlarındadır.
Her iki ayar da [Unit] bölümüne aittir. Eski systemd sürümlerinin kabul ettiği [Service] bölümüne bunları yerleştiren örnekler bulabilirsiniz; kafa karışıklığı buradan başlar. Bunları [Unit] içine yazın, ardından systemctl show ile systemd'ye neleri yüklediğini sorun; çünkü geçerli olan tek değer yüklenen değerdir.
[Unit]
Description=My application
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Type=exec
ExecStart=/usr/local/bin/myapp
Restart=on-failure
RestartSec=10sBu, birime pes etmeden önce beş dakikalık bir pencere içinde beş deneme hakkı verir. StartLimitIntervalSec=0, sınırı tamamen devre dışı bırakır; neyi seçtiğinizi bilmelisiniz: asla başlayamayan bir servis artık sonsuza kadar yeniden deneyecek ve her seferinde journal'a yazacaktır. Makine genelindeki varsayılanlar /etc/systemd/system.conf dosyasında DefaultStartLimitIntervalSec= ve DefaultStartLimitBurst= olarak bulunur.
Komşu bir ayar uyarıyı hak ediyor. StartLimitAction=, sınıra ulaşıldığında ne olacağına karar verir ve reboot, reboot-force ve poweroff gibi değerleri kabul eder. Varsayılan değer none'tür; bu, birimi başarısız duruma getirir ve makineyi kendi haline bırakır. Uzak bir VPS üzerinde poweroff, sağlayıcının konsolunu açana kadar kapalı kalan bir sunucu anlamına gelir.
Çözüm bir: birim başına tek süreç
Hemen hemen her durumda geçerli olan yanıt budur. İki programın çalışması gerekiyorsa, iki birim (unit) yazın. Böylece her birinin gerçek bir ana süreci, gerçek bir çıkış durumu ve kendine ait bir yeniden başlatma politikası olur. Ayrıca ayrı günlük kayıtlarına, ayrı kaynak limitlerine ve ayrı yeniden başlatma sayaçlarına sahip olursunuz; gece saat üçte ihtiyaç duyacağınız şey tam olarak budur.
Birimler arasındaki ilişkiyi bir kabuk betiğinde (shell script) değil, birim dosyalarının içinde ifade edin.
After=yalnızca başlatma sırasını düzenler. Hatalar hakkında hiçbir şey söylemez.Requires=diğer birimi bununla birlikte başlatır ve diğer birim açıkça durdurulursa bunu da durdurur.BindsTo=,Requires=ve sizin önemsemeniz gereken durumun birleşimidir: bu birim, diğer birim çökme dahil herhangi bir nedenle durduğunda durur. BunuAfter=ile eşleştirin, aksi takdirde sıralama belirsiz kalır.PartOf=durdurma ve yeniden başlatma komutlarını aşağıya doğru yayar, böylecesystemctl restart myapp.target, kendisinePartOf=olan her birime ulaşır.Upholds=(systemd 249 ve sonrası, yani Ubuntu 22.04 ve sonrası) belirtilen birimi çalışır durumda tutar: birim durursa systemd onu tekrar başlatır. Bu komut, diğer her şeyle aynı başlatma hızı sınırına tabidir.
API sunucusu olmadan asla çalışmaması gereken ve API ayaktayken systemd tarafından canlı tutulan bir işçi (worker) süreci:
# /etc/systemd/system/myapp-api.service
[Unit]
Description=myapp API server
Wants=network-online.target
After=network-online.target
Upholds=myapp-worker.service
[Service]
Type=exec
User=myapp
ExecStart=/usr/local/bin/myapp serve
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target# /etc/systemd/system/myapp-worker.service
[Unit]
Description=myapp background worker
BindsTo=myapp-api.service
After=myapp-api.service
StartLimitIntervalSec=120
StartLimitBurst=5
[Service]
Type=exec
User=myapp
ExecStart=/usr/local/bin/myapp worker
Restart=on-failure
RestartSec=5sİşçi biriminin [Install] bölümü yoktur ve asla manuel olarak etkinleştirilmez. API birimi, onu Upholds= ile içeri çeker, bu nedenle çalıştıracağınız tek komut systemctl enable --now myapp-api.service olur. Yapılandırmayı yeniden yükleyin ve systemd'nin bu ikiliyi nasıl işlediğini kontrol edin:
sudo systemctl daemon-reload
systemd-analyze verify /etc/systemd/system/myapp-worker.service
systemctl list-dependencies myapp-api.servicesystemd-analyze verify, dosya temiz olduğunda hiçbir çıktı vermez. Herhangi bir çıktı bir sorun olduğunu gösterir; bu genellikle yazdığınız bölümde systemd'nin tanımadığı bir anahtar veya mevcut olmayan bir birime olan bağımlılıktır.
İkinci çözüm: Type=notify ile systemd'ye PID'den fazlasını bildirmek
Eğer program systemd bildirim protokolünü destekliyorsa, bunu kullanın. Type=notify ile servis, hazır olduğunda systemd'ye haber verir; bu sayede servis sıralaması bir temenni olmaktan çıkıp kesinleşir. Ayrıca MAINPID= kullanılarak systemd'nin bir başlatıcıya değil, asıl önemli olan sürece odaklanması sağlanabilir.
WatchdogSec=, harcanan çabaya değen kısımdır. Bu ayar yapıldığında, servis en az belirtilen sıklıkta sd_notify(3) üzerinden WATCHDOG=1 mesajı göndermek zorundadır. Mesajlar kesildiğinde systemd servisi SIGABRT ile sonlandırır ve başarısız olarak işaretler; böylece Restart=on-failure veya Restart=on-watchdog servisi yeniden başlatır. Bu, çalışan ancak kilitlenmiş bir süreci yeniden başlatmanın tek yerleşik yoludur; hiçbir çıkış durumu politikası bu durumu yakalayamaz.
[Service]
Type=notify
NotifyAccess=main
ExecStart=/usr/local/bin/myapp serve
WatchdogSec=30s
Restart=on-failure
RestartSec=5sBir watchdog tetiklenmesi, günlük kayıtlarında myapp.service: Watchdog timeout (limit 30s)! ve ardından gelen sonlandırma işlemi olarak görünür. Eğer birim TimeoutStartSec süresi dolana kadar activating (start) durumunda kalıyorsa, READY=1 hiç ulaşmamış demektir: Ya program protokolü desteklemiyordur ya da NotifyAccess=main, bir alt süreçten gelen bildirimi reddediyordur; bu durum günlük kayıtlarında her iki PID ile birlikte raporlanır.
HTTP sağlık kontrolü uç noktası sunan ancak sd_notify desteği olmayan yazılımlar için dürüst seçenekler şunlardır: Uç noktayı sorgulayıp systemctl restart komutunu çalıştıran küçük bir zamanlayıcı birimi oluşturmak veya sorgulama işlemini bir container çalışma zamanına bırakmak. Compose sağlık kontrolleri ve yeniden başlatma davranışları tam olarak bu amaçla mevcuttur.
Üçüncü çözüm: birim içinde denetleyici, yalnızca başka seçenek kalmadığında
Bazı yazılımlar, birbirinden ayıramayacağınız bir başlatıcı arkasında çalışan süreçler paketi olarak sunulur. Bu durumda birim içinde bir denetleyici çalıştırırsınız ve sonuçlarını kabul edersiniz: systemd denetleyiciyi izler, denetleyici diğer her şeyi izler ve yeniden başlatma politikanız artık iki farklı dosyada yer alır.
Bunun yaygın biçimi bir container runtime kullanımıdır. Bir docker compose veya podman birimi tam olarak bu kalıptadır; container bazlı yeniden başlatma politikası Compose dosyasında belirtilir ve systemd birimi yalnızca runtime'ı ayakta tutar. Eğer yapınız bu şekildeyse, açılışta bir Compose yığınını ayağa kaldıran birim, Type=oneshot ile RemainAfterExit=yes kullanımının neden genellikle doğru olduğu dahil olmak üzere çalışan sürümü gösterir.
cgroup yine de lehinize çalışmaya devam eder. Denetleyicinin başlattığı her şey birimin cgroup'u içinde kalır; bu nedenle MemoryMax=, CPUQuota= ve durdurma anındaki temizlik işlemleri tüm ağacı kapsar. Yalnızca yeniden başlatma kararı devredilmiş olur.
Hangi denetleyiciyi seçerseniz seçin, dış birimde Restart=always ayarını yapıp aynı zamanda içinde agresif bir yeniden başlatma politikası belirlerken dikkatli olun. Her biri kendi bekleme süresine (backoff) sahip iki katmanlı yeniden başlatma mantığı, dakikalarca kararsız kalan (flapping) bir servis ve nedenini açıklamayan bir günlük kaydı oluşturur.
ExitType=cgroup ifadesi "herhangi bir süreç sonlandığında yeniden başlat" anlamına gelmez
ExitType= (systemd 250 ve daha yeni sürümler, yani Ubuntu 24.04 ve Debian 12 sürümlerinin her ikisinde de mevcuttur), bu sorunu araştıran kişilerin karşılaştığı ayardır ve isminin çağrıştırdığının tam tersini yapar. Varsayılan değer olan ExitType=main, ana süreç sonlandığında servisin durmuş kabul edileceği anlamına gelir. ExitType=cgroup ise cgroup içindeki son süreç sonlanana kadar servisin çalışıyor kabul edileceği anlamına gelir.
Bu nedenle ExitType=cgroup, birimi tek bir sürecin ölmesine karşı daha duyarlı değil, daha az duyarlı hale getirir. Bu ayar, gerçek işçi sürecini fork eden ve PID dosyası yazmadan ana süreci sonlandıran, dolayısıyla Type=forking değerinin daemon sürecini bulamadığı programlar için doğru tercihtir. Burada açıklanan hata durumu için ise yanlış bir ayardır.
systemd içerisinde "cgroup içindeki herhangi bir süreç öldüğünde birimi yeniden başlat" anlamına gelen bir Restart= değeri yoktur. Eğer bu davranışa ihtiyacınız varsa, birim başına tek bir süreç kullanmanız gerekir. Programı ayıramıyorsanız ve sarmalayıcı (wrapper) betiği kontrol edebiliyorsanız, en yakın çözüm ilk alt süreç sonlandığı anda geri dönen wait -n kullanımıdır:
#!/bin/bash
/usr/local/bin/myapp-web &
/usr/local/bin/myapp-worker &
wait -n
exit 1Artık herhangi bir alt sürecin ölmesi, sarmalayıcı betiği sıfır olmayan bir durum koduyla sonlandırır ve bu sayede Restart=on-failure tetiklenir. Bu bir çözüm değil, bir uzlaşmadır. İki program için tek bir yeniden başlatma sayacı ve tek bir günlük akışı ile karşılaşmaya devam edersiniz; ayrıca hata veren kısmı tek başına yeniden başlatmanın bir yolu yoktur.
Gerçekte ne olduğunu inceleme yöntemi
Bu sırada dört komut kullanılır.
systemctl status myapp.service
systemd-cgls --unit myapp.service
systemctl show -p MainPID -p NRestarts -p Result -p ExecMainStatus myapp.service
journalctl -u myapp.service -b -o short-precisesystemctl status komutu; durumu, ana PID değerini ve cgroup ağacını tek bir ekranda gösterir. Sağlıklı bir birim Active: active (running) durumundadır ve beklediğiniz süreci belirten bir Main PID: satırı içerir. Eğer alttaki ağaçta tanımadığınız süreçler listeleniyorsa veya beklediğiniz bir süreç eksikse, sorunun kaynağını bulmuşsunuz demektir.
systemd-cgls --unit komutu, aynı ağacı kesintiye uğratmadan yazdırır; bu, bir birim birkaç süreçten fazlasını barındırdığında önem kazanır.
systemctl show komutu, makine tarafından okunabilir veriler sunar. NRestarts= yeniden başlatma sayacıdır ve kırk kez yeniden başlatılmış bir servisi, açılıştan beri çalışan bir servisten ayırt etmenin en hızlı yoludur. Result= son hata nedenini tutar: exit-code, signal, timeout, oom-kill, watchdog veya start-limit-hit. ExecMainStatus=, son ana sürecin ham çıkış durumudur.
Günlük kayıtları (journal) süreci tutar. Aranması gereken üç satır şunlardır:
myapp.service: Main process exited, code=exited, status=1/FAILURE
myapp.service: Failed with result 'exit-code'.
myapp.service: Scheduled restart job, restart counter is at 1.code=exited, status=N, programın N değerini döndürmeyi seçtiği anlamına gelir; dolayısıyla hata programda veya yapılandırmasındadır. code=killed, signal=SEGV, programın çöktüğünü gösterir. code=killed, signal=TERM genellikle başka bir sürecin durdurma isteğinde bulunduğu anlamına gelir; bu bir hata değildir ve Restart=on-failure tetiklenmez. code=dumped, bir core dosyası bırakıldığı anlamına gelir; systemd-coredump yüklü olduğunda coredumpctl list bu dosyayı size gösterecektir.
Birden fazla makine genelinde, NRestarts düzenli olarak toplanması gereken bir değerdir. Sayacı her gün artan bir birim, kimse fark etmese bile her gün hata veriyor demektir. İki veya üç sunucuyu geçtiğinizde, her sunucuda tek bir komutu çalıştırmanın tutarlı bir yolu, bu durumu bir tahminden bir rapora dönüştürür.
FAQ
systemctl neden süreç sonlandığı halde servisimi "active" olarak gösteriyor?
systemd, her servis birimi için ana süreç olan tek bir süreci takip eder ve Restart= yalnızca bu sürecin çıkış durumunu okur. Birimin başlattığı diğer her şey aynı cgroup içinde yer alır; systemd birim durdurulduğunda bu süreçleri sonlandırır ancak çıkış durumlarını izlemez. systemctl show -p MainPID myapp.service komutunu çalıştırın ve çıkan sayıyı systemd-cgls --unit myapp.service ile karşılaştırın. Eğer sonlanan süreç ağaçta görünüyor ancak MainPID değilse, systemd tasarlandığı gibi çalışmıştır. Çözüm, birim başına tek bir süreç kullanmak ve birimler arasındaki ilişkiyi BindsTo= ve Upholds= direktifleri ile tanımlamaktır.
"start request repeated too quickly" ne anlama gelir?
Bu hata, birimin StartLimitIntervalSec= süresi içinde StartLimitBurst= defadan fazla başlatılmaya çalışıldığı anlamına gelir. Varsayılan değer 10 saniyede 5 denemedir; bu sınıra ulaşıldığında systemd denemeyi durdurur. Bu bir hız sınırlamasıdır ve servisin neden başarısız olduğuna dair bilgi vermez; bu nedenle hata mesajının üzerindeki günlük kayıtlarını inceleyin. Durumu systemctl reset-failed myapp.service ile temizleyin ve ardından temel hatayı giderin. Eğer servis yavaş ayağa kalkan bir bağımlılığı bekliyorsa RestartSec= değerini artırın; çünkü 100 milisaniyelik varsayılan boşluk, beş denemenin bir saniyeden kısa sürede tükenmesine neden olur.
Restart=always mi yoksa Restart=on-failure mı kullanmalıyım?
Hemen hemen her durum için on-failure kullanın. Bu ayar; çökmeleri, sıfır olmayan çıkış kodlarını, zaman aşımlarını ve watchdog tetiklenmelerini yeniden başlatır, ancak kasıtlı bir exit 0 durumuna müdahale etmez. always ayarını yalnızca program, eş tarafın bağlantıyı kesmesi durumunda 0 döndüren bir istemci gibi, kendi kontrolü dışındaki nedenlerle temiz bir şekilde kapandığında kullanın. always kullanmanın riski, bozuk bir yapılandırmayı okuyup tek bir hata günlüğü yazarak 0 koduyla çıkan bir servisin sonsuz döngüye girmesidir. Bu durumdaki tek belirti, systemctl show içinde NRestarts değerinin sürekli artmasıdır.
Süreci manuel olarak sonlandırdığımda neden yeniden başlatma tetiklenmiyor?
Çünkü systemd; SIGHUP, SIGINT, SIGTERM ve SIGPIPE sinyallerini temiz çıkış olarak kabul eder ve standart bir kill <pid> komutu SIGTERM gönderir. Restart=on-failure altında temiz bir çıkış hata sayılmadığı için yeniden başlatma gerçekleşmez ve yapılandırma hatalıymış gibi görünür. Test etmek için temiz olmayan bir sonlandırma olan ve politikayı tetikleyen kill -9 <pid> veya systemctl kill -s SIGKILL myapp.service kullanın. Aynı kural, systemctl stop komutunun neden yeniden başlatma politikanızla çakışmadığını da açıklar.
StartLimitIntervalSec ve StartLimitBurst nereye yazılmalıdır?
[Unit] bölümüne yazılmalıdır. Eski dokümanlar ve eski systemd sürümleri bunları [Service] bölümüne yerleştirir, bu nedenle kopyalanan örnekler birbiriyle çelişebilir. Sürümünüzün hangisini desteklediğini tahmin etmeyin. systemctl daemon-reload komutundan sonra, systemctl show -p StartLimitBurst -p StartLimitIntervalUSec myapp.service ile systemd'nin neyi yüklediğini sorgulayın ve bu değerleri esas alın. systemd-analyze verify /etc/systemd/system/myapp.service komutu, systemd tarafından tanınmayan anahtarları yakalar; dosya temizse herhangi bir çıktı vermez.