이전 글을 요약하면, 파이썬 asyncio로 tcp 서버를 만들었다.
클라의 요청을 받아서 외부 HTTP 요청(10ms 레이턴시)을 보내고, 응답을 돌려주는 형태의 서버이다.
Standard D4s v3(4개 vcpu, 16GiB 메모리)를 사용했는데, 4코어 100% 사용률임에도 불구하고 처리량이 초당 1,200건 정도 나왔다.
이게 너무 적은거 같은데, 비교군이 없어서 모르겟다.
그래서, 비교군을 만들려고 go로 서버 만들어봄. 사실 go 잘 모르고, 대충 아는 정도라 gpt로 만들었는데 결과가 의외로 비슷함...
서버는 내장 net 모듈 써서 만들었음. 비슷하게 CPU 사용률 4코어 모두 100% 찍었고 메모리 사용률은 매우 낮았음...
아 참고로, 외부 API 호출없이 그냥 패킷 받은거 돌려주는 에코 서버는 GO가 나았다.
현재 추측한건 이렇슴.
외부 API를 호출하고, 응답을 받으려면 대기 시간이 필요하다.
그 대기 시간동안 CPU가 놀면, 일을 안한거라 문제인게 분명하다.
그런데, 코어 4개가 100% 찍는거면, 응답을 받기 위한 대기시간동안 다른 일을 한다는 거라 괜찮아 보인다.
GO 코드
package main
import (
"context"
"encoding/binary"
"encoding/json"
"io"
"log"
"net"
"sync/atomic"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/data/aztables"
"github.com/google/uuid"
)
var (
msgCounter uint64
tableClient *aztables.Client
)
// sendPacket: 2바이트 길이 + 2바이트 msgType + payload
func sendPacket(conn net.Conn, msgType uint16, payload []byte) error {
header := make([]byte, 4)
binary.BigEndian.PutUint16(header[0:2], uint16(len(payload)))
binary.BigEndian.PutUint16(header[2:4], msgType)
if _, err := conn.Write(header); err != nil {
return err
}
if _, err := conn.Write(payload); err != nil {
return err
}
return nil
}
// readPacket: header를 읽고 payload를 io.ReadFull로 정확히 채움
func readPacket(conn net.Conn) (msgType uint16, payload []byte, err error) {
header := make([]byte, 4)
if _, err = io.ReadFull(conn, header); err != nil {
return 0, nil, err
}
length := binary.BigEndian.Uint16(header[0:2])
msgType = binary.BigEndian.Uint16(header[2:4])
payload = make([]byte, length)
if _, err = io.ReadFull(conn, payload); err != nil {
return 0, nil, err
}
return msgType, payload, nil
}
func handleConnection(conn net.Conn) {
defer conn.Close()
for {
msgType, payload, err := readPacket(conn)
if err != nil {
if err != io.EOF {
log.Printf("readPacket error: %v", err)
}
return
}
atomic.AddUint64(&msgCounter, 1)
// --- JSON 마샬링 후 비동기 업서트 ---
partitionKey := uuid.NewString()
rowKey := uuid.NewString()
entity := aztables.EDMEntity{
Entity: aztables.Entity{
PartitionKey: partitionKey,
RowKey: rowKey,
},
Properties: map[string]any{
"name": "홍길동",
"abc": "예시값",
},
}
// Marshal는 메인 고루틴에서 실행
data, err := json.Marshal(entity)
if err != nil {
log.Printf("marshal entity error: %v", err)
} else {
// 비동기 Fire-and-forget 업서트
go func(b []byte) {
if _, err := tableClient.UpsertEntity(context.Background(), b, nil); err != nil {
log.Printf("async upsert error: %v", err)
}
}(data)
}
// ------------------------------------
// 즉시 echo 응답
if err := sendPacket(conn, msgType, payload); err != nil {
log.Printf("sendPacket error: %v", err)
return
}
}
}
func main() {
// 1) Azure Table Storage 클라이언트 초기화
connStr := ""
serviceClient, err := aztables.NewServiceClientFromConnectionString(connStr, nil) // 연결 문자열 사용 :contentReference[oaicite:2]{index=2}
if err != nil {
log.Fatalf("failed to create service client: %v", err)
}
tableName := "AbcTable"
tableClient = serviceClient.NewClient(tableName) // 테이블 클라이언트 생성
// 2) TCP 서버 시작
ln, err := net.Listen("tcp", ":31000")
if err != nil {
log.Fatalf("listen error: %v", err)
}
defer ln.Close()
log.Println("Server listening on :31000")
// 초당 메시지 처리량 로깅
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for range ticker.C {
count := atomic.SwapUint64(&msgCounter, 0)
log.Printf("Throughput: %d messages/sec", count)
}
}()
for {
conn, err := ln.Accept()
if err != nil {
log.Printf("accept error: %v", err)
continue
}
go handleConnection(conn)
}
}
해당 댓글은 삭제되었습니다.
ㅇㅇ go 코드 올림.
나라면 시도해볼꺼 1. 외부 api 요청 코드를 단순히 1초 sleep 하는걸로 바꿔서 테스트 2. cpu profiling
1초 sleep으로 하면 흠, cpu 사용률이 확 낮아져야 하는건가?
와 CPU 사용률 개줄었고, 처리량은 늘었음 ㅋㅋ
저거 애져 스토리지 테이블이라고 NoSQL이고 배치 업데이트 있음.
멀티 프로세스가 아니라 스레드로 해본 후기도 부탁함
오 시발 고루틴에 세마포어로 숫자 조절하니까 갑자기 2천개 돌파했네 ㄷㄷ
오 고루틴 숫자를 15개로 조절하니까, 각 코어 사용률 50%에 처리량 2500개 정도 나오네 ㄷㄷ... 미쳐따
아 생각해보니까 고루틴으로 요청하고 그 응답을 안돌려주는 형태라 빠른거였네. 파이썬으로도 똑같이 해주니까 비슷하네.... 아~ 인생~
vibe coding의 정석 ㅋㅋ - dc App
흥미롭네 고루틴 생성개수가 무제한이라 발생하는문제 인건가 완전 고부하환경에서는 스레드풀처럼 고루틴 풀을 만드는게 낫다는거 같은데
벤치마킹 참못한다