Kapitel 4. Bewährte Methoden für die Instrumentierung
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Der erste Schritt einer jeden Reise ist der schwierigste - auch der, deine Anwendungen für verteiltes Tracing zu instrumentieren. Fragen über Fragen tauchen auf: Was soll ich zuerst tun? Woher weiß ich, dass ich alles richtig mache? Wann bin ich fertig? Jede Anwendung ist anders, aber dieses Kapitel bietet einige allgemeine Ratschläge und Strategien, um bewährte Methoden für die Instrumentierung von Anwendungen zu entwickeln.
Bewährte Methoden gibt es nicht in einem Vakuum. Die Daten, die deine Messgeräte erzeugen, werden von einem Spurenanalysesystem erfasst, das sie analysiert und verarbeitet. Als Messgerätehersteller ist es wichtig, dass du dem System die bestmöglichen Daten lieferst!
Als Grundlage für unsere Diskussion werden wir zunächst eine Anwendung besprechen, die nicht instrumentiert ist. Dann sprechen wir über die ersten Schritte, um eine bestehende Anwendung zu instrumentieren - mit Blick auf die Knoten und Kanten - und einige gängige Methoden, um dies zu erreichen. Wir gehen auf bewährte Methoden für die Erstellung von Spans ein und erläutern, welche Art von Informationen du ihnen hinzufügen möchtest. Wir besprechen, wie du Tracing als Teil der Anwendungsentwicklung einsetzen kannst, um zu überprüfen, ob deine Architektur so funktioniert, wie du es erwartest. Zum Schluss geben wir dir einige Signale, die dir zeigen, wann du "zu viel" Instrumentierung erreicht hast.
Spurensuche am Beispiel
Es ist eine Binsenweisheit, dass man am besten lernt, indem man etwas tut. Um zu verstehen, wie du eine Microservices-Anwendung für verteiltes Tracing instrumentieren solltest, musst du zunächst eine Microservices-Anwendung haben. Wir haben eine Beispielanwendung erstellt, die wir zur Veranschaulichung einiger Techniken und bewährter Methoden verwenden werden. In diesem Abschnitt beschreiben wir, wie du den Dienst auf deinem Computer ausführen kannst, um den Beispielen zu folgen, und zeigen dir einige Grundprinzipien der Instrumentierung, die du ganz allgemein zur Instrumentierung deiner eigenen Dienste anwenden kannst.
Installation der Beispielanwendung
Wir haben eine kleine Microservice-Anwendung entwickelt, um die wichtigen Konzepte zu demonstrieren, die für die Instrumentierung einer Anwendung erforderlich sind. Um sie auszuführen, brauchst du eine aktuelle Version der Go Runtime und Node.JS auf deinem Computer. Außerdem musst du eine Kopie des Quellcodes der Anwendung herunterladen, die du in diesem GitHub-Repositoryfindest . Dukannst sie mit der Versionskontrolle Git auschecken oder ein Zip-Archiv mit den Dateien herunterladen und entpacken. Sobald du eine lokale Kopie der Quelldateien hast, ist es ganz einfach, die Software zu starten: Führe in einem Terminalfenster go run cmd/<binary>/main.go
aus dem Verzeichnis microcalc
aus, um jeden Dienst zu starten. Um die Client-Anwendung zu starten, musst du npm install
im Unterverzeichnis web
und dann npm start
ausführen.
Die Anwendung selbst ist ein einfacher Taschenrechner mit drei Komponenten. Der Client ist eine in HTML und JavaScript geschriebene Webanwendung für den Browser, die eine Schnittstelle zum Backend-Dienst bietet. Die nächste Hauptkomponente ist ein API-Proxy, der Anfragen vom Client entgegennimmt und sie an den entsprechenden Worker-Dienst weiterleitet. Die letzte Komponente, die Operator-Worker, sind Dienste, die eine Liste von Operanden erhalten, die entsprechende mathematische Operation mit diesen Operanden durchführen und das Ergebnis zurückgeben.
Hinzufügen von grundlegenden verteilten Suchläufen
Bevor du Tracing hinzufügst, solltest du dir den Code selbst und seine Funktionsweise ansehen. Wir sehen uns den Code der Reihe nach an - zuerst den Web-Client, dann den API-Dienst und schließlich die Worker. Wenn du erst einmal verstanden hast, was die einzelnen Teile des Codes tun, ist es nicht nur einfacher zu verstehen, wie du den Dienst instrumentieren kannst, sondern auch warum (siehe Abbildung 4-1).
Der Client-Dienst ist sehr einfach - ein einfaches HTML- und JavaScript-Frontend. Das HTML stellt ein Formular dar, das wir in JavaScript abfangen und ein XMLHttpRequest
erstellen, das die Daten an die Backend-Dienste überträgt. Die uninstrumentierte Version dieses Codes ist in Beispiel 4-1 zu sehen. Wie du siehst, machen wir hier nichts besonders Kompliziertes - wir erstellen einen Hook für das Formularelement und warten auf das Ereignis onClick
, das beim Drücken der Schaltfläche Submit ausgelöst wird.
Beispiel 4-1. Uninstrumentierter Kundendienst
const
handleForm
=
()
=>
{
const
endpoint
=
'http://localhost:3000/calculate'
let
form
=
document
.
getElementById
(
'calc'
)
const
onClick
=
(
event
)
=>
{
event
.
preventDefault
();
let
fd
=
new
FormData
(
form
);
let
requestPayload
=
{
method
:
fd
.
get
(
'calcMethod'
),
operands
:
tokenizeOperands
(
fd
.
get
(
'values'
))
};
calculate
(
endpoint
,
requestPayload
).
then
((
res
)
=>
{
updateResult
(
res
);
});
}
form
.
addEventListener
(
'submit'
,
onClick
)
}
const
calculate
=
(
endpoint
,
payload
)
=>
{
return
new
Promise
(
async
(
resolve
,
reject
)
=>
{
const
req
=
new
XMLHttpRequest
();
req
.
open
(
'POST'
,
endpoint
,
true
);
req
.
setRequestHeader
(
'Content-Type'
,
'application/json'
);
req
.
setRequestHeader
(
'Accept'
,
'application/json'
);
req
.
send
(
JSON
.
stringify
(
payload
))
req
.
onload
=
function
()
{
resolve
(
req
.
response
);
};
});
};
Dein erster Schritt bei der Instrumentierung sollte darin bestehen, die Interaktion zwischen diesem Dienst und unseren Backend-Diensten zu verfolgen. OpenTelemetry bietet ein hilfreiches Instrumentierungs-Plug-in für die Nachverfolgung von XMLHttpRequest
, das du für deine grundlegende Instrumentierung verwenden solltest. Nachdem du die OpenTelemetry-Pakete importiert hast, musst du deinen Tracer und die Plug-ins einrichten. Wenn du das geschafft hast, kannst du deine Methodenaufrufe an XMLHttpRequest
mit Tracing-Code ummanteln, wie in Beispiel 4-2 gezeigt.
Beispiel 4-2. Erstellen und Konfigurieren deines Tracers
// After importing dependencies, create a tracer and configure it
const
webTracerWithZone
=
new
WebTracer
(
{
scopeManager
:
new
ZoneScopeManager
(
)
,
plugins
:
[
new
XMLHttpRequestPlugin
(
{
ignoreUrls
:
[
/localhost:8090\/sockjs-node/
]
,
propagateTraceHeaderCorsUrls
:
[
'http://localhost:3000/calculate'
]
}
)
]
}
)
;
webTracerWithZone
.
addSpanProcessor
(
new
SimpleSpanProcessor
(
new
ConsoleSpanExporter
(
)
)
)
;
const
handleForm
=
(
)
=>
{
const
endpoint
=
'http://localhost:3000/calculate'
let
form
=
document
.
getElementById
(
'calc'
)
const
onClick
=
(
event
)
=>
{
event
.
preventDefault
(
)
;
const
span
=
webTracerWithZone
.
startSpan
(
'calc-request'
,
{
parent
:
webTracerWithZone
.
getCurrentSpan
(
)
}
)
;
let
fd
=
new
FormData
(
form
)
;
let
requestPayload
=
{
method
:
fd
.
get
(
'calcMethod'
)
,
operands
:
tokenizeOperands
(
fd
.
get
(
'values'
)
)
}
;
webTracerWithZone
.
withSpan
(
span
,
(
)
=>
{
calculate
(
endpoint
,
requestPayload
)
.
then
(
(
res
)
=>
{
webTracerWithZone
.
getCurrentSpan
(
)
.
addEvent
(
'request-complete'
)
;
span
.
end
(
)
;
updateResult
(
res
)
;
}
)
;
}
)
;
}
form
.
addEventListener
(
'submit'
,
onClick
)
}
Führe die Seite in web
mit npm start
aus und klicke bei geöffneter Browserkonsole auf Absenden - du solltest sehen, dass Spans in die Konsolenausgabe geschrieben werden. Jetzt hast du deinem Client-Dienst eine grundlegende Ablaufverfolgung hinzugefügt!
Jetzt schauen wir uns die Backend-Dienste an - die API und die Worker. Der API-Anbieterdienst nutzt die Go net/http
Bibliothek, um ein HTTP-Framework bereitzustellen, das wir als RPC-Framework für die Übermittlung von Nachrichten zwischen dem Client, dem API-Dienst und den Workern verwenden. Wie in Abbildung 4-1 zu sehen ist, empfängt die API Nachrichten im JSON-Format vom Client, sucht den entsprechenden Worker in seiner Konfiguration, sendet die Operanden an den entsprechenden Worker-Dienst und gibt das Ergebnis an den Client zurück.
Der API-Dienst hat zwei Hauptmethoden, die uns interessieren: Run
und calcHandler
. Die Methode Run
in Beispiel 4-3 initialisiert den HTTP-Router und richtet den HTTP-Server ein. calcHandler
führt die Logik zur Bearbeitung eingehender Anfragen aus, indem sie den JSON-Body des Clients parst, ihn einem Worker zuordnet und dann eine wohlgeformte Anfrage an den Worker-Service erstellt.
Beispiel 4-3. Methode ausführen
func
Run
()
{
mux
:=
http
.
NewServeMux
()
mux
.
Handle
(
"/"
,
http
.
HandlerFunc
(
rootHandler
))
mux
.
Handle
(
"/calculate"
,
http
.
HandlerFunc
(
calcHandler
))
services
=
GetServices
()
log
.
Println
(
"Initializing server..."
)
err
:=
http
.
ListenAndServe
(
":3000"
,
mux
)
if
err
!=
nil
{
log
.
Fatalf
(
"Could not initialize server: %s"
,
err
)
}
}
func
calcHandler
(
w
http
.
ResponseWriter
,
req
*
http
.
Request
)
{
calcRequest
,
err
:=
ParseCalcRequest
(
req
.
Body
)
if
err
!=
nil
{
http
.
Error
(
w
,
err
.
Error
(),
http
.
StatusBadRequest
)
return
}
var
url
string
for
_
,
n
:=
range
services
.
Services
{
if
strings
.
ToLower
(
calcRequest
.
Method
)
==
strings
.
ToLower
(
n
.
Name
)
{
j
,
_
:=
json
.
Marshal
(
calcRequest
.
Operands
)
url
=
fmt
.
Sprintf
(
"http://%s:%d/%s?o=%s"
,
n
.
Host
,
n
.
Port
,
strings
.
ToLower
(
n
.
Name
),
strings
.
Trim
(
string
(
j
),
"[]"
))
}
}
if
url
==
""
{
http
.
Error
(
w
,
"could not find requested calculation method"
,
http
.
StatusBadRequest
)
}
client
:=
http
.
DefaultClient
request
,
_
:=
http
.
NewRequest
(
"GET"
,
url
,
nil
)
res
,
err
:=
client
.
Do
(
request
)
if
err
!=
nil
{
http
.
Error
(
w
,
err
.
Error
(),
http
.
StatusInternalServerError
)
return
}
body
,
err
:=
ioutil
.
ReadAll
(
res
.
Body
)
res
.
Body
.
Close
()
if
err
!=
nil
{
http
.
Error
(
w
,
err
.
Error
(),
http
.
StatusInternalServerError
)
return
}
resp
,
err
:=
strconv
.
Atoi
(
string
(
body
))
if
err
!=
nil
{
http
.
Error
(
w
,
err
.
Error
(),
http
.
StatusInternalServerError
)
return
}
fmt
.
Fprintf
(
w
,
"%d"
,
resp
)
}
Beginnen wir an der Kante des Dienstes und suchen nach Instrumenten für das RPC-Framework. Da wir in Beispiel 4-4 HTTP für die Kommunikation zwischen den Diensten verwenden, musst du den Code des HTTP-Frameworks instrumentieren. Du könntest das auch selbst schreiben, aber es ist in der Regel besser, nach Open-Source-Instrumenten für diese gängigen Komponenten zu suchen. In diesem Fall können wir das bestehende othttp
-Paket des OpenTelemetry-Projekts nutzen, um unsere HTTP-Routen mit Tracing-Instrumenten auszustatten.
Beispiel 4-4. Verwendung des bestehenden Pakets othttp
des OpenTelemetry-Projekts, um unsere HTTP-Routen mit Tracing-Instrumenten zu versehen
std
,
err
:=
stdout
.
NewExporter
(
stdout
.
Options
{
PrettyPrint
:
true
}
)
traceProvider
,
err
:=
sdktrace
.
NewProvider
(
sdktrace
.
WithConfig
(
sdktrace
.
Config
{
DefaultSampler
:
sdktrace
.
AlwaysSample
(
)
}
)
,
sdktrace
.
WithSyncer
(
std
)
)
mux
.
Handle
(
"/"
,
othttp
.
NewHandler
(
http
.
HandlerFunc
(
rootHandler
)
,
"root"
,
othttp
.
WithPublicEndpoint
(
)
)
)
mux
.
Handle
(
"/calculate"
,
othttp
.
NewHandler
(
http
.
HandlerFunc
(
calcHandler
)
,
"calculate"
,
othttp
.
WithPublicEndpoint
(
)
)
)
Behandle Fehler und dergleichen angemessen. Einige Codes wurden aus Gründen der Übersichtlichkeit gelöscht.
Als Erstes müssen wir einen Exporter registrieren, um die Telemetrie-Ausgabe zu sehen. Das kann auch ein externes Analyse-Backend sein, aber wir verwenden vorerst
stdout
.Dann registrierst du den Exporter beim Trace-Provider und stellst ihn so ein, dass er 100% der Spans abfragt.
Was bedeutet das für uns? Das Instrumentierungs-Plug-in übernimmt eine ganze Reihe von Aufgaben für uns, z. B. die Weitergabe von Spans aus eingehenden Anfragen und das Hinzufügen einiger nützlicher Attribute (siehe Beispiel 4-5), wie z. B. den HTTP-Methodentyp, den Antwortcode und mehr. Durch das Hinzufügen dieser Attribute können wir Anfragen an unser Backend-System zurückverfolgen. Achte besonders auf den Parameter othttp.WithPublicEndpoint
, den wir an unseren Instrumentation Handler übergeben haben - dadurch wird der Trace-Kontext vom Client an unsere Backend-Dienste weitergeleitet. Anstatt die gleiche TraceID vom Client zu behalten, wird der eingehende Kontext mit einem neuen Trace als Link verknüpft.
Beispiel 4-5. JSON-Spannungsausgabe
{
"SpanContext"
:
{
"TraceID"
:
"060a61155cc12b0a83b625aa1808203a"
,
"SpanID"
:
"a6ff374ec6ed5c64"
,
"TraceFlags"
:
1
},
"ParentSpanID"
:
"0000000000000000"
,
"SpanKind"
:
2
,
"Name"
:
"go.opentelemetry.io/plugin/othttp/add"
,
"StartTime"
:
"2020-01-02T17:34:01.52675-05:00"
,
"EndTime"
:
"2020-01-02T17:34:01.526805742-05:00"
,
"Attributes"
:
[
{
"Key"
:
"http.host"
,
"Value"
:
{
"Type"
:
"STRING"
,
"Value"
:
"localhost:3000"
}
},
{
"Key"
:
"http.method"
,
"Value"
:
{
"Type"
:
"STRING"
,
"Value"
:
"GET"
}
},
{
"Key"
:
"http.path"
,
"Value"
:
{
"Type"
:
"STRING"
,
"Value"
:
"/"
}
},
{
"Key"
:
"http.url"
,
"Value"
:
{
"Type"
:
"STRING"
,
"Value"
:
"/"
}
},
{
"Key"
:
"http.user_agent"
,
"Value"
:
{
"Type"
:
"STRING"
,
"Value"
:
"HTTPie/1.0.2"
}
},
{
"Key"
:
"http.wrote_bytes"
,
"Value"
:
{
"Type"
:
"INT64"
,
"Value"
:
27
}
},
{
"Key"
:
"http.status_code"
,
"Value"
:
{
"Type"
:
"INT64"
,
"Value"
:
200
}
}
],
"MessageEvents"
:
null
,
"Links"
:
null
,
"Status"
:
0
,
"HasRemoteParent"
:
false
,
"DroppedAttributeCount"
:
0
,
"DroppedMessageEventCount"
:
0
,
"DroppedLinkCount"
:
0
,
"ChildSpanCount"
:
0
}
In calcHandler
wollen wir etwas Ähnliches tun, um unsere ausgehende RPC an den Worker Service zu instrumentieren. Auch hier enthält OpenTelemetry ein Instrumentierungs-Plug-in für den HTTP-Client von Go, das wir verwenden können (siehe Beispiel 4-6).
Beispiel 4-6. API-Handler
client
:=
http
.
DefaultClient
// Get the context from the request in order to pass it to the instrumentation plug-in
ctx
:=
req
.
Context
()
request
,
_
:=
http
.
NewRequestWithContext
(
ctx
,
"GET"
,
url
,
nil
)
// Create a new outgoing trace
ctx
,
request
=
httptrace
.
W3C
(
ctx
,
request
)
// Inject the context into the outgoing request
httptrace
.
Inject
(
ctx
,
request
)
// Send the request
res
,
err
:=
client
.
Do
(
request
)
Dadurch werden der ausgehenden Anfrage W3C-Tracing-Header hinzugefügt, die vom Worker-Dienst aufgegriffen werden können und den Trace-Kontext über die Leitung weitergeben. Auf diese Weise können wir die Beziehung zwischen unseren Diensten sehr einfach visualisieren, da die im Worker-Dienst erstellten Spans denselben Trace-Identifier haben wie die übergeordneten Dienste.
Das Hinzufügen von Tracing zu den Worker-Diensten ist ebenso einfach, da wir einfach die Router-Methode mit dem OpenTelemetry-Trace-Handler ummanteln, wie in Beispiel 4-7 gezeigt.
Beispiel 4-7. Hinzufügen des Handlers
// You also need to add an exporter and register it with the trace provider,
// as in the API server, but the code is the same
mux
.
Handle
(
"/"
,
othttp
.
NewHandler
(
http
.
HandlerFunc
(
addHandler
),
"add"
,
othttp
.
WithPublicEndpoint
())
)
Die Instrumentierungs-Plug-ins übernehmen einen Großteil der Aufgaben, die in dieser und anderen Sprachen anfallen, wie z. B. das Extrahieren des Spankontextes aus der eingehenden Anfrage, das Erstellen eines neuen Child-Spans (oder ggf. eines neuen Root-Spans) und das Hinzufügen dieses Spans zum Anfragekontext. Im nächsten Abschnitt werden wir uns ansehen, wie wir diese grundlegende Instrumentierung mit benutzerdefinierten Ereignissen und Attributen aus unserer Geschäftslogik erweitern können, um den Nutzen unserer Spans und Traces zu erhöhen.
Kundenspezifische Instrumentierung
An diesem Punkt haben wir die entscheidenden Teile des Tracings in unseren Diensten eingerichtet; jeder RPC wird verfolgt, so dass wir eine einzelne Anfrage sehen können, wie sie von unserem Client-Service zu all unseren Backend-Diensten gelangt. Außerdem haben wir in unserer Geschäftslogik eine Spanne von zur Verfügung, die wir mit benutzerdefinierten Attributen oder Ereignissen erweitern können. Was sollen wir also tun? Im Allgemeinen liegt das wirklich an dir, dem Instrumentenbauer. Wir werden das im Abschnitt "Effektives Tagging" näher erläutern, aber es ist hilfreich, für einige Dinge in deiner Geschäftslogik benutzerdefinierte Instrumente hinzuzufügen - zum Beispiel für die Erfassung und Protokollierung von Fehlerzuständen oder die Erstellung von Child-Spans, die die Funktionsweise eines Dienstes näher beschreiben. In unserem API-Dienst haben wir ein Beispiel dafür implementiert, indem den lokalen Kontext an eine andere Methode (ParseCalcRequest
) weitergibt, in der wir einen neuen Span erstellen und ihn mit benutzerdefinierten Ereignissen erweitern, wie in Beispiel 4-8 gezeigt.
Beispiel 4-8. Erweitern eines Spans mit benutzerdefinierten Ereignissen
var
calcRequest
CalcRequest
err
=
tr
.
WithSpan
(
ctx
,
"generateRequest"
,
func
(
ctx
context
.
Context
)
error
{
calcRequest
,
err
=
ParseCalcRequest
(
ctx
,
b
)
return
err
})
In Beispiel 4-9 kannst du sehen, was wir mit dem übergebenen Kontext machen - wir holen uns den aktuellen Span aus dem Kontext und fügen ihm Ereignisse hinzu. In diesem Fall haben wir einige Informationsereignisse zu dem hinzugefügt, was die Funktion tatsächlich tut (das Parsen des Körpers unserer eingehenden Anfrage in ein Objekt) und den Status des Spans geändert, wenn die Operation fehlgeschlagen ist.
Beispiel 4-9. Hinzufügen von Ereignissen zur Spanne
func
ParseCalcRequest
(
ctx
context
.
Context
,
body
[]
byte
)
(
CalcRequest
,
error
)
{
var
parsedRequest
CalcRequest
trace
.
CurrentSpan
(
ctx
).
AddEvent
(
ctx
,
"attempting to parse body"
)
trace
.
CurrentSpan
(
ctx
).
AddEvent
(
ctx
,
fmt
.
Sprintf
(
"%s"
,
body
))
err
:=
json
.
Unmarshal
(
body
,
&
parsedRequest
)
if
err
!=
nil
{
trace
.
CurrentSpan
(
ctx
).
SetStatus
(
codes
.
InvalidArgument
)
trace
.
CurrentSpan
(
ctx
).
AddEvent
(
ctx
,
err
.
Error
())
trace
.
CurrentSpan
(
ctx
).
End
()
return
parsedRequest
,
err
}
trace
.
CurrentSpan
(
ctx
).
End
()
return
parsedRequest
,
nil
}
Nachdem du nun weißt, wie du einer Anwendung Instrumente hinzufügen kannst, lass uns einen Schritt zurückgehen. Du denkst vielleicht, dass "echte" Anwendungen natürlich viel komplexer und komplizierter sind als ein eigens erstelltes Beispiel. Die gute Nachricht ist jedoch, dass die Grundprinzipien, die wir hier gelernt und umgesetzt haben, generell für die Instrumentierung von Software jeder Größe und Komplexität anwendbar sind. Werfen wir einen Blick auf die Instrumentierung von Software und darauf, wie wir diese Grundprinzipien auf Microservice-Anwendungen anwenden können.
Wo man anfängt - Knoten und Kanten
Menschen neigen dazu, bei der Lösung von Problemen außen zu beginnen - egal ob es sich um organisatorische, finanzielle, rechnerische oder sogar kulinarische Probleme handelt. Am einfachsten ist es, dort anzufangen, wo man sich selbst am nächsten ist. Der gleiche Ansatz gilt für die Instrumentierung von Diensten zur verteilten Nachverfolgung.
In der Praxis ist es aus drei wichtigen Gründen effektiv, von außen zu beginnen. Der erste Grund ist, dass die Kanten deines Dienstes am einfachsten zu sehen sind - und damit auch am einfachsten zu bearbeiten. Es ist relativ einfach, Dinge hinzuzufügen, die einen Dienst umgeben, auch wenn es schwierig ist, den Dienst selbst zu ändern. Zweitens ist es in der Regel organisatorisch effizienter, von außen zu beginnen. Es kann schwierig sein, verschiedene Teams davon zu überzeugen, eine verteilte Ablaufverfolgung einzuführen, vor allem, wenn der Wert dieser Ablaufverfolgung für sich genommen schwer zu erkennen ist. Schließlich erfordert verteiltes Tracing die Weitergabe von Kontext - jeder Dienst muss über den Trace des Aufrufers Bescheid wissen, und jeder Dienst, den wir aufrufen, muss wissen, dass auch er in einem Trace enthalten ist. Aus diesem Grund ist es sehr sinnvoll, bei der Instrumentierung einer bestehenden Anwendung von außen nach innen zu gehen. Dies kann in Form einer Framework-Instrumentierung oder einer Service-Mesh-Instrumentierung (oder einer entsprechenden Komponente) geschehen.
Framework Instrumentation
In jeder verteilten Anwendung müssen die Dienste miteinander kommunizieren. Dieser RPC-Verkehr kann über eine Vielzahl von Protokollen und Transportmethoden erfolgen - strukturierte Daten über HTTP, Protokollpuffer über gRPC, Apache Thrift, benutzerdefinierte Protokolle über TCP-Sockets und mehr. Auf beiden Seiten dieser Verbindung muss eine gewisse Gleichwertigkeit herrschen. Deine Dienste müssen die gleiche Sprache sprechen, wenn sie miteinander kommunizieren!
Es gibt zwei entscheidende Komponenten, wenn es um die Instrumentierung auf Framework-Ebene geht. Erstens müssen unsere Frameworks es uns ermöglichen, context propagation, die Übertragung von Trace-Identifikatoren über das Netzwerk, durchzuführen. Zweitens sollten unsere Frameworks uns dabei helfen, Spans für jeden Dienst zu erstellen.
Die Kontextfortpflanzung ist vielleicht die leichter zu lösende Herausforderung. Werfen wir einen weiteren Blick auf MicroCalc, um sie zu diskutieren. Wie in Abbildung 4-2 dargestellt, verwenden wir nur eine Transportmethode (HTTP), aber zwei verschiedene Arten der Nachrichtenübermittlung: JSON und Abfrageparameter. Du kannst dir vorstellen, dass einige dieser Verbindungen auch anders hergestellt werden könnten. Wir könnten zum Beispiel die Kommunikation zwischen unserem API-Dienst und den Arbeitsdiensten so umgestalten, dass sie gRPC, Thrift oder sogar graphQL verwendet. Der Transport selbst ist weitgehend irrelevant, die Anforderung ist lediglich, dass wir in der Lage sind, den Trace-Kontext an den nächsten Dienst weiterzugeben.
Sobald du die Transportprotokolle identifiziert hast, über die deine Dienste kommunizieren, solltest du dir den kritischen Pfad für deine Dienstaufrufe ansehen. Kurz gesagt, identifiziere den Pfad der Aufrufe, wenn eine Anfrage durch deine Dienste läuft. In dieser Phase der Analyse solltest du dich auf die Komponenten konzentrieren, die als Knotenpunkt für Anfragen dienen. Warum das so ist? In der Regel kapseln diese Komponenten die Vorgänge im Backend logisch und stellen eine API für mehrere Clients bereit (z. B. browserbasierte Webclients oder native Anwendungen auf einem mobilen Gerät). Wenn du diese Komponenten zuerst instrumentierst, kannst du in kürzerer Zeit einen Nutzen aus dem Tracing ziehen. Im vorangegangenen Beispiel erfüllt unser API-Proxy-Dienst diese Kriterien - unser Kunde kommuniziert für alle nachgelagerten Aktionen direkt über ihn.
Nachdem du den Dienst, den du instrumentieren willst, identifiziert hast, solltest du dir überlegen, welche Transportmethode für die Anfragen verwendet wird, die in den Dienst hinein- und aus ihm herausgehen. Unser API-Proxy-Dienst kommuniziert ausschließlich über strukturierte Daten mit HTTP, aber das ist nur ein Beispiel, um es kurz zu machen - in der realen Welt gibt es oft Dienste, die mehrere Transporte akzeptieren und auch ausgehende Anfragen über mehrere Transporte senden können. Wenn du deine eigenen Anwendungen instrumentierst, solltest du dir dieser Komplikationen bewusst sein.
Nun schauen wir uns an, wie die Instrumentierung unseres Dienstes tatsächlich funktioniert. Bei der Framework-Instrumentierung geht es darum, das Transport-Framework deines Dienstes selbst zu instrumentieren. Dies kann oft als eine Art Middleware in deinem Anfragepfad implementiert werden: Code, der für jede eingehende Anfrage ausgeführt wird. Das ist ein gängiges Muster, um z. B. Logging zu deinen Anfragen hinzuzufügen. Welche Middlewares möchtest du für diesen Dienst implementieren? Logischerweise musst du die folgenden Aufgaben erfüllen:
-
Prüfe, ob eine eingehende Anfrage einen Trace-Kontext enthält, der anzeigt, dass die Anfrage verfolgt wird. Wenn ja, füge diesen Kontext zur Anfrage hinzu.
-
Prüfe, ob ein Kontext in der Anfrage existiert. Wenn der Kontext existiert, erstelle einen neuen Span als Kind des geflossenen Kontexts. Andernfalls erstellst du einen neuen Root-Span. Füge diesen Span zu der Anfrage hinzu.
-
Überprüfe, ob ein Bereich in der Anfrage existiert. Wenn ein Span vorhanden ist, füge ihm andere relevante Informationen aus dem Anfragekontext hinzu, z. B. die Route, Benutzerkennungen usw. Andernfalls tust du nichts und fährst fort.
Diese drei logischen Aktionen können mit Hilfe von Instrumentierungsbibliotheken, wie wir sie in Kapitel 3 besprochen haben, in einer einzigen Middleware kombiniert werden. Wir können eine einfache Version dieser Middleware in Golang implementieren, indem wir die OpenTracing-Bibliothek verwenden, wie Beispiel 4-10 zeigt, oder indem wir Instrumentierungs-Plug-ins verwenden, die mit Frameworks wie OpenTelemetry gebündelt sind, wie wir in "Tracing by Example" gezeigt haben .
Beispiel 4-10. Tracing Middleware
func
TracingMiddleware
(
t
opentracing
.
Tracer
,
h
http
.
HandlerFunc
)
http
.
HandlerFunc
{
return
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
spanContext
,
_
:=
t
.
Extract
(
opentracing
.
HTTPHeaders
,
opentracing
.
HTTPHeadersCarrier
(
r
.
Header
))
span
:=
t
.
StartSpan
(
r
.
Method
,
opentracing
.
ChildOf
(
spanContext
))
span
.
SetTag
(
"route"
,
r
.
URL
.
EscapedPath
())
r
=
r
.
WithContext
(
opentracing
.
ContextWithSpan
(
r
.
Context
(),
span
.
Context
()))
defer
span
.
Finish
()
h
(
w
,
r
)
span
.
SetTag
(
"status"
,
w
.
ResponseCode
)
}
)
}
Dieses Snippet erfüllt die oben genannten Ziele - wir versuchen zunächst, einen Span-Kontext aus den Headern der Anfrage zu extrahieren. In diesem Beispiel gehen wir von der Annahme aus, dass unser Span-Kontext über HTTP-Header und nicht über ein binäres Format übertragen wird. OpenTracing definiert diese Header im Allgemeinen in den folgenden Formaten:
ot-span-id
-
Eine 64- oder 128-Bit-Ganzzahl ohne Vorzeichen
ot-trace-id
-
Eine 64- oder 128-Bit-Ganzzahl ohne Vorzeichen
ot-sampled
-
Ein boolescher Wert, der angibt, ob der vorgelagerte Dienst den Trace abgetastet hat
Bitte beachte, dass dies nicht die einzigen Kopfzeilentypen sind, die einen Span-Kontext enthalten können. Mehr über andere beliebte Kopfzeilenformate erfährst du in "OpenTracing und OpenCensus".
Wie wir in Kapitel 2 gelernt haben, ist der Span-Kontext entscheidend für die Weitergabe eines Traces in unseren Diensten, weshalb wir ihn zunächst aus der eingehenden Anfrage extrahieren. Nachdem wir alle eingehenden Header extrahiert haben, erstellt unsere Middleware einen neuen Span, der nach der durchgeführten HTTP-Operation (GET, POST, PUT usw.) benannt ist, fügt ein Tag hinzu, das die angeforderte Route angibt, und fügt den neuen Span dann dem Go-Kontextobjekt hinzu. Schließlich setzt die Middleware die Anfragekette fort. Wenn die Anfrage aufgelöst wird, fügt sie den Antwortcode der Anfrage zum Span hinzu, der implizit durch unseren Aufruf von defer
geschlossen wird.
Stellen wir uns vor, wir würden hier aufhören. Wenn du diese Middleware zusammen mit einem Tracer und einem Trace-Analyzer zum API-Proxy-Dienst hinzufügen würdest, was würdest du dann sehen? Nun, zum einen würde jede einzelne eingehende HTTP-Anfrage aufgezeichnet werden. So kannst du deine API-Endpunkte bei jeder eingehenden Anfrage auf Latenzzeiten überwachen - ein wichtiger erster Schritt bei der Überwachung deiner Anwendung. Ein weiterer Vorteil ist, dass du deinen Trace jetzt in den Kontext übertragen hast, so dass weitere Funktions- oder RPC-Aufrufe Informationen hinzufügen oder neue Spans auf der Grundlage des Traces erstellen können. In der Zwischenzeit kannst du immer noch auf Latenzinformationen pro API-Route zugreifen und diese nutzen, um dich über Leistungsprobleme und potenzielle Hotspots in deiner Codebasis zu informieren.
Bei der Instrumentierung des Frameworks gibt es jedoch Kompromisse. Die Instrumentierung des Frameworks hängt stark von der Möglichkeit ab, Codeänderungen an deinen Diensten selbst vorzunehmen. Wenn du den Code der Dienste nicht ändern kannst, kannst du das Transport-Framework nicht wirklich instrumentieren. Die Instrumentierung des Frameworks kann sich als schwierig erweisen, wenn dein API-Proxy lediglich als Übersetzungsschicht fungiert, z. B. als dünner Wrapper, der JSON über HTTP in einen proprietären oder internen Transport übersetzt. Schließlich kann die Instrumentierung des Frameworks schwierig sein, wenn du keine Komponenten hast, die Anfragen zentralisieren - zum Beispiel einen Client, der mehrere Dienste direkt aufruft und nicht über eine Proxy-Schicht. In diesem Fall könntest du den Client als Zentralisierungspunkt verwenden und dort deine erste Instrumentierung hinzufügen.
Service Mesh Instrumentation
Bei der Diskussion über die Kompromisse bei der Framework-Instrumentierung haben wir als Erstes die Frage gestellt: "Was ist, wenn du den Code nicht ändern kannst?" Das ist keine unvernünftige oder abwegige Hypothese. Es gibt eine Vielzahl von Gründen, warum die Person, die die Software instrumentiert, nicht in der Lage ist, den Dienst zu ändern, den sie zu instrumentieren versucht. Meistens ist dies eine Herausforderung für größere Organisationen, in denen die Personen, die die Anwendung überwachen, von den Personen, die die Anwendung erstellen, räumlich, zeitlich und so weiter getrennt sind.
Wie instrumentiert man also Code, den man nicht anfassen kann? Kurz gesagt, du instrumentierst den Teil des Codes, den du anfassen kannst, und gehst von dort aus weiter.
Zuerst solltest du verstehen, was ein Service Mesh ist - wenn du das weißt, kannst du ruhig einen Absatz überspringen. Ein Service Mesh ist eine konfigurierbare Infrastrukturebene, die die prozessübergreifende Kommunikation zwischen den Diensten unterstützt. Dies geschieht in der Regel über Sidecar Proxies, Prozesse, die neben jeder Service-Instanz leben und die gesamte Interprozesskommunikation für den zugehörigen Service übernehmen. Neben der Kommunikation zwischen den Diensten können das Service Mesh und seine Sidecars auch die Überwachung, die Sicherheit, die Erkennung von Diensten, den Lastausgleich, die Verschlüsselung und vieles mehr übernehmen. Im Wesentlichen ermöglicht das Service Mesh eine Trennung zwischen den Belangen der Entwickler und den Belangen des Betriebs, so dass sich die Teams spezialisieren und auf die Entwicklung leistungsfähiger, sicherer und zuverlässiger Software konzentrieren können.
Jetzt, wo wir uns einig sind, lass uns darüber sprechen, wie die Instrumentierung des Dienstnetzes aussieht. Wie bereits erwähnt, ist eine der wichtigsten Eigenschaften des Sidecar Proxy, dass die gesamte Kommunikation zwischen den Prozessen über den Proxy läuft. Dies ermöglicht es uns, den Proxy selbst zu tracen. In vielen modernen Service-Mesh-Projekten wie Istio oder funktioniert diese Funktion bereits, aber auf einer eher hypothetischen Ebene ähnelt die Funktionsweise der Framework-Instrumentierung. Bei eingehenden Anfragen wird der Span-Kontext aus den Kopfzeilen entnommen, ein neuer Span mit diesem Kontext erstellt und mit Tags versehen, die die Operation beschreiben, mit der die Anfrage abgeschlossen wird.
Der größte Vorteil dieser Art der Instrumentierung ist, dass du dir ein vollständiges Bild von deiner Anwendung machen kannst. Erinnere dich an unsere Diskussion über die Instrumentierung des Frameworks - wir haben an einem zentralen Punkt begonnen und uns dann von dort aus weiter nach außen bewegt. Durch die Instrumentierung am Service Mesh werden alle Dienste, die vom Service Mesh verwaltet werden, in den Trace aufgenommen, wodurch du einen viel besseren Einblick in deine gesamte Anwendung erhältst. Außerdem ist die Service-Mesh-Instrumentierung unabhängig von der Transportschicht der einzelnen Dienste. Solange der Datenverkehr durch das Sidecar geleitet wird, wird er mitverfolgt.
Allerdings gibt es auch Kompromisse und Nachteile bei der Service-Mesh-Instrumentierung. In erster Linie handelt es sich bei der Service-Mesh-Instrumentierung um eine Blackbox-Form der Instrumentierung. Du hast keine Ahnung, was innerhalb des Codes passiert, und du kannst deine Spans nur mit den Daten anreichern, die bereits vorhanden sind. Realistisch betrachtet bedeutet das, dass du einige nützliche implizite Erkenntnisse gewinnen kannst - zum Beispiel, indem du Spans mit HTTP-Antwortcodes kennzeichnest und annimmst, dass jeder Statuscode, der eine fehlgeschlagene Anfrage repräsentiert (wie HTTP 500), ein Fehler ist - aber du brauchst ein spezielles Parsing oder Handling, um explizite Informationen in einen Span zu bekommen. Ein weiteres Problem bei der Instrumentierung des Servicenetzes ist, dass es für Services schwierig ist, die Spans aus dem Servicenetz anzureichern. Dein Sidecar wird zwar Tracing-Header an deinen Prozess weitergeben, aber du musst diese Header trotzdem extrahieren, einen Span-Kontext erstellen und so weiter. Wenn jeder Dienst seine eigenen Child-Spans erstellt, kann es sehr schnell passieren, dass deine Traces extrem groß werden und echte Kosten für die Speicherung oder Verarbeitung entstehen.
Letztendlich sind Service Mesh Instrumentation und Framework Instrumentation keine Entweder-Oder-Entscheidung. Sie funktionieren am besten zusammen! Realistischerweise müssen nicht alle deine Dienste von Anfang an instrumentiert werden, oder möglicherweise nie. Lasst uns darüber reden, warum.
Dein Dienstdiagramm erstellen
Unabhängig davon, mit welcher Methode du mit der Instrumentierung deiner Anwendung beginnst, solltest du dir den ersten Meilenstein überlegen, den du erreichen möchtest. Was willst du messen? Wir sind der Meinung, dass Tracing in erster Linie eine Möglichkeit ist, die Leistung und den Zustand einzelner Dienste im Kontext einer größeren Anwendung zu messen. Um diesen Kontext zu verstehen, musst du die Verbindungen zwischen deinen Diensten kennen und wissen, wie die Anfragen durch das System fließen. Ein guter erster Meilenstein wäre es daher, einen Servicegraphen für deine gesamte Anwendung oder eine wichtige Teilmenge davon zu erstellen, wie in Abbildung 4-3 dargestellt.
Dieser Vergleich soll zeigen, wie wichtig es ist, deinen Servicegraphen zu verstehen. Selbst bei einfachen Diensten mit wenigen Abhängigkeiten kann das Verständnis deines Servicegraphen ein entscheidender Faktor für die Verbesserung deiner MTTR (mittlere Wiederherstellungszeit) für Vorfälle sein. Da ein Großteil dieser Zeit von unabhängigen Faktoren abhängt, wie z. B. der Zeit, die für die Bereitstellung einer neuen Version eines Dienstes benötigt wird, ist die Reduzierung der Zeit, die für die Diagnose aufgewendet wird, der beste Weg, die MTTR insgesamt zu verringern. Ein entscheidender Vorteil des verteilten Tracing ist, dass es dir ermöglicht, deine Dienste und die Beziehungen zwischen ihnen implizit abzubilden, so dass du Fehler in anderen Diensten identifizieren kannst, die zur Latenz einer bestimmten Anfrage beitragen. Wenn die Anwendungen komplizierter und vernetzter werden, ist das Verständnis dieser Beziehungen nicht mehr optional, sondern wird zur Grundlage.
In der Beispielanwendung kannst du sehen, dass die Abhängigkeiten zwischen den Diensten ziemlich einfach und leicht zu verstehen sind. Selbst bei dieser einfachen Anwendung ist die Möglichkeit, den gesamten Graphen zu erstellen, sehr wertvoll. Stell dir vor, du hast eine Kombination von Techniken verwendet, um jeden unserer Dienste (API-Proxy, Authentifizierungsdienst, Worker-Dienste usw.) zu instrumentieren und einen Trace-Analyzer zu haben, der die von unserer Anwendung erzeugten Spans lesen und verarbeiten kann. Auf diese Weise kannst du Fragen beantworten, die schwierig wären, wenn du keinen Zugang zu diesen Dienstbeziehungen hättest. Diese Fragen können ganz banal sein ("Welche Dienste tragen am meisten zur Latenz dieses Vorgangs bei?") oder ganz spezifisch ("Welcher Dienst schlägt bei dieser bestimmten Kunden-ID und dieser bestimmten Transaktion fehl?"). Wenn du dich jedoch darauf beschränkst, nur die Kanten deiner Dienste zu verfolgen, steckst du in der Klemme. Du kannst Fehler nur sehr grob erkennen, z. B. ob eine Anfrage fehlgeschlagen ist oder erfolgreich war.
Wie bringst du das also in Ordnung? Du hast mehrere Möglichkeiten. Eine davon ist sicherlich, den Servicecode selbst mit Instrumenten zu versehen. Wie wir im nächsten Abschnitt erläutern werden, ist es eine Kunst und eine Wissenschaft, Spans zu erstellen, die für die Profilerstellung und das Debugging von getractem Code nützlich sind. Zum anderen kannst du die Kanten, die du aufgespürt hast, nutzen und sie für weitere Daten auswerten. Wir stellen drei fortgeschrittene Mechanismen vor, die die Konzepte des Frameworks und der Mesh-Instrumentierung nutzen, um die Lücken in deinem Service-Mesh zu schließen.
Die erste Methode besteht darin, den Detaillierungsgrad in unseren vom Framework bereitgestellten Spans zu erhöhen. In unserem HTTP-Middleware-Beispiel haben wir nur wenige Details über die Anfrage aufgezeichnet, wie die HTTP-Methode, die Route und den Statuscode. In Wirklichkeit würde jede Anfrage potenziell viel mehr Daten enthalten. Sind deine eingehenden Anfragen an einen Nutzer gebunden? Ziehe in Erwägung, die Benutzerkennung als Tag an jede Anfrage anzuhängen. Service-to-Service-Anfragen sollten mit semantischen Identifikatoren identifiziert werden, die von deiner Tracing-Bibliothek bereitgestellt werden, z. B. OpenTelemetry's SpanKind
Attribute oder spezifische Tags, mit denen du den Typ eines Dienstes (Cache, Datenbank usw.) identifizieren kannst. Bei Datenbankaufrufen kannst du durch die Instrumentierung des Datenbank-Clients eine Vielzahl von Informationen erfassen, wie z. B. die tatsächlich verwendete Datenbankinstanz, die Datenbankabfrage und so weiter. All diese Anreicherungen tragen dazu bei, dass dein Dienstgraph zu einer semantischen Darstellung deiner Anwendung und der Verbindungen zwischen ihr wird.
Die zweite Methode besteht darin, bestehende Instrumente und Integrationen für deine Dienste zu nutzen. Es gibt eine Reihe von Plug-ins für OpenTelemetry, OpenTracing und OpenCensus, die es gängigen Open-Source-Bibliotheken ermöglichen, Spans als Teil deines bestehenden Traces auszugeben. Wenn du eine große Menge an bestehendem Code zu instrumentieren hast, kannst du diese Plug-ins verwenden, um bestehende Frameworks und Clients neben der Instrumentierung auf höherer Ebene im Service Mesh/Framework Layer zu instrumentieren. Ein Beispiel für diese Plug-ins findest du in Anhang A.
Die dritte Methode ist die manuelle Instrumentierung, die wir im Abschnitt "Benutzerdefinierte Instrumentierung" behandelt haben, wobei die gleichen Prinzipien gelten. Du musst sicherstellen, dass ein Root-Span in jeden Service übertragen wird, von dem aus du Child-Spans erstellen kannst. Je nachdem, wie detailliert ein Dienst sein muss, brauchst du vielleicht nicht mehrere Child-Spans für einen einzigen Dienst; siehe den Pseudocode in Beispiel 4-11.
Beispiel 4-11. Eine Pseudocode-Methode für die Größenänderung und Speicherung von Bildern
func uploadHandler(request) { image = imageHelper.ParseFile(request.Body()) resizedImage = imageHelper.Resize(image) uploadResponse = uploadToBucket(resizedImage) return uploadResponse }
Was interessiert uns in diesem Fall die Rückverfolgung? Die Antwort hängt von deinen Anforderungen ab. Es spricht einiges dafür, dass die meisten Methoden, die hier aufgerufen werden, ihre eigenen Child-Spans haben, aber die eigentliche Abgrenzung wäre hier, die Child-Aufrufe auf Methoden zu beschränken, die nicht in den Verantwortungsbereich eines bestimmten Teams fallen. Du kannst dir eine Situation vorstellen, in der wir, wenn unser Dienst wächst, die Funktionen, die Bilder analysieren und ihre Größe ändern, in einen anderen Dienst auslagern. Wie wir bereits geschrieben haben, solltest du die gesamte Methode in einem einzigen Span unterbringen und Tags und Protokolle auf der Grundlage der Antworten auf deine Methodenaufrufe hinzufügen, etwa wie in Beispiel 4-12.
Beispiel 4-12. Manuelles Instrumentieren einer Methode
func uploadHandler(context, request) { span = getTracer().startSpanFromContext(context) image = imageHelper.ParseFile(request.Body()) if image == error { span.setTag("error", true) span.log(image.error) } // Etc. }
Jede oder alle dieser Methoden können miteinander kombiniert werden, um ein effektiveres und repräsentatives Service-Diagramm zu erstellen, das nicht nur die Service-Abhängigkeiten deiner Anwendung genau beschreibt, sondern auch die Art dieser Abhängigkeiten semantisch darstellt. Wir haben das Hinzufügen oder Anreichern von Spans besprochen; als Nächstes werden wir uns ansehen , wie man diese Spans erstellt und wie man die wichtigsten und wertvollsten Informationen bestimmt, die man einem Span hinzufügen sollte.
Was ist in einem Span?
Spans sind die Bausteine des verteilten Tracing, aber was bedeutet das eigentlich? Ein Span steht für zwei Dinge: die Zeitspanne, in der dein Dienst gearbeitet hat, und der Mechanismus, mit dem die Daten von deinem Dienst zu einem Analysesystem übertragen werden, das sie verarbeiten und interpretieren kann. Effektive Spans zu erstellen, die Einblicke in das Verhalten deines Dienstes gewähren, ist eine Kunst und eine Wissenschaft. Dazu gehören bewährte Methoden für die Benennung von Spans, die Kennzeichnung von Spans mit semantisch nützlichen Informationen und die Protokollierung strukturierter Daten.
Effektive Namensgebung
Was verbirgt sich hinter einem Namen? Wenn es um einen Span geht, ist das eine sehr gute Frage! Der Name eines Spans, auch bekannt als operation name, ist ein erforderlicher Wert in Open Source Suchbibliotheken, sogar einer der einzigen erforderlichen Werte. Warum ist dies der Fall? Wie wir bereits angedeutet haben, sind Spans eine Abstraktion über die Arbeit eines Dienstes. Das ist ein großer Unterschied zu der Art und Weise, wie du dir eine Anfragekette oder einen Aufrufstapel vorstellst. Es sollte keine Eins-zu-Eins-Zuordnung zwischen dem Funktionsnamen und dem Namen des Spans geben.
Aber was steckt in einem Namen für eine Spanne?
Erstens sollten die Namen aggregierbar sein. Kurz gesagt, du willst vermeiden, dass die Namen der Spans für jede Ausführung eines Dienstes eindeutig sind. Vor allem bei HTTP-Diensten sehen wir immer wieder, dass der Name der Spanne mit der vollständig übereinstimmenden Route (z. B. GET /api/v2/users/1532492
) identisch ist. Dieses Muster erschwert es, Vorgänge über Tausende oder Millionen von Ausführungen hinweg zu aggregieren, was den Nutzen deiner Daten stark einschränkt. Gestalte die Route stattdessen allgemeiner und verschiebe die Parameter in Tags, wie z. B. GET /api/v2/users/{id}
mit dem zugehörigen Tag userId: 1532492
.
Unser zweiter Ratschlag lautet, dass Namen Aktionen und nicht Ressourcen beschreiben sollten. Denken wir zum Beispiel an MicroCalc zurück. Wir könnten einen Datenspeicher hinzufügen, bei dem es sich um eine Blob-Speicherung oder SQL handeln könnte, oder um eine Benutzerdatenbank oder eine Historie früherer Ergebnisse. Anstatt einen Span nach der Ressource zu benennen, auf die er zugreift, die er verändert oder die er anderweitig verbraucht, ist es viel besser, die Aktion zu beschreiben und den Span mit dem Ressourcentyp zu kennzeichnen. So kannst du deine Spans über mehrere Typen hinweg abfragen und erhältst interessante analytische Einblicke. Ein Beispiel wäre der Unterschied zwischen den Namen WriteUserToSQL
und WriteUser
. Du kannst dir eine Situation vorstellen, in der diese unabhängigen Komponenten zu Testzwecken ausgetauscht werden (z. B. wenn wir einen NoSQL- oder Cloud-Datenspeicher für unsere Nutzer ausprobieren wollen); ein weniger aussagekräftiger Name würde Vergleiche zwischen den einzelnen Datenspeichern ermöglichen. Wenn du diese beiden Ratschläge befolgst, kannst du sicherstellen, dass deine Spans später bei der Analyse nützlicher sind.
Effektives Tagging
Du bist nicht verpflichtet, deine Spans zu taggen, aber du solltest es tun. Tags sind die wichtigste Methode, mit der du eine Spanne mit mehr Informationen darüber anreichern kannst, was bei einem bestimmten Vorgang passiert, und sie eröffnen dir viele Möglichkeiten für Analysen. Während Namen dir helfen, Daten auf einer hohen Ebene zusammenzufassen (damit du Fragen stellen kannst wie "Wie hoch ist die Fehlerquote bei der Suche nach Nutzern in allen Diensten?"), kannst du mit Tags diese Informationen aufschlüsseln, um das Warum deiner Abfrage besser zu verstehen. Daten mit einer hohen Kardinalität sollten in deiner Spanne als Tag dargestellt werden und nicht als etwas anderes - wenn du Daten mit hoher Kardinalität in einen Namen aufnimmst, kannst du sie weniger gut aggregieren, und wenn du sie in Log-Anweisungen aufnimmst, sind sie oft weniger indexierbar.
Was macht also ein effektives Tag aus? Tags sollten extern wichtig sein, d.h. sie sollten für andere Nutzer deiner Trace-Daten eine Bedeutung haben. Es gibt zwar Möglichkeiten, Tags und Traces in der Entwicklung zu verwenden, aber die Tags, die du in ein produktives Tracing-System sendest, sollten generell für alle nützlich sein, die verstehen wollen, was dein Dienst tut.
Die Tags sollten auch intern konsistent sein, d.h. dieselben Schlüssel für mehrere Dienste verwenden. In unserer Beispielanwendung könnte theoretisch jeder Dienst dieselbe Information (z. B. eine Benutzer-ID) mit unterschiedlichen Tag-Schlüsseln melden -userId
, UserId
, User_ID
, USERID
usw. -, aber das wäre in externen Systemen nur schwer abzufragen. Ziehe in Erwägung, Hilfsbibliotheken zu erstellen, die diese Schlüssel standardisieren, oder entscheide dich für ein Format, das mit den Codierungsstandards deiner Organisation übereinstimmt.
Achte nicht nur auf die Konsistenz der Tag-Schlüssel, sondern auch darauf, dass die Tag-Daten innerhalb eines Tag-Schlüssels so konsistent wie möglich gehalten werden. Wenn einige Dienste den Schlüssel userId
als Zeichenkette melden und andere als Ganzzahl, kann es zu Problemen in deinem Analysesystem kommen. Achte außerdem darauf, dass du dem Schlüssel die Einheit des Tags hinzufügst, wenn du einen numerischen Wert verfolgst. Wenn du z. B. die Bytes misst, die bei einer Anfrage zurückgegeben werden, ist message_size_kb
sinnvoller als message_size
. Tags sollten kurz und bündig sein, also z. B. keine Stack Traces in Tags enthalten. Vergiss nicht, dass Tags für die Abfrage deiner Trace-Daten und für die Gewinnung von Erkenntnissen entscheidend sind, also vernachlässige sie nicht!
Effektives Logging
Das Benennen und Markieren von Spans hilft dir dabei, Erkenntnisse aus deinen Spuren abzuleiten. Sie helfen dir, eine Art Beziehungsdiagramm zu erstellen, das dir zeigt, was passiert ist (durch Namen) und warum es passiert ist (durch Tags). Logs können als Teil dieses Puzzles betrachtet werden , denn sie bieten Entwicklern die Möglichkeit, strukturierte oder unstrukturierte Textstrings an einen bestimmten Bereich anzuhängen.
Effektives Logging mit Spans hat zwei zentrale Komponenten. Erstens solltest du dich fragen, was du wirklich protokollieren solltest. Benannte und getaggte Spans können die Anzahl der für deinen Code erforderlichen Logging-Anweisungen erheblich reduzieren. Im Zweifelsfall solltest du lieber einen neuen Span als eine neue Logging-Anweisung erstellen. Betrachte zum Beispiel den Pseudocode in Beispiel 4-13.
Beispiel 4-13. Benannte und getaggte Spans
func getAPI(context, request) { value = request.Body() outgoingRequest = new HttpRequest() outgoingRequest.Body = new ValueQuery(value) response = await HttpClient().Get(outgoingRequest) if response != Http.OK { request.error = true return } resValue = response.Body() // Go off and do more work }
Ohne Tracing würdest du hier einiges protokollieren wollen - zum Beispiel die eingehenden Parameter, vor allem den Wert, den du untersuchen willst. Der ausgehende Request Body wäre möglicherweise interessant zu protokollieren. Den Antwortcode würdest du auf jeden Fall protokollieren wollen, vor allem wenn es sich um einen Ausnahme- oder Fehlerfall handelt. Bei einer Spanne gibt es jedoch deutlich weniger, was als Log-Statement wertvoll ist - der eingehende Parameterwert könnte, wenn er allgemein nützlich ist, ein Tag wie value:foo
sein, der Antwortcode wäre sicherlich einer und so weiter. Trotzdem bist du vielleicht daran interessiert, den genauen Fehlerfall zu protokollieren, der dort auftritt. In diesem Fall solltest du stattdessen einen neuen Span für diese externe Anfrage erstellen. Hierfür gibt es zwei Gründe: Es handelt sich um eine Kante deines Anwendungscodes, und wie bereits erwähnt, ist es eine gute Praxis, die Kanten zu verfolgen.
Ein weiterer Grund ist, dass ein Log-Statement in Bezug auf die Daten weniger interessant wäre als eine andere Spanne. HTTP GET mag wie eine einfache Operation erscheinen, und das ist es auch oft, wenn wir darüber nachdenken, sie zu benutzen. Bedenke aber, was hinter den Kulissen passiert - DNS-Suchvorgänge, Routing über wer-weiß-wie-viele Hops, Wartezeit auf Socket-Lesevorgänge und so weiter. Diese Informationen können, wenn sie in einem Span durch Tags verfügbar gemacht werden, einen detaillierteren Einblick in Leistungsprobleme geben und sind daher in einem neuen Span besser aufgehoben als in einer separaten Log-Operation.
Der zweite Aspekt für eine effektive Protokollierung in deinen Spans ist, dass du, wenn möglich, strukturierte Protokolle schreibst und sicherstellst, dass dein Analysesystem sie verstehen kann. Dabei geht es vor allem um die spätere Nutzbarkeit deiner Spans - ein Analysesystem kann strukturierte Protokolldaten in etwas umwandeln, das in einer grafischen Benutzeroberfläche besser lesbar ist, und bietet Optionen für komplexe Abfragen (z. B. "Zeige mir alle Protokolle von Dienst X, in denen ein Ereignis mit einer bestimmten Art von Ausnahme protokolliert wurde" oder "Gibt es in meinen Diensten Protokolle auf INFO-Ebene?").
Leistungsüberlegungen verstehen
Der unerwünschte Nebeneffekt bei der Erstellung dieser umfangreichen, detaillierten Spans ist, dass sie alle irgendwo hin müssen, und das kostet Zeit. Betrachten wir eine Textdarstellung eines typischen Spans für eine HTTP-Anfrage (siehe Beispiel 4-14).
Beispiel 4-14. Typische Spanne für eine HTTP-Anfrage
{ context: { TraceID: 9322f7b2-2435-4f36-acec-f9750e5bd9b7, SpanID: b84da0c2-5b5f-4ecf-90d5-0772c0b5cc18 } name: "/api/v1/getCat", startTime: 1559595918, finishTime: 1559595920, endTime: tags: [ { key: "userId", value: 9876546 }, { key: "srcImagePath", value: "s3://cat-objects/19/06/01/catten-arr-djinn.jpg" }, { key: "voteTotalPositive", value: 9872658 }, { key: "voteTotalNegative", value: 72 }, { key: "http.status_code", value: 200 }, { key: "env", value: "prod" }, { key: "cache.hit", value: true } ] }
Das sind weniger als 1 KB an Daten - etwa 600 Byte. Wenn wir sie in base64 kodieren, sind es etwa 800 Byte. Wir sind immer noch unter 1 KB, das ist also gut - aber das ist nur eine Spanne. Wie würde es im Fehlerfall aussehen? Ein Stack-Trace würde uns wahrscheinlich von unter 1 KiB auf etwa 3-4 KiB aufblähen. Die Kodierung eines einzelnen Spans ist wiederum ein Bruchteil einer Sekunde -(time openssl base64
berichtet cpu 0.006 total
) - was nicht viel ist, wenn man es genau nimmt.
Jetzt multipliziere das mit tausend, zehntausend, hunderttausend ... irgendwann summiert es sich. Du wirst nichts davon umsonst bekommen, aber keine Angst, es ist nicht so schlimm, wie es vielleicht scheint. Das erste, was du beachten musst, ist, dass du es nicht weißt, bevor du es nicht weißt - es gibt keine einzige Regel, die wir dir geben können, um alles auf magische Weise besser zu machen. Die Höhe des Aufwands, den du bereit bist, für die Leistung deiner Anwendung einzuplanen und zu akzeptieren, hängt von einer Vielzahl von Faktoren ab, darunter:
-
Sprache und Laufzeit
-
Einsatzprofil
-
Ressourcennutzung
-
Horizontale Skalierbarkeit
Deshalb solltest du diese Faktoren sorgfältig berücksichtigen, wenn du mit der Instrumentierung deiner Anwendung beginnst. Denke daran, dass der stabile Anwendungsfall und das Worst-Case-Leistungsprofil oft sehr unterschiedlich aussehen werden. Mehr als ein Entwickler hat sich schon in einer brenzligen Situation wiedergefunden, in der eine externe Ressource plötzlich und unerwartet für einen langen Zeitraum verfügbar war, was zu extrem unschönen und ressourcenintensiven Service-Absturzschleifen oder Hängern führte. Eine Strategie, mit der du dies bekämpfen kannst, ist der Einbau von Sicherheitsventilen in dein internes Tracing-Framework. Je nach deiner Sampling-Strategie könnte dieses "Sicherheitsventil" darin bestehen, dass die Erstellung neuer Spans unterbrochen wird, wenn die Anwendung dauerhaft fehlschlägt, oder dass die Erstellung von Spans schrittweise bis zu einem asymptotischen Punkt reduziert wird.
Außerdem solltest du eine Methode einbauen, mit der du den Tracer in deinem Anwendungscode aus der Ferne deaktivieren kannst. Dies kann in einer Reihe von Szenarien nützlich sein, die über den bereits erwähnten unerwarteten Verlust externer Ressourcen hinausgehen; es kann auch hilfreich sein, wenn du ein Leistungsprofil deines Dienstes erstellen willst, wenn das Tracing eingeschaltet ist und wenn es ausgeschaltet ist.
Letztendlich sind die größten Ressourcenkosten beim Tracing nicht wirklich die Kosten für das Erstellen und Senden von Spans. Ein einziger Span macht wahrscheinlich nur einen Bruchteil der Daten aus, die in einem bestimmten RPC verarbeitet werden, was die Größe angeht. Du solltest mit den Spans, die du erstellst, mit der Datenmenge, die du ihnen hinzufügst, und mit der Geschwindigkeit, mit der du sie erstellst, experimentieren, um das richtige Gleichgewicht zwischen Leistung und Informationen zu finden, das erforderlich ist, um einen Nutzen aus dem Tracing zu ziehen.
Trace-Driven Development
Wenn das Tracing als Teil einer Anwendung oder eines Dienstes diskutiert wird, neigt man dazu, es sozusagen "aufzuschieben". Tatsächlich gibt es fast eine Hierarchie der Überwachung, die bei der Entwicklung von Diensten nacheinander angewendet wird. Du fängst mit nichts an, fügst dann aber schnell Log-Statements in deinen Code ein, damit du sehen kannst, was in einer bestimmten Methode vor sich geht oder welche Parameter übergeben werden. Oft ist dies der größte Teil der Überwachung eines neuen Dienstes, bis er kurz vor der Freigabe steht. Zu diesem Zeitpunkt kannst du dann noch einmal ein paar Metriken ermitteln, die dir wichtig sind (z. B. die Fehlerrate), und diese einfügen, bevor der ganze Wachsklumpen in die Produktionsimplementierung geht.
Warum wird das so gemacht? Aus mehreren Gründen - einige davon sind gut. Es kann sehr schwierig sein, Überwachungscode zu schreiben, wenn sich der Code, den du überwachen willst, unter deinen Füßen verändert - denk daran, wie schnell Codezeilen hinzugefügt, entfernt oder überarbeitet werden können, während ein Projekt in der Entwicklung ist.
Es gibt aber noch einen anderen Grund, und der ist vielleicht der interessantere. Es ist schwierig, in der Entwicklung Überwachungscode zu schreiben, weil du nicht wirklich weißt, was du überwachen sollst. Die Dinge, von denen du weißt, dass sie dir wichtig sind, wie z. B. die Fehlerrate, sind nicht wirklich interessant zu überwachen und können oft über eine andere Quelle beobachtet werden, z. B. über einen Proxy oder ein API-Gateway. Um Metriken auf Maschinenebene, wie z. B. den Speicherverbrauch deines Prozesses, müssen sich die meisten Entwickler/innen nicht kümmern, und wenn doch, dann werden diese Metriken von einer anderen Komponente und nicht von der Anwendung selbst überwacht.
Weder Metriken noch Logs eignen sich gut, um die Dinge zu erfassen, die du zu Beginn der Entwicklung deines Dienstes weißt, z. B. mit welchen Diensten er kommunizieren oder wie er intern Funktionen aufrufen soll. Tracing ist eine Option, die es dir ermöglicht, während der Entwicklung deiner Anwendung Traces zu erstellen, die sowohl den notwendigen Kontext für die Entwicklung und das Testen deines Codes liefern als auch ein fertiges Toolset für die Beobachtbarkeit innerhalb deines Anwendungscodes bereitstellen. In diesem Abschnitt werden wir die beiden wichtigsten Teile dieses Konzepts behandeln: Entwickeln mit Traces und Testen mit Traces.
Entwickeln mit Traces
Ganz gleich, in welcher Sprache, auf welcher Plattform oder in welchem Stil du einen Dienst schreibst, der Ausgangspunkt ist immer derselbe: ein Whiteboard. Auf dieser Fläche erstellst du das Modell der Funktionen deines Dienstes und zeichnest Linien, die die Verbindungen zwischen ihm und anderen Diensten darstellen. Vor allem in den frühen Prototyping-Phasen der Entwicklung ist es sehr sinnvoll, mit einer so anpassungsfähigen Oberfläche zu beginnen.
Das Problem kommt, wenn es an der Zeit ist, dein Modell in den Code zu übertragen. Wie stellst du sicher, dass das, was du an die Tafel geschrieben hast, mit deinem Code übereinstimmt? Traditionell wird die Verwendung von Testfunktionen empfohlen, aber das ist vielleicht ein zu kleines Ziel, um dir wirklich etwas Nützliches zu sagen. Ein Unit-Test soll das Verhalten von sehr kleinen Funktionseinheiten überprüfen, z. B. einen einzelnen Methodenaufruf. Du kannst natürlich auch größere Unit-Tests schreiben, die andere Annahmen über deine Funktionen überprüfen, z. B. dass Methode A Methode B Methode C aufruft... aber letztendlich schreibst du nur einen Test, der jeden Codepfad aus falschen Gründen überprüft.
Wenn du versuchst, die Beziehung deines Dienstes zu vorangehenden und abhängigen Diensten zu testen, wird es noch komplizierter. Im Allgemeinen werden diese Tests als Integrationstests bezeichnet. Das Problem bei Integrationstests zur Überprüfung deines Modells ist jedoch ein zweifaches. Das erste Problem ist, dass du, wenn du anfängst, Dienste zu mocken, nicht den tatsächlichen Dienst testest, sondern nur einen Ersatz, der einem bestimmten Befehl folgt. Das zweite und vielleicht noch größere Problem besteht darin, dass Integrationstests zwangsläufig auf deine Testumgebung beschränkt sind und die Kommunikation über Prozessgrenzen hinweg nur unzureichend unterstützen (zumindest, wenn du nicht mit großem Aufwand ein Integrationstest-Framework einrichten oder dein eigenes schreiben willst).
Wenn Unit-Tests und Integrationstests nicht funktionieren, was dann? Kommen wir noch einmal auf den ursprünglichen Punkt zurück: Es ist wichtig, dass du dein mentales Modell deiner Anwendung überprüfen kannst. Das bedeutet, dass du in der Lage sein solltest, deinen Code so zu schreiben, dass du sicherstellen kannst, dass sowohl interne Methoden als auch externe Dienste in der richtigen Reihenfolge und mit den richtigen Parametern aufgerufen werden und dass Fehler vernünftig behandelt werden. Ein häufiger Fehler, den wir vor allem bei Code mit starken Abhängigkeiten von externen Diensten beobachtet haben, ist die Frage, was bei einem dauerhaften Ausfall eines Dienstes passiert.
Beispiele dafür gibt es in der realen Welt immer wieder. Denk nur an die Ausfälle, die vor einigen Jahren auftraten, als AWS S3-Buckets stundenlang nicht mehr verfügbar waren. Wenn du über Trace-Daten verfügst, sowohl im Test als auch in der Produktion, kannst du Tools schreiben, die den gewünschten Zustand deines Systems schnell mit der Realität vergleichen. Sie sind auch von unschätzbarem Wert, wenn du versuchst, Chaos-Systeme als Teil deiner kontinuierlichen Integration/kontinuierlichen Bereitstellung (CI/CD) zu bauen - die Unterschiede zwischen deinem System im stabilen Zustand und dem System im Chaos zu finden, wird deine Fähigkeit, widerstandsfähigere Systeme zu bauen, dramatisch verbessern.
Die Nachverfolgung als Teil deines Entwicklungsprozesses funktioniert ähnlich wie die Nachverfolgung an anderen Stellen in deiner Codebasis, mit ein paar bemerkenswerten Vorteilen. Erinnere dich zunächst daran, wie du mit dem Tracing eines Dienstes beginnst ("Where to Start - Nodes and Edges"). Das gleiche Prinzip gilt für das Schreiben eines neuen Dienstes und für das Instrumentieren eines bestehenden Dienstes. Du solltest auf jede eingehende Anfrage eine Middleware anwenden, die nach Spandaten sucht, und wenn diese vorhanden sind, einen neuen Child-Span erstellen. Wenn dein neuer Dienst ausgehende Anfragen sendet, sollte er ebenfalls deinen aktuellen Span-Kontext in die ausgehende Anfrage injizieren, damit nachgelagerte Dienste, die Tracing-fähig sind, an der Verfolgung teilnehmen können. Die Änderungen des Prozesses finden meist zwischen diesen Punkten statt, da du mit der Frage konfrontiert wirst, wie viel du tracen sollst.
Wie wir am Ende dieses Kapitels besprechen werden, gibt es so etwas wie ein Zuviel an Tracing. Vor allem in der Produktion solltest du deine Traces auf die Daten beschränken, die für externe Beobachter und Nutzer wichtig sind, wenn sie einen End-to-End-Trace sehen. Wie lässt sich also ein einzelner Dienst mit mehreren internen Aufrufen genau modellieren? Du wirst eine Art Verbosity-Konzept für deinen Tracer erstellen wollen. Das ist bei der Protokollierung sehr üblich, wo es Protokollstufen wie Info, Debug, Warnung und Fehler gibt. Jede Ausführlichkeitsstufe legt fest, welche Mindestanforderungen das Log-Statement erfüllen muss, um gedruckt zu werden. Das gleiche Konzept kann auch für Traces gelten. Beispiel 4-15 zeigt eine Methode in Golang, um ausführliche Traces zu erstellen, die über eine Umgebungsvariable konfiguriert werden können.
Beispiel 4-15. Ausführliche Traces erstellen
var
traceVerbose
=
os
.
Getenv
(
"TRACE_LEVEL"
)
==
"verbose"
...
func
withLocalSpan
(
ctx
context
.
Context
)
(
context
.
Context
,
opentracing
.
Span
)
{
if
traceVerbose
{
pc
,
_
,
_
,
ok
:=
runtime
.
Caller
(
1
)
callerFn
:=
runtime
.
FuncForPC
(
pc
)
if
ok
&&
callerFn
!=
nil
{
span
,
ctx
:=
opentracing
.
StartSpanFromContext
(
ctx
,
callerFn
.
Name
()
)
return
ctx
,
span
}
}
return
ctx
,
opentracing
.
SpanFromContext
(
ctx
)
}
func
finishLocalSpan
(
span
opentracing
.
Span
)
{
if
traceVerbose
{
span
.
Finish
()
}
}
Das Einstellen von Trace-Verbosity ist nicht nur auf Go-Aspekte, Attribute oder andere dynamische/Metaprogrammierungstechniken beschränkt, sondern kann auch in anderen Sprachen verwendet werden, die sie unterstützen. Die Grundidee ist jedoch die gleiche wie oben beschrieben. Vergewissere dich zunächst, dass die Ausführlichkeitsstufe richtig eingestellt ist. Dann bestimmst du die aufrufende Funktion und beginnst einen neuen Span als Kind des aktuellen Spans. Schließlich gibst du den Span und das Sprachkontextobjekt zurück. Beachte, dass wir in diesem Fall nur eine Start-/End-Methode bereitstellen - das bedeutet, dass alle Logs oder Tags, die wir einfügen, nicht unbedingt zum ausführlichen Child hinzugefügt werden, sondern möglicherweise zum Parent, wenn das Child nicht existiert. Wenn das nicht erwünscht ist, solltest du überlegen, ob du nicht Hilfsfunktionen für das Loggen oder Markieren einrichtest, um dieses Verhalten zu vermeiden. Die Verwendung unserer ausführlichen Traces ist ebenfalls recht einfach (siehe Beispiel 4-16).
Beispiel 4-16. Ausführliche Traces verwenden
import
(
"github.com/opentracing-contrib/go-stdlib/nethttp"
"github.com/opentracing/opentracing-go"
)
func
main
()
{
// Create and register tracer
mux
:=
http
.
NewServeMux
()
fs
:=
http
.
FileServer
(
http
.
Dir
(
"../static"
))
mux
.
HandleFunc
(
"/getFoo"
,
getFooHandler
)
mux
.
Handle
(
"/"
,
fs
)
mw
:=
nethttp
.
Middleware
(
tracer
,
mux
)
log
.
Printf
(
"Server listening on port %s"
,
serverPort
)
http
.
ListenAndServe
(
serverPort
,
mw
)
}
func
getFooHandler
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
foo
:=
getFoo
(
r
.
Context
())
// Handle response
}
func
getFoo
(
ctx
context
.
Context
)
{
ctx
,
localSpan
:=
withLocalSpan
(
ctx
)
// Do stuff
finishLocalSpan
(
localSpan
)
}
In diesem Beispiel erstellen wir einen einfachen HTTP-Server in Golang und tracen ihn mit dem Paket go-stdlib
. Dadurch werden eingehende HTTP-Anfragen nach Tracing-Headern geparst und entsprechende Spans erstellt, sodass die Kanten unseres Dienstes behandelt werden. Durch das Hinzufügen der Methoden withLocalSpan
und finishLocalSpan
können wir einen Span erstellen, der lokal zu einer Funktion gehört und nur existiert, wenn unsere Trace-Verbosity entsprechend eingestellt ist.
Diese Spans können in einem Trace-Analyzer bei der lokalen Entwicklung betrachtet werden. So kannst du genau einschätzen, ob die Aufrufe so ablaufen, wie du es dir vorstellst, und sicherstellen, dass du deinen Dienst beobachten kannst, wenn er andere Dienste aufruft (oder von ihnen aufgerufen wird), und als Bonus kannst du Open-Source-Frameworks als Standardauswahl für Fragen wie "Welche Logging-/Metrik-/Tracing-API sollte ich verwenden?" nutzen, da diese über deine Telemetrie-API ausgeführt werden können. Erfinde das Rad nicht neu, wenn du es nicht brauchst!
Testen mit Traces
Trace-Daten können als direktionaler azyklischer Graph dargestellt werden. Obwohl er normalerweise als Flammengraph dargestellt wird, sind Traces einfach gerichtete azyklische Graphen einer Anfrage, wie in Abbildung 4-4 dargestellt. Ein gerichteter azyklischer Graph, oder DAG, ist dir vielleicht sehr vertraut, wenn du einen Informatik- oder Mathematikhintergrund hast; er hat mehrere Eigenschaften, die sehr nützlich sind. DAGs sind endlich (sie haben ein Ende) und sie haben keine gerichteten Zyklen (sie drehen sich nicht in einer Schleife um sich selbst - diejenigen, die das tun, werden zyklische Referenzen genannt). Eine weitere nützliche Eigenschaft von DAGs ist, dass sie ziemlich einfach miteinander verglichen werden können.
Was sind nun die Möglichkeiten? Zunächst fragst du dich vielleicht: "Na und?" Wie bereits erwähnt, sind Integrationstests und andere Formen von übergeordneten Tests ausreichend und notwendig, um den Betrieb unseres Dienstes bei der Bereitstellung sicherzustellen. Dennoch gibt es mehrere Gründe, warum du dein Testrepertoire um Trace-Vergleiche erweitern solltest. Der einfachste Weg, Trace-Daten als eine Form des Testens zu betrachten, sind einfache Vergleiche zwischen Umgebungen. Stell dir vor, wir setzen eine Version unserer Anwendung in einer Staging- oder Vorproduktionsumgebung ein, nachdem wir sie lokal getestet haben. Nehmen wir weiter an, dass wir unsere Tracedaten in eine Art Flat File exportieren, das sich zur Verarbeitung eignet, wie in Beispiel 4-17 gezeigt.
Beispiel 4-17. Exportieren von Trace-Daten
[ { name: "getFoo", spanContext: { SpanID: 1, TraceID: 1 }, parent: nil }, { name: "computeFoo", spanContext: { SpanID: 2, TraceID: 1 }, parent: spanContext{ SpanID: 1, TraceID: 1 } }, ... ]
In einem realen System kann es vorkommen, dass diese nicht in der richtigen Reihenfolge oder nicht in einem sortierten Zustand vorliegen, aber wir sollten davon ausgehen, dass der Aufrufgraph für einen einzelnen API-Endpunkt zwischen ihnen identisch ist.
Eine mögliche Anwendung besteht darin, die einzelnen Datensätze topografisch zu sortieren und dann anhand der Länge oder durch einen anderen Vergleichsprozess zu vergleichen. Wenn sich unsere Spuren unterscheiden, wissen wir, dass wir ein Problem haben, weil die Ergebnisse nicht mit unseren Erwartungen übereinstimmen.
Eine andere Anwendung wäre es, proaktiv zu erkennen, wenn Dienste beginnen, Abhängigkeiten von deinem Dienst zu übernehmen. Nehmen wir an, unser Authentifizierungs- oder Suchdienst wurde bei anderen Teams bekannt gemacht. Ohne dass wir es bemerken, beginnen sie, in ihren neuen Diensten eine Abhängigkeit davon zu nutzen. Ein automatisiertes Trace-Diffing würde uns proaktiv Einblicke in diese neuen Verbraucher geben, vor allem wenn es eine Art zentrales Framework gibt, das diese Traces im Laufe der Zeit erstellt und vergleicht.
Eine weitere Anwendung ist die Verwendung von Tracing als Grundlage für die Sammlung von Service Level Indikatoren und Zielen für deinen Dienst und den automatischen Vergleich dieser Ziele, wenn du neue Versionen einsetzt. Da Traces von Natur aus in der Lage sind, das Timing deines Dienstes zu verstehen, sind sie eine großartige Möglichkeit, Leistungsänderungen bei einer Vielzahl von Anfragen zu verfolgen, während du deine Dienste iterierst und weiterentwickelst.
Letztlich ist vieles davon spekulativ - uns ist nicht bekannt, dass jemand verteiltes Tracing in großem Umfang als Teil seiner Testsuiten einsetzt. Das heißt nicht, dass es nicht nützlich ist, aber als neue Technologie sind noch nicht alle Facetten erforscht und genutzt worden. Vielleicht bist du der/die Erste!
Erstellen eines Instrumentierungsplans
Wohl oder übel kommen die meisten Menschen erst spät in der Entwicklung einer Anwendung oder Software auf verteiltes Tracing und Monitoring. Das hat zum Teil mit der Art der iterativen Entwicklung zu tun - wenn du ein Produkt oder einen Dienst entwickelst, kann es schwierig sein zu verstehen, was du wissen musst, bis du einige Zeit damit verbracht hast, es tatsächlich zu entwickeln und zu betreiben. Die verteilte Rückverfolgung ist ein weiterer Aspekt, denn Entwickler/innen kommen oft auf diese Methode zurück, um Probleme zu lösen, die durch die Größe und das Wachstum des Unternehmens entstehen, sei es in Bezug auf die Anzahl der Dienste oder die Komplexität der Organisation. In beiden Fällen hast du oft schon eine große, vermutlich komplizierte Anzahl von Diensten installiert und musst wissen, wie du verteiltes Tracing nutzen kannst, um die Zuverlässigkeit und den Zustand nicht nur deiner Software, sondern auch deines Teams zu verbessern. Vielleicht fängst du gerade mit der Entwicklung einer neuen Software an und fügst verteiltes Tracing von Anfang an hinzu - dann musst du einen Plan erstellen, wie du Tracing in deinem Team und deiner Organisation einführen und ausbauen kannst. In diesem Abschnitt gehen wir darauf ein, wie du die Instrumentierung neuer oder bestehender Dienste in deinem Unternehmen effektiv begründen kannst und wie du die Zustimmung deines (und anderer) Teams erhältst.
Argumente für die Instrumentierung
Nehmen wir an, du bist bereits von der Idee des verteilten Suchens überzeugt, weil du dieses Buch gelesen hast. Die Herausforderung besteht nun darin, deine Kolleginnen und Kollegen davon zu überzeugen, dass die Idee so gut ist, wie du denkst, denn auch sie müssen einiges tun, um sicherzustellen, dass ihre Dienste mit der Suchfunktion kompatibel sind.
Wenn du andere Teams von den Vorteilen und Kosten der verteilten Nachverfolgung überzeugst, solltest du dir viele der Lektionen über die Instrumentierung, die wir in diesem Kapitel besprochen haben, vor Augen halten. Kurz gesagt: Instrumentierung kann auch dann wertvoll sein, wenn sie relativ einfach ist. Wenn jeder Dienst einen Span mit einigen grundlegenden Attributen ausgibt, die keinen Laufzeit-Overhead erfordern (d.h. String-Werte, die bei der Initialisierung des Dienstes vorberechnet werden können), dann besteht der gesamte zusätzliche Overhead bei jeder Anfrage lediglich in der Weitergabe von Trace-Kontext-Headern, eine Aufgabe, die 25 Bytes auf der Leitung und eine vernachlässigbare Anzahl von Zyklen für die anschließende Dekodierung erfordert.
Der Nutzen - die lückenlose Verfolgung einer Anfrage - ist für einen so geringen Preis äußerst hilfreich. Diese anfragezentrierte Art des verteilten Tracings wurde von Unternehmen wie Google übernommen, das Dapper zur Diagnose von Anomalien und stabilen Leistungsproblemen sowie zur Zuordnung der Ressourcennutzung verwendet.1 Zahlreiche andere große und kleine Ingenieurteams und Organisationen haben verteiltes Tracing eingeführt, um MTTR für Störungen und andere Produktionsausfälle zu reduzieren. Darüber hinaus ist verteiltes Tracing als Teil einer umfassenderen Überwachungs- und Beobachtungspraxis äußerst wertvoll, da es dir ermöglicht, den Suchraum der Daten zu reduzieren, die du untersuchen musst, um Vorfälle zu diagnostizieren, ein Leistungsprofil zu erstellen und deine Dienste wieder in einen gesunden Zustand zu versetzen.
Es kann sinnvoll sein, verteiltes Tracing als "gleiches Spielfeld" zu betrachten, wenn es um die Leistung von Diensten geht. Vor allem in einer polyglotten Umgebung oder in einem global verteilten Unternehmen kann es schwierig sein, sicherzustellen, dass alle Beteiligten in Bezug auf die Leistungsdaten auf derselben Seite stehen. Einige dieser Herausforderungen sind technischer Natur, aber viele sind politischer Natur. Hier ist vor allem die Verbreitung von Eitelkeitsmetriken zu nennen. Du kannst eine Menge Dinge über die Leistung deiner Software messen, die nicht von Bedeutung sind, und vielleicht tust du das bereits, um nebulöse "Qualitäts"-Ziele zu erreichen, die aus Gründen festgelegt wurden, die wir nicht kennen. Verteilte Tracing-Daten liefern jedoch standardmäßig kritische Signale für alle deine Dienste, ohne dass synthetische Endpunkte oder Ansätze zur Sicherstellung des Zustands der Dienste erforderlich sind. Diese Trace-Daten können dann verwendet werden, um etwas Ruhe und Vernunft in einen möglicherweise kaputten Prozess zu bringen. Der erste Schritt zur Bereitstellung dieser Trace-Daten ist natürlich die Instrumentierung der Dienste, also musst du dort anfangen.
Es muss nicht schwierig sein, deine Dienste zu instrumentieren. Gute Tools - Open Source und proprietär - erleichtern die Instrumentierung. In Anhang A findest du Beispiele für die automatische Instrumentierung und die Integration von Bibliotheken für gängige Frameworks, die das Tracing ermöglichen - manchmal sogar ohne Codeänderungen. Du solltest deine Frameworks und den gemeinsam genutzten Code kennen, wenn du dich für eine Instrumentierung entscheidest, damit du diese vorhandenen Tools nutzen kannst. Unserer Erfahrung nach ist eines der überzeugendsten Argumente für verteiltes Tracing, ein bestehendes Microservice-Framework zu instrumentieren, das bereits in deinem Unternehmen eingesetzt wird, und zu demonstrieren, wie Dienste, die dieses Framework nutzen, durch einfaches Aktualisieren einer Abhängigkeit nachverfolgt werden können. Wenn du einen internen Hackathon oder Hack Day veranstaltest, kann das ein lustiges und interessantes Projekt sein, das du in Angriff nehmen kannst!
Egal, wie du es machst, die Argumente für die Instrumentierung laufen letztlich auf die Argumente für verteiltes Tracing im Allgemeinen hinaus. Wie wir bereits erwähnt haben, gibt es viele interessante Anwendungen für Tracing außerhalb der Leistungsüberwachung: Tracing als Teil deines Entwicklungszyklus, Tracing beim Testen anderer Anwendungen. Du könntest verteiltes Tracing als Teil deines CI- und CD-Frameworks nutzen, um zu messen, wie lange bestimmte Teile deines Builds und Deployments dauern. Tracing könnte in Task-Runner zur Erstellung virtueller Maschinen oder zur Bereitstellung von Containern integriert werden, damit du weißt, welche Teile deines Build- und Deployment-Lebenszyklus am meisten Zeit in Anspruch nehmen. Tracing kann als Mehrwert für Dienste genutzt werden, die eine Art API als Service anbieten - wenn du bereits die Ausführungszeit deines Backends verfolgst, könntest du deinen Kunden eine Version dieser Trace-Daten zur Verfügung stellen, um ihnen zu helfen, ihre Software zu profilieren. Die Möglichkeiten der Rückverfolgung sind grenzenlos, und die Argumente für die Instrumentierung deiner Software sollten das widerspiegeln.
Checkliste für die Qualität von Instrumenten
Wenn du einen bestehenden Dienst instrumentierst oder Richtlinien für die Instrumentierung neuer Dienste erstellst, kann es nützlich sein, eine Checkliste mit den Punkten zu haben, die wichtig sind, um die Qualität der Instrumentierung in deiner gesamten Anwendung sicherzustellen. Wir haben eine empfohlene Checkliste in das Repository des Buches aufgenommen, aber du kannst sie auch gerne als Ausgangspunkt für deine eigene Liste verwenden.
Vieles von dem, was in unserer Checkliste für die Instrumentierung steht, stammt aus anderen Teilen dieses Kapitels, daher werden wir nicht zu sehr darauf eingehen. Ein paar Notizen sind wichtig:
-
Viele Open-Source-Bibliotheken für die Instrumentierung oder Framework-Bibliotheken für die Instrumentierung instrumentieren standardmäßig alle eingehenden Anfragen oder Endpunkte, die in deinem Servicecode definiert sind, einschließlich der Diagnoseendpunkte. In der Regel solltest du einen Filter oder Sampler in deinem Service implementieren, um zu verhindern, dass Spans von diesen Endpunkten erstellt werden, es sei denn, du hast einen dringenden Bedarf dafür.
-
Sei sehr vorsichtig, wenn du personenbezogene Daten in deinen Attributen und Ereignissen preisgibst. Die Kosten für die Nichteinhaltung der Vorschriften können sehr hoch sein, vor allem wenn du Trace-Daten zur Analyse und Speicherung an Dritte weitergibst.
-
Versionsattribute sind besonders bei Trace-Vergleichen sehr wertvoll, da sie es dir ermöglichen, eine Anfrage über zwei oder mehr Versionen eines Dienstes hinweg zu vergleichen, um Leistungsrückschritte oder -verbesserungen zu entdecken.
-
Die Integration deiner Feature Flags und anderer Experimente mit deinen Trace-Daten ist eine nützliche Methode, um zu verstehen, wie diese Experimente die Leistung und Zuverlässigkeit deines Dienstes verändern.
Du kannst diese Checkliste gerne mit spezifischen Informationen anpassen, die sie für dein Team nützlich machen, und sie in die Checklisten für die Einführung von Diensten aufnehmen.
Wissen, wann man aufhören sollte zu instrumentieren
Wir haben die Kosten der Instrumentierung in diesem Kapitel schon mehrmals angesprochen; lass uns einen genaueren Blick darauf werfen. Im Großen und Ganzen ist die Instrumentierung ein Kompromiss wie alles andere in der Software. Du tauschst ein gewisses Maß an Leistung gegen einen hoffentlich viel besseren Einblick in den Betrieb deines Dienstes, und zwar auf einer Ebene, die anderen in deinem Team oder deiner Organisation leicht zu vermitteln ist. In diesem Abschnitt weisen wir auf einige bemerkenswerte Anti-Patterns hin, auf die du bei der Instrumentierung achten solltest. Es besteht das Risiko, dass die Kompromisse zu kostspielig werden und dazu führen, dass du die Instrumentierung abbrichst oder dass du zu viele Proben nimmst und die Auflösung deiner Traces verlierst.
Ein Antipattern ist die Implementierung einer zu hohen Standardauflösung. Eine gute Regel ist, dass dein Dienst so viele Spans ausgeben sollte, wie er logische Operationen durchführt. Übernimmt dein Dienst die Authentifizierung und Autorisierung von Benutzern? Zerlege diese Funktion logisch: Sie bearbeitet eine eingehende Anfrage, führt einige Abfragen in einem Datenspeicher durch, wandelt das Ergebnis um und gibt es zurück. Hier gibt es zwei logische Vorgänge: die Bearbeitung der Anfrage/Antwort und die Suche nach den Daten. Es ist immer sinnvoll, Aufrufe an externe Dienste zu trennen. In diesem Beispiel brauchst du vielleicht nur einen einzigen Span, wenn der Datenspeicher eine lokale Datenbank ist, aber du brauchst vielleicht keinen Span, um die Antwort in ein neues Format umzuwandeln, das dein Aufrufer erwartet.
Wenn dein Dienst komplizierter ist, kann es in Ordnung sein, mehr Spans hinzuzufügen, aber du musst bedenken, wie wertvoll deine Trace-Daten für die Nutzer sind und ob sie in weniger Spans zusammengefasst werden können. Daraus ergibt sich, dass du die Möglichkeit haben solltest, die Ausführlichkeit der von einem Dienst ausgegebenen Spans zu erhöhen - siehe "Trace-Driven Development" für Ideen, wie du die Auflösung deiner Spans erhöhen oder verringern kannst. Aus diesem Grund sprechen wir von einer Standardauflösung. Du möchtest sicherstellen, dass die standardmäßig ausgegebene Informationsmenge klein genug ist, um sich gut in einen größeren Trace zu integrieren, aber groß genug, um sicherzustellen, dass sie nützliche Informationen für Verbraucher enthält, die nicht zu deinem Team gehören (aber von Problemen mit deinem Dienst betroffen sein könnten!).
Ein weiterer Fehler ist, dass du dein Propagierungsformat nicht standardisierst. Das kann eine Herausforderung sein, vor allem wenn du ältere Dienste oder Dienste, die von verschiedenen Teams entwickelt wurden, integrierst. Der entscheidende Wert eines Trace liegt in der Vernetzung der Traces untereinander. Wenn du 20, 50, 200 oder mehr Dienste hast, die einen Mischmasch von Tracing-Formaten verwenden, wirst du es schwer haben, den Wert deiner Traces zu nutzen. Vermeide dies, indem du deine Tracing-Praktiken so weit wie möglich standardisierst und Unterbrechungen für Altsysteme oder zwischen verschiedenen Formaten bereitstellst.
Eine Methode zur Bekämpfung von nicht standardisierten Propagierungsformaten ist die Erstellung eines Stapels von Tracing-Propagatoren, die verschiedene Header (z. B. X-B3
oder opentracing
) erkennen und für jede Anfrage den passenden auswählen können. Möglicherweise stellt sich heraus, dass es weniger Aufwand bedeutet, bestehende Systeme auf das neue Format zu aktualisieren, als Kompatibilitätsschichten zu erstellen - entscheide nach bestem Wissen und Gewissen und unter Berücksichtigung der bestehenden Standards und Methoden deines Unternehmens, was für dich das Richtige ist.
Der letzte Ratschlag bezieht sich auf die Überschrift des Abschnitts: Du musst wissen, wann du aufhören solltest. Leider gibt es hier keine eindeutige Antwort, aber es gibt einige Signale, auf die du achten solltest. Generell solltest du dir überlegen, wann dein Dienst an seine Grenzen stößt, ohne dass du eine Stichprobe deiner Trace-Daten nimmst.
Beim Sampling wird ein bestimmter Prozentsatz deiner Spuren nicht zur Analyse aufgezeichnet, um die Gesamtbelastung deines Systems zu verringern. Eine Erläuterung zum Sampling findest du unter "Sampling", aber wir raten dir, die Samplingrate beim Schreiben der Instrumente nicht zu berücksichtigen. Wenn du dir Sorgen über die Anzahl der von deinem Dienst erzeugten Spans machst, solltest du Verbosity-Flags verwenden, um die Anzahl der erzeugten Spans dynamisch anzupassen. Das ist wichtig, denn Sampling ist der beste Weg, um versehentlich potenziell wichtige Daten wegzuwerfen, die bei der Fehlersuche oder der Diagnose eines Problems in der Produktion nützlich sein könnten. Im Gegensatz dazu trifft ein traditioneller Sampling-Ansatz die Entscheidung am Anfang eines Traces, so dass es keinen Grund gibt, die Frage "wird das gesampelt oder nicht" zu optimieren - dein Trace wird in seiner Gesamtheit verworfen, wenn es gesampelt wird.
Ein Zeichen dafür, dass du weitermachen musst, ist, wenn die Inter-Service-Auflösung deines Traces zu niedrig ist. Wenn du zum Beispiel mehrere abhängige Dienste in einem einzigen Span ausklammerst, solltest du so lange instrumentieren, bis diese Dienste unabhängige Spans sind. Du musst die abhängigen Dienste nicht unbedingt instrumentieren, aber deine RPC-Aufrufe zu jedem von ihnen sollten instrumentiert werden, vor allem wenn jeder dieser Aufrufe am Ende deiner Anfragekette steht. Genauer gesagt, stell dir einen Worker Service vor, der mit mehreren Datenspeicher-Wrappern kommuniziert. Es ist vielleicht nicht notwendig, diese Datenspeicher-Wrapper zu instrumentieren, aber du solltest für jeden Aufruf deines Dienstes an sie separate Spans haben, um Latenzzeiten und Fehler besser zu verstehen (fehlt es mir beim Lesen oder beim Schreiben?).
Hör auf zu tracen, wenn die Anzahl der Spans, die du standardmäßig ausgibst, anfängt, wie der tatsächliche Aufrufstapel eines Dienstes auszusehen. Instrumentiere weiter, wenn du unbehandelte Fehler in deinem Dienstcode hast - es ist wichtig, Spans mit Fehlern von Spans ohne Fehler unterscheiden zu können. Schließlich solltest du die Instrumentierung fortsetzen, wenn du neue Dinge entdeckst, die instrumentiert werden müssen. Ziehe in Erwägung, deinen Standardprozess für die Fehlerbehandlung so zu ändern, dass du nicht nur neue Tests schreibst, um den Fehler zu beheben, sondern auch neue Instrumente, um sicherzustellen, dass du ihn in Zukunft abfangen kannst.
Intelligentes und nachhaltiges Wachstum der Instrumentierung
Es ist eine Sache, einen einzelnen Dienst oder eine Demonstrationsanwendung zu instrumentieren, die dir einige Konzepte über Tracing vermitteln soll. Eine ganz andere und viel schwierigere Aufgabe ist es, herauszufinden, wie es weitergeht. Je nachdem, wie du mit der Instrumentierung beginnst, kann es schnell passieren, dass du dich in unerprobten Gewässern wiederfindest und herausfinden musst, wie du einen Nutzen aus dem Tracing ziehen und gleichzeitig die Akzeptanz in deinem Unternehmen oder Team erhöhen kannst.
Es gibt verschiedene Strategien, die du anwenden kannst, um die Instrumentierung innerhalb deiner Anwendung zu verbessern. Diese Strategien lassen sich grob in technische und organisatorische Lösungen unterteilen. Wir werden uns zuerst mit den technischen Strategien befassen und dann über die organisatorischen Strategien sprechen. Es gibt einige Überschneidungen zwischen den beiden Strategien - wie du dir denken kannst, arbeiten technische und organisatorische Lösungen Hand in Hand und ermöglichen sich gegenseitig.
Technisch gesehen ist der beste Weg, die Instrumentierung in deiner Anwendung zu erweitern, sie einfach zu nutzen. Wenn du Bibliotheken bereitstellst, die dir die Arbeit abnehmen, die zum Einrichten von Tracing und zur Integration in deine RPC-Frameworks oder anderen gemeinsam genutzten Code erforderlich ist, können die Dienste Tracing leicht integrieren. Auch die Erstellung von Standard-Tags, -Attributen und anderen Metadaten für dein Unternehmen unter ist ein guter Weg, um sicherzustellen, dass neue Teams und Dienste, die Tracing einführen, einen Fahrplan haben, um Tracing schnell zu verstehen und davon zu profitieren. Wenn die Teams in der Lage sind, die Rückverfolgung im Alltag zu nutzen, wird sie Teil ihres Arbeitsablaufs und steht ihnen zur Verfügung, sobald sie ihre Dienste in die Produktion überführen.
Letzten Endes sollte das Ziel des Wachstums der Instrumentierung mit der Einfachheit der Einführung der Instrumentierung verbunden sein. Es wird schwierig sein, die Akzeptanz von Tracing zu erhöhen, wenn es für einzelne Entwickler viel Arbeit bedeutet, es zu implementieren. Alle großen Unternehmen, die verteiltes Tracing eingeführt haben (einschließlich Google und Uber), haben Tracing zu einer erstklassigen Komponente ihrer Microservice-Architektur gemacht, indem sie ihre Infrastrukturbibliotheken in Tracing-Code verpackt haben. Diese Strategie ermöglicht es, die Instrumentierung ganz natürlich zu erweitern - wenn neue Dienste bereitgestellt oder migriert werden, werden sie automatisch instrumentiert.
Organisatorisch gibt es noch ein bisschen mehr zu besprechen. Alle technischen Lösungen, die wir dir zuvor vorgestellt haben, sind nicht viel wert, wenn sie nicht von der Organisation mitgetragen werden. Wie solltest du diese Zustimmung also erreichen? Die einfachste Option, die sich als unglaublich erfolgreich erwiesen hat, ist einfach eine Anweisung von oben, die verteilte Rückverfolgung zu nutzen. Das bedeutet nicht unbedingt, dass du deinem Vizepräsidenten für Technik eine E-Mail schreiben solltest, und in vielen Fällen ist das auch nicht die effektivste Strategie. Wenn du ein Plattformteam, ein SRE-Team, ein DevOps-Team oder andere Infrastrukturingenieure hast, können diese Teams der richtige Ort sein, um den Anstoß für die Ausweitung der verteilten Rückverfolgung in deiner Software zu geben. Überlege dir, wie Probleme in deiner Entwicklungsorganisation kommuniziert und verwaltet werden. Wer hat Performance Management in seinem Portfolio? Sie können Verbündete und Fürsprecher für die anfängliche Einführung der Rückverfolgung in allen deinen Diensten sein.
Wenn dein SRE-Team Tools wie Start-Checklisten verwendet, füge der Checkliste die Kompatibilität der Nachverfolgung hinzu und beginne, sie auf diese Weise auszurollen. Du solltest auch prüfen, wie dein Tracing funktioniert, wenn du Postmortems zu Incidents durchführst - gab es Services, die nicht getraced wurden, aber hätten getraced werden sollen? Gab es Daten, die für die Behebung des Vorfalls wichtig waren, aber nicht in den Spans vorhanden waren? Instrumentierung über die Grundlagen hinaus kann auch ein definiertes Ziel für deine Teams sein, das wie jeder andere Aspekt der Codequalität gemessen wird. Es ist auch sinnvoll, Verbesserungen der Instrumentierung zu verfolgen, anstatt einfach nur neue Dienste hinzuzufügen - eine effektive Instrumentierung ist genauso wichtig wie eine flächendeckende Instrumentierung.
Stelle sicher, dass es ein Verfahren gibt, mit dem Endnutzer deiner Traces Verbesserungen vorschlagen können, insbesondere für gemeinsam genutzte Bibliotheken, um eine kontinuierliche Verbesserung zu erreichen. Achte beim Refactoring auf den bestehenden Instrumentierungscode, insbesondere bei Refactors, die die Instrumentierung selbst verändern. Du willst doch nicht die Auflösung deiner Traces verlieren, weil jemand versehentlich Spans entfernt hat! Dies ist ein Bereich, in dem die Entwicklung von Tests rund um die Instrumentierung wertvoll ist, da du den Zustand deiner Traces vor und nach den Änderungen leicht vergleichen und Entwickler bei unerwarteten Abweichungen automatisch warnen oder benachrichtigen kannst.
Letztendlich ist die Instrumentierung ein wichtiger Teil des verteilten Tracing, aber sie ist nur der erste Schritt. Ohne die Instrumentierung hast du nicht die nötigen Trace-Daten, um die Anfragen zu beobachten und zu verstehen, wie sie sich durch dein System bewegen. Sobald du deine Dienste instrumentiert hast, wirst du plötzlich mit einer Flut von Daten konfrontiert. Wie kannst du diese Daten sammeln und analysieren, um Erkenntnisse und Leistungsinformationen über deine Dienste insgesamt zu gewinnen? In den nächsten Kapiteln werden wir uns mit der Kunst des Sammelns und Speicherns von Trace-Daten beschäftigen.
Get Verteilte Rückverfolgung in der Praxis now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.