Dans un projet personnel avec Go, qui obtient des informations sur les actifs financiers auprès de Bovespa.
Le système utilise intensivement la concurrence et le parallélisme avec les goroutines, mettant à jour les informations sur les actifs (ainsi que les calculs commerciaux) toutes les 8 secondes.
Au départ, aucune erreur ni avertissement n'est apparu, mais j'ai remarqué que certaines goroutines prenaient plus de temps que d'autres à s'exécuter.
Pour être plus précis, alors que le temps p99 était de 0,03 ms, à certains moments, il a augmenté à 0,9 ms. Cela m'a amené à approfondir le problème.
J'ai découvert que j'utilisais un pool de goroutines de sémaphore, créé sur la base de la variable GOMAXPROCS.
Cependant, j'ai réalisé qu'il y avait un problème avec cette approche.
Lorsque nous utilisons la variable GOMAXPROCS, elle ne capture pas correctement le nombre de cœurs disponibles dans le conteneur. Si le conteneur a moins de cœurs disponibles que le total de la VM, il prend en compte le total de la VM. Par exemple, ma VM a 8 cœurs disponibles, mais le conteneur n'en avait que 4. Cela a entraîné la création de 8 goroutines pour s'exécuter en même temps, provoquant une limitation.
Après de nombreuses recherches du jour au lendemain, j'ai trouvé une bibliothèque développée par Uber qui ajuste automatiquement la variable GOMAXPROCS plus efficacement, qu'elle se trouve dans un conteneur ou non. Cette solution s'est avérée extrêmement stable et efficace : automaxprocs
Définissez automatiquement GOMAXPROCS pour qu'il corresponde au quota de CPU du conteneur Linux.
aller chercher -u go.uber.org/automaxprocs
import _ "go.uber.org/automaxprocs"
func main() {
// Your application logic here.
}
Données mesurées à partir de l'équilibreur de charge interne d'Uber. Nous avons exécuté l'équilibreur de charge avec un quota de CPU de 200 % (c'est-à-dire 2 cœurs) :
GOMAXPROCS | RPS | P50 (ms) | P99.9 (ms) |
---|---|---|---|
1 | 28 893,18 | 1,46 | 19.70 |
2 (égal au 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 |
Par défaut (24) | 22 191,40 | 0,45 | 76,19 |
Lorsque GOMAXPROCS est augmenté au-dessus du quota de CPU, nous constatons une légère diminution de P50, mais une augmentation significative jusqu'à P99. Nous constatons également que le total des RPS traités diminue également.
Lorsque GOMAXPROCS est supérieur au quota de processeur alloué, nous avons également constaté une limitation importante :
$ cat /sys/fs/cgroup/cpu,cpuacct/system.slice/[...]/cpu.stat nr_periods 42227334 nr_throttled 131923 throttled_time 88613212216618
Une fois GOMAXPROCS réduit pour correspondre au quota du processeur, nous n'avons constaté aucune limitation du processeur.
Après avoir implémenté l'utilisation de cette bibliothèque, le problème a été résolu et le temps p99 est désormais resté constamment à 0,02 ms. Cette expérience a mis en évidence l'importance de l'observabilité et du profilage dans les systèmes concurrents.
Ce qui suit est un exemple très simple, mais qui démontre la différence de performances.
À l'aide du package de tests natifs et de benckmak de Go, j'ai créé deux fichiers :
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; iLa différence entre eux est que l'on utilise l'importation de la bibliothèque Uber.
Lors de l'exécution du benchmark en supposant que 2 processeurs seraient utilisés, le résultat était :
ns/op : fournit une moyenne en nanosecondes du temps nécessaire pour effectuer une opération spécifique.
Notez que le total disponible de mon processeur est de 8 cœurs, et c'est ce que la propriété runtime.NumCPU() a renvoyé. Cependant, comme lors de l'exécution du benchmark, j'ai défini que l'utilisation serait de seulement deux processeurs, et le fichier qui n'utilisait pas automaxprocs, j'ai défini que la limite d'exécution à la fois serait de 8 goroutines, tandis que la plus efficace serait de 2, car de cette façon, utiliser moins d'allocation rend l'exécution plus efficace.
Ainsi, l'importance de l'observabilité et du profilage de nos applications est claire.
Clause de non-responsabilité: Toutes les ressources fournies proviennent en partie d'Internet. En cas de violation de vos droits d'auteur ou d'autres droits et intérêts, veuillez expliquer les raisons détaillées et fournir une preuve du droit d'auteur ou des droits et intérêts, puis l'envoyer à l'adresse e-mail : [email protected]. Nous nous en occuperons pour vous dans les plus brefs délais.
Copyright© 2022 湘ICP备2022001581号-3