In einem persönlichen Projekt mit Go, das Informationen über finanzielle Vermögenswerte von Bovespa erhält.
Das System nutzt intensiv die Parallelität und Parallelität mit Goroutinen und aktualisiert alle 8 Sekunden Asset-Informationen (zusammen mit Geschäftsberechnungen).
Anfangs erschienen keine Fehler oder Warnungen, aber ich bemerkte, dass die Ausführung einiger Goroutinen länger dauerte als andere.
Um genauer zu sein: Während die p99-Zeit bei 0,03 ms lag, stieg sie an einigen Stellen auf 0,9 ms. Dies veranlasste mich, das Problem weiter zu untersuchen.
Ich habe festgestellt, dass ich einen Semaphor-Goroutine-Pool verwende, der auf der Grundlage der Variablen GOMAXPROCS erstellt wurde.
Mir wurde jedoch klar, dass es bei diesem Ansatz ein Problem gab.
Wenn wir die Variable GOMAXPROCS verwenden, erfasst sie die Anzahl der im Container verfügbaren Kerne nicht korrekt. Wenn der Container weniger verfügbare Kerne hat als die Gesamtzahl der VM, wird die Gesamtzahl der VM berücksichtigt. Beispielsweise verfügt meine VM über 8 verfügbare Kerne, der Container jedoch nur über 4. Dies führte dazu, dass 8 Goroutinen zur gleichzeitigen Ausführung erstellt wurden, was zu einer Drosselung führte.
Nach langer Recherche über Nacht habe ich eine von Uber entwickelte Bibliothek gefunden, die die Variable GOMAXPROCS automatisch effizienter anpasst, unabhängig davon, ob sie sich in einem Container befindet oder nicht. Diese Lösung erwies sich als äußerst stabil und effizient: automaxprocs
GOMAXPROCS automatisch so einstellen, dass es dem CPU-Kontingent des Linux-Containers entspricht.
go get -u go.uber.org/automaxprocs
import _ "go.uber.org/automaxprocs"
func main() {
// Your application logic here.
}
Daten, die vom internen Load Balancer von Uber gemessen wurden. Wir haben den Load Balancer mit 200 % CPU-Quote (d. h. 2 Kernen) ausgeführt:
GOMAXPROCS | RPS | P50 (ms) | P99,9 (ms) |
---|---|---|---|
1 | 28.893,18 | 1,46 | 19,70 |
2 (entspricht dem Kontingent) | 44.715,07 | 0,84 | 26,38 |
3 | 44.212,93 | 0,66 | 30.07 |
4 | 41.071,15 | 0,57 | 42,94 |
8 | 33.111,69 | 0,43 | 64,32 |
Standard (24) | 22.191,40 | 0,45 | 76,19 |
Wenn GOMAXPROCS über das CPU-Kontingent hinaus erhöht wird, sehen wir, dass P50 leicht abnimmt, aber deutliche Steigerungen auf P99 zu verzeichnen sind. Wir sehen auch, dass auch die gesamten verarbeiteten RPS sinken.
Wenn GOMAXPROCS höher ist als das zugewiesene CPU-Kontingent, haben wir auch eine erhebliche Drosselung festgestellt:
$ cat /sys/fs/cgroup/cpu,cpuacct/system.slice/[...]/cpu.stat nr_periods 42227334 nr_throttled 131923 throttled_time 88613212216618
Sobald GOMAXPROCS auf das CPU-Kontingent reduziert wurde, sahen wir keine CPU-Drosselung.
Nach der Implementierung der Verwendung dieser Bibliothek wurde das Problem behoben und die p99-Zeit blieb nun konstant bei 0,02 ms. Diese Erfahrung verdeutlichte die Bedeutung von Beobachtbarkeit und Profilierung in gleichzeitigen Systemen.
Das Folgende ist ein sehr einfaches Beispiel, das jedoch den Unterschied in der Leistung zeigt.
Mit dem nativen Test- und Benckmak-Paket von Go habe ich zwei Dateien erstellt:
benchmarking_with_enhancement_test.go:
package main import ( _ "go.uber.org/automaxprocs" "runtime" "sync" "testing" ) // BenchmarkWithEnhancement Função com melhoria, para adicionar o indice do loop em um array de inteiro func BenchmarkWithEnhancement(b *testing.B) { // Obtém o número de CPUs disponíveis numCPUs := runtime.NumCPU() // Define o máximo de CPUs para serem usadas pelo programa maxGoroutines := runtime.GOMAXPROCS(numCPUs) // Criação do semáforo semaphore := make(chan struct{}, maxGoroutines) var ( // Espera para grupo de goroutines finalizar wg sync.WaitGroup // Propriade mu sync.Mutex // Lista para armazenar inteiros list []int ) // Loop com mihão de indices for i := 0; ibenchmarking_without_enhancement_test.go:
package main import ( "runtime" "sync" "testing" ) // BenchmarkWithoutEnhancement Função sem a melhoria, para adicionar o indice do loop em um array de inteiro func BenchmarkWithoutEnhancement(b *testing.B) { // Obtém o número de CPUs disponíveis numCPUs := runtime.NumCPU() // Define o máximo de CPUs para serem usadas pelo programa maxGoroutines := runtime.GOMAXPROCS(numCPUs) // Criação do semáforo semaphore := make(chan struct{}, maxGoroutines) var ( // Espera para grupo de goroutines finalizar wg sync.WaitGroup // Propriade mu sync.Mutex // Lista para armazenar inteiros list []int ) // Loop com mihão de indices for i := 0; iDer Unterschied zwischen ihnen besteht darin, dass man den Uber-Bibliotheksimport verwendet.
Beim Ausführen des Benchmarks unter der Annahme, dass 2 CPUs verwendet würden, war das Ergebnis:
ns/op: Gibt einen Durchschnitt in Nanosekunden an, wie lange es dauert, einen bestimmten Vorgang auszuführen.
Beachten Sie, dass die insgesamt verfügbare CPU meiner CPU 8 Kerne beträgt, und das ist es, was die runtime.NumCPU()-Eigenschaft zurückgegeben hat. Wie beim Ausführen des Benchmarks habe ich jedoch definiert, dass nur zwei CPUs verwendet werden würden, und die Datei, die Automaxprocs nicht verwendet, hat definiert, dass die Ausführungsbeschränkung gleichzeitig 8 Goroutinen betragen würde, während die effizienteste 2 wäre, weil Auf diese Weise wird die Ausführung durch die Verwendung einer geringeren Zuweisung effizienter.
Die Bedeutung der Beobachtbarkeit und Profilerstellung unserer Anwendungen ist also klar.
Haftungsausschluss: Alle bereitgestellten Ressourcen stammen teilweise aus dem Internet. Wenn eine Verletzung Ihres Urheberrechts oder anderer Rechte und Interessen vorliegt, erläutern Sie bitte die detaillierten Gründe und legen Sie einen Nachweis des Urheberrechts oder Ihrer Rechte und Interessen vor und senden Sie ihn dann an die E-Mail-Adresse: [email protected] Wir werden die Angelegenheit so schnell wie möglich für Sie erledigen.
Copyright© 2022 湘ICP备2022001581号-3