verteilte anwendungen teil 7: remote procedure call (rpc...
TRANSCRIPT
08.04.18 1VA – SS 2018 - Teil 7/RPC II
Verteilte Anwendungen
Teil 7: Remote Procedure Call (RPC)Realisierung
2VA – SS 2018 - Teil 7/RPC II
Literatur
[7-1] https://de.wikipedia.org/wiki/Stapelspeicher
[7-2] https://de.wikipedia.org/wiki/Reflexion_(Programmierung)
[7-3] http://openbook.rheinwerk-verlag.de/javainsel9/javainsel_25_001.htm
[7-4] http://openbook.rheinwerk-verlag.de/javainsel9/javainsel_11_002.htm
[7-5] http://www-inst.eecs.berkeley.edu/~cs162/sp06/hand-outs/p149-presser-linker-loader.pdf
[7-6] https://de.wikipedia.org/wiki/Linker_(Computerprogramm)
3VA – SS 2018 - Teil 7/RPC II
Übersicht
• Stubs und Marshalling
• Stackaufbau eines Programms
• Dynamische Aufrufe: Call
• Reflexion
• Parallele Verarbeitung im Server
4VA – SS 2018 - Teil 7/RPC II
Realisierung der RPCs
• Der Transfer der Kontrolle wird auf den Transfer von Daten zurück geführt.
• Die Realisierung der Umsetzung der Kontrolle nach Daten und umgekehrt erfolgt in einem Stub:
• Stub = Realisierung der Schnittstelle zum Netzwerk mit der Aufgabe der Durchführung von Remote Procedure Calls
Object1
Stub
BS
Object2
Stub
BSNetzTCP/IP
RPC-Protokoll
Datenfluss
Call, Kontrollfluss
OSI-Schichten 1 bis 4
5VA – SS 2018 - Teil 7/RPC II
Aufgaben der Stubs I
• Serialisieren = Konvertieren der internen Repräsentation von Daten in ein mit allen Kommunikationspartnern abgesprochenes Format, meist ASCII.
• Deserialisieren = Umgekehrter Vorgang des Serialisierens und damit Wiederherstellung der semantisch gleichen Repräsentation
• Bitte beachten: Semantisch gleich bedeutet nicht unbedingt dieselbe Bitkombination.
• Realisieren des Protokolls: Reihenfolge der Pakete, Formate von Paketen etc.
• Feststellen und Behandeln von Fehlern
• Anpassung unterschiedlicher unterliegender Schnittstellen, d.h. Stubs sind eigentlich Wrapper.
6VA – SS 2018 - Teil 7/RPC II
Aufgaben der Stubs II
• Marshalling ist ein anderer Begriff für das Serialisieren und Deserialisieren
• Externe Data Repräsentation (XDR) = Serialisierte Daten im verabredeten Datenformat zwischen den Kommuikationspartnern
Object1
StubXDR
Lokale Daten
Repräsentation
Object2
Stub
Lokale Daten
Repräsentation
BS BSNetz
RPC-Protokoll
BS = Betriebssystemmit dem Transportsystem
7VA – SS 2018 - Teil 7/RPC II
Serialisieren in Java und PHP
Java
• In Java geht die Serialisierung über das Interface java.io.serializable sowie über die I/O-Stream-Klassen.
• Siehe– http://docs.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html
– https://docs.oracle.com/javase/tutorial/jndi/objects/serial.html
– http://openbook.rheinwerk-verlag.de/javainsel9/javainsel_17_010.htm
– http://www.tutorialspoint.com/java/java_serialization.htm
– http://www.vogella.com/tutorials/JavaSerialization/article.html
PHP
• In PHP erfolgt dies durch den Aufruf von serialize() und unserialize().
• Siehe– http://php.net/manual/de/function.serialize.php
– http://php.net/manual/de/language.oop5.serialization.php
– http://serialize.onlinephpfunctions.com/
8VA – SS 2018 - Teil 7/RPC II
Serialisierung I
Datentyp Beispiel für ein Format
int (8,16,32,64 bit) ASCII-Hex
subrange-int ASCII-Hex
float, double, extended Dezimal
char (1 Byte) ASCII-Hex
char (UNICODE) ASCII-Hex
String ASCII-Hex
boolean Dezimal
Aber wie sieht es aus mit Enumeration Types?Z.B. mit einem Paar [Symbol als String,Wert als int]
Aber wie sieht es aus mit BigInteger?
Und mit BigDecimal?
Und mit BCD-Arithmetik?
9VA – SS 2018 - Teil 7/RPC II
Serialisierung II – Schnittstelle einer Routine
• Die Liste der Parameter
• Die Liste der Resultate bzw. das Resultat
• Exception-Informationen
• Hier gibt es verschiedene Probleme, u.a.– Wie werden Referenzen (Pointer) behandelt?
– Werden Zeichenketten (Strings) kodiert?
– Werden Objekte übertragen, die ja Code enthalten?
• Wir gehen in dieser Lehrveranstaltung vereinfachend davon aus,– dass nur primitive Daten übertragen werden: int, float, char,
– dass nur Maschinen desselben Typs betroffen sind,
– und dass es keine Seiteneffekte der aufgerufenen Routinen gibt.
10VA – SS 2018 - Teil 7/RPC II
Serialisierung III – Pointer/Adressen
1) Einfach verbietenNachteil: wird häufig gebraucht, z.B. Output-Parameter in Routinen wie read(…)
2) Alle referenzierten Objekte müssen mit serialisiert werden.– Aber wie weit muss das gehen?
Liste von Objekten: die ganze Liste?
3) Alle referenzierten Objekte müssen jeweils lokal verfügbar sein bzw. gemacht werden, d.h. die Objekte müssen zwischen Knoten verschiebbar sein.
Drei Strategien für Zeiger:
Alle drei Strategien gibt es in der Praxis.Alle drei Strategien sind nicht schön.
11VA – SS 2018 - Teil 7/RPC II
Allgemeine Realisierung des Stubs auf der Client-Seite
(01)proc call(Name,ParamList) {(02) data:= serialize(ParamList)(03) msg:= createMSG(Name,data)(04) send(Server,msg)(05) result:= receive()(06) if result=error {(07) throw Exception(08) } else {(09) data:= unserialize(result)(10) return(data)(11) }
• Das ist eine allgemeine Call-Routine des klassischen synchronen RPCs.
• Im Fehlerfalle wird eine Exception geworfen.
• Dies hier stellt nur das Prinzip dar.
// Client Beispiel... avg:= call("avg",1,2,3)... print avg// avg = Durchschnitt// eine 2 wird ausgegeben
12VA – SS 2018 - Teil 7/RPC II
Bemerkungen
• In Zeile (3) wird eine Nachricht mit den serialisierten Informationen erstellt.
• Dieses Datenpaket ist eine Struktur mit zwei Werten: – Name der Routine
– Parameterliste
• In Zeile (4) wird das Paket gesendet und in (5) die Antwort empfangen.
• Zeilen (6) bis (8) zeigen die Fehlerbehandlung, d.h. das Weiterreichen einer Exception.
• Bitte beachten Sie, dass der Pseudocode sich auf eine allgemeine Routine bezieht, die alle möglichen Routinen entfernt aufrufen vermag.
• Der Name der aufzurufenden Routine wird als String übergeben.
13VA – SS 2018 - Teil 7/RPC II
Realisierung des Stubs auf der Server-Seite I
(01)proc processCalls {(02) do {(03) msg:= receive()(04) proc:= msg.Name(05) if exists(proc) {(06) data:= unserialize(msg.ParamList)(07) try(08) result:= call "proc"(data)(09) catch(10) result:= Error(11) } else {(12) result:= Error(13) }(14) data:= serialize(result)(15) reply(data)(16) }
• Das ist die Callee-Routine des synchronen RPCs.
• Auf der Server-Seite befindet sich eine Endlos-Schleife, die alle Requests abarbeitet und sonst nichts tut.
Das ist eine ArtHauptprogramm
14VA – SS 2018 - Teil 7/RPC II
Bemerkungen
• In Zeile (4) wird der Name der Routine extrahiert.
• In (5) wird anhand einer Tabelle geprüft, ob die betreffende Routine geladen ist bzw. existiert.
• Der Aufruf in (8) muss in einem TRY-Block erfolgen, da die aufgerufene Routine eine Exception werfen kann, die in in (10) weitergereicht wird.
• In (15) wird entweder das Ergebnis zurück geliefert oder eine Exception bzw. eine Fehlernummer.
• Bitte beachten Sie, dass der Pseudocode sich auf eine allgemeine Call-Routine bezieht, die alle möglichen Routinen lokal aufrufen vermag.
• Der Name der aufzurufenden Routine ist als String vorhanden.
15VA – SS 2018 - Teil 7/RPC II
Generieren des Stubs I
• Die skizzierten Stubs sind genereller Natur: sie werden ein einziges Mal programmiert und werden für alle Routinen benutzt.Das geht nur mit dynamisch parametrisierten Klassen oder in Skriptsprachen, wie z.B. PHP oder Python.
• Alternativ werden Stubs per Generator (IDL-Compiler) entsprechend der Signatur der Routinen individuell generiert. Dieser Code kann dann nur für diese eine Routine verwendet werden.
• Beispiel für Java:
16VA – SS 2018 - Teil 7/RPC II
Generieren des Stubs II
• Bei der Verwendung eines IDL-Compilers wird auf der Klientenseite für jede Routine ein eigener Stub generiert; dieser hat genau die in der IDL spezifizierten Parameter samt Typen.
• Auf der Seite des Servers erfolgt dies genauso, aber der Aufruf muss dann in einem großen Switch/Case erfolgen bzw. im Falle der Objekt-Orientierung durch Polymorphie (intern durch Zeiger und Tabellen).
17VA – SS 2018 - Teil 7/RPC II
Realisierung des Stubs auf der Server-Seite II
...(07) try(08) result:= call "proc"(data)(09) catch(10) result:= Error...
... (07) try(07a) switch proc(07b) case "Routine1": Routine1(data.p1,data.p2...);(07c) case "Routine2": Routine2(data.p1,data.p2...);(07d) ... (07e) end_switch(09) catch(10) result:= Error...
Die einzelnen Parameter müssenextrahiert und angegeben werden.
Dieser Teil wird vomIDL-Compiler generiert
18VA – SS 2018 - Teil 7/RPC II
Nachteile dieses Verfahrens
• Dieses Verfahren hat folgende Nachteile:– Kein dynamisches Laden möglich
– Starre und feste Anzahl von Routinen mit fester Parameteranzahl
• Es fehlt die Flexibilität der dynamischen Typen von Skript-Sprachen.
• So implementiert, können nur die im switch-Konstrukt explizit berücksichtigten Routinen aufgerufen werden.
• Was aber wenn ein allgemeiner Stub, der alle möglichen Routinen mit beliebigen Parametern aufrufen können soll, benutzt werden soll?
• Eine Lösung dazu wird nun präsentiert.
19VA – SS 2018 - Teil 7/RPC II
Flexibler Aufruf von Routinen I
• Über eine Nachricht wird der Name einer Routine dem Server übergeben.
• Der Server muss nun diese Routine aufrufen, d.h.
der Aufruf muss schon programmiert sein.
• Es geht also um das call:
proc processCalls do { pdu:= receive() proc:= pdu.Name if exists(proc) { data:= unserialize(pdu.ParamList) try result:= call "proc"(data) catch result:= Error } else { result:= Error } data:= serialize(result) reply(data) }
20VA – SS 2018 - Teil 7/RPC II
Aufbau des Datenbereichs eines Prozesses
Stack
Heap
Frei
BelegtdurchStack
BelegtdurchHeap
SP ->
Date
n-S
egm
ent
Richtung desWachsens desStacks
SP = Stackpointer(Register in der CPU)
• Die Aufgabe des Stacks besteht in der Bereitstellung und Vernichtung lokaler Variablen und Parameter.
• Die Aufgabe des Heaps besteht in der Bereitstellung und Vernichtung von langlebigen Objekten, z.B. Instanzen von Klassen
21VA – SS 2018 - Teil 7/RPC II
Aufruf einer Prozedur mit Parametern
Stack
SP ->
Parameter N
...
Parameter 2
Parameter 1
Return-Adr.
Situation direkt nach Aufruf einer RoutineS(Parameter1,Parameter2,…,ParameterN)
Mechanismus des Aufrufsder Prozedur S
22VA – SS 2018 - Teil 7/RPC II
Call "Name"(Parameterliste) I - Wiederholung
proc processCalls { do { pdu:= receive() proc:= pdu.Name if exists(proc) { data:= unserialize(pdu.ParamList) try result:= call "proc"(data) catch result:= Error } else { result:= Error } data:= serialize(result) reply(data) }
Das ist der Aufruf derRPC-Routine im Server
Hier wird eine Routineaufgerufen, dessen Namein einem String vorliegt.
Das ist die Seite des Servers
call("proc",data)
call() ist nun die allgemeine Aufrufroutine
23VA – SS 2018 - Teil 7/RPC II
Call "Name"(Parameterliste) II
Stack
SP ->
result
ref data
ref "proc"
Return-Adr.
Parameter N
...
Parameter 2
Parameter 1
Name(String)
Situation direkt nach Aufrufder Routine Namens call:
result:= call("proc",data)
• Auf dem Stack ist der Platz für das Resultat reserviert.
• Der 1. Parameter (proc) ist die Adresse eines Strings.
• Der 2. (data) ist die Adresse einer Tabelle mit den Parametern der Routine, deren Namen als String vorliegt.
• ref data bedeutet: Adresse-von(data)
Situation direkt nachdem Aufruf von call()
24VA – SS 2018 - Teil 7/RPC II
Call "Name"(Parameterliste) III
push(Array an der Param-Addr)
Stack
SP ->
result
ref data
Return-Adr.
Parameter N
...
Parameter 2
Parameter 1
Parameter N
...
Parameter 2
Parameter 1
Der 1. Schritt in der Routine callbesteht darin, die Liste derParameter (2. Parameter) aufden Stack zu kopieren.Das erfolgt über die push-Operation.
ref "proc"
25VA – SS 2018 - Teil 7/RPC II
Call "Name"(Parameterliste) IV
push(Return-Adresse)
Stack
SP ->
result
Return-Adr.
Parameter N
...
Parameter 2
Parameter 1
Return-Adr.
Dieser Block wirdnach oben kopiert
Der 2. Schritt in der Routine callbesteht darin, die Rückkehradresseauf den Stack zu bringen.
Der 3. Schritt besteht darin, den unteren Bereich so weit nach obenzu kopieren, dass er unterhalb desResultats steht.Damit wird der graue Bereichüberschrieben.
ref data
ref "proc"
26VA – SS 2018 - Teil 7/RPC II
Call "Name"(Parameterliste) V
push(Array an Param-Addr)push(Return-Adresse)
Routine:= reflection(Name)copy(Block,Stack)pop(unterer Block)goto Routine
Stack
result
Parameter N
...
Parameter 2
Parameter 1
Return-Adr.
Situation desStacks direkt
vor goto
Bestimmen derAdresse derRoutine mit demNamen "Name"
Sprung direkt indie Routine
SP ->
Durch dieses Kopieren ist genau die Strukturauf dem Stack entstanden, die erforderlich ist,um die Routine direkt aufzurufen.
27VA – SS 2018 - Teil 7/RPC II
Call "Name"(Parameterliste) VI
proc call(Name,ParameterListe) { push(Array an Param-Addr) push(Return-Adresse) Routine-Adresse:= reflection(Name) copy(Block,Stack) pop(unterer Block) goto Routine-Adresse}
• Es wird der aktuelle Aufruf von call auf dem Stack beseitigt.
• Stattdessen wird anhand der Parameter ein anderer Aufruf simuliert.
• Die Rückkehradresse dieses neuen Aufrufs ist die alte zum Aufrufer der call-Routine.
• Das Ganze lässt sich nur in Assembler realisieren.
28VA – SS 2018 - Teil 7/RPC II
Was macht reflection(Name)? I
• Reflexion = Möglichkeit, die eigenen Programmstrukturen zur Laufzeit selbst zu analysieren und zu benutzen
• Introspektion = Selbstanalyse = Reflexion
• Die Routine reflection(Name) durchsucht die Symboltabellen zur Laufzeit nach dem Namen der Routine und liefert deren Adresse bzw. Null.
29VA – SS 2018 - Teil 7/RPC II
Was macht reflection(Name)? II
Name Adresse
"blabla" ref blabla
"abc" ref abc
... ...
Code
Code vonblabla()
Code vonabc()
Hohe Adressen
Niedrige Adressen
• Die Symboltabellen beinhalten die Zuordnung zwischen dem Namen zur Compile-Zeit und der Adresse im Code.
• Diese Tabellen liegen zur Laufzeit im RAM vor und werden bei der Reflexion durchsucht.
30VA – SS 2018 - Teil 7/RPC II
Was macht reflection(Name)? III
• reflection(Name) durchsucht also die Tabelle mit allen geladenen Routinen.
• Wenn der gesuchte Eintrag gefunden ist, wird die dort gespeicherte Start-Adresse zurück geliefert.
• Diese Startadresse wird dann als Wert beim goto benutzt.
proc call(Name,ParameterListe) { push(Array an Param-Addr) push(Return-Adresse) Routine-Adresse:= reflection(Name) copy(Block,Stack) pop(unterer Block) goto Routine-Adresse}
31VA – SS 2018 - Teil 7/RPC II
Wie geht das mit dem call in PHP?
• In PHP kann über eine Variable mit dem Inhalt des Namens der Routine indirekt eine Routine aufgerufen werden.
• Die Konstruktion mit der Parameterliste – wie oben - setzt ja Assembler voraus, was hier nicht geht; daher wird die Parameterliste als Array übergeben.
• In der Deklaration der aufzurufenden Routine muss dies berücksichtigt werden.
$routine= "blabla";$result= $routine($params);
function blabla($params) { ... ... $params[0]... ... return(...); }
Aufruf Deklaration
32VA – SS 2018 - Teil 7/RPC II
Wie geht das Laden in PHP?
function load($name) { require_once($name);}
function __autoload($Klasse) { require("$Klasse.php");}
Routinen Klassen
Dass dies so überschaubar ist, wird aber durch Performanz-Verluste erkauft:Der Compiler, der Lader und der Binder sind immer vorhanden undlaufen permanent mit.
33VA – SS 2018 - Teil 7/RPC II
Zusammenfassung
• Mit dem vorgestellten Verfahren kann genau der PHP-Mechanismus realisiert werden, der oben vorgestellt wurde:
und im Prinzip arbeitet auch so der PHP-Prozessor....
$routine= "blabla";$result= $routine($params);
entspricht routine= "blabla";result= call(routine,params);
34VA – SS 2018 - Teil 7/RPC II
Parallele Verarbeitung des receive() im Server I
• Der Router empfängt die Nachricht, generiert einen Thread und gibt an diesen die Nachricht zur Bearbeitung weiter. Im Router befindet sich die obige Endlos-Schleife mit einem wartenden receive().
• Alle Requests werden parallel verarbeitet, sofern genügend Threads vorhanden sind – ansonsten wartet der Router auf einen frei werdenden Thread.
Router
Empfang vonNachrichten
ProcessRequest
ProcessRequest
ProcessRequest
...
Generierenvon Threads
"Worker"
"Worker"
"Worker"
35VA – SS 2018 - Teil 7/RPC II
Parallele Verarbeitung des receive() im Server II
(1) proc Router(2) do {(3) msg:= receive()(4) tid:= new processCalls(msg)(5) }
thread processCalls(msg) proc:= msg.Name if exists(proc) { data:= unserialize(msg.ParamList) try result:= call "proc"(data) catch result:= Error } else { result:= Error } data:= serialize(result) reply(data)
36VA – SS 2018 - Teil 7/RPC II
Erläuterungen
• In Zeile (3) wird wartend(!) eine Nachricht empfangen.
• Dann wird in Zeile (4) für diese Nachricht ein neuer Thread erzeugt.
• Den Code der Threads zeigt die rechte Seite.
• Der Router erzeugt immer Threads ohne dass er auf deren Beendigung wartet. Jeder Thread beendet sich nach dem reply().
• Dann kommt er in den Pool für freie Threads.
• Wenn nun in Zeile (4) ein neuer Thread erzeugt wird, dann erfolgt dies nur dann, wenn im Pool ein freier Thread vorhanden ist, ansonsten wird auf einen frei werdenden gewartet.
• Dieser Mechanismus wird hier nicht dargestellt, auch fehlt die initiale Erzeugung der Threads für den Pool.
• Die maximale Threadanzahl ist daher begrenzt.
37VA – SS 2018 - Teil 7/RPC II
Alternative Realisierung
• Der Router schreibt die empfangene Nachricht in eine Warteschlange.
• Ist sie voll, wartet der Router auf einen freien Platz.
• Umgekehrt holen sich alle Threads die Aufträge aus der Warteschlange und legen sich schlafen, falls diese leer ist.
• Alle Threads werden initial generiert.
Router
Empfang vonNachrichten
Thread1
...
enqueue()
Thread2
ThreadN
dequeue()"Worker"
"Worker"
"Worker"
38VA – SS 2018 - Teil 7/RPC II
Hinweise
• Durch die parallele Verarbeitung von Nachrichten entstehen kritische Abschnitte, wenn auf gemeinsame Ressourcen, z.B. Dateien, zugegriffen wird.
• Daher sind Synchronisationen, wie Semaphoren, in den Routinen erforderlich.
• Im Prinzip arbeiten so alle Server, u.a. auch der Apache.
39VA – SS 2018 - Teil 7/RPC II
Eine komplexere Aufrufstruktur I
• Eine Komponente kann beide Rollen einnehmen: Als Server wird eine Routine ausgeführt, die wiederum eine Routine eines anderen Servers als Klient aufruft.
• Beim klassischen wartenden RPC sollten keine Probleme auftreten, da k2a() und k3a() nicht parallel ablaufen.
• Alle Komponenten haben einen Router und können daher die Aufrufe parallel verarbeiten.
Komponente1
Komponente2
Komponente3
Call k2a()Call k3a()
Call k2b()
Rollen:ServerClient
Rollen:Server
Rollen:Client
40VA – SS 2018 - Teil 7/RPC II
Eine komplexere Aufrufstruktur II
• Bei asynchronen send()-Operationen entsteht innerhalb der Komponente 2 ein kritischer Abschnitt, denn k2a() und k2b() können in verschiedenen Threads parallel dort ausgeführt werden.
• Die letzten beiden Fälle gelten für Parallelität innerhalb einer verteilten Anwendung mit privaten Daten.
Komponente1
Komponente2
Komponente3
Call k2a()Call k3a()
Call k2b()
Rollen:ServerClient
Rollen:Server
Rollen:Client
41VA – SS 2018 - Teil 7/RPC II
Eine komplexere Aufrufstruktur III
• Was ist, wenn dieselbe Komponente von mehreren unabhängigen Anwendungen benutzt wird?
• Die Daten der Anwendung1 müssen von den Daten der Anwendung2 getrennt – besonders innerhalb von der Komponenten3 - werden.
Komponente1Komponente2
Komponente3
Call k2a()Call k2a()
Anwendung1 Anwendung2
42VA – SS 2018 - Teil 7/RPC II
Wie lässt sich das realisieren? I
• Es werden innerhalb einer Anwendung bei der erstmaligen Benutzung einer Komponente mit einer Art new() eine Anwendungs-lokale Instanz der betreffenden Komponente gebildet. Mit dieser Instanz werden auch anwendungslokale Daten erzeugt.
• Eine Anwendung besteht dann aus mehreren Instanzen von Komponenten auf mehreren Knoten.
• Oder anders formuliert: Jede laufende Anwendung ist eine Instanz einer Anwendungsdefinition.
• Alle Instanzen der Anwendung bekommen dieselbe eindeutige ID, so dass anhand dieser ID festgestellt werden kann, welche Instanz einer Komponente zu welcher Anwendungsinstanz gehört.
43VA – SS 2018 - Teil 7/RPC II
Wie lässt sich das realisieren? II
• Bei jedem RPC und jeder Nachricht wird die ID der Anwendung mitgeteilt, so dass der aufgerufene Server die richtige Instanz bestimmen und dieser die Nachrichten zur Verarbeitung übergeben kann.
• Falls keine Instanz zu der ID auf einem Knoten existiert, wird sie dynamisch erzeugt, um dann wie im vorherigen Schritt fortzufahren.
• Bei diesem Verfahren breitet sich die verteilte Anwendung schrittweise über das Netz aus.
• Es ist aber möglich, dass Instanzen von Komponenten in mehreren Anwendungen gemeinsam benutzt werden, dann entfällt das dynamische Generieren einer Komponenteninstanz.
Strategie 1: Dynamisches Erzeugen
44VA – SS 2018 - Teil 7/RPC II
Wie lässt sich das realisieren? III
• Zum Start einer Applikation läuft ein Konstruktor-Prozess (Booter), der alle Instanzen aller benutzten Komponenten auf den verteilten Knoten erzeugt. Erst dann läuft die Applikation los.
• Dieses Hochfahren (Boot) wird durch eine Datei gesteuert.
• Für die Applikation und für jede Komponente gibt es einen Management-Teil, der die jeweiligen Konstruktoren lokal aufruft.(Siehe dazu den Teil über die Container für Komponenten)
• Dieses Vorgehen hat den Nachteil, dass es nur starre Strukturen von Anwendungen erlaubt.
• Es hat aber den Vorteil, dass gemeinsam benutzte Komponenten leicht berücksichtigt werden können.
Strategie 2: Erzeugen durch einen Konstruktor/Booter
45VA – SS 2018 - Teil 7/RPC II
Eine komplexere Aufrufstruktur IV - Rekursion
• Beim klassischen wartenden RPC ohne Parallelität auf der Seite des Servers entsteht bei Rekursion ein Deadlock.
• Mit Parallelität benutzt jeder Aufruf einen anderen Thread, der bei Benutzung gemeinsamer Ressourcen in kritische Abschnitte eintreten.
• Damit entsteht eine Kette von einander abhängigen Threads über verschiedene Systeme verteilt.
Komponente1
Komponente2
Call k1a()
Call k2a()
Rollen:ServerClient
Rollen:ServerClient
46VA – SS 2018 - Teil 7/RPC II
Behandlung von Rekursion
• Einfach verbieten :-( Es ist sowieso eine Deadlock-Erkennung notwendig...
• Die Behandlung jeder Nachricht in einen eigenen Thread legen, wie beim nicht-wartenden Server. Es entstehen kritische Abschnitte.
Lösungen
Thread1App1
Thread2App1
ThreadNApp1
LokaleDaten
Globale Daten von App1
LokaleDaten
LokaleDaten
Thread1App2
Thread2App2
LokaleDaten
Globale Daten von App2
LokaleDaten
47VA – SS 2018 - Teil 7/RPC II
Also...
• Jede Instanz einer verteilten Applikation erhält eine eindeutige ID.
• Alle verteilten Komponenten dieser Instanz erhalten dieselbe ID.
• Diese ID entspricht einer Session bzw. einem Kontext, innerhalb dessen die RPCs und die Nachrichten verarbeitet werden.
• Jede Instanz muss hochgefahren (gebootet) werden.
• Jede Instanz muss heruntergefahren (shutdown) werden.
48VA – SS 2018 - Teil 7/RPC II
Instanzen ohne ID
• Wenn eine Instanz einer Komponente keine ID hat, ist sie auch keiner verteilten Anwendung zugeordnet.
• Derartige Instanzen können dann gemeinsam von allen Anwendungen benutzt werden.
• Jetzt wäre noch der Fall möglich, dass eine Komponenten-Instanz zu einer Teilmenge von Applikationen gehört; dieser Instanz wird dann eine Menge von Applikationen-IDs zugeordnet, die diese Komponente benutzen können.
• Zur Verwaltung, welche Instanz zu welcher Anwendung mit welcher ID gehört, sind Verzeichnisse notwendig. Die dafür notwendigen Namensdienste werden später behandelt.
49VA – SS 2018 - Teil 7/RPC II
Verteilte Anwendungen
ObjectID1
ObjectID1
ObjectID1
ObjectID1
ObjectID2
ObjectID2
ObjectID2
Object
ObjectID1+2
Anwendung ID1
Anwendung ID2
Zwei verteilte Anwendungen mit zwei gemeinsam benutzten Objekten.Alle Objekte sind Instanzen von Komponenten, die irgendwo im Netzresidieren.
Diese Komponente wird von allen Anwendungengemeinsam benutzt.
Diese Komponente wird nur von ID1 und ID2gemeinsam benutzt.
50VA – SS 2018 - Teil 7/RPC II
Leider fehlt noch etwas...
• Anhand der ID der Anwendung wird die Nachricht einer Instanz (Objekt) auf einem Server zugeordnet – schön.
• Aber bei Rekursion zwischen zwei Knoten kommt diese Zuordnung durcheinander, denn es müssen Daten den einzelnen Aufrufen zugeordnet werden.
• Dazu das folgende Szenario zwischen zwei Rechnern Alice und Bob – beide Objekte gehören zur selben Anwendung.
51VA – SS 2018 - Teil 7/RPC II
Das RPC-Protokoll bei Rekursion I
send() receive()
receive()
Request
reply()
send()receive()
reply()
Request
send() Request receive()
reply()receive()
receive()
Response
Alice Bob
Von welchemAufruf ist dasdas Ergebnis?
Response
Response
Die bunten Striche zeigen die drei Threads während ihres Ablaufs.Request: initiale Nachricht eines RPC, Response: Rückantwort eines RPCs
52VA – SS 2018 - Teil 7/RPC II
Das RPC-Protokoll bei Rekursion II
• Die Routine A auf Alice ruft die Routine B von Bob auf (grün).
• Während der Ausführung von B auf Bobs Rechner ruft Bob die Routine A von Alices Rechner auf (lila).
• Innerhalb von A auf Alices Rechner wird die Routine B von Bob aufgerufen (braun)
• Also liegt eine Rekursion vor: A() -> B(grün) -> A(lila) -> B(braun)
• Es wird also 2x B() auf Bob aufgerufen. Auf dem Rückweg von Bob zu Alice (Response) ist nicht klar zu welchem Thread das Ergebnis gehört,
• es sei denn, jeder Aufruf bekommt eine eindeutige ID; dann kann beim receive() eines Responses festgestellt werden, von welchem Aufruf dies das Ergebnis ist.
53VA – SS 2018 - Teil 7/RPC II
Das RPC-Protokoll bei Rekursion III
• Jeder RPC wird daher mit einer eindeutigen ID des Aufrufs versehen. Beim Empfang einer Nachricht wird diese dann dem richtigen Thread zugeordnet, wenn dort auf eine Antwort mit der betreffenden ID gewartet wird.
• Diese Routine heiße receiveReply().
• Jetzt könnte das send() auch immer ein no-wait-send() sein, während receive*()-Operationen immer wartend sind...
send()
receive()
reply()
receiveReply()
54VA – SS 2018 - Teil 7/RPC II
Die vielen IDs
• Nun kann auch ein Klient mehrere nicht-wartende send() ausführen und trotzdem die Antworten richtig anhand der ID seinen eigenen Routinen zuordnen.
• In jeder Nachricht befinden sich also zwei IDs:– ID der Anwendung (Adressierung der Instanzen)
– ID des Aufrufs (RPC) bzw. der Nachricht
• Es fehlt noch die ID der Epoche – hier wird davon ausgegangen, dass alle Beteiligten durchhalten.
• Falls dies nicht so ist, müssen Bereiche von Aufruf-IDs jeweils den Epochen zugeordnet werden.