Windows Services Grundlagen

16. März 2016

Einen robusten Windows Service zu schreiben ist alles andere als trivial…

Es gibt immer mal wieder Themen, bei denen die offensichtliche Antwort “Windows Service” ist: Eine langlaufende Hintergrundverarbeitung die nicht in eine Web-Anwendung gepackt werden kann. Eine dauerhafte Überwachung von Kommunikationskanälen. Regelmäßige Datenbereinigungen.

Einen Windows Service mit .NET zu bauen ist simpel: Projekttemplate verwenden, fertig.
Einen Windows Service mit .NET zu bauen, der auf 24×7-Betrieb ausgelegt ist und der mit dem Operating zusammenspielt ist allerdings etwas ganz anderes.

Leider sorgt die vermeintliche Einfachheit des Projekttemplates dafür, dass zu viele Windows Services geschrieben werden, die im Betrieb unnötig Probleme machen. (Und oft genug solche, die mit einer einfachen Konsolenanwendung im Scheduler einfacher zu erschlagen wären.)

Wer sich mit dem Thema auseinandersetzt, der sollte zumindest mit den Grundlagen vertraut sein:

  • Service Control Manager (SCM): Dies ist der Windows-Dienst, der für die Verwaltung der Services zuständig ist.
  • Service API: Dokumentation der Betriebssystem-API. Auch für .NET-Entwickler wichtig, weil hier Antwortzeitvorgaben u.ä. beschrieben sind, die einzuhalten sind.
  • SC.EXE: Kommandozeilenanwendung um Services zu verwalten. Das meiste – aber nicht alles – geht auch über die Management Console.
  • ServiceBase class: Der Dreh- und Angelpunkt für Services in .NET

Grundanforderungen
Wer bislang nur Web- oder Windows-Anwendungen geschrieben hat, der muss bei Windows Services umdenken: Services laufen…

  • Im 24×7-Betrieb
  • In der Regel unbeaufsichtigt
  • In der Regel ohne UI
  • In einer volatilen Umgebung; d.h. Ressourcen können auch mal wegbrechen (z.B. wird der DB-Server rebootet oder das verwendete Netzwerklaufwerk steht vorrübergehend nicht zur Verfügung)
  • Mit dem Betrieb als wesentlichen Nutzer. Er muss den Service verwalten, d.h. konfigurieren, überwachen und ggf. Probleme diagnostizieren.

Daraus ergeben sich natürlich ganz andere Anforderung und Randbedingungen, als dies bei herkömmlichen Anwendungen der Fall ist.

Anforderungen an einen Service aus Sicht des Betriebes…
Services müssen vom Betrieb parametrisiert und kontrolliert werden. Außerdem ist die Überwachung des Service im laufenden Betrieb ein Thema, sowie das Erkennen und die Diagnose von Problemen. Im Einzelnen…

Parametrisierung und Steuerung des Service:

Verfügbare Wege um mit dem Service zu reden sind:

  • Konfigurationsdatei: Dies funktioniert wie in jeder anderen .NET-Anwendung.
  • Startparameter: In der Management Console können Startparameter für einen Service angegeben werden.
  • Custom Commands: Man kann numerische Kommandos an einen Service senden. Zwar ist das auf die Werte 128-255 beschränkt, aber das reicht als Trigger um im Service z.B. Diagnoseinformationen wegzuschreiben oder ein Lebenszeichen abzusetzen.

Kommunikation durch den Service:

Die typischen Wege auf denen sich der Service meldet sind:

  • EventLog: Stellt den korrekten Weg dar um Fehler aus dem Service zu melden.
  • PerfLog: Ist ideal um über Art und Umfang der durch den Service erbrachten Leistung zu informieren. Dies kann auch der Fehlerdiagnose dienen.

Insbesondere sind EventLog und PerfLog in die üblichen IT-Management-Systeme integriert und können so automatisiert überwacht werden. Natürlich kann man darüber hinaus eigene Kommunikationswege aufbauen (Logfiles, WCF Schnittstellen, etc.), aber dann muss man dem Betrieb auch die notwendigen Tools und Überwachungsmöglichkeiten an die Hand geben.

Überwachung:

Ein typisches Problem bei Services sind Services die nominell (d.h. in der Management Console so ausgewiesen) zwar laufen aber faktisch nichts mehr tun. Daher gilt: Der Betrieb will sehen, dass es dem Service gut geht. Anders ausgedrückt: Wenn ein Fehler auftritt sollte man dafür sorgen, dass der Service nicht in einem undefinierten Zustand verbleibt. Besser ist es, den Service z.B. durch eine unbehandelte Exception zu beenden.

