SSD Nodes Learn 🎉 VPS vanaf $5.50/mnd
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-08-21

systemd dependencies en conditions begrijpen

Leer het verschil tussen Requires, After en Condition. Voorkom dat uw unit faalt tijdens het opstarten door de juiste volgorde en afhankelijkheden in uw servicebestand toe te passen.

Requires betekent niet After

systemd-dependencies en -condities zijn vier afzonderlijke mechanismen die in de meeste unit-bestanden worden gebruikt alsof het er één is. Requires= en Wants= bepalen welke andere units worden meegetrokken. After= en Before= bepalen de volgorde waarin units starten. ExecStartPre= voert een controle uit die de unit kan laten falen. De families Condition en Assert bepalen of de unit überhaupt wordt uitgevoerd. Elk mechanisme is onafhankelijk van de andere, waardoor een unit een andere unit kan vereisen en toch op exact hetzelfde moment kan starten.

Die laatste zin is de oorzaak achter bijna elk rapport met de strekking: "het werkt als ik het handmatig start, maar het faalt tijdens het opstarten".

[Unit]
Description=Inventory API
Requires=postgresql.service

[Service]
ExecStartPre=/usr/bin/pg_isready -h 127.0.0.1 -t 5
ExecStart=/usr/local/bin/inventory-api

Requires=postgresql.service trekt PostgreSQL in dezelfde starttransactie. Het wacht er niet op. systemd start beide taken parallel, waardoor pg_isready wordt uitgevoerd terwijl PostgreSQL nog bezig is met het openen van zijn datamap. Het proces stopt met exitcode 2 omdat er nog niets luistert, en de unit faalt voordat ExecStart ooit wordt bereikt. Het uitvoeren van sudo systemctl start inventory-api een uur later werkt wel, omdat PostgreSQL tegen die tijd al actief is. Er is niets in het unit-bestand veranderd, en daarom ziet het bestand er onschuldig uit.

De oplossing is één regel.

[Unit]
Requires=postgresql.service
After=postgresql.service

Een scherper detail schuilt op dezelfde plek. Een falende Requires=-dependency voorkomt alleen dat uw unit start wanneer u ook After= instelt. Zonder de volgorde heeft systemd uw unit al gestart tegen de tijd dat de andere unit faalt, waardoor er niets meer is om te annuleren. Requires= op zichzelf biedt niet de bescherming die men denkt te kopen. Noteer After= naast elke Requires= en elke Wants=, tenzij u een specifieke reden heeft om dit niet te doen.

Wat Requires, Wants, Requisite en BindsTo beloven

Dit zijn allemaal afhankelijkheidsinstellingen. Geen van deze instellingen bepaalt de volgorde.

  • Wants=: trekt de andere unit erbij. Als deze faalt of niet bestaat, start deze unit alsnog. Dit is wat systemctl enable aanmaakt als een symlink in een .wants/-directory.
  • Requires=: trekt de andere unit erbij. Als deze faalt en u heeft ook After= ingesteld, dan start deze unit niet. Als de andere unit later expliciet wordt gestopt, wordt deze unit daarmee ook gestopt.
  • Requisite=: trekt de andere unit er niet bij. Als deze nog niet actief is, faalt deze unit onmiddellijk.
  • BindsTo=: vergelijkbaar met Requires=, en deze unit stopt ook zodra de andere unit om welke reden dan ook stopt, inclusief hardware die verdwijnt.
  • PartOf=: stoppen en herstarten plant zich voort van de andere unit naar deze unit. Starten plant zich niet voort.
  • Conflicts=: het starten van deze unit stopt de andere unit.

Voor een daemon die communiceert met een andere daemon, is Wants= plus After= meestal de juiste combinatie. Requires= koppelt de levenscycli: stop de database voor onderhoud en uw applicatie gaat daarmee offline, en deze komt niet automatisch terug wanneer de database weer beschikbaar is. Wants= plus After= biedt de opstartvolgorde zonder die koppeling, en een herstartbeleid vangt het geval op waarin de afhankelijkheid later wegvalt.

