SSD Nodes Learn 🎉 VPS dari $5.50/bln
Panduan Matt ConnorOleh Matt Connor

Mengapa systemd tidak mulakan semula servis anda

Arahan Restart hanya memantau proses utama, menyebabkan proses anak yang mati tidak dikesan. Ketahui cara sebenar Type, had restart, dan journal systemd berfungsi di sini.

Jawapan ringkas: polisi mulakan semula systemd memantau satu proses

Polisi mulakan semula systemd memantau satu proses bagi setiap unit: proses utama. Restart= membaca status keluar bagi proses tersebut sahaja dan tiada yang lain. Kumpulan kawalan (control group) sesuatu unit boleh memuatkan dua puluh proses, salah satu daripadanya boleh mati, dan unit tersebut kekal active (running) kerana proses utama masih wujud. Tiada apa-apa yang gagal pada pandangan systemd, jadi tiada apa-apa yang dimulakan semula.

systemd memang mengetahui tentang proses-proses lain tersebut. Ia mematikan proses-proses itu apabila unit dihentikan, ia mengira penggunaan memori proses tersebut terhadap had unit, ia mengenakan kuota CPU unit ke atasnya, dan ia mencetaknya dalam systemctl status. Ia cuma tidak pernah membaca status keluar proses-proses tersebut. Logik mulakan semula dan cgroup adalah dua perkara yang berbeza, dan kebanyakan panduan ini adalah mengenai jurang antara kedua-duanya.

Kandungan cgroup dan perkara yang dibaca oleh logik mulakan semula

Cgroup (control group) ialah objek kernel yang memiliki sekumpulan proses. Setiap unit servis mendapat satu cgroup, yang dinamakan sempena unit tersebut. Sesuatu proses tidak boleh keluar daripadanya. Proses anak mewarisi cgroup induknya, dan proses yang tidak mempunyai keistimewaan tidak boleh memindahkan dirinya ke tempat lain. Inilah sebabnya systemd boleh membersihkan daemon yang melakukan fork dua kali, sesuatu yang tidak dapat dilakukan oleh skrip init lama dengan boleh harap.

Lihat kedua-dua fakta ini secara bersebelahan:

systemd-cgls --unit myapp.service
systemctl show -p MainPID -p NRestarts -p Restart -p RestartUSec myapp.service

systemd-cgls menyenaraikan setiap proses dalam unit tersebut. MainPID ialah nombor tunggal yang dibaca oleh polisi mulakan semula. Apabila kedua-duanya tidak selari dengan model mental anda, ketidakselarian itulah pepijatnya. MainPID=0 adalah lebih buruk daripada PID yang salah: ini bermakna systemd tidak menjejaki apa-apa langsung, jadi tiada nilai Restart= yang boleh dicetuskan.

Terdapat satu pengecualian sebenar kepada peraturan proses utama. Jika pembunuh out-of-memory kernel membunuh mana-mana proses di dalam cgroup unit tersebut, systemd akan mengetahuinya kerana ia memantau fail memory.events cgroup itu. OOMPolicy= menentukan perkara yang berlaku seterusnya, dan tetapan lalainya ialah stop: keseluruhan unit dihentikan, hasilnya direkodkan sebagai oom-kill, dan itu dikira sebagai kegagalan, maka Restart=on-failure dicetuskan. Jurnal menyatakan perkara ini dengan jelas.

myapp.service: A process of this unit has been killed by the OOM killer.
myapp.service: Failed with result 'oom-kill'.

Oleh itu, proses anak yang dibunuh kerana memori akan menyebabkan unit tersebut terhenti, manakala proses anak yang sama mati akibat segmentation fault tidak akan menyebabkan unit terhenti. Jika anda menetapkan had memori pada sesuatu unit, baca bagaimana MemoryMax dan CPUQuota digunakan pada cgroup unit sebelum melaraskan polisi mulakan semula, kerana kedua-dua ciri tersebut bertemu di sini dan tidak di tempat lain.

Bagaimana Type= memilih proses utama

