Skalierbare Anwendungsarchitekturen
Die Skalierbarkeit eines Systems und ihre Berücksichtigung bei der Wahl einer Software-Architektur ist eine Kernforderung bei der Erstellung tragfähiger Software-Systeme. Doch welche Möglichkeiten bestehen, ein Software-System skalierbar zu machen und was ist dabei zu beachten? Dieser Artikel beginnt mit einer Einführung in die Welt der Nicht-Funktionalen-Anforderungen und ihrer systematischen Erfassung, behandelt dann die verschiedenen Arten von Skalierbarkeit und beschreibt daraufhin deren Umsetzung in einem von dem Autoren begleiteten Großprojekt. Dort wird für jeden Software-Layer einzeln untersucht, welche Skalierungsmaßnahmen ergriffen werden können. Schließlich endet der Artikel mit der Zusammenfassung der wichtigsten Thesen in einem kurzen Fazit.
Einführung in die Welt der NFAs
Softwareprojekte haben meist drei Arten von Anforderungen: Organisatorische, funktionale und nichtfunktionale. Einen Überblick dazu gibt Abb.1.
Organisatorische Anforderungen beinhalten die organisatorischen Vorgaben und politischen Leitplanken, die das Projekt einhalten muss: Beispiele dafür sind Vorgaben wie das zu verwendende Vorgehensmodell, das Software-Framework, die Kodierrichtlinien, die Hardware und die Entwicklungswerkzeuge.
Die funktionalen Anforderungen entsprechen den User-Stories und Anwendungsfällen, die die neue Software unterstützen muss. Die Nichtfunktionalen Anforderungen, kurz NFAs, sind die dritte Art von Anforderungen und bilden die Basis zur Erstellung tragfähige Software-Systeme. Unter tragfähige Software-Systeme verstehen wir eine Software mit qualitativ hochwertiger Architektur, die in der Lage ist, funktionale, nichtfunktionale und organisatorische Änderungen umzusetzen, ohne zu verwässern.
Die NFAs leiten sich von den Zielen ab, die mit einem Software-System erreicht werden sollen. Sie beschreiben diejenigen Anforderungen, unter denen funktionale Anforderungen zu erbringen sind. Neben der Skalierbarkeit gibt es noch eine große Anzahl weiterer NFAs wie z. B.
- Performanz, das Antwortzeitverhalten des Systems gegenüber dem Anwender
- Anpassbarkeit/Wartbarkeit der Software gegenüber neuen Anforderungen in Form von Requests for Change (RfCs)
- Verfügbarkeit, also die zeitliche Verfügbarkeit der Software für den Anwender
- Ergonomie, sprich: die intuitive Bedienbarkeit der Software und auch die Barrierefreiheit
Installierbarkeit, nämlich der Aufwand, den die Betriebsmannschaft benötigt, um neue Softwareversionen in die Produktion zu bringen
- Verständlichkeit – wie nachvollziehbar ist der Programmiercode für einen anderen Entwickler
Zu beachten ist bei den NFAs weiterhin, dass sie nicht separat voneinander betrachtet werden sollten, da sie sich oftmals gegenseitig beeinflussen wie z. B. bei den NFAs Verständlichkeit und Performanz. Auf der einen Seite ist optimierter Quellcode sehr performant, auf der anderen Seite aber oftmals nur schwer nachvollziehbar.
Die NFAs machen also Aussagen über die Qualität eines Software-Systems. Daher ist es notwendig, möglichst früh in einem Projekt mit der systematischen Erfassung der NFAs zu beginnen. Aus unserer Erfahrung heraus haben wir ein einfaches Tabellen-Schema zur Erfassung von NFAs entwickelt, das sich ein wenig am Schema der Entwurfsmuster (Design Patterns) orientiert. Das Schema besteht aus den folgenden sechs Bestandteilen:
- Eindeutiger Identifier
- Kurze, prägnante Beschreibung der NFA
- Rang (Priorität)
- Darstellung der Herausforderung
- Beschreibung, wo die Anforderung herkommt
- Messbare Kriterien, um zu überprüfen, ob die Anforderung erfüllt wird
Auf diese Weise können die für das Projekt wichtigen NFAs systematisch im Lastenheft erfasst werden. Da sich NFAs aber oftmals auch gegenseitig beeinflussen, ist es notwendig, sie mit Hilfe eines Rangs zu priorisieren. Ein Rang ist eine ganze Zahl, die angibt, wie wichtig die NFA für das Projekt ist. Für jeden Rang gibt es genau eine Zahl, wobei eins die wichtigste NFA und n die unbedeutendste NFA ist. Durch die Erfassung eines Rangs wird verhindert, dass es mehrere gleich wichtige NFAs geben kann. NFAs stellen also die Qualitätseigenschaften eines Software-Systems dar und bilden die Grundlage für die Analyse, die Evaluierung, die Konzeption, das Design, die Implementierung und den Test von tragfähigen Architekturen. Doch wenden wir uns jetzt der Skalierbarkeit zu. Was bedeutet eigentlich Skalierbarkeit genau? Und gibt es nur eine Art von Skalierbarkeit?
Strategien und Arten von Skalierbarkeit
Skalierbarkeit scheint zuerst einmal ein weiches Thema und nicht so recht greifbar zu sein. Wann ist ein Software-System eigentlich skalierbar und wann nicht? Zuerst einmal müssen wir uns um eine Definition für Skalierbarkeit bemühen. Wir definieren Skalierbarkeit folgendermaßen: Skalierbarkeit ist die Eigenschaft eines Software-Systems, mit einem größeren Lastaufkommen fertig zu werden, indem zusätzlich bereitgestellte Ressourcen genutzt werden. Man spricht dann davon, dass das System das größere Lastaufkommen kompensieren kann.
Die Leistungssteigerung, die man durch die zusätzlichen Ressourcen erhält, sollte sich dabei möglichst linear verhalten, ansonsten gibt es deutliche Hinweise darauf, dass das System eventuell doch nicht geeignet skaliert.
Scale-up und Scale-out
Wie erreicht man nun aber eine solche zusätzliche Bereitstellung von Ressourcen? Dies kann auf zwei Arten geschehen: lokal oder global. Bei der Anwendung der beiden Strategien unterscheidet man die beiden grundsätzlichen Arten von Skalierung: Die vertikale Skalierung und die horizontale Skalierung.
Bei der vertikalen Skalierung, die international gerne als "Scale-up" bezeichnet wird, werden die Ressourcen lokal zu einem Rechnerknoten hinzugefügt. Dabei wird der bestehende Rechner einfach mit leistungsfähigerer Hardware, wie z. B. CPUs, Hauptspeicher, Festplatten oder Netzwerkschnittstellen, erweitert. Diese Strategie der vertikalen Skalierung ist einfach zu verstehen und hat in der Regel keinen Einfluss auf die Software-Architektur. Doch erkauft man sich den Zuwachs an Möglichkeiten oftmals durch teure Spezialhardware und man kommt schnell an die physischen Grenzen des Rechners.
Bei der horizontalen Skalierung, die international gerne als "Scale-out" bezeichnet wird, werden die Ressourcen dem Netzwerk hinzugefügt. Dazu werden einfach weitere Rechner zu einem bestehenden Rechnernetz hinzugefügt und die Aufgaben werden auf mehrere Rechner verteilt. Um die horizontale Skalierungsstrategie nutzen zu können braucht man einen Verbund aus mehreren Rechnern. Außerdem muss sich das Problem, das man mit der horizontalen Skalierung lösen möchte, auch verteilen lassen – sonst sind den Skalierungsmaßnahmen schnell Grenzen gesetzt und man ist ggf. gezwungen, das ganze Rechnernetz einschließlich Datenbank zu duplizieren. Dafür hat man aber in der Regel bei einer horizontalen Skalierung eine preisgünstige Möglichkeit, einem System neue Ressourcen hinzuzufügen, da man nach Aufbau des verteilten Netzes auf die Anschaffung teurer Spezialhardware verzichten kann.
Es ist nun wichtig zu erkennen, dass sich die beiden Skalierungsstrategien der horizontalen und der vertikalen Skalierung nicht gegenseitig ausschließen, vielmehr ist es oftmals möglich, beide Strategien sinnvoll zu kombinieren.
Nachdem wir uns mit den beiden Strategien zur Durchführung von Skalierungsmaßnahmen beschäftigt haben, kommen wir nun zu den sechs verschiedenen Arten von Skalierbarkeit:
- Lastskalierbarkeit
Bei der Lastskalierbarkeit geht es darum, wie das System mit einer Erhöhung der aktiven Nutzerzahl umgehen kann. Dazu teilt man die Anwender eines Systems gerne in Power-User und Gelegenheitsnutzer ein. Power-User sind Vielnutzer, die das System über einen Großteil der Arbeitszeit hinweg nutzen, während Gelegenheitsnutzer das System erfahrungsgemäß nur in den zwei bis fünf Stunden der höchsten Beanspruchung aktiv nutzen. Um nun mit einer erhöhten Nutzerzahl umgehen zu können, werden Ressourcen lastabhängig hinzugefügt oder eingeschränkt. Die Verschiebung von Prozessen und Daten auf Ressourcen erfolgt für den Benutzer und die Anwendung verdeckt und unbemerkt. Welche Prozesse und welche Daten wohin verschoben werden, entscheidet das System automatisch. - Räumliche Skalierbarkeit
Die räumliche Skalierbarkeit gibt an, inwiefern das System mit dem erhöhten Speicherbedarf umgeht, wenn die Anzahl der Datenobjekte ansteigt. - Zeitlich-räumliche Skalierbarkeit
Die zeitlich-räumlichen Skalierbarkeit dagegen zeigt an, wie das System mit der erhöhten Antwortzeit umgeht, wenn die Anzahl der Datenobjekte ansteigt. - Strukturelle Skalierbarkeit
Eine gute strukturelle Skalierbarkeit sagt aus, dass sich die Software-Architektur des Systems nicht hinderlich auf die Erhöhung der Anzahl der Datenobjekte auswirkt. - Geographische Skalierbarkeit
Bei der geographischen Skalierbarkeit geht es darum, das die geographische Entfernung des Systems von Benutzer und Ressourcen keinen oder nur wenig Einfluss auf die Leistungsfähigkeit des Systems hat. - Administrative Skalierbarkeit.
Bei der administrativen Skalierbarkeit geht es darum, dass die ggf. variierende Anzahl der Organisationen, die sich ein System teilen, nicht beschränkt ist. Anhand dieser Art der Skalierbarkeit soll das Managen, Überwachen und der Gebrauch eines Software-Systems einfach zu bewerkstelligen und von überall auf der Welt aus möglich sein.
Nachdem wir die verschiedenen Arten von Skalierung kennengelernt haben, werfen wir nun einen Blick auf die Maßnahmen zur Verbesserung der Skalierung.
Maßnahmen zur Verbesserung der Skalierbarkeit
Um die verschiedenen Maßnahmen zur Verbesserung der Skalierbarkeit besser einordnen zu können, kann man die sog. "Skalierungspyramide" verwenden (s. Abb.2). Die Skalierungspyramide ist eine Darstellung mit vier Ebenen. An der Spitze steht die Ebene 4 mit der Hardware-Optimierung, darunter folgt in Ebene 3 die System- und Produkt-Optimierung. Darunter folgt in Ebene 2 die Code-Optimierung und als unterste Ebene 1 schließlich der Entwurf. Die Auswirkungen auf die Skalierbarkeit nehmen von oben nach unten zu. Dies bedeutet, dass man mit Hardware-Optimierungen (also z. B. der Strategie der vertikalen Skalierung) die geringsten Auswirkungen auf die Skalierbarkeit eines Systems hat und im Entwurf die größten Auswirkungen auf die Skalierbarkeit erreichen kann.
Neben der Skalierungspyramide zur Einordnung der Skalierungsmaßnahmen gibt es noch eine Reihe weiterer Best-Practices – Tipps & Tricks die auch im folgenden Projektbeispiel immer wieder eine Rolle spielen:
- Nutzen Sie die vertikale und die horizontale Skalierungsstrategie
- Nutzen Sie asynchrone Kommunikation vor synchroner Kommunikation
- Verwenden Sie Clustersysteme
- Halten Sie Ressourcenkonflikte möglichst klein
- Machen Sie Komponenten möglichst zustandslos
- Trennen Sie transaktionale Aufrufe von nicht-transaktionalen Aufrufen
Wie schon erläutert, gibt es gute Gründe für die Verwendung beider Skalierungsstrategien: Also nutzen Sie möglichst auch die vertikale und die horizontale Skalierungsstrategie in einer Softwareschicht.
Die meisten heutigen Frameworks arbeiten mit synchronen Aufrufen. Synchrone Aufrufe haben die Eigenschaft, nach einem Aufruf zuerst auf eine Antwort zu warten bis die Verarbeitung weiter geht. Asynchrone Aufrufe haben dagegen den Vorteil, dass man nicht auf die Antwortnachricht warten muss. Daher kann die Verwendung eines Frameworks mit asynchroner Kommunikation bei hohem Kommunikationsaufkommen die Verarbeitung deutlich beschleunigen.
Heutige Datenbanken haben ausgereifte Clustersysteme. In Clustern sind die Verteilungsalgorithmen und der automatische Neustart bei Absturz einzelner Rechnerknoten schon fest mit integriert. Es gibt also keinen Grund mehr, eigene Lösungen für Clustersysteme zu basteln oder auf Cluster zu verzichten.
Das Minimieren von Ressourcenkonflikten ist eine alte Weisheit – es lohnt sich aber oft, einmal wirklich darauf zu achten, dass man im Code Ressourcen nur so lange blockiert wie man sie wirklich benötigt. Wird nicht auf das Minimieren von Ressourcenkonflikten geachtet, dann hat das System bei vielen Zugriffen auf die knappen Ressourcen wahrscheinlich schnell Probleme zu skalieren.
Eine weitere Strategie zur Verbesserung der Skalierbarkeit ist es, Komponenten möglichst zustandslos ("stateless") zu machen, um sie besser verwenden zu können. Schließlich geben wir noch den Rat, transaktionale Aufrufe von nicht-transaktionalen Aufrufen zu trennen. Die Trennung führt unter anderem dazu, dass für die beiden Aufrufarten auch zwei verschieden teure Sperrkonzepte genutzt werden können – doch dazu im nächsten Kapitel mehr. In diesem Kapitel werden die Skalierungsmaßnahmen anhand des Projekts "Scale-Me-Up" erläutert.
Skalierbarkeit im Großen: Ein Anwendungsbericht
Das Projekt "Scale-Me-Up" umfasst eine Webanwendung, die im Intranet einer deutschen Organisation betrieben wird, um die Mitarbeiter bei der Bearbeitung ihrer Geschäftsprozesse zu unterstützen. Bei der Webanwendung handelt es sich um einen Thin-Client mit einer Drei-Schichten-Architektur mit rund 70.000 Clients, davon sind 20.000 Power-User und 50.000 Gelegenheitsanwender. Die Anwendung verwaltet rund 10 Millionen Datenobjekte. Mehrmals im Monat laufen unterschiedliche Batches, um die generierten Daten auf externe Systeme zu verteilen.
Zu Beginn des Projekts ist bereits bekannt, dass das zu entwickelnde Software-System mit großen Schwankungen des Lastaufkommens zurechtkommen muss. Können diese Lastspitzen nicht kompensiert werden, so würde dies zum Stillstand eines der Kernsysteme des Unternehmens führen. Weitere NFAs sind eine durchschnittliche Antwortzeit von maximal fünf Sekunden, die Transaktionssicherheit aller Geschäftsprozesse, die Ermöglichung paralleler Lesezugriffe und die Verhinderung konkurrierender Verarbeitung von Datenobjekten.
Werfen wir einen Blick auf die Architektur des Systems, so sehen wir, dass durch die Vorgabe eines Thin-Clients und einer Drei-Schichtenarchitektur die Anwendungskern-Schicht und die Datenbankschicht als mögliche Bestandteile für Maßnahmen zur Verbesserung der Skalierbarkeit des Systems übrig bleiben. Die vertikale und die horizontale Skalierungsstrategie werden dort auf den Anwendungskern und die Datenbank ausgerichtet.
Der Anwendungskern
Der Anwendungskern übernimmt die zentrale Verarbeitung und versorgt die Oberfläche mit Informationen bzw. liest oder speichert Informationen in der Datenbank. Der Anwendungskern bietet somit die Ablaufumgebung für die fachlichen Anwendungskomponenten zur Umsetzung der Geschäftsprozesse und der Geschäftslogik.
Um dies optimal gewährleisten zu können und um Ansatzpunkte für die horizontale Skalierungsstrategie zu liefern, ist der Anwendungskern selbst in mehrere Schichten aufgeteilt. Diese Situation ist in Abb.3 dargestellt:
- Schicht 1: Zugangsschicht
- Schicht 2: Geschäftslogikschicht
- Schicht 3: Datenzugriffsschicht
Die Strategie der vertikalen Skalierung setzt nun im Anwendungskern durch Einsatz weiterer Hardware wie CPUs, Hauptspeichererweiterungen, zusätzlicher und performanterer Festplatten und Netzwerkschnittstellen weitere Ressourcen ein, die nur einen geringen Einfluss auf die Architektur des Systems haben. Um diese zusätzlichen Ressourcen effektiv nutzen zu können, sollte das System Multithreading unterstützen. Auf der anderen Seite können diese Maßnahmen aber nur zu einer begrenzten Leistungssteigerung beitragen, da die Hardware technologischen und physischen Grenzen unterliegt. Nicht zu unterschätzen ist aber auch die Tatsache, dass Hardware aktuell im Betrieb oft nur für wenige Jahre betrieben wird, dann aus der Wartung läuft und durch aktuellere Hardware ersetzt wird. Durch diese vertikalen Skalierungsmaßnahmen bekommt man innerhalb weniger Jahre quasi automatisch neue Ressourcen zur Verfügung gestellt.
Um auf den Anwendungskern die Strategie der vertikalen Skalierung optimal anwenden zu können, hat es sich bewährt, einige Voraussetzungen zu schaffen:
- Der Anwendungskern ist zustandslos
Um den Anwendungskern zustandslos zu halten werden die Sitzungsdaten am Client gehalten und bei Bedarf an den Anwendungsserver übergeben. - Ein Sperrkonzept stellt das konkurrenzlose Schreiben sicher
Ein Sperrkonzept stellt sicher, dass es beim Schreiben zu keinen Nebenläufigkeiten kommt. Zwei Benutzer dürfen also nicht gleichzeitig schreibend an einem Geschäftsobjekt arbeiten. Da das strikte Sperren eine teure Operation darstellt, muss festgelegt werden, welche Schreibzugriffe isoliert und bei welchen es ausreicht ein ressourcenschonenderes relatives Sperren zu verwenden. Dabei wird uns unsere Aufteilung nach transaktionalen und nicht-transaktionalen Zugriffen zu Gute kommen. - Die EJB 3.0-Spezifikation wird strikt eingehalten
Das Einhalten der EJB 3.0-Spezifikation ist wichtig, um möglichst viele der in den EJB-Standard eingebauten Möglichkeiten zur Skalierung zu nutzen und möglichst wenige Funktionalitäten neu erfinden zu wollen.
Um eine gute horizontale Skalierbarkeit des Anwenderkerns zu erreichen, verlagern wir die skalierbaren Teile des Systems und die hierdurch realisierten Aufgaben in den Anwendungskern. Dadurch beinhaltet der Anwendungskern folgende Teile des Systems:
- Die Geschäftslogik und die Umsetzung der Geschäftsprozesse
- Die Verteilung der Geschäftsdaten
- Den Zugriff auf zentrale Ressourcen
- Den Aufruf externer Dienste und die Bereitstellung von Diensten
- Die Erstellung von zentralen Reports und Auswertungen.
Um nun eine Verteilung der Last auf mehrere Rechnerknoten zu erreichen, wird ein Server-Cluster aufgebaut. Auf den verschiedenen Servern des Clusters werden gleiche Teile des Anwendungskerns verteilt. Dies wird in Abb.4 dargestellt. Um die Anfragen der Anwender nun auf die einzelnen Server im Cluster verteilen zu können, wird ein Load-Balancer verwendet, der sich um die Verteilung der Aufgaben und ggf. um den Neustart der einzelnen Server kümmert. Als Verteilungsalgorithmus hat sich nach vielen Versuchen das Round-Robin-Verfahren (Zeitscheibenverfahren) bewährt. Erstaunlicherweise reicht dieses einfache und robuste Verfahren für die Verteilung der Last auf die Clients aus, da sich die Last mit zunehmender Anzahl an Clients automatisch stabilisiert.
Steigen die Antwortzeiten trotz der vertikalen Skalierungsmaßnamen mit zunehmender Zeit an, weil z. B. eine erneute Berechnung von Daten ab einem bestimmten Datum in der Vergangenheit durchgeführt wird oder weil immer mehr Bilder geladen werden, dann sollte man eine Maßnahme zur zeitlich-räumlichen Skalierung des Anwendungskerns durchführen: den Einsatz von Caches im Anwendungskern. Dabei ist die Erstellung und die Entfernung von Caches abhängig von der Anwendungslogik.
Die Datenbank
Bei der Datenbank kommt ein Oracle 11g RAC (Real Application Cluster) zum Einsatz. Die Datenbank wird als Aktiv/Aktiv-Cluster auf zwei x86 Enterprise-Servern betrieben. Der Oracle 11g RAC hat die Möglichkeit, eine automatische Lastenverteilung auf Rechnerknoten in der Datenbank durchzuführen und ausgefallene Komponenten neu zu starten bzw. neue Komponenten ohne Neustart in den Cluster zu integrieren.
Die Strategie der vertikalen Skalierung setzt auch in der Datenbank auf den Ausbau der Leistungsfähigkeit der einzelnen Knoten im Cluster durch zusätzliche Hardware in Form von zusätzlichen CPUs, Hauptspeichererweiterungen und zusätzliche und performantere Festplatten. Diese Komponenten können oftmals ohne Neustart des Clusters in das Datenbanksystem hinein konfiguriert werden.
Bei der horizontalen Skalierung der Datenbank erhöht man die Anzahl der Knoten im Cluster. Diese können oft durch das Einfügen von zusätzlichen Rechnerboards zur Laufzeit in das Datenbanksystem hinein konfiguriert werden. Unsere Erfahrungen haben aber auch gezeigt, dass man in einem Datenbank-Cluster schnell an die Grenzen des Systems kommt: Ein Ausbau auf mehr als vier Knoten in einem Cluster ist nicht sinnvoll, da die zu erwartende Mehrleistung pro Knoten auf unter 50 Prozent sinkt. Ein gangbarer Weg zur Nutzung der horizontalen Skalierungsstrategie ist die Verteilung der Lasten auf mehrere Knoten im Cluster, die auf dieselbe Datenbank zugreifen.
Um sicherzustellen, dass nicht mehrere Benutzer gleichzeitig identische Daten des Datenbestandes verändern können, wird ein Sperrkonzept benötigt. Es gibt die einfache und kostengünstige Optimistische Sperrstrategie und die komplexe und aufwändigere Pessimistische Strategie.
Die Optimistische Sperrstrategie ist als Optimistisches Locking oder als OCC (Optimistic Concurrency) bekannt und wird für technische Transaktionen und für Entitäten, die nicht zu einem Geschäftsobjekt gehören, verwendet. Sie lässt Nebenläufigkeiten zu. Am Ende einer Transaktion wird aber geprüft, ob Konflikte aufgetreten sind. Hat es parallele Zugriffe auf ein Datum gegeben, so wird dieser Konflikt durch ein Rollback von Operationen behoben. Die Pessimistische Sperrstrategie ist als Pessimistic Locking bekannt und ist verglichen mit der Optimistischen Sperrstrategie teurer, da hier Nebenläufigkeit durch ein explihzites Sperren und Freigeben des Datenobjekts verhindert wird. Daher wird dieses Sperrkonzept nur für Entitäten, die zu einem Geschäftsobjekt gehören, eingesetzt. Eine Sperre kann hier nur durch den Bearbeiter, einen Administrator oder einen entsperrenden Batch-Job entfernt werden.
Fazit
Zusammenfassend kann also festgehalten werden, dass die Skalierung eine wichtige Nichtfunktionale Anforderung ist, an deren Umsetzung man von der ersten Planung bis zur Einführung eines neuen Software-Systems arbeiten muss. Dazu sind für jede Komponente des Systems sowohl für die vertikale, als auch für die horizontale Skalierungsstrategie, die Einflüsse auf die Leistungssteigerung zu prüfen und die Anforderungen an die Architektur zu beschreiben. Die gewählte Architektur hat einen erheblichen Einfluss auf die Skalierbarkeit eines Systems. Denn eine einmal gewählte Architektur kann nur mit hohem Aufwand oder gar nicht mehr geändert werden. Somit ist die Analyse der Skalierbarkeit und deren Berücksichtigung bei der Wahl einer Software-Architektur unerlässlich, um die Ziele, die ein System erfüllen soll, zu erreichen.