Was auch Sinn macht ist ein spezieller Performance Counter, ein “Heartbeat”, der regelmäßig pingt. So bekommt man z.B. mit, ob der Service in einem Deadlock hängt, oder ob der Arbeitsthread sich stillschweigend beendet hat.

Weitere Probleme sind Services die sich unmittelbar nach dem Start oder irgendwann im laufenden Betrieb stillschweigend beenden. Da freut sich der Betrieb natürlich, wenn er keine Information hat, woran das liegt. Fehler abfangen und in das EventLog schreiben ist die offensichtliche Anforderung, die Fehlermeldung sollte aber auch genügend Informationen über die eigentliche Ursache beinhalten. Außerdem ist es sinnvoll, gleich beim Start des Services einmal auf alle(!) Ressourcen zugreifen. Nur so lassen sich z.B. Konfigurationsfehler zeitnah diagnostizieren und von später auftretenden Infrastrukturproblemen unterscheiden.

Robustheit:

Bedingt durch den unbeaufsichtigten 24×7-Betrieb muss ein Service fast zwangsläufig damit rechnen, dass Ressourcen mit denen er arbeitet vorrübergehend nicht verfügbar sind. Das betrifft im Grunde jede Verbindung zur Außenwelt, also DB-Server, Netzwerkshares, MQ-Connections, HTTP-Verbindungen, etc.. Ein Service sollte damit umgehen können, dass z.B. der DB-Server rebootet wird.

Folglich sind bei der Arbeit mit Ressourcen im Fehlerfall mehrere Versuche (sprich Retries) angesagt. Dabei sollte ein Retry schnell erfolgen, weitere sollten aber nicht im Millisekundentakt stattfinden – so schnell geht ein Reboot  dann doch nicht – sondern eher im Sekunden- oder gar Minutenabstand. Außerdem ist auch hier ein EventLog-Eintrag sinnvoll (Einer! Und nicht gleich beim ersten Retry!). Auch über Performance Counter für Fehler kann man nachdenken; so dokumentiert der Service, dass er das Problem erkannt hat und damit umgehen kann.

Nebenbei muss der Service damit rechnen, dass der Betrieb erst den DB-Server durchbootet und dann den Rechner mit dem Service. Der Service muss also damit umgehen können, dass er beendet wird, während er noch Daten hat, die er eigentlich in der DB speichern wollte.

Soweit sollte das alles Standard sein. Sollte. Bei besonderen Anforderungen kann man aber noch weiter gehen.

Ein besonders perfides Problem sind Memory-Leaks. Ja, auch in .NET mit der Garbage Collection gibt es die noch! Und für einen Service der prinzipiell beliebig lange – Tage, Monate, Jahre(!) – laufen kann, können auch kleinste Leaks ein Problem werden. Dummerweise helfen hier keine Prozess-Monitore bei der Diagnose und Performance Counter nur eingeschränkt, weil die Speicherverwaltung prinzipbedingt den verfügbaren Speicher ausnutzt. Daher macht es Sinn regelmäßig die Garbage Collection aufzurufen und den dann akuten Speicherbedarf per Performance Counter zu melden. Damit ist zumindest eine einfache Überwachung möglich. Will man in Richtung Selbstheilung gehen, dann kann man über eine eigene AppDomain nachdenken, ähnlich wie der IIS das macht; allerdings steht der Aufwand dafür nur selten in vernünftigem Verhältnis zum Nutzen. Die einfache Lösung ist ein Job im Scheduler, der den Service regelmäßig restartet.

Für besonders kritische Fälle kann man auch einen weiteren Service – einen Watchdog – bauen, der den ersten Service überwacht und damit auch Diagnose und Reaktion automatisiert.

Implementierung
Nach dem ich nun die Messlatte für die Umsetzung eines Service deutlich höher gelegt habe, noch ein paar Tips und Hinweise aus der Praxis…

ServiceBase…

.NET stellt die Basisklasse ServiceBase zur Verfügung, die die Benachrichtigungen des SCM (Start, Stop, Pause, etc.) auf entsprechende Methoden abbildet.

Muss man eine langlaufende Verarbeitung umsetzen, dann ist das offensichtliche Vorgehen, die eigentliche Arbeit in einen separaten Thread zu verlegen. Startet man eine Engine (z.B. für das Hosting eines WCF-Service oder der WorkFlow-Engine) – d.h. dass Start und Stop eigentlich ausreichend wären – dann kann es trotzdem eine gute Idee sein, über einen separaten Thread nachzudenken. Der Start oder das herunterfahren einer Engine kann durchaus einmal länger dauern (etwa wenn dazu Netzwerk oder Datenbank-Zugriffe erforderlich sind) und es liegt in der Verantwortung des Entwicklers, die Zeitvorgaben einhalten, die der SCM für diese Benachrichtigungen vorgibt.