U erft ook afhankelijkheden die u zelf niet heeft gedefinieerd. Met DefaultDependencies=yes, wat de standaardinstelling is, krijgt een normale service automatisch Requires=sysinit.target, After=sysinit.target basic.target en Conflicts=shutdown.target. Daarom start een service met een vrijwel lege [Unit]-sectie nog steeds laat tijdens het opstarten en wordt deze nog steeds netjes gestopt bij het afsluiten.

After en Before bepalen de volgorde van de transactie, niets anders

After= en Before= zijn puur voor de volgorde. Ze bevatten geen enkele vereiste. After=redis.service in een unit waar niets anders Redis in de transactie trekt, is een no-op: als redis.service geen onderdeel is van de transactie, is er niets om op te wachten, dus start uw unit onmiddellijk.

Dat is het waard om twee keer te zeggen, omdat het precies de vorm is van de network-online.target-fout verderop. De volgorde wacht alleen op units die al worden gestart in dezelfde transactie.

Het paar is symmetrisch. After=b.service geschreven in a.service betekent hetzelfde als Before=a.service geschreven in b.service, dus gebruik er een van en plaats deze in de unit die u beheert. De volgorde wordt automatisch omgekeerd bij het afsluiten, dus After=b.service betekent ook dat uw unit wordt gestopt voordat b.service dat wordt.

After= wacht op "gestart", en Type= bepaalt wat dat betekent

After= wacht totdat de andere unit is gestart. Wat "gestart" betekent, wordt volledig bepaald door de Type= van die unit.

  • Type=simple: zodra systemd het proces heeft geforkt. Het programma heeft zijn configuratie mogelijk nog niet ingelezen, laat staan een socket geopend.
  • Type=exec: zodra execve() is geslaagd. Iets sterker. Zegt nog steeds niets over gereedheid.
  • Type=forking: wanneer het oorspronkelijke ouderproces wordt afgesloten.
  • Type=oneshot: wanneer het proces wordt afgesloten. Hier betekent "gestart" echt dat het werk is voltooid.
  • Type=notify: wanneer de service READY=1 verstuurt via zijn notificatiesocket. Dit is het enige type dat daadwerkelijke gereedheid rapporteert.

Dus After= op een Type=simple daemon is een zwakke belofte, en dat is de tweede helft van de raceconditie in het eerste voorbeeld. Als de unit waarvan u afhankelijk bent wordt geleverd als Type=simple, betekent het ordenen na deze unit niet dat deze al verbindingen accepteert. Er zijn twee correcte oplossingen. Orden na de bijbehorende socket-unit, zodat de kernel inkomende verbindingen in de wachtrij plaatst terwijl de daemon nog opstart. Of laat uw eigen service opnieuw proberen te verbinden en laat het herstartbeleid dit afhandelen. Welk type een unit gebruikt is zichtbaar in systemctl cat, en de Type=-instelling en wat elke waarde aan systemd vertelt is het lezen waard voordat u vertrouwt op de volgorde.

ExecStartPre is een poortwachter die de unit kan laten falen

ExecStartPre= wordt uitgevoerd vóór ExecStart=. Als dit een exit-code ongelijk aan nul teruggeeft, wordt de activatie afgebroken en gaat de unit naar failed. ExecStart= wordt nooit uitgevoerd. Dit is de reden waarom een groot aantal units faalt zonder melding van het eigenlijke programma; het programma is simpelweg nooit gestart.

Feiten die vaak voor verwarring zorgen:

  • Het is geen shell. Pipes, redirects, globs en && werken niet. Het eerste token moet een absoluut pad zijn. Gebruik /bin/sh -c '...' om de regel te verpakken als u shell-syntaxis nodig heeft.
  • Een --prefix maakt een exit-code ongelijk aan nul niet-fataal: ExecStartPre=-/usr/bin/optional-check.
  • Elke ExecStartPre= moet zijn afgerond voordat de volgende start. Het kan geen langlopend proces starten.
  • Alle ExecStartPre=-regels delen TimeoutStartSec= met ExecStart=. Een pre-check die in een lus wacht op een database verbruikt de start-timeout, waarna de unit faalt met Result: timeout nadat start operation timed out. Terminating. in de journal verschijnt.

De foutmelding benoemt het controleproces, niet het hoofdproces:

inventory-api.service: Control process exited, code=exited, status=2/INVALIDARGUMENT
inventory-api.service: Failed with result 'exit-code'.