Type= dalam bahagian [Service] bukan sekadar mengenai urutan permulaan. Ia merupakan peraturan yang menentukan PID (process ID) mana yang menjadi MainPID, yang sama maksudnya dengan menentukan apa yang Restart= boleh lihat.

  • Type=simple ialah tetapan lalai. Proses yang difork oleh systemd daripada ExecStart= adalah proses utama. systemd menandakan unit sebagai telah bermula serta-merta, sebelum ia mengetahui sama ada exec tersebut berjaya atau tidak. Kesilapan taip pada laluan binari akan menyebabkan tugasan permulaan berjaya, dan kemudian Main process exited, code=exited, status=203/EXEC sejurus selepas itu.
  • Type=exec berkelakuan seperti simple, kecuali tugasan permulaan menunggu sehingga exec berjaya. Ini menukarkan kesilapan taip di atas menjadi kegagalan permulaan yang sebenar. Ia memerlukan systemd 240 atau lebih baharu, yang dimiliki oleh setiap pengedaran yang disokong. Utamakan ia berbanding simple.
  • Type=forking menjangkakan proses daripada ExecStart= untuk memfork daemon latar belakang dan kemudian keluar. systemd menunggu proses induk keluar, kemudian mencari daemon yang sebenar. Berikan ia PIDFile=. Tanpanya, GuessMainPID= (dihidupkan secara lalai) hanya berfungsi apabila tepat satu proses ditinggalkan dalam cgroup. Jika dua proses ditinggalkan, MainPID kekal 0.
  • Type=notify bermaksud servis memanggil sd_notify(3) dan menghantar READY=1 apabila ia boleh melayani trafik. Ia juga mungkin menghantar MAINPID= untuk memberikan systemd proses berbeza untuk dijejaki. NotifyAccess= ditetapkan secara lalai kepada main, jadi pemberitahuan yang dihantar oleh anak proses diabaikan dan jurnal menamakan PID asal ia datang.
  • Type=oneshot tidak mempunyai proses utama yang kekal. Unit menjadi tidak aktif sebaik sahaja ExecStart= selesai, melainkan anda menetapkan RemainAfterExit=yes. Restart=always dan Restart=on-success ditolak di sini, dengan mesej Service has Restart= set to either always or on-success, which isn't allowed for Type=oneshot services. Refusing.. Nilai lain, termasuk on-failure, diterima.

Dua ralat Type=forking perlu diingati, kerana setiap satunya menyebabkan unit kelihatan rosak tanpa sebab yang jelas:

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.

Yang pertama bermaksud daemon menulis fail PID-nya di tempat lain, atau menulisnya lewat daripada masa systemd mencarinya. Yang kedua bermaksud fail PID menamakan proses di luar cgroup unit tersebut, yang systemd enggan ambil alih, kerana fail PID yang boleh ditulis akan menjadi cara untuk membuat systemd menghantar isyarat kepada mana-mana proses pada mesin tersebut.

Mengapa skrip pembungkus menyembunyikan kematian proses anaknya

Berikut adalah bentuk yang menimbulkan persoalan dalam tajuk di atas.

#!/bin/bash
/usr/local/bin/myapp-web &
/usr/local/bin/myapp-worker &
wait

Unit tersebut adalah Type=simple, jadi proses utama ialah shell. wait tanpa argumen hanya akan kembali selepas setiap proses anak tamat. Matikan pekerja tersebut dan shell akan terus menunggu proses web, jadi shell tidak tamat, maka MainPID tidak tamat, dan Restart= tidak pernah dirujuk. Cgroup kini memegang satu proses yang kurang, systemctl status mencetak pepohon yang lebih pendek, dan unit tersebut masih active (running). Tiada apa-apa dalam systemd yang memantau perubahan pada pepohon tersebut.

Versi kedua bagi kesilapan yang sama adalah lebih senyap:

ExecStart=/bin/sh -c 'export APP_ENV=production; /usr/local/bin/myapp'

Proses utama ialah shell, bukan myapp. Pada systemctl stop, systemd menghantar SIGTERM kepada proses utama, dan shell yang sedang menunggu proses anak di latar depan tidak menyalurkan isyarat tersebut. Proses berhenti kemudian mengambil masa penuh TimeoutStopSec, iaitu 90 saat secara lalai, dan berakhir seperti ini:

myapp.service: State 'stop-sigterm' timed out. Killing.
myapp.service: Killing process 4711 (myapp) with signal SIGKILL.

