En un proyecto personal con Go, que obtiene información sobre activos financieros de Bovespa.
El sistema hace un uso intensivo de la concurrencia y el paralelismo con gorutinas, actualizando la información de los activos (junto con los cálculos comerciales) cada 8 segundos.
Inicialmente, no aparecieron errores ni advertencias, pero noté que algunas rutinas tardaban más que otras en ejecutarse.
Para ser más específicos, mientras que el tiempo p99 fue de 0,03 ms, en algunos puntos aumentó a 0,9 ms. Esto me llevó a investigar más el problema.
Descubrí que estaba usando un grupo de rutinas de semáforo, que se creó en función de la variable GOMAXPROCS.
Sin embargo, me di cuenta de que había un problema con este enfoque.
Cuando usamos la variable GOMAXPROCS, no captura correctamente la cantidad de núcleos disponibles en el contenedor. Si el contenedor tiene menos núcleos disponibles que el total de la VM, considera el total de la VM. Por ejemplo, mi máquina virtual tiene 8 núcleos disponibles, pero el contenedor solo tenía 4. Esto resultó en la creación de 8 gorutinas para ejecutarse al mismo tiempo, lo que provocó una limitación.
Después de mucha investigación durante la noche, encontré una biblioteca desarrollada por Uber que ajusta automáticamente la variable GOMAXPROCS de manera más eficiente, independientemente de si está en un contenedor o no. Esta solución demostró ser extremadamente estable y eficiente: automaxprocs
Configura automáticamente GOMAXPROCS para que coincida con la cuota de CPU del contenedor de Linux.
ve a buscar -u go.uber.org/automaxprocs
import _ "go.uber.org/automaxprocs"
func main() {
// Your application logic here.
}
Datos medidos desde el balanceador de carga interno de Uber. Ejecutamos el equilibrador de carga con una cuota de CPU del 200 % (es decir, 2 núcleos):
GOMAXPROCS | RPS | P50 (ms) | P99.9 (ms) |
---|---|---|---|
1 | 28.893,18 | 1.46 | 19.70 |
2 (igual a cuota) | 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 |
Predeterminado (24) | 22.191,40 | 0,45 | 76.19 |
Cuando GOMAXPROCS aumenta por encima de la cuota de CPU, vemos que P50 disminuye ligeramente, pero vemos aumentos significativos a P99. También vemos que el total de RPS manejados también disminuye.
Cuando GOMAXPROCS es mayor que la cuota de CPU asignada, también vimos una limitación significativa:
$ cat /sys/fs/cgroup/cpu,cpuacct/system.slice/[...]/cpu.stat nr_periods 42227334 nr_throttled 131923 throttled_time 88613212216618
Una vez que se redujo GOMAXPROCS para igualar la cuota de CPU, no vimos ninguna limitación de CPU.
Después de implementar el uso de esta biblioteca, el problema se resolvió y ahora el tiempo p99 se mantuvo en 0,02 ms constantemente. Esta experiencia destacó la importancia de la observabilidad y la elaboración de perfiles en sistemas concurrentes.
El siguiente es un ejemplo muy simple, pero que demuestra la diferencia en el rendimiento.
Utilizando las pruebas nativas de Go y el paquete benkmak, creé dos archivos:
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 diferencia entre ellos es que uno usa la importación de la biblioteca de Uber.
Al ejecutar el punto de referencia suponiendo que se usarían 2 CPU, el resultado fue:
ns/op: proporciona un promedio en nanosegundos de cuánto tiempo lleva realizar una operación específica.
Tenga en cuenta que el total disponible de mi CPU es de 8 núcleos, y eso es lo que devolvió la propiedad runtime.NumCPU(). Sin embargo, al igual que al ejecutar el benchmark, definí que el uso sería solo de dos CPU, y el archivo que no usaba automaxprocs, definí que el límite de ejecución a la vez sería de 8 gorutinas, mientras que la más eficiente sería 2, porque de esta manera, usar menos asignación hace que la ejecución sea más eficiente.
Por lo tanto, la importancia de la observabilidad y la elaboración de perfiles de nuestras aplicaciones es clara.
Descargo de responsabilidad: Todos los recursos proporcionados provienen en parte de Internet. Si existe alguna infracción de sus derechos de autor u otros derechos e intereses, explique los motivos detallados y proporcione pruebas de los derechos de autor o derechos e intereses y luego envíelos al correo electrónico: [email protected]. Lo manejaremos por usted lo antes posible.
Copyright© 2022 湘ICP备2022001581号-3