Lees deze symbolische naam zorgvuldig. systemd vertaalt kleine exit-codes via een vaste tabel, waardoor 2 altijd wordt weergegeven als INVALIDARGUMENT, ongeacht wat het programma ermee bedoelde. status=203/EXEC bevat de werkelijke informatie: systemd kon het binaire bestand helemaal niet uitvoeren, omdat het pad onjuist is of het bestand niet uitvoerbaar is.

Gebruik geen ExecStartPre= om mappen aan te maken. RuntimeDirectory=, StateDirectory=, LogsDirectory= en CacheDirectory= maken deze aan met de juiste eigenaar en rechten, en RuntimeDirectory= wordt opgeschoond wanneer de service stopt. Deze werken ook correct onder DynamicUser=, wat bij een handgeschreven mkdir niet het geval is.

Een conditie faalt geruisloos. Een assert faalt luidruchtig.

De families Condition en Assert voeren dezelfde tests uit. Ze verschillen enkel in wat er gebeurt wanneer een test faalt.

Een gefaalde Condition...= slaat de unit over. De starttaak wordt gerapporteerd als succesvol. De unit blijft inactive (dead), er wordt niets als gefaald gemarkeerd, er gaat geen waarschuwing af en het logboek registreert één regel:

Condition check resulted in Inventory API being skipped.

Op systemd 250 en nieuwer print systemctl status de reden direct:

     Active: inactive (dead)
  Condition: start condition unmet at Thu 2026-08-20 09:14:02 UTC; 2min ago

De ingesprongen regel daaronder benoemt de exacte richtlijn die faalde, bijvoorbeeld ConditionPathExists=/etc/inventory/api.conf was not met.

Een gefaalde Assert...= laat de unit falen. Het logboek vermeldt Assertion failed for Inventory API. en de unit eindigt in failed (Result: assert), wat luidruchtig genoeg is voor monitoring om op te merken.

Maak een keuze tussen beide door uzelf af te vragen wat een gefaalde test betekent. Condition betekent "deze unit is niet van toepassing op deze machine". Assert betekent "dit moet waar zijn, en als dat niet zo is, waarschuw dan iemand". De meeste units vereisen Condition. Kies pas voor Assert wanneer niets doen geruisloos erger is dan een gefaalde unit.

Er zijn twee valkuilen bij de Condition-familie.

Ten eerste: een gefaalde conditie laat de units die ervan afhankelijk zijn niet falen. Als a.service een Requires=b.service heeft en b.service wordt overgeslagen vanwege een conditie, dan telt de starttaak voor b.service nog steeds als voltooid. Hierdoor start a.service normaal in een omgeving waar b niet draait. Een conditie beschermt alleen de unit waarin deze is geschreven.

Ten tweede: condities worden elke keer geëvalueerd wanneer de unit start, op het moment dat de taak wordt uitgevoerd. Een unit die wordt getriggerd door een systemd timer op een VPS kan honderd keer achter elkaar worden overgeslagen zonder ooit als gefaald te worden aangemerkt. Dit is dezelfde klasse van geruisloze no-op als een cron job die draait maar niets doet, en u vindt dit op dezelfde manier: lees het logboek van de unit in plaats van te vertrouwen op de exit-status.

De condities die de moeite waard zijn om te kennen op een server:

  • ConditionPathExists=/etc/inventory/api.conf, en de ontkenning daarvan ConditionPathExists=!/etc/inventory/api.conf.
  • ConditionFileNotEmpty= en ConditionDirectoryNotEmpty=, voor een configuratiebestand of een datamap die door een pakket is aangemaakt maar leeg is gelaten.
  • ConditionVirtualization=, zodat een unit die een echte kernel-interface nodig heeft, ConditionVirtualization=!container kan bevatten. Controleer wat uw machine rapporteert met systemd-detect-virt.
  • ConditionHost= komt overeen met de hostnaam of de machine-ID; dit is hoe één gedeeld unit-bestand zich verschillend kan gedragen op twee servers.
  • ConditionKernelCommandLine= en ConditionKernelVersion=, voor units die gekoppeld zijn aan een boot-parameter of een minimale kernelversie.