Penyelesaiannya ialah exec. Tulis exec /usr/local/bin/myapp dan shell akan digantikan oleh program tersebut, jadi MainPID ialah program itu dan isyarat akan sampai kepadanya. Lebih baik lagi, buang shell dan gunakan Environment= atau EnvironmentFile= dalam unit tersebut. Perhatikan bahawa pepijat ini menyembunyikan dirinya apabila rentetan -c mengandungi satu arahan sahaja, kerana bash dan dash kedua-duanya mengoptimumkan kes tersebut kepada exec terus. Tambahkan arahan kedua pada rentetan tersebut dan shell akan terus hidup di hadapan program anda.

Hasilkan semula pada VPS ujian dalam dua minit

Simpan pembungkus di atas sebagai /usr/local/bin/two-children.sh, jadikan ia boleh laksana dengan chmod +x, dan gantikan dua laluan program dengan sleep 3600. Halakan unit kepadanya dengan Type=simple dan Restart=on-failure, kemudian systemctl daemon-reload dan mulakannya. Jalankan systemd-cgls --unit two-children.service dan perhatikan tiga PID tersebut: shell dan dua anaknya. Matikan satu anak dengan sudo kill <pid>. Semak unit itu semula. Pepohon tersebut kurang satu proses, statusnya masih active (running), dan jurnal tidak mempunyai apa-apa perkara baharu untuk dilaporkan. Sekarang jalankan sudo kill -9 <shell pid> sebaliknya. Unit tersebut gagal, anak yang terselamat dibersihkan kerana KillMode=control-group adalah lalai, dan jurnal menunjukkan Scheduled restart job, restart counter is at 1.

Kosa kata penuh Restart=, dan bila on-failure lebih baik daripada always

Restart= menerima satu daripada tujuh nilai, dan perbezaan yang memisahkan nilai-nilai tersebut adalah apa yang dianggap sebagai penamatan bersih (clean). systemd menganggap kod keluar 0, sebarang kod yang disenaraikan dalam SuccessExitStatus=, serta isyarat SIGHUP, SIGINT, SIGTERM dan SIGPIPE sebagai penamatan bersih. Segala yang lain, termasuk SIGKILL dan SIGSEGV, adalah tidak bersih.

  • no ialah tetapan lalai. Unit tidak akan dimulakan semula secara automatik, itulah sebabnya unit tanpa baris Restart= akan mati pada kegagalan pertama dan kekal mati.
  • on-success dimulakan semula hanya selepas penamatan bersih.
  • on-failure dimulakan semula apabila berlaku kod keluar bukan sifar, isyarat tidak bersih, tamat masa (timeout) mula atau henti, atau tamat tempoh watchdog.
  • on-abnormal dimulakan semula apabila berlaku isyarat tidak bersih, tamat masa, atau tamat tempoh watchdog, tetapi tidak akan dimulakan semula jika kod keluar hanya sekadar bukan sifar.
  • on-abort dimulakan semula hanya apabila berlaku isyarat tidak bersih, yang bermaksud berlakunya kerosakan (crash).
  • on-watchdog dimulakan semula hanya apabila WatchdogSec= tamat tempoh.
  • always dimulakan semula selepas setiap kes di atas, termasuk penamatan bersih dengan status 0.

on-failure ialah tetapan lalai yang tepat untuk daemon yang berjalan lama. Ia memulakan semula servis yang rosak dan membiarkan servis yang ditamatkan secara sengaja exit 0 tidak terusik. always sesuai untuk program yang keluar secara bersih atas sebab di luar kawalannya, contohnya klien terowong yang mengembalikan 0 apabila hujung jauh terputus sambungan. Kos bagi always ialah ia menyembunyikan pepijat: servis yang bermula, membaca fail konfigurasi yang rosak, mencatat ralat dan keluar dengan kod 0 akan bergelung selama-lamanya, dan satu-satunya tanda ialah pembilang mulakan semula yang terus meningkat.

