Ich habe ein Golang-Programm geschrieben, das zur Laufzeit 1,2 GB Speicher benötigt.
Das Aufrufen von go tool pprof http://10.10.58.118:8601/debug/pprof/heap
Führt zu einem Speicherauszug mit nur 323,4 MB Heap-Nutzung.
Mit gcvis
bekomme ich folgendes:
.. und dieses Heap-Formularprofil:
Hier ist mein Code: https://github.com/sharewind/Push-server/blob/v3/broker
Das Heap-Profil zeigt den aktiven Speicher an, den Speicher, von dem die Laufzeit glaubt, dass er vom go-Programm verwendet wird (dh er wurde nicht vom Garbage Collector gesammelt). Wenn der GC Speicher sammelt, verkleinert sich das Profil, aber es wird kein Speicher an das System zurückgegeben . Ihre zukünftigen Zuordnungen versuchen, Speicher aus dem Pool zuvor gesammelter Objekte zu verwenden, bevor Sie das System nach weiteren fragen.
Von außen bedeutet dies, dass die Speichernutzung Ihres Programms entweder zunimmt oder konstant bleibt. Was das externe System als "Resident Size" Ihres Programms darstellt, ist die Anzahl der Bytes, die Ihrem Programm RAM zugewiesen sind, unabhängig davon, ob es in Gebrauch befindliche oder gesammelte go-Werte enthält.
Der Grund, warum diese beiden Zahlen oft sehr unterschiedlich sind, ist folgender:
Wenn Sie genau wissen möchten, wie Go den Speicher sieht, können Sie den Aufruf runtime.ReadMemStats verwenden: http://golang.org/pkg/runtime/#ReadMemStats
Alternativ können Sie, da Sie webbasiertes Profiling verwenden, über Ihren Browser auf die Profiling-Daten zugreifen: http://10.10.58.118:8601/debug/pprof/
Wenn Sie auf den Heap-Link klicken, wird die Debug-Ansicht des Heap-Profils angezeigt, in der unten die Struktur "runtime.MemStats" ausgedruckt ist.
In der Dokumentation zu runtime.MemStats ( http://golang.org/pkg/runtime/#MemStats ) sind alle Felder erklärt, aber die für diese Diskussion interessanten sind:
Es wird immer noch Unstimmigkeiten zwischen Sys und dem geben, was das Betriebssystem meldet, da die Go-Anforderungen des Systems und die des Betriebssystems nicht immer gleich sind. Auch CGO/Syscall-Speicher (zB: malloc/mmap) wird von go nicht nachverfolgt.
Als Ergänzung zu @Cookie of Nines Antwort, kurz gesagt: Sie können das --alloc_space
Möglichkeit.
go tool pprof
verwenden --inuse_space
standardmäßig. Es wird die Speichernutzung abgetastet, sodass das Ergebnis eine Teilmenge der realen ist.
Durch --alloc_space
pprof gibt den gesamten zugewiesenen Speicher seit dem Programmstart zurück.
Ich war immer verwirrt über das wachsende Arbeitsgedächtnis meiner Go-Anwendungen, und schließlich musste ich lernen, welche Profilerstellungstools im Go-Ökosystem vorhanden sind. Runtime bietet viele Metriken in einer runtime.Memstats -Struktur, aber es ist möglicherweise schwer zu verstehen, welche von ihnen helfen können, die Gründe für das Speicherwachstum herauszufinden. Daher sind einige zusätzliche Tools erforderlich.
Profilerstellungsumgebung
Verwenden Sie https://github.com/tevjef/go-runtime-metrics in Ihrer Anwendung. Zum Beispiel können Sie dies in Ihre main
einfügen:
import(
metrics "github.com/tevjef/go-runtime-metrics"
)
func main() {
//...
metrics.DefaultConfig.CollectionInterval = time.Second
if err := metrics.RunCollector(metrics.DefaultConfig); err != nil {
// handle error
}
}
Führen Sie InfluxDB
und Grafana
in Docker
Containern aus:
docker run --name influxdb -d -p 8086:8086 influxdb
docker run -d -p 9090:3000/tcp --link influxdb --name=grafana grafana/grafana:4.1.0
Richten Sie die Interaktion zwischen Grafana
und InfluxDB
Grafana
ein (Grafana-Hauptseite -> Linke obere Ecke -> Datenquellen -> Neue Datenquelle hinzufügen):
Importiere Dashboard # 3242 von https://grafana.com (Grafana Hauptseite -> Linke obere Ecke -> Dashboard -> Importieren):
Starten Sie abschließend Ihre Anwendung: Sie überträgt Laufzeitmetriken an die generierte Influxdb
. Setzen Sie Ihre Anwendung einer angemessenen Last aus (in meinem Fall war sie ziemlich klein - 5 RPS für einige Stunden).
Speicherverbrauchsanalyse
Sys
(Synonym für RSS
) ist der Kurve HeapSys
ziemlich ähnlich. Es stellt sich heraus, dass die dynamische Speicherzuweisung der Hauptfaktor für das allgemeine Speicherwachstum war, sodass die geringe Menge an Speicher, die von Stapelvariablen belegt wird, konstant zu sein scheint und ignoriert werden kann.HeapIdle
wächst mit der gleichen Rate wie ein Sys
, während HeapReleased
immer Null ist . Offensichtlich gibt die Laufzeit keinen Speicher an das Betriebssystem zurück überhaupt, zumindest unter den Bedingungen dieses Tests:HeapIdle minus HeapReleased estimates the amount of memory that could be returned to the OS, but is being retained by the runtime so it can grow the heap without requesting more memory from the OS.
Für diejenigen, die versuchen, das Problem des Speicherverbrauchs zu untersuchen, würde ich empfehlen, die beschriebenen Schritte zu befolgen, um einige geringfügige Fehler (wie Goroutine Leak) auszuschließen.
Speicher explizit freigeben
Es ist interessant, dass man mit expliziten Aufrufen von debug.FreeOSMemory()
den Speicherverbrauch deutlich senken kann:
// in the top-level package
func init() {
go func() {
t := time.Tick(time.Second)
for {
<-t
debug.FreeOSMemory()
}
}()
}
Tatsächlich sparte dieser Ansatz etwa 35% des Speichers im Vergleich zu den Standardbedingungen.
Sie können auch StackImpact verwenden, um reguläre und durch Anomalien ausgelöste Speicherzuordnungsprofile automatisch aufzuzeichnen und an das Dashboard zu melden, die in einer historischen und vergleichbaren Form verfügbar sind. Weitere Informationen finden Sie in diesem Blogeintrag Speicherleckerkennung in Production Go-Anwendungen
Haftungsausschluss: Ich arbeite für StackImpact