>백엔드 개발 >Golang >Golang: 관찰 가능성과 프로파일링을 통해 거의 감지할 수 없는 제한을 밝혀낸 방법

Golang: 관찰 가능성과 프로파일링을 통해 거의 감지할 수 없는 제한을 밝혀낸 방법

Susan Sarandon
Susan Sarandon원래의
2024-10-10 06:13:02515검색

Bovespa로부터 금융 자산에 대한 정보를 얻는 Go와의 개인 프로젝트에서.
시스템은 고루틴과의 동시성 및 병렬성을 집중적으로 사용하여 8초마다 자산 정보(비즈니스 계산과 함께)를 업데이트합니다.
처음에는 오류나 경고가 나타나지 않았지만 일부 고루틴이 다른 고루틴보다 실행하는 데 시간이 오래 걸리는 것을 발견했습니다.

구체적으로 p99 시간이 0.03ms였는데 어느 순간 0.9ms까지 늘어나더군요. 이로 인해 문제를 더 자세히 조사하게 되었습니다.

GOMAXPROCS 변수를 기반으로 생성된 세마포어 고루틴 풀을 사용하고 있다는 사실을 발견했습니다.
그런데 이 접근 방식에 문제가 있다는 것을 깨달았습니다.

GOMAXPROCS 변수를 사용하면 컨테이너에서 사용 가능한 코어 수가 올바르게 캡처되지 않습니다. 컨테이너에 VM의 전체 코어보다 사용 가능한 코어 수가 적은 경우 VM의 전체 코어를 고려합니다. 예를 들어 내 VM에는 사용 가능한 코어가 8개 있지만 컨테이너에는 4개만 있었습니다. 이로 인해 동시에 실행할 8개의 고루틴이 생성되어 제한이 발생했습니다.

밤새 많은 연구 끝에 컨테이너에 있든 없든 GOMAXPROCS 변수를 더 효율적으로 자동 조정하는 Uber에서 개발한 라이브러리를 발견했습니다. 이 솔루션은 매우 안정적이고 효율적인 것으로 입증되었습니다: automaxprocs

Golang: Como a observabilidade e profiling revelaram um throttling quase indetectável 우버고 / 자동 최대 프로시저

Linux 컨테이너 CPU 할당량과 일치하도록 GOMAXPROCS를 자동으로 설정합니다.

automaxprocs Golang: Como a observabilidade e profiling revelaram um throttling quase indetectável Golang: Como a observabilidade e profiling revelaram um throttling quase indetectável Golang: Como a observabilidade e profiling revelaram um throttling quase indetectável

Linux 컨테이너 CPU 할당량과 일치하도록 GOMAXPROCS를 자동으로 설정합니다.

설치

가서 -u go.uber.org/automaxprocs

빠른 시작

import _ "go.uber.org/automaxprocs"

func main() {
  // Your application logic here.
}
전체 화면 모드로 전환 전체 화면 모드 종료

공연

Uber 내부 로드 밸런서에서 측정한 데이터입니다. 200% CPU 할당량(즉, 2개 코어)으로 로드 밸런서를 실행했습니다.

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.

View on GitHub
.

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:

Golang: Como a observabilidade e profiling revelaram um throttling quase indetectável

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.

위 내용은 Golang: 관찰 가능성과 프로파일링을 통해 거의 감지할 수 없는 제한을 밝혀낸 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.