SuccessExitStatus= mengubah sempadan antara bersih dan tidak bersih. Borg keluar dengan kod 1 untuk amaran dan 2 untuk ralat, jadi unit sandaran tanpa SuccessExitStatus=1 akan ditandakan sebagai gagal setiap kali ia melangkau satu fail yang tidak boleh dibaca. RestartPreventExitStatus= menyenaraikan kod yang menghalang mulakan semula walaupun di bawah always, yang merupakan cara bersih bagi program untuk menyatakan bahawa ia tidak sepatutnya dihidupkan semula. RestartForceExitStatus= melakukan perkara sebaliknya. Tugasan sandaran sepatutnya berada dalam unit Type=oneshot yang dipacu oleh pemasa (timer) dan bukannya dalam gelung mulakan semula, dan pasangan servis dan pemasa yang menjalankan tugasan mengikut jadual adalah bentuk yang perlu disalin di sana.

Satu amaran mengenai pengujian. Mematikan servis anda dengan kill <pid> biasa akan menghantar SIGTERM, yang berada dalam senarai bersih, jadi Restart=on-failure akan bertindak dengan betul dengan tidak melakukan apa-apa dan anda mungkin menyimpulkan konfigurasi anda rosak. Gunakan kill -9 <pid> atau systemctl kill -s SIGKILL myapp.service sebaliknya. Ingat juga bahawa tiada nilai Restart= akan tercetus selepas systemctl stop, atau apabila unit dihentikan kerana kebergantungan BindsTo= atau PartOf= telah tiada. Tugasan henti (stop job) bukanlah satu kegagalan.

RestartSec, dan nilai lalai 100 milisaat

RestartSec= ialah jeda antara unit berhenti dan systemd memulakannya semula, dengan nilai lalai sebanyak 100 milisaat. Semak nilai yang dimuatkan oleh unit anda:

systemctl show -p RestartUSec -p StartLimitIntervalUSec -p StartLimitBurst myapp.service

Unit yang tidak menetapkan nilai ini akan memaparkan RestartUSec=100ms. Nilai lalai tersebut sesuai untuk servis yang terhenti sekali-sekala dan pulih semula. Ia tidak sesuai untuk servis yang langsung tidak dapat bermula, kerana lima percubaan mula semula akan berlaku dalam masa kurang daripada setengah saat, yang merupakan punca utama had kadar (rate limit) yang diterangkan seterusnya dicetuskan. Bagi sebarang servis yang menunggu pangkalan data, mount, atau laluan rangkaian, tetapkan RestartSec=5s atau lebih.

Sehingga Ogos 2026, systemd 254 dan versi lebih baharu juga menawarkan RestartSteps= dan RestartMaxDelaySec=, yang meningkatkan tempoh jeda daripada RestartSec= sehingga mencapai had maksimum berdasarkan bilangan percubaan tersebut. Ubuntu 24.04 menyertakan systemd 255 dan menyokong ciri ini. Debian 12 menyertakan systemd 252 dan tidak menyokongnya. Peningkatan jeda secara berperingkat adalah penyelesaian yang tepat apabila dependency mungkin tidak tersedia untuk tempoh yang lama.

Apakah maksud sebenar "start request repeated too quickly"

Ini adalah status yang dianggap pembaca sebagai tindakan systemd berputus asa secara sewenang-wenangnya. Ia sebenarnya adalah pembilang. Peraturannya: jika unit dimulakan lebih daripada StartLimitBurst= kali dalam tempoh StartLimitIntervalSec=, systemd enggan memulakannya lagi dan meletakkannya dalam status gagal. Nilai lalai ialah 5 kali permulaan dalam 10 saat.

Jurnal menunjukkan urutan tersebut:

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.

dan systemctl start memberikan jawapan dengan penyelesaian yang telah ditulis:

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 mengosongkan pembilang dan status gagal tersebut. Tiada arahan lain yang melakukannya, jadi systemctl start biasa akan terus ditolak sehingga anda menjalankan arahan tersebut. Permulaan manual juga dikira dalam had tersebut, jadi beberapa kali systemctl restart yang dilakukan dengan tidak sabar semasa anda menyunting fail konfigurasi boleh mencetuskan had ini tanpa sebarang kerosakan berlaku.