Een lege toewijzing wist de lijst; dit is hoe een drop-in een conditie verwijdert die door een pakket werd meegeleverd:

[Unit]
ConditionPathExists=
ConditionPathExists=/srv/inventory/api.conf

Waarom network.target niet betekent dat het netwerk actief is

network.target is een synchronisatiepunt, geen status. Tijdens het opstarten betekent een volgorde na dit punt alleen dat de netwerkbeheersoftware is gestart. Het betekent niet dat een interface een adres heeft gekregen of dat er een route naar het internet bestaat. Het target bestaat voornamelijk voor de andere richting: een unit met de volgorde After=network.target wordt gestopt voordat het netwerk wordt afgesloten bij het uitschakelen van het systeem.

network-online.target is het target dat daadwerkelijk wacht. Dit wordt ondersteund door een wait-online-service die hoort bij de netwerkbeheerder die u gebruikt:

  • systemd-networkd-wait-online.service wanneer systemd-networkd de verbindingen beheert, wat de standaard is op een Ubuntu-server die via netplan is geconfigureerd.
  • NetworkManager-wait-online.service onder NetworkManager.

Oudere ifupdown-configuraties bereiken hetzelfde effect met networking.service. Welke u ook heeft, het correct gebruiken van het target vereist twee regels, niet één.

[Unit]
Wants=network-online.target
After=network-online.target

network-online.target maakt geen deel uit van de standaard opstarttransactie en wordt door niets automatisch geactiveerd. Als u alleen After= schrijft, ordent u ten opzichte van een unit die nooit in de wachtrij is geplaatst, waardoor de volgorde niets doet. Dat is de eerder beschreven no-op, in zijn meest kostbare vorm. De regel Wants= zorgt ervoor dat het target in de transactie wordt opgenomen, zodat de regel After= ergens op kan wachten.

Het tweede punt is dat "online" wordt gedefinieerd door de wait-online-implementatie, niet door systemd. systemd-networkd-wait-online keert terug zodra de verbindingen die het beheert een geconfigureerde status bereiken. Het controleert niet of DNS werkt en het controleert niet of een externe host bereikbaar is.

Deze definitie veroorzaakt een veelvoorkomende VPS-fout. Een machine met een tweede interface voor een privénetwerk, die wel in netplan is gedeclareerd maar nooit een adres heeft gekregen, zorgt ervoor dat de wait-service blijft wachten totdat deze opgeeft:

systemd-networkd-wait-online[612]: Timeout occurred while waiting for network connectivity.
systemd-networkd-wait-online.service: Failed with result 'exit-code'.

Het opstarten duurt twee minuten langer omdat de standaard time-out 120 seconden is. Er zijn twee oplossingen. Markeer de ongebruikte interface als optional: true in het netplan-bestand, zodat networkd stopt met wachten. Of voeg een drop-in toe aan de wait-service die de relevante verbinding specificeert met --interface=, of die --any doorgeeft om terug te keren zodra één verbinding actief is.

Beter nog, voorkom dat u het target nodig heeft. Veel services worden na network-online.target geplaatst omdat ze aan één specifiek adres binden en bij het opstarten falen met een melding zoals deze:

nginx: [emerg] bind() to 203.0.113.10:443 failed (99: Cannot assign requested address)

De kernel weigert de bind omdat dat adres nog niet actief is. Door net.ipv4.ip_nonlocal_bind=1 in te stellen, kan een proces een adres binden dat de machine nog niet bezit, en een restart-policy vangt de rest op. Het vertragen van het volledige opstartproces op basis van netwerkbeschikbaarheid is een zwaar middel voor een probleem dat meestal slechts één socket betreft.

Hoe u de werkelijke systemd-afhankelijkheden op een draaiend systeem uitleest

Ga nooit uit van het unit-bestand alleen. Drop-ins, .wants/ symlinks en impliciete standaardafhankelijkheden voegen allemaal verbindingen toe die niet in het bestand zichtbaar zijn.

systemctl cat inventory-api.service

Dit commando toont het unit-bestand en elke drop-in, in de volgorde waarin ze worden toegepast, met het bronpad boven elk blok. Voer dit als eerste uit. Een override van vijf regels in /etc/systemd/system/inventory-api.service.d/ overschrijft het pakketbestand en is anders onzichtbaar.