Konsequenz aus einem separaten Thread ist aber, dass alle Benachrichtigungen an ihn übergeben werden müssen und die eigentliche Verarbeitung asynchron stattfindet, was entsprechende Datenstrukturen und Threadsynchronisation voraussetzt. Außerdem kommt dazu, dass die im Fehlerfall von ServiceBase geschriebenen EventLog-Einträge nicht mehr aussagekräftig sind; diesen Informationsverlust muss man selbst ausgleichen.

Zudem muss das Fehlerhandling im Thread sauber umgesetzt sein. Arbeitet man mit der Klasse Thread, dann führt eine unbehandelte Exception zum Abbruch des Prozesses, was eine gute Sache ist! Nutzt man einen Threadpool-Thread oder beendet sich der Thread ohne Exception, dann sieht die Sache anders aus und der Service läuft stillschweigend weiter.

PerfCounter und Eventlog…

Zur Nutzung von Performance Countern müssen diese registriert sein. Dafür benötigt man i.d.R. Admin-Rechte, das sollte also nicht im Service passieren, sondern im Installer. Eine Installer-Komponente für Services gibt es von .NET; sie kümmert sich um den Service selbst, sowie um eine EventSource mit dem Namen des Service. Um die Performance Counter muss man sich als selbst kümmern; wer für seine EventLog-Einträge lieber den DisplayName verwenden will, der muss die EventSource ebenfalls selbst registrieren.

Kleine Fallen bei EventSources: Läuft der Service unter dem Account des Entwicklers bzw. mit Admin-Rechten, dann wird die Source bei Bedarf still und leise automatisch angelegt (entgegen der Doku!). Dazu kommt, dass EventSources normalerweise nicht gelöscht werden. Beides führt in Kombination leider dazu, dass Probleme mit fehlender Registrierung erst relativ spät – schlimmstenfalls in Produktion – erstmalig auftreten. Dummerweise meldet sich das Problem dann auch noch missverständlich als Security-Exception, weil bei der Suche nach der Source auf bestimmte Logs nicht zugegriffen werden kann – was natürlich bei der Diagnose kein Stück weiter hilft.

Das Projekt-Template…

Das Projekttemplate baut eine service.exe, die eine ServiceBase-Instanz als Service startet. Prinzipiell ist ServiceBase aber auch in der Lage, mehrere Instanzen als unabhängige Services zu starten, die getrennt über den SCM gestartet werden können. DON’T!  Von der unnötigen Komplexität mal abgesehen, ist es eben nicht so, dass diese Services vollständig unabhängig voneinander wären: Sie teilen sich die service.exe (wichtig bei Updates), damit auch die Konfigurationsdatei, außerdem den übergeordneten Installer. Man fährt besser, wenn man jedem Service seine eigene serviceXY.exe spendiert.

Die Service-EXE…

Wo wir gerade dabei sind: Wenn ich schon eine service.exe bekomme, kann ich sie auch nutzen. Wird die vom Template generierte service.exe über den SCM als Service gestartet läuft sie in Main() und übergibt dort die Kontrolle an ServiceBase.Run(…). Startet man sie auf der Kommandozeile läuft das gleiche ab und man bekommt eine Fehlermeldung. Das ist aber nicht zwingend. Man kann (über Environment.UserInteractive) prüfen, ob die Anwendung als Service oder interaktiv von der Kommandozeile gestartet wurde:

   1: static void Main()

   2: {

   3:     if (Environment.UserInteractive)

   4:     {

   5:         Console.WriteLine("SYNTAX: ....");

   6:         // [...]

   7:     }

   8:     else

   9:     {

  10:         ServiceBase[] ServicesToRun;

  11:         ServicesToRun = new ServiceBase[] { new Service1() };

  12:         ServiceBase.Run(ServicesToRun);

  13:     }

  14: }

Damit kann man dann z.B. Hilfetexte ausgeben oder Funktionen zur Fernsteuerung des laufenden Service umsetzen. Oder man kann einen Schritt weitergehen, die Service-Klasse anlegen und OnStart/OnStop selbst aufrufen – das macht Test und Debugging erheblich einfacher, insbesondere wenn es um den Startup- und Shutdown-Code geht.

Fazit:
Wie eingangs gesagt: Einen Windows Service mit .NET zu bauen, der auf 24×7-Betrieb ausgelegt ist und der mit dem Operating zusammenspielt geht etwas über das einfache Template hinaus. Wenn man den hier vorgeschlagenen Empfehlungen folgt, wird der Service deutlich robuster und für den Betrieb einfacher zu überwachen.

Was man dann noch tun muss ist testen. Einfach mal den DB-Server abklemmen und schauen, wie sich der Service verhält…