Bahagian yang mengelirukan orang ramai: start-limit-hit tidak pernah menyatakan mengapa servis tersebut gagal. Ia hanya menyatakan bahawa servis itu gagal berulang kali dan dengan pantas. Sebab sebenar terdapat dalam baris jurnal di atasnya.

Kedua-dua tetapan tersebut tergolong dalam bahagian [Unit]. Anda akan menemui contoh yang meletakkannya dalam [Service], yang diterima oleh systemd versi lama, dan di situlah kekeliruan bermula. Tulis tetapan tersebut dalam [Unit], kemudian tanya systemd apa yang telah dimuatkan dengan systemctl show, kerana nilai yang dimuatkan adalah satu-satunya nilai yang diambil kira.

[Unit]
Description=My application
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Type=exec
ExecStart=/usr/local/bin/myapp
Restart=on-failure
RestartSec=10s

Ini memberikan unit tersebut lima percubaan dalam tempoh lima minit sebelum ia berputus asa. StartLimitIntervalSec=0 mematikan had tersebut sepenuhnya, dan anda harus tahu apa yang anda pilih: servis yang tidak dapat bermula akan terus mencuba selama-lamanya dan menulis ke dalam jurnal setiap kali. Nilai lalai seluruh mesin disimpan dalam /etc/systemd/system.conf sebagai DefaultStartLimitIntervalSec= dan DefaultStartLimitBurst=.

Satu tetapan bersebelahan memerlukan amaran. StartLimitAction= menentukan apa yang berlaku apabila had dicapai, dan ia menerima nilai termasuk reboot, reboot-force dan poweroff. Nilai lalai ialah none, yang menyebabkan unit gagal dan membiarkan mesin seperti sedia ada. Pada VPS jauh, poweroff bermaksud mesin yang kekal mati sehingga anda membuka konsol pembekal.

Pembaikan satu: satu proses bagi setiap unit

Ini adalah jawapan bagi hampir setiap kes. Jika dua program perlu dijalankan, tulis dua unit. Setiap satu kemudiannya mempunyai proses utama yang sebenar, status keluar yang sebenar, dan polisi mulakan semula tersendiri. Anda juga mendapat log yang berasingan, had sumber yang berasingan, dan pembilang mulakan semula yang berasingan, iaitu perkara yang anda perlukan pada pukul tiga pagi.

Nyatakan hubungan antara unit di dalam fail unit, bukan di dalam skrip shell.

  • After= hanya mengarahkan permulaan. Ia tidak menyatakan apa-apa tentang kegagalan.
  • Requires= memulakan unit lain bersama-sama dengan unit ini, dan menghentikan unit ini jika unit yang satu lagi dihentikan secara eksplisit.
  • BindsTo= ialah Requires= ditambah dengan kes yang anda ambil berat: unit ini berhenti apabila unit yang satu lagi berhenti atas sebarang sebab, termasuk ranap. Gandingkan ia dengan After=, atau susunannya tidak akan ditakrifkan.
  • PartOf= menyebarkan henti dan mulakan semula ke bawah, supaya systemctl restart myapp.target mencapai setiap unit yang PartOf= dengannya.
  • Upholds= (systemd 249 dan lebih baharu, jadi Ubuntu 22.04 dan lebih baharu) memastikan unit yang dinamakan terus berjalan: jika ia berhenti, systemd akan memulakannya semula. Ia tertakluk kepada had kadar permulaan yang sama seperti yang lain.

Pekerja yang tidak boleh berjalan tanpa pelayan API-nya, dan yang systemd pastikan terus hidup apabila API tersebut aktif:

# /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

Pekerja tersebut tidak mempunyai bahagian [Install] dan tidak pernah didayakan secara manual. Unit API menariknya masuk dengan Upholds=, jadi systemctl enable --now myapp-api.service ialah satu-satunya arahan yang anda jalankan. Muat semula dan semak apa yang systemd lakukan terhadap pasangan tersebut:

sudo systemctl daemon-reload
systemd-analyze verify /etc/systemd/system/myapp-worker.service
systemctl list-dependencies myapp-api.service

systemd-analyze verify tidak mencetak apa-apa langsung apabila fail tersebut bersih. Sebarang output adalah masalah, biasanya kunci yang tidak dikenali oleh systemd dalam bahagian tempat anda menulisnya, atau kebergantungan pada unit yang tidak wujud.

