Go-Größen-Semantik
Die Funktion proto.Size gibt die Größe der Wire-Format-Kodierung einer proto.Message in Bytes zurück, indem sie alle ihre Felder (einschließlich Unter-Nachrichten) durchläuft.
Insbesondere gibt sie die Größe zurück, wie **Go Protobuf die Nachricht kodieren wird**.
Ein Hinweis zu Protobuf-Editionen
Mit Protobuf-Editionen können .proto-Dateien Features aktivieren, die das Serialisierungsverhalten ändern. Dies kann den von proto.Size zurückgegebenen Wert beeinflussen. Zum Beispiel bewirkt das Setzen von features.field_presence = IMPLICIT, dass Skalarfelder, die auf ihre Standardwerte gesetzt sind, nicht serialisiert werden und somit nicht zur Größe der Nachricht beitragen.
Typische Anwendungsfälle
Leere Nachrichten identifizieren
Das Überprüfen, ob proto.Size 0 zurückgibt, ist eine gängige Methode, um leere Nachrichten zu prüfen.
if proto.Size(m) == 0 {
// No fields set; skip processing this message,
// or return an error, or similar.
}
Programmausgabe größenbeschränken
Nehmen wir an, Sie entwickeln eine Stapelverarbeitungs-Pipeline, die Arbeitsaufgaben für ein anderes System erstellt, das wir in diesem Beispiel als „nachgelagertes System“ bezeichnen. Das nachgelagerte System ist für die Verarbeitung kleiner bis mittelgroßer Aufgaben ausgelegt, aber Lasttests haben gezeigt, dass das System bei einer Arbeitsaufgabe von über 500 MB in einen kaskadierenden Ausfall gerät.
Die beste Lösung ist, das nachgelagerte System zu schützen (siehe https://cloud.google.com/blog/products/gcp/using-load-shedding-to-survive-a-success-disaster-cre-life-lessons). Wenn jedoch die Implementierung von Lastableitung nicht machbar ist, könnten Sie beschließen, Ihrer Pipeline eine schnelle Korrektur hinzuzufügen.
func (*beamFn) ProcessElement(key string, value []byte, emit func(proto.Message)) {
task := produceWorkTask(value)
if proto.Size(task) > 500 * 1024 * 1024 {
// Skip every work task over 500 MB to not overwhelm
// the brittle downstream system.
return
}
emit(task)
}
Falsche Verwendung: Kein Bezug zu Unmarshal
Da proto.Size die Anzahl der Bytes für die Kodierung der Nachricht durch Go Protobuf zurückgibt, ist es nicht sicher, proto.Size beim Entpacken (Dekodieren) eines Streams von eingehenden Protobuf-Nachrichten zu verwenden.
func bytesToSubscriptionList(data []byte) ([]*vpb.EventSubscription, error) {
subList := []*vpb.EventSubscription{}
for len(data) > 0 {
subscription := &vpb.EventSubscription{}
if err := proto.Unmarshal(data, subscription); err != nil {
return nil, err
}
subList = append(subList, subscription)
data = data[:len(data)-proto.Size(subscription)]
}
return subList, nil
}
Wenn data eine Nachricht im nicht-minimalen Wire-Format enthält, kann proto.Size eine andere Größe zurückgeben als tatsächlich entpackt wurde, was im besten Fall zu einem Parsing-Fehler oder im schlimmsten Fall zu falsch geparsten Daten führt.
Daher funktioniert dieses Beispiel nur zuverlässig, solange alle Eingabenachrichten von (derselben Version von) Go Protobuf generiert werden. Dies ist überraschend und wahrscheinlich nicht beabsichtigt.
Tipp: Verwenden Sie stattdessen das Paket protodelim, um größenbegrenzte Streams von Protobuf-Nachrichten zu lesen/schreiben.
Fortgeschrittene Anwendung: Puffer vorab dimensionieren
Eine fortgeschrittene Anwendung von proto.Size ist die Ermittlung der erforderlichen Puffergröße vor dem Kodieren.
opts := proto.MarshalOptions{
// Possibly avoid an extra proto.Size in Marshal itself (see docs):
UseCachedSize: true,
}
// DO NOT SUBMIT without implementing this Optimization opportunity:
// instead of allocating, grab a sufficiently-sized buffer from a pool.
// Knowing the size of the buffer means we can discard
// outliers from the pool to prevent uncontrolled
// memory growth in long-running RPC services.
buf := make([]byte, 0, opts.Size(m))
var err error
buf, err = opts.MarshalAppend(buf, m) // does not allocate
// Note that len(buf) might be less than cap(buf)! Read below:
Beachten Sie, dass bei aktivierter Lazy-Dekodierung proto.Size mehr Bytes zurückgeben kann, als proto.Marshal (und Varianten wie proto.MarshalAppend) schreibt! Stellen Sie daher beim Übertragen von kodierten Bytes (oder beim Speichern auf Festplatte) sicher, dass Sie mit len(buf) arbeiten und alle vorherigen Ergebnisse von proto.Size verwerfen.
Speziell kann sich eine (Unter-)Nachricht zwischen proto.Size und proto.Marshal „schrumpfen“, wenn
- Lazy-Dekodierung aktiviert ist
- und die Nachricht im nicht-minimalen Wire-Format eingegangen ist
- und die Nachricht vor dem Aufruf von
proto.Sizenicht abgerufen wird, d.h. sie noch nicht dekodiert ist - und die Nachricht nach
proto.Size(aber vorproto.Marshal) abgerufen wird, was zu einer Lazy-Dekodierung führt
Die Dekodierung führt dazu, dass alle nachfolgenden Aufrufe von proto.Marshal die Nachricht kodieren (im Gegensatz zum einfachen Kopieren ihres Wire-Formats), was zu einer impliziten Normalisierung auf die Art und Weise führt, wie Go Nachrichten kodiert, was derzeit im minimalen Wire-Format geschieht (aber verlassen Sie sich nicht darauf!).
Wie Sie sehen können, ist das Szenario ziemlich spezifisch, aber dennoch ist es **Best Practice, die Ergebnisse von proto.Size als Obergrenze zu behandeln** und niemals davon auszugehen, dass das Ergebnis der tatsächlich kodierten Nachrichtengröße entspricht.
Hintergrund: Nicht-minimales Wire-Format
Beim Kodieren von Protobuf-Nachrichten gibt es ein *minimales Wire-Format* und eine Reihe von größeren *nicht-minimalen Wire-Formaten*, die zu derselben Nachricht dekodiert werden.
Nicht-minimales Wire-Format (manchmal auch „denormalisiertes Wire-Format“ genannt) bezieht sich auf Szenarien wie mehrfach vorkommende nicht-wiederholte Felder, nicht-optimale Varint-Kodierung, gepackte wiederholte Felder, die auf dem Draht nicht als gepackt erscheinen, und andere.
Wir können auf nicht-minimale Wire-Formate in verschiedenen Szenarien stoßen
- Absichtlich. Protobuf unterstützt die Verkettung von Nachrichten durch Verkettung ihres Wire-Formats.
- Versehentlich. Ein (möglicherweise von Drittanbietern stammender) Protobuf-Encoder kodiert nicht ideal (z. B. verwendet er mehr Speicher als nötig bei der Kodierung eines Varints).
- Bösartig. Ein Angreifer könnte Protobuf-Nachrichten speziell so gestalten, dass er Abstürze über das Netzwerk auslöst.