Systemd birimi neden başlatılamıyor: Hata kodlarını okuma
Systemctl status komutu ile hata ayıklama yöntemlerini öğrenin. 203/EXEC ve 226/NAMESPACE kodlarının anlamlarını ve birimin neden hemen kapandığını analiz etmeyi keşfedin.
Bir systemd birimi neden başlatılamaz
Başlatılamayan bir systemd birimi, nedenini tek bir alanda belirtir. systemctl status <unit> komutunu çalıştırın ve hatayı bildiren satırda code= ile status= değerlerini inceleyin. 200'lü aralıktaki bir durum kodu, systemd'nin programınıza hiç ulaşamadığı anlamına gelir: birim dosyanızın talep ettiği ortam oluşturulurken hata meydana gelmiştir. 200'ün altındaki bir durum kodu ise programınızın çalıştığını ancak kendi kendine kapandığını gösterir; bu durumda birim dosyası muhtemelen doğrudur ancak uygulama hatalıdır.
Bu ayrım, karar yolunu belirler. Aşağıdaki her şey, numaraların gösterdiği sırayla bu ayrımdan kaynaklanır.
Soruyu yanıtlayan üç komut, sırasıyla
systemctl status myapp.service
journalctl -u myapp.service -b --no-pager
systemd-analyze verify /etc/systemd/system/myapp.servicesystemctl status kararı verir. Önce Loaded: satırını okuyun; çünkü bu satır systemd'nin gerçekten ayrıştırdığı dosya yolunu belirtir ve birimin etkinleştirilmiş, maskelenmiş veya hiç bulunamamış olup olmadığını söyler. Ardından Active: satırını ve altındaki code= ile status= çiftini inceleyin.
journalctl -u myapp.service -b --no-pager ayrıntıları sunar. -u çıktıyı ilgili birime göre filtreler, -b çıktıyı mevcut önyükleme ile sınırlar; böylece geçen haftadan kalan bir hatayı okumazsınız ve --no-pager çıktıyı doğrudan terminale yazdırarak grep içine aktarmanıza olanak tanır. status yalnızca son birkaç günlük kaydını gösterir ve uzun satırları kısaltır. Journal, programın çökmeden önce yazdırdığı her şeyi gösterir; bu genellikle gerçek hata mesajıdır. Daha fazla geçmiş için -n 100 ekleyin veya birimi yeniden başlatırken ikinci bir terminalde -f ile çalıştırın.
systemd-analyze verify bir birim dosyasını çalıştırmadan yükler. Bilinmeyen bölümler ve yönergeler hakkında uyarı verir ve ExecStart= içinde yürütülemeyen komutları işaretler. Bu, iki sessiz hata sınıfını yakalar: systemd'nin yükleme sırasında çoğu kişinin okumadığı bir uyarıyla görmezden geldiği yanlış yazılmış anahtarlar ve mevcut olmayan yollar.
Herhangi bir birim dosyasını düzenledikten sonra sudo systemctl daemon-reload komutunu çalıştırın. Siz bunu yapana kadar systemd daha önce yüklediği kopyayı kullanmaya devam eder ve systemctl status diskteki dosyanın değiştiğine dair bir uyarı ekler. "Hiçbir işe yaramayan" bir düzeltme, genellikle systemd'nin henüz okumadığı bir düzeltmedir.
İki komut daha yerini hak etmektedir. systemctl cat myapp.service, ana dosya ile /etc/systemd/system/myapp.service.d/ altındaki tüm ek dosyaların birleşiminden oluşan etkin birim yapılandırmasını yazdırır. systemctl show myapp.service -p ExecStart -p User -p WorkingDirectory ise bu değerleri systemd'nin ayrıştırdığı biçimiyle, yani gerçekten çalışacak olan haliyle yazdırır.
status=203/EXEC ne anlama gelir?
203/EXEC, systemd'nin kurulumu tamamladığını, execve() komutunu çalıştırdığını ancak çekirdeğin (kernel) bunu reddettiğini belirtir. Programınız kendi kodundan tek bir satır dahi çalıştırmamıştır. Neredeyse tüm durumlar dört temel nedenden kaynaklanır.
ExecStart=içindeki yol yanlıştır veya mutlak (absolute) yol değildir. Birim dosyasındaki (unit file) tam metin ilels -lkomutunu kullanarak yolu doğrulayın.- Dosyanın çalıştırma izni (execute bit) yoktur.
sudo chmod +x /opt/myapp/run.shbu sorunu giderir. Bir arşivden çıkarılan veya başka bir makineden kopyalanan dosyalar genellikle bu izni kaybeder. - Shebang satırı hatalıdır. Çekirdek, bir betiğin ilk satırını okur ve orada belirtilen yorumlayıcıyı çalıştırır; bu nedenle servis PATH değişkeni
python3içermediğinde#!/usr/bin/env python3başarısız olur. Ayrıca, Windows satır sonları ile kaydedilmiş bir dosya, mevcut olmayan/bin/bash\radlı bir yorumlayıcıyı arar. - Dosya bu makinede çalıştırılamaz bir formattadır: yanlış mimari veya shebang içermeyen bir metin dosyası olabilir.
Herhangi bir değişiklik yapmadan önce, servis kullanıcısı olarak işlemi manuel şekilde yeniden oluşturmayı deneyin.
sudo -u appuser /opt/myapp/run.sh
file /opt/myapp/run.sh
head -1 /opt/myapp/run.sh | cat -Afile mimariyi belirtir ve satır sonları sorunlu olduğunda "with CRLF line terminators" uyarısını verir. cat -A, aynı durumu sonunda ^M karakteri ile gösterir. Bunları sed -i 's/\r$//' /opt/myapp/run.sh ile temizleyin.
Aralık hakkında önemli bir uyarı: 200 ve üzeri değerler bir kuraldır, garanti değildir. Kendi programınız 203 çıkış koduyla sonlanabilir ve systemd bu ikisi arasındaki farkı ayırt edemez. systemd-analyze exit-status 203, herhangi bir kodun adını ve sınıfını yazdırır; bu, tabloyu okumanıza yardımcı olur. Ancak uygulamanız 199'un üzerinde çıkış kodları kullanıyorsa, bunları değiştirin.
Neden 217/USER veya 216/GROUP hatası alıyorum?
217/USER, servis başladığı anda User= içinde belirtilen hesabın mevcut olmadığı anlamına gelir. 216/GROUP, Group= veya SupplementaryGroups= için aynı hatanın oluştuğunu gösterir. Bunu her biri için tek bir komutla doğrulayın.
getent passwd appuser
getent group appgroupHer komut ya bir satır çıktı verir ya da hiçbir şey yazdırmadan sıfır olmayan bir değer döndürür. Hiçbir şey yazdırılmaması, ismin sistem tarafından bilinmediği anlamına gelir; bu durumda systemd ilgili hesaba geçiş yapamaz ve yürütme (exec) aşamasından önce durur. Çözüm, User=root ayarını değiştirmek değil, ilgili hesabı oluşturmaktır. Bir servisi en düşük yetkiye sahip özel bir sistem hesabı altında çalıştırmak, bu yönergenin temel amacıdır.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin appuserDynamicUser=yes, systemd'nin her başlatma için geçici bir hesap atamasını sağlayarak bu sorunu aşar. Bu yöntem, herhangi bir durum (state) tutmayan servisler için uygundur. Dosya yazan her servis, yanında StateDirectory= ayarını da gerektirir; çünkü kullanıcı kimliği her başlatmada değişir ve düz bir dizin altındaki dosyalar, artık mevcut olmayan bir hesabın sahipliğinde kalır.
226/NAMESPACE nedir?
226/NAMESPACE, korumalı alan (sandboxing) yönergelerinden kaynaklanır. Bir birim ProtectSystem=, ProtectHome=, PrivateTmp=, ReadWritePaths= veya benzeri herhangi bir ayar içerdiğinde, systemd programı çalıştırmadan önce o servis için özel bir mount namespace oluşturur. Buradaki namespace, bir sürecin dosya sistemine dair özel bir görünümüdür. Bu plandaki herhangi bir mount işlemi başarısız olursa, başlatma 226 hatasıyla sonuçlanır ve programınız asla çalışmaz.
Genel neden, ReadWritePaths= içinde mevcut olmayan bir yoldur. ProtectSystem=strict tüm dosya sistemini salt okunur olarak bağlar ve ReadWritePaths= belirtilen yolları yazma erişimi için yeniden açar. systemd, mevcut olmayan bir dizini yeniden açamaz. İki çözüm yolu uygundur. StateDirectory= ile dizini systemd'nin oluşturmasına izin verin; bu, her başlatmada /var/lib/<name> oluşturur ve dizini servis kullanıcısına atar. Alternatif olarak, yolun başına - ekleyin; bu, kaynak eksik olduğunda systemd'ye ilgili girdiyi görmezden gelmesini söyler. Kötü çözüm ise güvenlik ayarlarını kaldırmaktır; bu, beş dakikalık bir sorunu kalıcı bir zafiyete dönüştürür.
[Service]
ProtectSystem=strict
ProtectHome=yes
StateDirectory=myapp
ReadWritePaths=-/srv/uploadsHangi satırın soruna yol açtığını tespit edemediğinizde, tüm güvenlik bloğunu kaldırın, yapılandırmayı yeniden yükleyin ve servisi başlatın. Servis çalışıyorsa, satırları tek tek geri ekleyin ve her eklemeden sonra yeniden başlatın. Bu ailenin iki benzer üyesi 233/RUNTIME_DIRECTORY ve 238/STATE_DIRECTORY'tür. Bunlar, systemd'nin RuntimeDirectory= veya StateDirectory= içinde belirtilen dizini oluşturamadığı veya sahipliğini alamadığı anlamına gelir; bu durum genellikle ilgili yolun zaten mevcut olması ve farklı bir kullanıcıya ait olmasından kaynaklanır.
WorkingDirectory doğru görünmesine rağmen neden 200/CHDIR hatası alınıyor?
200/CHDIR, chdir() işleminin WorkingDirectory= dizinine yapılamadığını belirtir. Dizin mevcut değildir veya servis kullanıcısının bu dizine erişim yetkisi yoktur. Bir dizine girmek için o dizin üzerinde ve üst dizinlerin tamamında çalıştırma (execute) izni gereklidir; bu nedenle /home/deploy dizini 700 modunda olduğunda ve servis appuser kullanıcısı ile çalıştığında, tamamen okunabilir durumdaki bir /home/deploy/app dizinine erişilemez.
sudo -u appuser test -x /srv/myapp && echo ok
namei -l /srv/myappnamei -l komutu, yol üzerindeki her bileşenin sahibini ve modunu yazdırır; bu, erişimi engelleyen dizini bulmanın en hızlı yoludur. WorkingDirectory=-/srv/myapp ayarını yazmak, eksik bir dizinin hata oluşturmasını engeller. Bu durum, çalışma dizininin neresi olduğuyla ilgilenmeyen programlar için doğru, dosyaları göreli yollarla açan programlar için ise yanlıştır.
Servis neden başlıyor ve bir saniye sonra duruyor?
Burada 200 serisi bir kod yoktur ve genellikle hiçbir hata metni de görünmez. Birim, başlatıldıktan hemen sonra inactive (dead) durumuna geçer veya activating (auto-restart) döngüsüne girer. systemd ortamı doğru şekilde oluşturmuştur. Uyumsuzluk, programınızın yaptığı iş ile Type= değerinin ne yapacağını vaat ettiği arasındadır.
Varsayılan değer olan Type=simple, programın ön planda kalacağını belirtir. Arka plana çatallanan (fork) ve ardından sonlanan bir daemon çalıştırırsanız, systemd ana sürecin bittiğini görür ve servisi tamamlanmış kabul eder. Çoğu daemon'ın ön planda kalmasını sağlayan nginx -g 'daemon off;' gibi bir bayrağı bulunur.
Type=forking, ilk sürecin çocuğu hazır olduğunda sonlanacağını belirtir. Eğer ön planda çalışan bir program verirseniz, başlatma işi varsayılan olarak 90 saniye olan TimeoutStartSec= süresi dolana kadar bekler; ardından systemd süreci öldürür ve bir zaman aşımı hatası günlüğe kaydeder.
Type=notify, programın hazır olduğunu bildirmek için sd_notify() çağrısı yapacağını belirtir. Bu desteğe sahip olmayan bir program hiçbir şey bildirmez, bu nedenle başlatma zaman aşımına uğrar ve günlük kaydı sonucu bir protokol hatası olarak işler.
Programın gerçekte ne yaptığına göre uygun tipi seçin. simple, forking, oneshot ve notify tipleri arasındaki farklar bu tür hataların tamamını çözen tek karardır.
Bir servis sürekli olarak sonlanıyorsa, systemd denemeyi bırakır ve başlatma isteğinin çok hızlı tekrarlandığını bildirir. Birim, hız sınırı penceresi geçene veya siz sudo systemctl reset-failed myapp.service komutunu çalıştırana kadar başarısız durumda kalır. Sınırı yükseltmek yalnızca belirtiyi gizler. Son hatadan ziyade ilk hatayı görmek için günlük kayıtlarını inceleyin ve değiştirmeden önce Restart=on-failure gerçekten neleri yeniden dener konusuna göz atın.
Birim neden hiçbir hata vermeden inaktif durumda?
Bir birim başlatılmak yerine atlanabilir. Condition* yönergeleri tasarım gereği sessizdir: kontrol başarısız olduğunda systemd işi başarılı olarak işaretler ve hiçbir işlem yapmaz. ConditionPathExists=/etc/myapp/config.yml içeren bir birim, ilgili dosya eksik olduğu sürece asla başlamaz ve hiçbir hata raporu da oluşturmaz.
systemctl show myapp.service -p ConditionResult -p ConditionTimestamp
journalctl -u myapp.service -b --no-pager | grep -i conditionConditionResult=no atlama işlemini doğrular, günlük kayıtları ise gerçekleşmeyen kontrolü belirtir. Eksik bir ön koşulun belirgin bir hata ile başarısız olması gerektiğinde bunun yerine Assert* yönergesini kullanın. Koşullar, iddialar ve birim sıralaması bölümü, hangi kontrolün nerede kullanılması gerektiğini açıklar.
Sessiz kalan birkaç durum daha mevcuttur. "Could not be found" (bulunamadı) hatası genellikle dosyanın yanlış dizinde olduğunu veya yapılandırmayı yeniden yüklemediğinizi gösterir: oluşturduğunuz birim dosyaları /etc/systemd/system/ dizininde bulunmalıdır. Maskelenmiş bir birim, sudo systemctl unmask myapp.service komutu ile maske kaldırılana kadar her türlü başlatma isteğini reddeder. Ayrıca systemctl enable, [Install] bölümü olmayan bir birimde başarısız olur; bu nedenle birime bir WantedBy=multi-user.target bölümü ekleyin.
Peki süreç başarısız olmak yerine öldürüldüyse ne olur?
code=killed durumu code=exited durumundan farklıdır. Süreci dışarıdan bir müdahale sonlandırmıştır. status=9/KILL, out of memory (OOM) killer mekanizmasını işaret eder ve günlük kayıtlarında seçilen süreç belirtilir. Kendi belirlediğiniz bir sınır da cgroup (control group) içerisinde aynı sonucu doğurabilir; bu nedenle free -m ile ana makinedeki boş belleği kontrol edin ve birim için MemoryMax= değerine bakın. MemoryMax, CPUQuota ve diğer cgroup sınırları başlıklı bölüm, hangi sınırın süreci sonlandırdığını, hangisinin ise sadece yavaşlattığını açıklar.
Başlatma denemesinden hemen sonra gelen status=15/TERM, genellikle systemd'nin başlatma süresini aşması ve süreci sonlandırması anlamına gelir; bu durum sizi tekrar Type= bölümüne yönlendirir.
Bu hataların çoğunu önleyen iki alışkanlık
Her yerde mutlak yolları (absolute paths) kullanın. systemd, oturum açtığınız kabuğu (login shell) çalıştırmaz; bu nedenle .bashrc, .profile ve etkinleştirilmiş bir sanal ortam mevcut değildir. Bir sistem servisi için $PATH, /opt veya bir dil sürümü yöneticisinin shim dosyalarını içermeyen kısa ve yerleşik bir listedir. /usr/bin/python3 veya /opt/myapp/venv/bin/python ifadelerini tam olarak yazın. Kabuğunuzda command -v myapp komutunu çalıştırmak, yapıştırılacak yolu ekrana yazdırır. Aynı kural WorkingDirectory=, EnvironmentFile= ve ReadWritePaths= içindeki her yol için geçerlidir.
ExecStart= bir kabuk değildir. systemd satırı kelimelere böler ve execve() komutunu doğrudan çağırır. Pipe, yönlendirme, glob, &&, backtick ve ~ ifadelerinin hiçbir anlamı yoktur; bunlar programınıza değişmez argümanlar olarak ulaşır. ExecStart=/usr/bin/myapp --flag > /tmp/out.log, > ve /tmp/out.log ifadelerini myapp komutuna iletir; bu da systemd sorunu gibi görünmeyen bir kullanım hatasıyla sonlanır. Kabuk özelliklerine ihtiyaç duyduğunuzda, bir kabuk kullanmayı açıkça belirtin.
ExecStart=/bin/sh -c '/usr/bin/myapp --flag | /usr/bin/tee -a /var/log/myapp.log'Yalnızca çıktı almak için buna ihtiyacınız yoktur. Servis çıktısı varsayılan olarak journal'a gider ve StandardOutput=append:/var/log/myapp.log, herhangi bir kabuk süreci olmadan doğrudan bir dosyaya yazar.
Değişken genişletme (variable expansion) aynı şekilde sınırlıdır. $MYVAR ve ${MYVAR}, Environment= ve EnvironmentFile= üzerinden değiştirilir; başka hiçbir şey genişletilmez. Siz tanımlamadığınız sürece, bir sistem servisi için $HOME ayarlı değildir. Bir EnvironmentFile= de kabuk betiği değildir: export burada kullanılamaz, tırnak işaretleme kuralları bash'ten farklıdır ve dosya yolu - ile ön eklenmedikçe eksik bir dosya ölümcül hata oluşturur.
Canlı sunucuda sorun giderme
Kodu inceleyin, sorunun nedenini kanıtlayın, tek bir değişiklik yapın ve yeniden başlatın. Bu sıra, her ayrıntıyı bilmekten daha önemlidir; çünkü üç farklı tahmini değişikliği aynı anda yapıp hangisinin işe yaradığını gözden kaçırmanızı engeller. Aynı yöntem, siz yazmadığınız birimler için de geçerlidir. Hiç tetiklenmeyen bir zamanlayıcı, hiç başlamamış bir servistir; bu nedenle önce serviste hata ayıklayın: systemd zamanlayıcısı ve tetiklediği servis, tam olarak yukarıda belirtilen şekillerde başarısız olur; zamanlayıcı, siz journal üzerinden sorgulayana kadar çıktıyı gizler.
FAQ
systemctl status çıktısındaki status=203/EXEC ne anlama gelir?
systemd, unit dosyasının talep ettiği tüm hazırlıkları tamamlamış ancak execve() çağrısı başarısız olmuştur; bu nedenle programınız hiç çalışmamıştır. Sırasıyla şu dört noktayı kontrol edin: ExecStart= içindeki yolun mevcut ve mutlak (absolute) olması, dosyanın çalıştırılabilir (execute) iznine sahip olması, shebang satırında belirtilen yorumlayıcının servis PATH üzerinde bulunması ve dosyanın Unix satır sonu karakterlerini kullanması. Son madde için file komutu "with CRLF line terminators" çıktısı veriyorsa, bu durum yorumlayıcı adını /bin/bash\r haline getirir ve çekirdek (kernel) çalıştırmayı reddeder.
Servisim neden başlıyor ve hemen ardından duruyor?
Unit dosyası, programın sahip olmadığı bir davranış biçimini vaat etmektedir. Type=simple ile systemd, programın ön planda (foreground) kalmasını bekler; bu nedenle arka plana (background) fork olan bir daemon, fork olduğu anda işini bitirmiş gibi görünür. Type=forking ile systemd, ilk sürecin çıkış yapmasını bekler; bu nedenle ön planda çalışan bir program, TimeoutStartSec= süresi dolana kadar başlangıç işleminin askıda kalmasına neden olur. Type= ayarını programın çalışma biçimine göre eşleştirin; eğer program bir ön plan bayrağı sunuyorsa, bu bayrağı varsayılan Type=simple ile birlikte kullanın.
Kısa durum çıktısı yerine gerçek hatayı nasıl görebilirim?
systemctl status yalnızca günlük kayıtlarının son birkaç satırını yazdırır ve uzun satırları keser. Bu önyükleme (boot) sırasında unit tarafından kaydedilen her şeyi görmek için journalctl -u myapp.service -b --no-pager komutunu çalıştırın, daha geniş bir pencere için -n 200 ekleyin veya çıktıyı grep aracına yönlendirin. Uygulama kendi günlük dosyasını oluşturuyorsa onu da okuyun; çünkü systemd yalnızca programın standart çıktıya (stdout) ve standart hata akışına (stderr) gönderdiği verileri yakalar.
Unit dosyam neden hata mesajı vermeden inaktif durumda?
Çoğu durumda bir Condition* yönergesi işlemi atlamıştır. Bu kontroller sessizdir: başarısız bir koşul, başlangıç işlemini başarılı olarak işaretler. systemctl show myapp.service -p ConditionResult komutunu çalıştırın ve ConditionResult=no ifadesini arayın, ardından kontrolün adını belirten günlük satırını okuyun. Diğer yaygın neden ise unit dosyasının maskelenmiş (masked) olmasıdır; bu durum sudo systemctl unmask ile temizlenene kadar her türlü başlatma isteğini reddeder.
Her unit dosyası değişikliğinden sonra daemon-reload yapmam gerekir mi?
Evet, bir unit dosyası veya drop-in üzerinde yapılan her değişiklik için gereklidir. sudo systemctl daemon-reload, systemd'nin dosyaları diskten yeniden okumasını sağlar, ardından sudo systemctl restart myapp.service bu değişiklikleri çalışan servise uygular. Sizin yerinize yeniden yükleme yapan systemctl edit komutundan sonra veya systemd'ye değil de doğrudan uygulamaya ait bir yapılandırma dosyasını değiştirdiğinizde buna ihtiyaç duymazsınız.