Pembaikan dua: Type=notify, supaya systemd mengetahui lebih daripada sekadar PID

Jika program tersebut menyokong protokol pemberitahuan systemd, gunakannya. Dengan Type=notify, servis memberitahu systemd apabila ia sudah bersedia, yang menjadikan aturan (ordering) benar-benar berfungsi dan bukan sekadar jangkaan, dan ia boleh menghantar MAINPID= untuk menunjuk kepada proses yang penting bagi systemd dan bukannya kepada pelancar (launcher).

WatchdogSec= adalah bahagian yang berbaloi untuk diusahakan. Tetapkannya, dan servis tersebut mesti menghantar WATCHDOG=1 melalui sd_notify(3) sekurang-kurangnya pada kekerapan tersebut. Apabila mesej berhenti, systemd akan menamatkan servis dengan SIGABRT dan menandakannya sebagai gagal, supaya Restart=on-failure atau Restart=on-watchdog akan memulihkannya semula. Ini adalah satu-satunya cara terbina dalam untuk memulakan semula proses yang masih hidup tetapi terhenti (stuck), yang tidak dapat dikesan oleh mana-mana polisi status keluar (exit-status).

[Service]
Type=notify
NotifyAccess=main
ExecStart=/usr/local/bin/myapp serve
WatchdogSec=30s
Restart=on-failure
RestartSec=5s

Pencetusan watchdog akan muncul dalam jurnal sebagai myapp.service: Watchdog timeout (limit 30s)! diikuti dengan tindakan penamatan (kill). Jika unit sebaliknya berada dalam status activating (start) sehingga TimeoutStartSec tamat, bermakna READY=1 tidak pernah sampai: sama ada program tersebut tidak menyokong protokol itu, atau NotifyAccess=main menolak pemberitahuan yang datang daripada proses anak (child process), yang akan dilaporkan oleh jurnal dengan kedua-dua PID tersebut.

Bagi perisian yang mendedahkan endpoint kesihatan HTTP tetapi tidak mempunyai sokongan sd_notify, pilihan yang jujur adalah menggunakan unit pemasa (timer unit) kecil yang memeriksa endpoint tersebut dan memanggil systemctl restart, atau membiarkan runtime kontena melakukan pemeriksaan tersebut, yang merupakan tujuan Compose healthchecks dan gelagat mulakan semula mereka wujud.

Pembaikan tiga: penyelia di dalam unit, hanya apabila tiada pilihan lain

Sesetengah perisian sebenarnya diedarkan sebagai himpunan proses di sebalik pelancar yang tidak boleh dipisahkan. Dalam keadaan ini, anda perlu menjalankan penyelia di dalam unit tersebut dan menerima akibatnya: systemd memantau penyelia, penyelia memantau segala proses lain, dan polisi mulakan semula anda kini berada dalam dua fail berasingan.

Bentuk lazim bagi perkara ini ialah runtime kontena. Unit docker compose atau podman adalah contoh tepat bagi corak ini, dengan polisi mulakan semula bagi setiap kontena dinyatakan dalam fail Compose, manakala unit systemd hanya memastikan runtime tersebut terus berjalan. Jika ini adalah konfigurasi anda, unit yang menjalankan stack Compose semasa but menunjukkan versi yang berfungsi, termasuk sebab mengapa Type=oneshot dengan RemainAfterExit=yes biasanya merupakan pilihan yang tepat di situ.

Cgroup masih berfungsi untuk kelebihan anda. Segala proses yang dimulakan oleh penyelia kekal di dalam cgroup unit tersebut, jadi MemoryMax=, CPUQuota= dan proses pembersihan semasa berhenti masih merangkumi keseluruhan pepohon proses. Hanya keputusan untuk memulakan semula sahaja yang diwakilkan.

Walau apa pun penyelia yang anda pilih, jangan tetapkan Restart=always pada unit luaran dan polisi mulakan semula yang agresif di dalamnya tanpa pertimbangan yang teliti. Dua lapisan logik mulakan semula, masing-masing dengan tempoh backoff tersendiri, akan menghasilkan servis yang tidak stabil (flapping) selama beberapa minit dan jurnal yang tidak menjelaskan puncanya.