systemctl show inventory-api.service -p Requires -p Wants -p After -p Before -p ConditionResult -p AssertResult

Dit toont de opgeloste waarden, na verwerking van drop-ins en de impliciete afhankelijkheden die systemd toevoegt. ConditionResult=no is het directe antwoord op de vraag "de unit rapporteerde succes maar deed niets".

systemctl list-dependencies inventory-api.service
systemctl list-dependencies --reverse inventory-api.service
systemctl list-dependencies --after inventory-api.service
systemctl list-dependencies --before inventory-api.service

De standaardvorm doorloopt Requires= en Wants= in neerwaartse richting. --reverse toont welke units de uwe opstarten; zo vindt u het doel (target) dat de unit bij het opstarten activeert. --after en --before tonen de volgorde; dit is het paar om te lezen wanneer de vraag is of er daadwerkelijk op iets werd gewacht.

journalctl -b -u inventory-api.service --no-pager
journalctl -b -o short-precise -u inventory-api.service -u postgresql.service

Het tweede commando verweeft twee units met milliseconden-tijdstempels. Zo bewijst u een raceconditie in de opstartvolgorde in plaats van erover te gissen. De pg_isready-fout treedt op vóór de PostgreSQL-logs database system is ready to accept connections, en het tijdsverschil daartussen is direct zichtbaar in de uitvoer.

systemd-analyze verify /etc/systemd/system/inventory-api.service
systemd-analyze critical-chain inventory-api.service

verify laadt de unit zoals systemd dat zou doen en rapporteert onbekende richtlijnen, afhankelijkheden van niet-bestaande units, cyclische afhankelijkheden en syntaxis die niet kan worden geparseerd. Het wijzigt niets aan het systeem. critical-chain toont de volgordeketen die de unit vertraagde, inclusief de tijd waarop elke stap actief werd; dit werkt alleen voor een unit die tijdens de huidige boot is gestart.

Voer na het bewerken van een unit-bestand sudo systemctl daemon-reload uit. Gebruik sudo systemctl edit inventory-api.service om een unit uit een pakket te wijzigen; dit maakt automatisch een drop-in voor u aan. Het bewerken van het vendor-bestand onder /usr/lib/systemd/system/ werkt totdat de volgende pakket-upgrade dit overschrijft. Hetzelfde drop-in-mechanisme wordt gebruikt om geheugen- en CPU-limieten aan een service toe te voegen zonder een bestand te wijzigen dat eigendom is van het pakket.

Ordeningscycli en de sporen die zij achterlaten in het journal

Voeg ordening in beide richtingen toe en systemd doorbreekt de lus door een van de taken te verwijderen:

systemd[1]: Found ordering cycle on inventory-api.service/start
systemd[1]: Job postgresql.service/start deleted to break ordering cycle starting with inventory-api.service/start

systemd kiest zelf welke taak wordt verwijderd, en dit is niet altijd de taak die u zou kiezen. Het resultaat is een service die na sommige reboots ontbreekt en na andere weer aanwezig is; dit is uiterst lastig te debuggen vanaf de buitenkant. De meeste cycli ontstaan door units die DefaultDependencies=no instellen en zichzelf vervolgens alsnog ordenen ten opzichte van basic.target, of door het toevoegen van Before= aan een unit die al een After= had die naar u terugverwijst. systemd-analyze verify vindt deze cycli zonder dat een reboot nodig is.

De vaste unit

[Unit]
Description=Inventory API
Wants=postgresql.service network-online.target
After=postgresql.service network-online.target
ConditionPathExists=/etc/inventory/api.conf

[Service]
Type=notify
StateDirectory=inventory
ExecStart=/usr/local/bin/inventory-api
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

