Verbindung verschiedener Verabeitungsschritte

Häufig beinhaltet eine Software verschiedene Verarbeitungsschritte, die von den Daten nacheinander durchlaufen werden.

Im einfachsten Fall kann man mit Verabeitungsschritten f, g und h man so etwas machen wie (Pseudocode):


for x in input_data {
y = h(g(f(x));
  store_partial_result(y);
}

Das stimmt, wenn die Datenmengen jeweils übereinstimmen. Häufig ist aber in der Praxis der Fall, dass die Datengrößen nicht so übereinstimmen. Man muss also Daten zwischenspeichern, bis für den jeweiligen Verarbeitungsschritt genug Eingaben da sind. Im Grunde genommen ist ein sehr einfacher und robuster Mechanismuns seit Jahrhunderten bewährt, zumindest wenn man in Jahrhunderten von zehn Jahren Dauer rechnet.. 😉


cat input_data_file| process_f | process_g | process_h > output_data_file

Der Ansatz lässt sich in einem Programm direkt nachbauen. Man muss named oder anonymous Pipes, TCP/IP, Message-Queues oder Messaging (wie JMS, RabbitMQ, MQ-Series,…) verwenden, um die Programmteile miteinander zu verbinden. Wenn man mit Akka arbeitet, kann man für jeden Verarbeitungsschritt einen (oder mehrere parallele) Aktor verwenden und diese mit Messages verknüpfen. Die Aktoren müssen und Daten sammeln, bis sie genug haben, um sie sinnvoll zu verarbeiten. Etwas unschön ist, dass man hier plötzlich „state“ im Aktor braucht.

Mehrere Threads sind auch möglich, wenn man für die Daten jeweils den augenblicklichen „Owner“ kennt und nur diesem Zugriff darauf erlaubt. Die zu implementierende Logik passt eigentlich sehr gut dazu, da definiert sein sollte, welchem Verarbeitungsschritt die Daten gerade unterzogen werden.

Es lässt sich aber auch mit einem Thread etwas machen, etwa entlang dieser Linie:

while (true) {
  if (enough_for_h(g_out)) {
    h_in = get_block(g_out, required_size_for_h)
    h_out = h(h_in)
    store_result(h_out)
  } else if (enough_for_g(f_out)) {
    g_in = get_block(f_out, required_size_for_g)
    append(g_out, g(g_in));
  } else if (enough_for_f(new_data)) {
    f_in = get_block(new_data, required_size_for_f)
    append(f_out, f(f_in));
  } else {
    append(new_data, read_block(input_data));
  }
}

Man muss immer aufpassen, dass sich das System nicht totläuft, was bei schlechten Implementierungen leicht passieren kann, weil entweder die Eingabedaten ausgehen oder die Buffer überlaufen. Die obige Implementierung strebt an, die Buffer immer auf der minimalen Füllung zu belassen.

Oft hat man natürlich die Möglichkeit und auch die Notwendigkeit, alle Daten auf einmal ins Memory zu laden. Dann gehen viele Dinge einfacher, man denke nur an Sortierfunktionen, die ja alle Daten sehen müssen.

Für große Datenmengen ist das aber nicht möglich und da sind die richtigen Ansätze nützlich, um nur eine verträgliche Datenmenge im Speicher zu halten.

Ein recht radikaler Ansatz ist es, alle Zwischenergebnisse in Dateien oder Datenbanken zu speichern. Dann kann man die Schritte sogar nacheinander ausführen.

In jedem Fall sollte man sich über die richtige Architektur Gedanken machen, bevor man munter drauf los programmiert.

In der Regel fährt man am besten, wenn die Quelldaten den Prozess treiben, also wenn diese sequentiell gelesen werden und zwar jeweils in den Mengen, die auch verabeitet werden. Das ist das Prinzip des „Backpressure“.

Share Button

Advanced Akka

In der vergangenen Wochenende hat sich die Möglichkeit ergeben, an der Type-Safe-Schulung über „Advanced Akka“ teilzunehmen. Akka ist ein Framework zur Parallelisierung und Verteilung von Verabeitungsoperationen einer größeren Applikation, das auf Scala basiert. Akka ist selbst in Scala geschrieben, aber es wurde darauf geachtet, dass es auch mit Java benutzbar ist. Unabhängig von der konkreten Implementierung ist es aber auch konzeptionell interessant, weil Akka Ideen umsetzt, wie man massiv parallele Applikationen entwickeln kann. Erlang, LFE und Elixir verwenden zum Beispiel ähnliche Konzepte, vielleicht noch etwas radikaler, während Scala ja einen „sanften“ Übergang zur funktionalen Welt ermöglichen soll, ähnlich wie C++ für den Einstieg in die Objektorientierung von ein paar Jahren.

Im Fall, dass man Akka verteilt betreibt, sind natürlich wieder die Serialisierungsmechanismen interessant. Man sollte das berücksichtigen und zumindest nicht zu feingranulare Zugriffe über das Netzwerk verteilt durchführen, wenn die Parallelisierung eine hohe Performance bieten soll.

Share Button

Threads oder Prozesse

Zur Parallelisierung einer Software kann man auf Threads und Prozesse zugreifen. Es lohnt sich, diese beiden Ansätze etwas genauer anzuschauen, um die geeignete Wahl treffen zu können.

Warum Parallelisierung?

  • Mehr CPUs nutzen
  • Moore’s law: Anzahl der Transistoren in IC verdoppelt sich ca. alle 2 Jahre.
  • Bis vor ca. 10 Jahren verdoppelte sich die CPU-Leistung auch regelmäßig.
  • Seit ca. 10 Jahren nur noch mehr Cores
  • Nur nutzbar durch Parallelisierung
  • Parallelisierung kann aber auch Dinge vereinfachen.
  • Beispiel Pipes:
cat `find /usr/bin/* -type d -prune -o -type f -print` \
| perl -p -e 's/sch/\\v s/g;s/\r/\n/g;' | strings \
| sort > /tmp/out.log &
  • Startet pro Pipe-Komponente einen Prozess, viel flexibler und meist perfomanter als wenn man Zwischenergebnisse in Dateien zwischenspeichern muss.
  • Integration verschiedener Software-Komponenten.
  • Typische Beispiele:
    • Datenbank
    • Messaging-System
    • SAP
    • Heterogene Software (mehrere Programmiersprachen…)

Probleme bei Parallelisierung

Unabhängig von der verwendeten Technologie muss man sich mit u.a. diesen Problemen auseinandersetzen:

  • Deadlocks (Verklemmung)
  • Fehler bei gleichzeitigem Ressourcenzugriff
  • Overhead der Parallelisierung
  • Erschwerte Fehlersuche

Deadlocks

Beispiel für Deadlock:

  • Zwei parallel laufende Programmteile A und B greifen exklusiv auf zwei Ressourcen X und Y zu.
  • A reserviert sich erst X und dann Y, B reserviert sich erst Y und dann X.
  • Bei ungünstigem Verlauf wartet A endlos auf Y und B endlos auf X.

Fehler bei gleichzeitigem Ressourcenzugriff

  • Beispiel Datenstruktur wird von Programmteil A und Programmteil B gleichzeitig verwendet.
  • Wenn beide Zugriffe nur lesend sind, ist das ok.
  • Wenn einer schreibt, kann es Inkonsistenzen geben, wegen Pointern sogar Programmabstürze, merkwürdige Fehler u.s.w.
  • Schwierig zu finden
  • Tritt oft erst auf dem Produktivsystem auf, beim Testen scheint alles gut zu sein.

Overhead

Wenn man Software parallelisiert muss man zusätzlich Dinge tun, die man sonst nicht braucht:

  • Locking
  • Kommunikation
  • Synchronisation
  • Memory
  • Mehr Entwicklungsaufwand

Das kann sich aber lohnen.
Besser frühzeitig daran denken!!!

Wie zähmt man die Parallelisierung?

  • In Applikationsentwicklung mit geeigneten Werkzeugen, Frameworks,…
  • In der Praxis viele Probleme und oft wenig Gewinn
  • Aber es gibt Wege die funktionieren..
  • Traum: Compiler parallelisiert automatisch
  • Aber für Systemprogrammierung explizite Kontrolle wichtig!

Threads oder Prozesse

Threads oder Prozesse???

Man kann für die Parallelisierung drei Wege gehen (oder Kombinationen daraus):

  • Prozesse
  • Threads auf OS-level
  • Eigener Mechanismus (User Threads)

Prozesse

  • Prozesse haben ihren eigenen Speicherbereich
  • Können auf verschiedenen Rechnern laufen
  • Explizite Kommunikation
  • Mit shared memory kann man auch gemeinsamen Speicher nutzen
  • Mehr Overhead als Threads

Threads

  • Threads laufen auf derselben Maschine
  • Teilen sich dasselbe Memory
  • Trennung muss man explizit erzwingen

Funktionsweise Prozess (POSIX)

  • Ein neuer Prozess wird mit fork() erzeugt
  • Dupliziert den ganzen Speicher
  • Effizient wegen copy-on-write
  • Achtung: overcommit
  • Danach (meistens) exec

Unter MS-Windows wird das Erzeugen eines neuen Prozesses und das Starten eines neuen Programms gleichzeitig in einem Schritt ausgeführt.

IPC

Interprozesskommunikation:

  • Signale
  • Pipe (named oder anonym)
  • Semaphore (Locks)
  • Shared Memory
  • Socket
  • Message Queue
  • File
  • Memory-mapped File
  • High-Level-Mechanismen (DB, Messaging, Corba, Rest, Soap, RPC,…)

Threads (Posix)

  • Mit pthread_create(…) erzeugen
  • Mit pthread_join(…) “einsammeln”
  • Funktion void *f(void *) übergeben
  • In C++ 11 in die Sprache integriert

Was soll man bevorzugen

Es hängt von der Ausgangslage ab.

Entwickelt man mit Ruby oder Perl, sind vielleicht mehrere Prozesse besser, weil Threads in diesen Sprachen nicht so gut unterstützt werden. Entwickelt man mit Scala oder Java, sind Threads vielleicht besser, weil das dort das gängige und gut unterstützte Parallelisierungsverfahren ist und weil JVM-Prozesse jeweils recht schwergewichtig sind, was den Speicherverbrauch betrifft.

Entwickelt man in C oder C++ und hat mehr oder weniger alle Möglichkeiten des Betriebssystems zur Verfügung, dann sind beide Wege gut gangbar. Man kann mit Shared Memory und anderen IPC-Mechanismen mehrere Prozesse zusammenspannen, aber auch mehrere Threads. Man muss sich in jedem Fall darum kümmern, dass gemeinsam genutzte Ressourcen konsistent bleiben und insbesondere nicht in inkonsistentem Zustand gelesen werden.

Man sollte beide Möglichkeiten kennen und im Einzelfall entscheiden, welcher Weg sich am besten eignet.

Share Button

Testbarkeit von Software

Viele haben inzwischen gelernt, dass man Software in erster Linie für den Anwender schreibt und nicht für die Verwendung möglichst cooler Technologie. Das vertrete ich hier auch immer wieder, aber nun kommt doch ein kleiner Gedanke in die Gegenrichtung. Betrachten wir einmal nicht nur die eigentlichen Softwareentwickler, sondern auch Softwaretester, -architekten, diejenigen die die Software später installieren und betreiben, DBAs u.s.w. als das Team das am Ende die Software dem Endanwender zur Verfügung stellt. Ich habe gute Erfahrungen mit dieser Sichtweise gemacht. Es geht jetzt also darum, Software im Interesse der Effizienz dieses Teams, speziell der Softwaretester zu optimieren.

Es gibt es oft die Situation, dass sich reale Szenarien während des Software-Tests nicht beliebig gut nachstellen lassen, weil die umgebende Infrastruktur irgendwo aufhören muss und durch Simulationen ersetzt wird. Die Frage der Testdaten ist auch immer wichtig und oft wird aus Datenschutzgründen der Zugang zu den realen Daten erschwert und es wird mit mehr oder weniger guten synthetischen Daten getestet. Besonders interessant wird es bei Software, die ein Zeitverhalten aufweisen soll, z.B. einen Fahrplanwechsel zu einem bestimmten Zeitpunkt berücksichtigen. Man kann ein eigenes Testnetzwerk mit eigenem Timeserver aufbauen und dann Datum und Uhrzeit manipulieren. Weil so ein Testnetzwerk aufwendig ist, sieht man es nicht überall und wenn man es hat, wird es von vielen Anwendern gleichzeitig verwendet, so dass das Stellen der Uhr koordiniert ablaufen muss und nur wenige Tests möglich sind. Die Uhr bei einzelnen Rechnern zu verstellen ist verlockend, aber doch oft problematisch, weil die Applikation auf verschiedenen Servern läuft und es interessante Effekte gibt, wenn die Uhr nicht überall auf dieselbe Art verstellt wird. Der Timeserver ist ja oft so wichtig, damit alle Server dieselbe Uhrzeit haben und nicht einmal in erster Linie damit die Zeit richtig ist. Man hört, dass Google sogar in den größeren Rechenzentren jeweils eine Atomuhr aufstellt, um die Zeit synchron zu halten, was zwischen verschiedenen Rechenzentren wegen der Latenz der Netzwerkverbindungen gar nicht so einfach ist. Die Zeitstempel in übertragenen, gespeicherten und verarbeiteten Daten sind häufig sehr relevant, um zu entscheiden, in welcher Reihenfolge gewisse Ereignisse erfolgt sind. Für verteilte Applikationen und insbesondere für verteilte Datenbanken ist das sehr wichtig.

Dieses Beispiel zeigt es, dass es sich lohnen kann, bei der Softwareentwicklung die Testbarkeit zu berücksichtigen. Man sollte den Testern halbwegs effiziente und machbare Möglichkeiten bieten, ihre Arbeit zu machen. Software könnte also Wege anbieten, speziell mit der Zeit umzugehen, Zwischenergebnisse abzugreifen oder zu ändern, die im Produktiveinsatz eigentlich gar nicht relevant wären. So kann man die Zeit des Testens nutzbringender einsetzen, mehr Tests machen und mehr Fehler finden. Dass sich diese extrem aufwendigen Tests in einer idealen Testumgebung am Schluss auch noch lohnen, ist unbestritten, aber man bremst die ganze Entwicklung und damit den Teamerfolg aus, wenn man die Hürden für alle Tests zu hoch ansetzt.

Share Button

OR-mappings: Hibernate & Co.

Die meisten Software-Projekte verwenden eine Datenbank. Es ist immer eine Herausforderung, die Datenbank und die Software zusammenzubringen, weil das zwei verschiedene Welten sind, die ihre eigene Entwicklung genommen haben.

Hierzu haben sich ganz verschiedene Ansätze etabliert.

  • DB-zentrisches Vorgehen: Ein starker DBA oder ein starkes DB-Team fängt zunächst an, das Datenmodell zu erstellen. Die Software muss sich daran anpassen. Für die performance-kritischen Zugriffe erhalten die Entwickler Unterstützung von einem DBA.
  • Eine Variante davon ist die Verwendung des Active-Record-Patterns, wo die Objekte aus der Datenbankstruktur generiert werden. Dieses Active-Record-Pattern verwendet man bei Ruby on Rails.
  • OR-zentrisches Vorgehen: Man verwendet objekt-relationale Mapper und definiert Klassen in seiner Software und ein paar XML-Dateien oder Annotationen dazu und hofft dann, dass der OR-Mapper nicht nur relationale Daten in Objekte und Objekte in relationale Daten wandelt, sondern dass auch noch gleich das Datenmodell und die Zugriffe automatisch generiert werden. Das ist der typische Ansatz bei Hibernate, JDO, Eclipselink etc.
  • Auch in der Rails-Welt versucht man es oft und gerne ohne DBA und lässt das DB-Schema generieren.
  • Man verzichtet auf die relationale SQL-Datenbank und arbeitet mit NoSQL-Datenbanken, die oft etwas näher an der Software liegen

Letztlich muss man sich aber die Fragen beantworten, ob man die Datenbank in den Mittelpunkt stellt und die Software passend zur Datenbank entwickelt oder ob man die Software in den Mittelpunkt stellt und die Datenbank nur als eine Art Persistenzmechanismus dafür verwendet.

Dann kommt die zweite Frage ins Spiel, wie man die objektorientierte, prozedurale und funktionale Welt der Software mit der relationalen Welt der SQL-Datenbanken oder der jeweiligen Struktur der NoSQL-Datenbanken zusammenbringt.

Eine dritte Frage taucht auf, ob man auf Softwareebene Caching-Mechanismen einbaut. Das muss man nicht, aber Hibernate verspricht einem so etwas gut zu können und auch bietet sogar Caching auf der Platte an, nicht nur im Memory.

Die erste Frage ist legitim und kennt wohl keine allgemeingültige Antwort. Man kann durchaus von einer Datenbank im Mittelpunkt ausgehen, wenn diese von mehreren Applikationen genutzt wird oder wenn man davon ausgeht, dass die Daten wesentlich langlebiger als die Software sind. Das könnte bei einer Kundendatenbank so sein. Oft behilft man sich mit dem Versprechen, dass es eine Export-Funktionalität gibt und wenn man dann irgendwann die Software umstellen will, kann man die Daten als XML exportieren und gleich in die neue Software übernehmen. Das kann Routine sein, ich wäre aber skeptisch und würde diese Migration sorgfältig planen und testen. Wahrscheinlich wird es nicht so einfach und so billig, wie man gehofft hat.

Die zweite Frage ist auch recht relevant. Man hat ja nicht nur Objekte in Relationen zu übersetzen, sondern muss sich auch mit Transaktionen auseinandersetzen und mit der Frage, wie tief man große Objekte mit vielen Unterobjekten wirklich lesen und schreiben will. Hier werden gute Antworten schwierig. Liest man sie nur in kleine Portionen, stößt man später auf Probleme, weil Unterobjekte fehlen, z.B. LazyLoadingException in Hibernate. Man muss hierfür eine Lösung finden und diese konsequent umsetzen. Ein radikaler Ansatz, der etwas umständlich ist, aber recht zuverlässig funktioniert, ist die aggregierten Objekte zu entkoppeln und stattdessen nur Schlüsselfelder zu speichern. Beim Zugriff muss man sie dann explizit mit dem Schlüsselfeld holen und stellt so sicher, dass sie aktuell sind. Hier stellen sich natürlich auch interessante Konsistenzfragen, wenn man Daten aus der Datenbank miteinander kombiniert, die von verschiedenen Zeitpunkten stammen und die deshalb nicht mehr notwendigerweise miteinander konsistent sind.

Ein beliebter Ansatz ist, recht viel in den Hauptspeicher zu laden und darauf zuzugreifen. Damit landen wir bei dem dritten Thema, dem Caching. Eines sollte zunächst einmal klargestellt werden: Wenn man eine relationale SQL-Datenbank benutzt und sich dabei auf das Caching des OR-Mapping-Frameworks verlässt, dann verlässt man die transaktionale Welt. Man handelt sich sehr viele Probleme ein, weil Fragen der Konsistenz nicht mehr einfach handhabbar sind, schon gar nicht auf DB-Ebene. Das lässt sich scheinbar in den Griff bekommen, wenn man sagt, alle Zugriffe laufen über die Applikation und die Datenbank ist nur noch eine Persistenzschicht. Nun laufen aber mehrere Threads gleichzeitig und man stößt dann auch noch in diesen heiklen Bereichen auf Hibernate-Fehler. Eclipselink, JDO, etc. sind da wohl nicht wesentlich anders. Kurz gesagt, das Caching ist für veränderliche Daten bestenfalls kaum handhabbar, aber ich gehe sogar soweit, dass das konzeptionell nicht korrekt ist. Nun kann man natürlich so etwas wie „Stammdaten“ haben, die sich selten verändern. Die lassen sich tatsächlich im Memory halten, wenn es einen Prozess gibt, diesen Cache aufzufrischen, wenn sich die Stammdaten ändern, vielleicht sogar mit einem Neustart der Applikation. Ja, das will niemand zugeben, dass man so etwas heute braucht, aber besser zweimal im Jahr die Applikation neu starten als zwei Monate im Jahr obskure Fehler suchen, die durch unsauberes Caching und Fehler im OR-Mapping-Framework entstehen.

Zusammenfassend kann man sagen, dass diese OR-Mapper durchaus zumindest in der Java-Welt ihren Bereich haben, wo sie nützlich sind. Aufpassen sollte man aber bei sehr großen Tabellen und bei sehr stark miteinander verknüpften Tabellen und vor allem beim Caching von veränderlichen Daten. Hier lohnt es sich, etwas mehr Aufwand zu treiben, denn der scheinbare Komfortgewinn durch den OR-Mapper existiert hier nicht oder nur zu einem unverhältnismäßig hohen Preis.

Share Button

Das Richtige entwickeln – richtig gefragt

Wenn eine neue Software entwickelt oder erweitert wird, ist es ja immer eine wichtige Frage, was eigentlich entwickelt werden soll. Die Entwickler wissen es selten selbst, und auch die Kunden oder die Besteller oder die Nutzer der Software muß man gelegentlich erst ein Stück weit begleiten, bis man herausbekommt, was sie wirklich wollen und benötigen. Requirements-Engineering oder Anforderungsanalyse ist eine anspruchsvolle und wichtige Aufgabe. Oft gibt es eine sogenannte „Fachseite“, die in Zusammenarbeit mit Businessanalysten diese Anforderungen definiert.

Im reinen Wasserfallmodell hat man diesen Schritt am Anfang sehr vollständig durchgezogen und dann später auf dieser Basis die Software entwickelt, zu Korrekturzwecken gab es immerhin Änderungsanträge. Bei agileren Prozessen entwickeln sich die Details der Anforderungen oft noch während der Entwicklung weiter und solange der Teil, der gerade entwickelt wird, klar genug spezifiziert werden kann, hat man so die Möglichkeit, aufgrund der Erkenntnisse aus früheren Lieferungen etwas für die später möglichen Anforderungen zu lernen. Man sagt, daß der Appetit mit dem Essen kommt.

Es lohnt sich aber in jedem Fall, noch einen Schritt weiter zu denken. Es kommt auch oft genug vor, daß die Anforderungen durchaus schon gut genug bekannt sind. Nun ist die Erfahrung aber die, daß die Software am Ende nur 80% von dem kann, was man sich eigentlich erhofft hat und so werden die Anforderungen sicherheitshalber etwas höher gestellt, in der Hoffnung, daß von der wirklich gewollten Funktionalität dann am Ende genug da ist. Außerdem kommt es oft vor, daß Anforderungen mit einer bestimmten Implementierung im Hinterkopf gestellt werden, die sich aber später auf Seite von Softwarearechitektur oder -entwicklung als suboptimal erweist.

Hier lohnt es sich, miteinander zu reden, um die wirklichen Anforderungen in Erfahrung zu bringen. Oft stellt sich dann heraus, daß eine wesentlich robustere Implementierung möglich ist, die den eigentlichen Anforderungen, nicht aber den kommunzierten Anforderungen, gleich gut oder sogar besser genügt.

Ein Klassiker sind Tabellen, zum Beispiel für ein Gebührenmodell oder Steuern. Natürlich kann man Tabellen in Software umsetzen, man kann auch eine Möglichkeit anbieten, sie zu ändern. Diese Tabellen können aber recht groß werden und auch wenn das unter Aspekten wie Speicherverbrauch, Verarbeitungsgeschwindigkeit und Durchsatz heute in der Regel keine Rolle spielt, müssen diese Tabellen doch aktuell gehalten werden, was den Prozess zum Betrieb der Software erschwert, vor allem, wenn die Änderung der Tabellen einen Software-Update erfordern, weil sie hardcodiert sind.

Bei genauerem Nachfragen stellt sich heraus, daß die Tabelle eigentlich aufgrund einer Formel gebildet wurde und diese approximiert. Wenn man nun die Formel in die Software einbaut und vielleicht ermöglicht, daß 2-3 Parameter dazu geändert werden kann, bekommt man eine Lösung die „glatter“ ist, weil es keine solchen Sprünge mehr zwischen 4999 und 5000 gibt, was man natürlich als Vorteil erst einmal verkaufen muß, man bekommt eine besser wartbare Software, weil sie viel weniger Code braucht und weniger kaputt gehen kann, aber vor allem bekommt man im Betrieb einen Vorteil, weil man nur noch 2-3 Parameter der Formel aktuell halten muß und nicht mehr eine riesige Tabelle. Daß sich der Softwaretest vereinfacht, ist ein weiterer Vorteil.

Ein konkreter Fall betraf vor vielen Jahren eine Software für die Verarbeitung von Fahrplänen eines Verkehrsbetriebs. Das Zielsystem war für reguläre Schweizer Fahrpläne entwickelt worden, und man konnte so Linien, Routen, das sind verschiedene Fahrtverläufe innerhalb der Linie, mindestens zwei für die beiden Fahrtrichtungen, und ein gutes Dutzend Fahrzeitmuster für Hauptverkehrszeit (langsam), Nacht (schnell) u.s.w. festlegen. Das paßt für mitteleuropäischen Stadtverkehr überall sehr gut. Eine kanadische Stadt weist aber eine viel niedrigere Bevölkerungsdichte auf und der Anteil der öffentlichen Verkehrsmttel an den zurückgelegten Wegen ist auch typischerweise kleiner als in durchschnittlichen mitteleuropäischen Städten. So hat man etwas, was zwischen einem Überlandverkehr und Stadtverkehr in Mitteleuropa liegt. Das Budget für den Verkehrsbetrieb ist begrenzt und man bemüht sich, trotz allem noch eine regelmäßige Bedienung der Stadtregionen zu bewerkstelligen. Dafür war eine Fahrplanerstellungssoftware vorteilhaft, die die Fahrzeiten individuell pro Fahrt wählbar macht. Die Sprachbarriere kam noch hinzu und so meinte der Schweizer Lieferant, die Kanadier sollten nur ihre Fahrplan ein bißchen aufräumen, dann würde es schon passen und die Kanadier meinten, sie wollten sich ihre gute Arbeit nicht wegen einer restriktiven Software verhindern lassen. Als das einige Zeit nach Projektbeginn festgestellt wurde, gab es erst einmal einen Scherbenhaufen.

Die Lösung war schließlich, noch einmal genau die Bedürfnisse des Kunden anzuschauen und konkret die kompliziertesten Linien unter die Lupe zu nehmen. Nun sollte die Software eine bestimmte Zuverlässigkeit bei der Verwendung der Daten erzielen. Es stellte sich am Ende heraus, daß das alles lösbar war. Hierzu mußte man die Fahrzeitmuster der einzelnen Fahrten ermitteln und jeweils die häufigsten Muster in die Zielplattformübernehmen. Bei den meisten Linien reichte das aus, aber für einzelne Fahrten mußte das passendste unter den übernommenen Fahrzeitmustern verwendet werden, was zu einem Fehler führte, der aber noch in die Toleranz für das Gesamtsystem fiel. Die richtige Lösung, das Zielsystem zu erweitern, um die Daten vollständig aufzunehmen, sollte man natürlich nicht aus dem Auge verlieren, was aber ein längerfristiges Projekt war. Mit so einer Kompromißlösung ließ sich dafür etwas Zeit gewinnen und letztlich die unter den gegebenen Umständen bestmögliche Lösung für alle beteiligten umsetzen.

Man sieht also, es lohnt sich auf jeden Fall, genauere Fragen zu stellen, um unter Berücksichtigung der Möglichkeiten, die man von der Softwarearchitektur im gegebenen Zeit- und Budgetrahmen hat, die wirklichen Bedürfnisse möglichst zielgerichtet zu erfüllen.

Share Button

Große Tabellen

Es ist interessant zu sehen, wie große Tabellen sich in Echtzeit oder zumindest interaktiv ohne gefühlte Verzögerung handhaben lassen.

Vielleicht war es einmal praktisch, daß Telefonnummern oder Nummernschilder in vielen regionalen kleinen Tabellen strukturiert waren, die dann jeweils maximal wenige Millionen Einträge enthielten. Auch Bankkonten wurden an den einzelnen Standorten geführt und man mußte zum Abheben bei einer anderen Filiale derselben Bank warten, bis die zuständige Filiale kontaktiert worden war. Fahrkarten konnte man für inländische Zugverbindungen kaufen, aber für ausländische Verbindungen war ein Sonderschalter zuständig, und für Auslandsreservierungen mußte man zweimal vorbeikommen. Telefonbuchabfragen im Internet waren möglich, aber man mußte doch erst den Ort angeben und dann den Namen, selbst wenn es ein seltener Name war.

Heute sieht man, daß es kein Problem mehr ist, einen flachen Nummernraum für ein ganzes Land für Telefonnummern, Nummernschilder oder andere in ähnlicher Häufigkeit vorhandene Daten zu haben. Praktisch ist das zum Beispiel, weil man innerhalb von der Schweiz umziehen kann, ohne die Telefonnummer zu ändern, aber für die Mobiltelefone hat man ja schon von Anfang an auf regionalisierte Nummern verzichtet, früher weil es so wenige Mobiltelefone gab und heute, weil man mit 100 Millionen Mobiltelefonnummern in Echtzeit umgehen kann.

Share Button

Micro-Komponenten-Antipattern

Eine größere Software muß man sicher strukturieren, sonst baut man sich ein Monster.

Nun kann man versuchen, Komponenten zu definieren, die so klein wie nur möglich sind. Die Komplexität innerhalb der Komponenten wird dadurch reduziert und überschaubar. Aber man bekomt ein Problem, weil die Menge der Komponenten dabei zu groß wird und man eine sehr hohe Komplexität bekommt, diese vielen Komponenten miteinander zu verknüpfen.

Man sieht es bei Büchern, bei denen die Gliederung gut gelungen ist, daß es eine Hierarchie von Gliederungen gibt und in jeder Ebene findet man etwa 2-10 Untereinträge zu den Einträgen aus der nächst höheren Ebene, nicht aber 50 Kapitel, die nicht in irgendeiner Form gruppiert sind.

Solche Ebenen wie „Teil“, „Kapitel“, „Abschnitt“… hat man in der Software-Architektur auch zur Verfügung, wobei es von der Technologie abhängt, welche Hierarchieebenen für dieses Gliederung und Strukturierung zur Verfügung stehen, z.B. Methode – Klasse – Package – Library – Applikation und man sollte sie mit Bedacht einsetzen. Was gehört zusammen und was nicht?

Es gibt aber noch einen anderen Aspekt. Oft verbaut man sich durch zu feingranulare Aufteilung Wege.

Ein Beispiel: Es soll eine Multiplikation von Matrizen mit kmoplexen Zahlen als Elementen implementiert werden. Nun ist es sehr elegant, die Matrizenmulitplikation einmal zu implementieren und dabei einen abstrakten numerischen Typ zu verwenden. Für jeden dieser numerischen Typen implementiert man die Grundoperationen.

Dies kann aber in Bezug auf die Perforamnce und auch in Bezug auf die Rechengenauigkeit beim Arbeiten mit Fließkommazahlen zu Problemen führen. Es lassen sich viel performantere Algorithmen für diese Matrizenmultiplikation finden, wenn man sie speziell für komplexe Zahlen schreibt und auch auf die Realteile und Imagniärteile der Matrizenelemente zugrifen kann. Besonders tückisch sind aber auch die Rundungsfehler. Um Rundungsfehler zu verringern muß man auf die Kalkulationen Zugriff haben und deren Reihenfolge und Assoziierung steuern können.

Hier ein Beispiel:

a=3.0
b=4.0
c=5e30
d=-5e30

Berechnet man nun (a+b)+(c+d), erhält man 7.0, aber mit (a+c)+(b+d) erhält man 0.0.
In irb (ruby) sieht es etwa so aus:


$ irb
irb(main):001:0> a=3.0
=> 3.0
irb(main):002:0> b=4.0
=> 4.0
irb(main):003:0> c=5e30
=> 5.0e+30
irb(main):004:0> d=-5e30
=> -5.0e+30
irb(main):005:0> (a+b)+(c+d)
=> 7.0
irb(main):006:0> (a+c)+(b+d)
=> 0.0
irb(main):007:0>

Der Fehler kann sich nun natürlich noch beliebig fortpflanzen.

Das ändert aber nichts daran, daß ein auf Polymorphie basierender Ansatz in den allermeisten Fällen der richtige Weg ist, solange man nicht auf die entsprechende Optimierung angewiesn ist.

Es bleibt aber dabei, daß bei einer Aufteilung in Komponenten die richtige Granularität gewählt werden sollte, also keine Microkomponenten, aber auch nicht wahlloses Zusammenfügen von Dingen, die nicht zusammengehören, nur um die richtige Größe der Komponenten zu erreichen.

Share Button