ExitType=cgroup tidak bermaksud "mulakan semula apabila mana-mana proses mati"

ExitType= (systemd 250 dan lebih baharu, jadi Ubuntu 24.04 dan Debian 12 kedua-duanya memilikinya) ialah tetapan yang ditemui pengguna apabila mereka mencari masalah ini, dan ia melakukan perkara yang bertentangan dengan maksud namanya. Nilai lalai, ExitType=main, bermaksud servis dianggap berhenti apabila proses utama keluar. ExitType=cgroup bermaksud servis dianggap sedang berjalan sehingga proses terakhir dalam cgroup keluar.

Oleh itu, ExitType=cgroup menjadikan unit kurang sensitif terhadap kematian satu proses, bukannya lebih sensitif. Ia adalah tetapan yang betul untuk program yang melakukan fork kepada pekerja sebenar dan mengeluarkan proses induk tanpa menulis fail PID, di mana Type=forking tidak dapat mencari daemon tersebut. Ia adalah tetapan yang salah untuk kegagalan yang diterangkan di sini.

Tiada nilai Restart= yang bermaksud "mulakan semula unit apabila mana-mana proses dalam cgroup mati". Jika anda memerlukan tingkah laku tersebut, anda memerlukan satu proses bagi setiap unit. Jika anda tidak boleh memisahkan program tersebut dan anda mengawal skrip pembungkus (wrapper script), perkara yang paling hampir ialah wait -n, yang kembali sebaik sahaja anak proses pertama keluar:

#!/bin/bash
/usr/local/bin/myapp-web &
/usr/local/bin/myapp-worker &
wait -n
exit 1

Mana-mana anak proses yang mati sekarang akan menjatuhkan pembungkus dengan status bukan sifar, jadi Restart=on-failure akan bertindak. Ini adalah satu kompromi, bukan penyelesaian. Anda masih mendapat satu pembilang mulakan semula untuk dua program, satu aliran log, dan tiada cara untuk memulakan semula bahagian yang gagal secara berasingan.

Cara memeriksa apa yang sebenarnya berlaku

Empat arahan, mengikut urutan ini.

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-precise

systemctl status memberikan status, PID utama dan pepohon cgroup pada satu skrin. Unit yang sihat akan memaparkan Active: active (running) dengan baris Main PID: yang menamakan proses yang anda jangkakan. Jika pepohon di bahagian bawah menyenaraikan proses yang tidak anda kenali, atau kehilangan proses yang sepatutnya ada, anda sudah mendapat jawapannya.

systemd-cgls --unit mencetak pepohon yang sama tanpa pemotongan, yang menjadi penting apabila sesuatu unit mengandungi lebih daripada beberapa proses.

systemctl show memberikan fakta yang boleh dibaca oleh mesin. NRestarts= ialah pembilang mulakan semula, dan ia merupakan cara terpantas untuk membezakan servis yang telah dimulakan semula sebanyak empat puluh kali dengan servis yang telah berjalan sejak but. Result= menyimpan sebab kegagalan terakhir: exit-code, signal, timeout, oom-kill, watchdog atau start-limit-hit. ExecMainStatus= ialah status keluar mentah bagi proses utama yang terakhir.

Jurnal menyimpan urutan tersebut. Ini adalah tiga baris untuk dicari:

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 bermaksud program memilih untuk mengembalikan N, jadi kerosakan berpunca daripada program itu sendiri atau konfigurasinya. code=killed, signal=SEGV bermaksud ia terhempas (crash). code=killed, signal=TERM biasanya bermaksud sesuatu yang lain memintanya untuk berhenti, yang bukan merupakan kegagalan dan tidak akan mencetuskan Restart=on-failure. code=dumped bermaksud ia meninggalkan fail core, yang akan ditunjukkan oleh coredumpctl list kepada anda apabila systemd-coredump dipasang.

Merentasi lebih daripada satu mesin, NRestarts ialah nombor yang wajar dikumpulkan mengikut jadual. Unit yang pembilangnya meningkat setiap hari bermakna ia gagal setiap hari, sama ada sesiapa menyedarinya atau tidak. Apabila anda menguruskan lebih daripada dua atau tiga kotak, cara konsisten untuk menjalankan satu arahan pada setiap pelayan mengubah perkara itu daripada tekaan kepada laporan.

