-
Go: Fiber 서버 최적화 하기 (optimization)컴퓨터/Go language 2024. 9. 12. 21:39728x90반응형
Go언어 웹 프레임워크 fiber v2를 사용하면서 얻은 팁들을 정리한 글이다.
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 이 있다.
바로 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."목적: 고루틴에서 생성되고 금방 사용이 끝나는 short-term 객체를 재사용하기 위한 메모리 풀
여러 고루틴에서 동시 사용도 안전하며, 저장된 객체는 언제든 자동으로 제거될 수 있다. (하지만 TTL 보장 안됨)
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, ... }
참고
handling 1 million requests @Article
fiber issue: prefork의 목적은? @Github
728x90'컴퓨터 > Go language' 카테고리의 다른 글
Go: Bleve 인덱싱 한국어 주소 검색 서버 만들기 (Apache Lucene-like) (0) 2024.06.02 Go/Java: 금칙어 검사 함수들 벤치마크 및 향상 (3) 2024.05.02 Go: math/rand/v2 Lemire's algorithm 알고리즘 (0) 2024.04.18