Elke regel voert één taak uit. Wants= haalt beide afhankelijkheden in de transactie zonder de levensduur van deze unit aan hen te koppelen. After= regelt het wachten, en moet beide namen herhalen omdat afhankelijkheid en volgorde afzonderlijke instellingen zijn. ConditionPathExists= betekent dat een machine die het pakket wel heeft maar de configuratie niet, de unit geruisloos overslaat in plaats van een waarschuwing te geven; dit is het juiste gedrag voor een configuratiegestuurde service. Type=notify betekent dat alles wat na deze unit is geplaatst, wacht op daadwerkelijke gereedheid in plaats van op een fork. Restart=on-failure dekt het scenario waarbij de database lang na het opstarten wegvalt, omdat de volgorde alleen van toepassing is op de eerste start. Hoe agressief die herstart moet zijn, wordt bepaald door de instellingen Restart= en RestartSec=.

Controleer de unit voordat u deze vertrouwt:

sudo systemctl daemon-reload
systemd-analyze verify /etc/systemd/system/inventory-api.service
systemctl list-dependencies --after inventory-api.service
sudo systemctl start inventory-api.service
systemctl show inventory-api.service -p ConditionResult -p ActiveState -p Result

Een gezonde unit leest ConditionResult=yes met ActiveState=active, en Result=success bevestigt dat er bij de laatste uitvoering niets is mislukt. ConditionResult=no in combinatie met ActiveState=inactive betekent dat de unit is overgeslagen, en de journal-regel die de conditie benoemt, geeft aan welke test is mislukt.

FAQ

Wacht Requires= tot de andere unit is gestart?

Nee. Requires= en After= zijn afzonderlijke instellingen. Requires= trekt de andere unit in dezelfde transactie, waarna systemd beide taken parallel start. Om te wachten, voegt u After= toe met de naam van dezelfde unit. Er is een tweede reden om dit toe te voegen: een Requires=-afhankelijkheid die faalt, voorkomt alleen dat uw unit start wanneer ook After= is ingesteld, omdat uw unit zonder volgorde al gestart is op het moment dat de andere faalt.

Moet ik na network.target of network-online.target ordenen?

Bij het opstarten betekent network.target alleen dat de netwerkbeheersoftware is gestart; het garandeert niets over adressen of routes. Gebruik network-online.target wanneer uw service bij de start een werkend adres nodig heeft, en schrijf zowel Wants=network-online.target als After=network-online.target, omdat het target niet in de standaard opstarttransactie zit en After= alleen wacht op een unit die niemand in de wachtrij heeft geplaatst. Als de service alleen faalt omdat deze aan één specifiek IP-adres bindt, is net.ipv4.ip_nonlocal_bind=1 met Restart=on-failure lichter dan het vertragen van het opstartproces.

Waarom rapporteert mijn unit succes maar draait deze nooit?

Een mislukte Condition...=-test slaat de unit over en rapporteert de starttaak als succesvol, waardoor er nooit iets als gefaald wordt gemarkeerd. Voer systemctl show <unit> -p ConditionResult uit en ConditionResult=no bevestigt dit. Lees vervolgens journalctl -b -u <unit> voor de regel Condition check resulted in <description> being skipped. Op systemd 250 en nieuwer benoemt systemctl status <unit> ook de exacte richtlijn waaraan niet werd voldaan.

Wat is het verschil tussen Condition en Assert?

Ze voeren identieke tests uit. Een mislukte Condition slaat de unit geruisloos over en de taak slaagt alsnog. Een mislukte Assert laat de unit falen, logt Assertion failed for <description>. en laat deze achter in failed (Result: assert). Gebruik Condition voor "deze unit is niet van toepassing op deze machine", wat bijna elk reëel geval dekt. Gebruik Assert alleen wanneer een ontbrekende randvoorwaarde zichtbaar moet zijn voor degene die mislukte units monitort.

Waarom faalt ExecStartPre met status=203/EXEC?

203/EXEC betekent dat systemd het commando in het geheel niet kon uitvoeren. De gebruikelijke oorzaken zijn een pad dat niet absoluut is, een binary die niet op die machine bestaat, een bestand zonder uitvoerrechten, of een script waarvan de #!-regel naar een ontbrekende interpreter wijst. De andere kleine codes van systemd komen uit een vaste tabel, dus status=2/INVALIDARGUMENT betekent simpelweg dat het commando met exitcode 2 is afgesloten en zegt niets over de argumenten. Onthoud dat ExecStartPre= niet via een shell wordt uitgevoerd, dus pipes en globs vereisen /bin/sh -c '...'.

#systemd#units#dependencies#ordering#troubleshooting