ABOUT ME

-

Total
-
  • Go: Fiber 서버 최적화 하기 (optimization)
    컴퓨터/Go language 2024. 9. 12. 21:39
    728x90
    반응형

    Go언어 웹 프레임워크 fiber v2를 사용하면서 얻은 팁들을 정리한 글이다.

     

    GitHub - gofiber/fiber: ⚡️ Express inspired web framework written in Go

    ⚡️ Express inspired web framework written in Go. Contribute to gofiber/fiber development by creating an account on GitHub.

    github.com

     

    1. zero memory alloc []byte <-> string

    byte 배열과 문자열 (json marshal/unmarshal 시 많은 사용)을 아래처럼 하면 allocation을 새롭게 하게 된다.

    string(byteArr) // var byteArr []byte
    
    []byte(buffer) // var buffer string

     

    fiber는 fasthttp을 기반으로 만들어졌고, fasthttp의 글을 읽다 보면 trick 이 있다.

     

    GitHub - valyala/fasthttp: Fast HTTP package for Go. Tuned for high performance. Zero memory allocations in hot paths. Up to 10x

    Fast HTTP package for Go. Tuned for high performance. Zero memory allocations in hot paths. Up to 10x faster than net/http - valyala/fasthttp

    github.com

     

    바로 unsafe를 이용한 메모리 할당 없이 변환하는 것이다. 이름을 바꾸고 최신 함수로 바꾸었다.

    (주의: unsafe여도 대부분 safe 하게 사용할 수 있다. 단, 메모리를 수정하므로 변환 후 바꾸지 않는 게 좋다)

    // BytesToString converts a byte slice to a string without making a copy.
    //
    // Warning: This method uses unsafe operations. The conversion is safe as long as the original
    // byte slice is not modified after conversion, as the resulting string will reference
    // the same underlying memory.
    //
    // Example:
    //
    //	b := []byte("Hello")
    //	s := BytesToString(b) // Converts []byte to string without memory allocation
    //	fmt.Println(s)        // Prints: Hello
    //
    // Important: Do not modify the byte slice `b` after calling this function, as the string `s`
    // references the same memory, and changes to `b` will lead to undefined behavior.
    func BytesToString(b []byte) string {
    	return *(*string)(unsafe.Pointer(&b))
    }
    
    // StringToBytes converts a string to a byte slice without making a copy.
    //
    // Warning: This method uses unsafe operations. The conversion is safe as long as the
    // resulting byte slice is not modified. Since strings in Go are immutable, modifying
    // the byte slice will result in undefined behavior.
    //
    // Example:
    //
    //	s := "Hello"
    //	b := StringToBytes(s) // Converts string to []byte without memory allocation
    //	fmt.Println(b)        // Prints: [72 101 108 108 111] (ASCII values of "Hello")
    //
    // Important: Do not modify the byte slice `b` after calling this function, as strings in Go
    // are immutable, and modifying the byte slice can lead to undefined behavior.
    func StringToBytes(s string) []byte {
    	stringData := unsafe.StringData(s)
    	return unsafe.Slice(stringData, len(s))
    }
    // example
    
    func (s *AuthService) ResetPassword(token string, newPassword string) error {
    ...
        hashedPassword, err := bcrypt.GenerateFromPassword(util.StringToBytes(newPassword), bcrypt.DefaultCost)
    ...
    }

     

    2. GOMAXPROCS=1

    Go언어 런타임에서 프로세스 개수를 변경할 수 있다.

    runtime.GOMAXPROCS(1)

     

    (부가설명: Go 런타임에는 크게 3가지, 고루틴 개수, 머신 스레드 갯수 [운영 체제 쓰레드], PROCS [고루틴과 머신 쓰레드 파이프라인]. 고루틴을 실행하면 proc (큐)에 들어가고, 머신 스레드가 처리하길 기다린다. 자세한 사항은 PMG 모델을 검색)

    fasthttp를 보면 scale-out 할 때 그냥 여러 개 실행하기보다 GOMAXPROCS=1로 한 후에 linux 기준 taskset으로 각 코어마다 인스턴스를 고정시키면 좋다고 한다.

    taskset -c 0 ./server_instance_1
    taskset -c 1 ./server_instance_2

    (이 방식은 캐시 로컬성과 고루틴 스케줄러의 복잡도를 줄일 수 있음, 하지만 벤치마킹 후 사용하길 권장)

     

    1코어 CPU에서 GOMAXPROCS를 1 초과로 설정해도, 물리 코어가 하나여서 동시에 한 개의 스레드만 실행될 수 있다.

    (병렬 처리는 불가능 하지만 Go 스케줄러가 더 많은 고루틴을 번갈아가며 실행하게 되는데 context switching 자주 발생)

     

    3. sync.Pool

    "sync.Pool is your best friend."

     

    Go sync.Pool and the Mechanics Behind It

    Instead of just throwing these objects after each use, which would only give the garbage collector more work, we stash them in a pool (sync.Pool). The next time we need something similar, we just grab it from the pool instead of making a new one from scrat

    victoriametrics.com

     

    목적: 고루틴에서 생성되고 금방 사용이 끝나는 short-term 객체를 재사용하기 위한 메모리 풀

    여러 고루틴에서 동시 사용도 안전하며, 저장된 객체는 언제든 자동으로 제거될 수 있다. (하지만 TTL 보장 안됨)

     

    https://cs.opensource.google/go/go/+/go1.23.1:src/sync/pool.go

     

    cs.opensource.google

     

    pool.go 를 보면 Get(), Put() 메소드가 public 이다.

    구현을 간단하게 보면

    • local: 각 논리 프로세서 (P) 가 고유의 풀을 갖고 고루틴 간의 경합 (contention)을 최소화하기 위해 존재하는 배열
    • private: 여기에 저장된 객체는 current P만 접근 가능
    • shared: 여기에 저장된 객체는 모든 고루틴과 프로세서가 접근 가능 (private 이 비어있으면 이게 대신함)
    • victim cache: Go의 가비지 컬렉션이 발생하면, 객체는 여기로 이동됨. (primary pool 이 비어있으면 여기서 참조되며 GC 후에도 일부 객체가 재사용되도록 보장)

     

    아래는 []byte 버퍼를 재사용해서 메모리 할당을 줄이는 방법이다. (하지만 아래

    package main
    
    import (
    	"fmt"
    	"sync"
    )
    
    // 전역 Pool을 정의 (포인터를 반환)
    var bytePool = sync.Pool{
    	New: func() interface{} { // Go 1.18+ any
    		buffer := make([]byte, 1024) // 1KB 크기의 byte 슬라이스를 생성
    		return &buffer               // 슬라이스에 대한 포인터를 반환
    	},
    }
    
    func main() {
    	// Pool에서 객체를 가져옴 (포인터를 받음)
    	buffer := bytePool.Get().(*[]byte)
    
    	// buffer 사용 예제 (여기서는 1KB의 슬라이스 사용)
    	(*buffer)[0] = 'H'
    	(*buffer)[1] = 'i'
    	fmt.Println(string((*buffer)[:2])) // 출력: Hi
    
    	// 사용한 객체를 다시 Pool에 반환
    	bytePool.Put(buffer)
    
    	// 다시 Pool에서 객체를 가져옴 (위에서 반환한 객체가 재사용됨)
    	reusedBuffer := bytePool.Get().(*[]byte)
    	fmt.Println(string((*reusedBuffer)[:2])) // 출력: Hi (같은 객체 재사용)
    
    	// 객체를 다시 반환
    	bytePool.Put(reusedBuffer)
    }

     

    4. SO_REUSEPORT

    여러 프로세스들이 같은 포트를 이용할 수 있게 해주는 소켓 옵션이다.

    그러면 커널이 거의 균등하게 incoming 연결들을 프로세스들에게 나눠준다. (LB)

    Fiber 에서는 어떻게 쓸까? @Prefork.go

    Prefork 옵션을 사용하면 자동으로 켜지는데, GOMAXPROCS를 1로 해도 child process가 생겨버린다.

    따라서 prefork 를 사용하고 싶지 않고 SO_REUSEPORT 만을 사용하려면 fasthttp의 리스너를 사용하면 된다.

    import "github.com/valyala/fasthttp/reuseport"
    
    ln, err := reuseport.Listen("tcp4", serverAddr) // tcp4 나 tcp6만 지원
    if err != nil {
        log.Fatalf("Error while setting up listener: %s", err)
    }
    
    // 커스텀 리스너 설정
    if err := app.Listener(ln); err != nil {
        logger.Fatal("Failed to start Fiber v2", zap.Error(err))
    }

     

    5. json

    여러 json 라이브러리들이 있다.

    크게 @goccy-json 을 많이 사용하는데 틱톡 회사 bytedance 의 sonic도 괜찮은 것 같다. @Github

    app := fiber.New(fiber.Config{
    	JSONEncoder:   sonic.Marshal,
    	JSONDecoder:   sonic.Unmarshal,
        ...
    }

    Marshal 벤치마킹 (2,4,8,16 코어) (낮을 수록 좋음)
    Unmarshal 벤치마킹 (2,4,8,16 코어) (낮을 수록 좋음)

     

    참고

    handling 1 million requests @Article

    fiber issue: prefork의 목적은? @Github

    728x90

    댓글