FAQ

Mengapakah systemctl menyatakan servis saya aktif sedangkan proses telah mati?

systemd menjejaki satu proses bagi setiap unit servis, iaitu proses utama, dan Restart= hanya membaca status keluar proses tersebut. Segala perkara lain yang dimulakan oleh unit tersebut berada dalam cgroup yang sama, dan systemd akan mematikan proses-proses tersebut apabila unit dihentikan, namun ia tidak memantau status keluar proses-proses itu. Jalankan systemctl show -p MainPID myapp.service dan bandingkan nombor tersebut dengan systemd-cgls --unit myapp.service. Jika proses yang mati muncul dalam pepohon tetapi bukan MainPID, systemd telah bertindak tepat seperti yang direka bentuk. Penyelesaiannya adalah satu proses bagi setiap unit, dengan hubungan yang ditulis sebagai BindsTo= dan Upholds= antara unit-unit tersebut.

Apakah maksud "start request repeated too quickly"?

Ini bermakna unit tersebut telah dimulakan lebih daripada StartLimitBurst= kali dalam tempoh StartLimitIntervalSec=, yang secara lalai adalah 5 kali permulaan dalam 10 saat, jadi systemd berhenti mencuba. Ini merupakan had kadar (rate limit) dan ia tidak menyatakan sebab servis gagal, jadi baca baris jurnal di atasnya. Kosongkan status dengan systemctl reset-failed myapp.service, kemudian baiki kegagalan asasnya. Jika servis menunggu sesuatu yang lambat untuk muncul, tingkatkan RestartSec=, kerana jurang lalai 100 milisaat akan menghabiskan kesemua lima percubaan dalam masa kurang daripada satu saat.

Patutkah saya menggunakan Restart=always atau Restart=on-failure?

Gunakan on-failure untuk hampir semua perkara. Ia memulakan semula servis sekiranya berlaku kerosakan (crash), status keluar bukan sifar, tamat masa (timeout) dan kegagalan watchdog, serta membiarkan exit 0 yang disengajakan. Gunakan always hanya apabila program keluar dengan bersih atas sebab di luar kawalannya, seperti klien yang mengembalikan 0 apabila peer-nya terputus sambungan. Kos menggunakan always ialah servis yang membaca konfigurasi rosak, mencatat satu ralat dan keluar dengan status 0 akan bergelung selama-lamanya, dan satu-satunya simptom yang kelihatan ialah NRestarts yang meningkat dalam systemctl show.

Mengapakah mematikan proses saya secara manual tidak mencetuskan permulaan semula?

Kerana systemd mengira SIGHUP, SIGINT, SIGTERM dan SIGPIPE sebagai status keluar yang bersih, dan kill <pid> biasa menghantar SIGTERM. Di bawah Restart=on-failure, status keluar yang bersih bukanlah satu kegagalan, jadi tiada apa-apa yang dimulakan semula dan konfigurasi kelihatan rosak sedangkan ia tidak. Uji dengan kill -9 <pid> atau systemctl kill -s SIGKILL myapp.service, yang merupakan penamatan tidak bersih dan ia memang mencetuskan polisi tersebut. Peraturan yang sama menjelaskan mengapa systemctl stop tidak pernah melawan polisi permulaan semula anda.

Di manakah StartLimitIntervalSec dan StartLimitBurst diletakkan?

Dalam bahagian [Unit]. Bahan lama dan versi systemd yang lebih lama meletakkannya dalam [Service], jadi contoh yang disalin mungkin bercanggah antara satu sama lain. Jangan meneka versi mana yang diiktiraf oleh sistem anda. Selepas systemctl daemon-reload, tanya systemd apa yang dimuatkannya dengan systemctl show -p StartLimitBurst -p StartLimitIntervalUSec myapp.service, dan anggap nombor tersebut sebagai kebenaran. systemd-analyze verify /etc/systemd/system/myapp.service akan mengesan kunci yang tidak dikenali oleh systemd sama sekali, dan tidak mencetak apa-apa apabila fail tersebut bersih.

#systemd#restart#service-unit#cgroups#reliability