Heim > Artikel > Backend-Entwicklung > Golang: Wie Beobachtbarkeit und Profilerstellung eine nahezu nicht nachweisbare Drosselung aufdeckten
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 0,03 ms betrug, 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 gemessen vom internen Load Balancer von Uber. 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 (equal to quota) | 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 |
Default (24) | 22,191.40 | 0.45 | 76.19 |
When GOMAXPROCS is increased above the CPU quota, we see P50 decrease slightly, but see significant increases to P99. We also see that the total RPS handled also decreases.
When GOMAXPROCS is higher than the CPU quota allocated, we also saw significant throttling:
$ cat /sys/fs/cgroup/cpu,cpuacct/system.slice/[...]/cpu.stat nr_periods 42227334 nr_throttled 131923 throttled_time 88613212216618
Once GOMAXPROCS was reduced to match the CPU quota, we saw no CPU throttling.
Após implementar o uso dessa biblioteca, o problema foi resolvido, e agora o tempo p99 se manteve em 0.02 ms constantemente. Essa experiência destacou a importância da observabilidade e do profiling em sistemas concorrentes.
A seguir um exemplo bem simples, mas que consegue demonstrar a diferença de desempenho.
Utilizando o pacote nativo de testes e benckmak do Go, criei dois arquivos:
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; i < 1000000; i++ { semaphore <- struct{}{} // Adiciona ao waitGroup que existe mais uma goroutine para ser executada wg.Add(1) // Atribui a função a uma nova goroutine go func(i int) { // Quando a função finalizar, informa o semáforo e finaliza um registro do waitGroup defer func() { <-semaphore wg.Done() }() // Faz o bloqueio do array para outra goroutine não sobreescrever mu.Lock() // Adiciona o indice, em mais uma posição no array list = append(list, i) // Desbloqueia o array mu.Unlock() }(i) } }
benchmarking_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; i < 1000000; i++ { semaphore <- struct{}{} // Adiciona ao waitGroup que existe mais uma goroutine para ser executada wg.Add(1) // Atribui a função a uma nova goroutine go func(i int) { // Quando a função finalizar, informa o semáforo e finaliza um registro do waitGroup defer func() { <-semaphore wg.Done() }() // Faz o bloqueio do array para outra goroutine não sobreescrever mu.Lock() // Adiciona o indice, em mais uma posição no array list = append(list, i) // Desbloqueia o array mu.Unlock() }(i) } }
A diferença entra elas, é que uma esta com a importação de biblioteca da Uber.
Ao executar o benchmark passando que seriam usados 2 CPUs, o resultado foi:
ns/op: fornece uma média em nanosegundos de quanto tempo leva para executar uma operação específica.
Percebam, que o total disponível da minha CPU são 8 núcleos, e foi o que a propriedade runtime.NumCPU() retornou. Porém, como na execução do benchmark, defini que o uso seriam de apenas duas CPUs, a o arquivo que não utilizou a automaxprocs, definiu que o limite de execução por vez, seriam de 8 goroutines, enquanto o mais eficiente seriam 2, pois dessa maneira se usa menos alocação deixa mais eficiente a execução.
Então, fica nítido a importância de observabilidade e proffiling das nossas aplicações.
Das obige ist der detaillierte Inhalt vonGolang: Wie Beobachtbarkeit und Profilerstellung eine nahezu nicht nachweisbare Drosselung aufdeckten. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!