How I Scaled a Go Backend to Handle 1 Million Requests per Second
์ฒ์์๋ ๋จ์ํ API์์ต๋๋ค.
์ฌ์ฉ์ ์ธ์ฆ๊ณผ ๊ฒฐ์ ์ฒ๋ฆฌ๋ฅผ ๋ด๋นํ๋ ๊ฐ๋ฒผ์ด Go ์๋น์ค์๊ณ ,
ํ๋ฃจ์ ๋ช ์ฒ ๊ฑด ์ ๋์ ์์ฒญ๋ง ์ฒ๋ฆฌํ๋ฉด ๋์๊ธฐ์ ๋ฌธ์ ์์ด ์ ๋์๊ฐ์ต๋๋ค.
ํ์ง๋ง ํธ๋ํฝ์ด ์ ์ ๋์ด๋๋ฉด์,
ํ๋ ๋น ๋ฆฟํ๋ ๋ฐฑ์๋๋ ์ ์ ๋๋ ค์ง๊ธฐ ์์ํ์ต๋๋ค.
์ง์ฐ ์๊ฐ์ ๊ธ๊ฒฉํ ๋์๊ณ , ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ๋ ๋ณ๋ชฉ์ด ๋์์ผ๋ฉฐ,
์๋ฒ๋ ๋์ ๋ถํ๋ฅผ ๊ฒฌ๋์ง ๋ชปํ๊ณ ์ ์ ์ง์ณ๊ฐ์ต๋๋ค.
๊ทธ๋ฌ๋ ์ด๋ ๋ ,
์ ํฌ ์๋น์ค๊ฐ ํ ์ ๋ช
๋ด์ค ์ฌ์ดํธ์ ์๊ฐ๋๋ฉด์ ์ํฉ์ด ๊ธ๋ณํ์ต๋๋ค.
๋จ ๋ช ๋ถ ๋ง์ ํธ๋ํฝ์ด 10๋ฐฐ ์ด์ ํญ์ฆํ๊ณ ,
Go ๋ฐฑ์๋๋ ๊ฐ์ ํ ๋ฒํฐ๊ณ ์์์ผ๋ฉฐ, ์ธํ๋ผ ํ์ ์ธ์ณค์ต๋๋ค.
โ์๋ฒ๋ฅผ ๋ ๋๋ ค์ผ ํด์!โ
๊ทธ๋ ์ ๋ ๊ฒฐ์ฌํ์ต๋๋ค.
์ง๊ธ์ด์ผ๋ง๋ก ์ด ์์คํ
์ ์ง์ง๋ก ์ต์ ํํ ๋๋ค.
๊ฒฐ๊ตญ, ์ ๋ ์ด ๋ฐฑ์๋๋ฅผ
์ด๋น 100๋ง ์์ฒญ(RPS)์ ์ฒ๋ฆฌํ ์ ์๋ ์์ค๊น์ง ํ์ฅ์์ผฐ์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ ์ง๊ธ,
๊ทธ ๊ณผ์ ์ ์ฌ๋ฌ๋ถ๊ป ๊ณต์ ํ๋ ค ํฉ๋๋ค.
์ฒ์์๋ Go์ ๊ณ ๋ฃจํด์ด ๋ง๋ฒ ๊ฐ๋ค๊ณ ์๊ฐํ์ต๋๋ค.
โ๊ฐ๋ณ๋ค! ๋ฌดํํ ์ค์ผ์ผ๋๋ค!โ โ ๊ทธ๋ ๊ฒ ๋ฏฟ์์ฃ .
ํ์ง๋ง ๊ณง ๊นจ๋ฌ์์ต๋๋ค.
๋ฌด์ ํ์ผ๋ก ๊ณ ๋ฃจํด์ ์์ฑํ๋ฉด ์คํ๋ ค CPU ์ปจํ
์คํธ ์ค์์นญ์ด ๊ณผ๋ํ๊ฒ ๋ฐ์ํ๊ณ , ๋ฉ๋ชจ๋ฆฌ๋ ๊ธ๋ฐฉ ๊ณ ๊ฐ๋๋ค๋ ์ฌ์ค์์.
๊ณ ๋ฃจํด ๊ณผ๋ถํ ํด๊ฒฐ: ์์ปค ํ ๋์
๋ฌด์์ ๊ณ ๋ฃจํด์ ๋์ฐ๋ ๋์ ,
๊ณ ์ ๋ ์์ ์์ปค๊ฐ ํ์ ์์ธ ์์ฒญ์ ์ฒ๋ฆฌํ๋ ์์ปค ํ(worker pool)์ ์ง์ ๊ตฌํํ์ต๋๋ค.
์ด ๋ฐฉ์์ ๋ค์๊ณผ ๊ฐ์ ์ด์ ์ ์คฌ์ต๋๋ค:
์์ฒญ ์๊ฐ ๋ง์๋ ๊ณ ๋ฃจํด ์๊ฐ ์ผ์ ํ๊ฒ ์ ์ง๋ฉ๋๋ค.
CPU ์์์ ์์ธก ๊ฐ๋ฅํ๊ฒ ์ฌ์ฉํ ์ ์์ด ์์ ์ ์ธ ์ฒ๋ฆฌ ์ฑ๋ฅ์ ์ ์งํฉ๋๋ค.
๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋๋ ๊ธ๊ฒฉํ ๋์ด๋์ง ์์, GC์ ์ํ ์ง์ฐ๋ ์ค์ด๋ญ๋๋ค.
package main
import (
"fmt"
"sync"
"time"
)
const maxWorkers = 100 // Controls concurrency level
const numJobs = 1000000
type Job struct {
ID int
}
func worker(id int, jobs <-chan Job, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
// Simulating CPU-intensive processing
time.Sleep(1 * time.Millisecond)
fmt.Printf("Worker %d processed job %d\n", id, job.ID)
}
}
func main() {
jobs := make(chan Job, numJobs)
var wg sync.WaitGroup
for w := 1; w <= maxWorkers; w++ {
wg.Add(1)
go worker(w, jobs, &wg)
}
for j := 1; j <= numJobs; j++ {
jobs <- Job{ID: j}
}
close(jobs)
wg.Wait()
}
โ
์ ํจ๊ณผ๊ฐ ์์์๊น?
์๋ฐฑ๋ง ๊ฐ์ ๊ณ ๋ฃจํด์ ๋ฌด์์ ์์ฑํ๋ ๋์ ,
๋์์ฑ ์์ค์ maxWorkers๋ก ์ ํํจ์ผ๋ก์จ
๐ ์ปจํ
์คํธ ์ค์์นญ์ ์ค์ด๊ณ , CPU ํจ์จ์ ํฌ๊ฒ ํฅ์์ํฌ ์ ์์์ต๋๋ค.
์ฒ์์๋ HTTP ๊ธฐ๋ฐ์ REST API๋ก ์์คํ
์ ๊ตฌ์ถํ์ต๋๋ค.
ํ์ง๋ง ํธ๋ํฝ์ด ๋ง์์ง์ ๋ค์๊ณผ ๊ฐ์ ๋ฌธ์ ๋ค์ด ๋ํ๋ฌ์ต๋๋ค:
โ
ํด๊ฒฐ์ฑ
: gRPC๋ก ๋ง์ด๊ทธ๋ ์ด์
gRPC๋ ๋ค์๊ณผ ๊ฐ์ ํน์ง์ผ๋ก ์ฑ๋ฅ์ ํ๊ธฐ์ ์ผ๋ก ๊ฐ์ ํด์ค๋๋ค
๋ฐ์ด๋๋ฆฌ ์ง๋ ฌํ ๋ฐฉ์(Protobuf) ์ฌ์ฉ โ JSON๋ณด๋ค ํจ์ฌ ๋น ๋ฆ
HTTP/2 ๊ธฐ๋ฐ ๋ฉํฐํ๋ ์ฑ โ ํ๋์ ์ฐ๊ฒฐ๋ก ์ฌ๋ฌ ์์ฒญ ์ฒ๋ฆฌ ๊ฐ๋ฅ
์คํธ๋ฆฌ๋ฐ ์ง์ โ ์๋ฐฉํฅ ๋ฐ์ดํฐ ์ก์์ ์ ์ ๋ฆฌ
๐งช gRPC ์ ์ฉ ์์: ๊ณ ์ Go API
๐ payment.proto (protobuf ์ ์)
syntax = "proto3";
package pb;
service PaymentService {
rpc ProcessPayment (PaymentRequest) returns (PaymentResponse);
}
message PaymentRequest {
string user_id = 1;
double amount = 2;
}
message PaymentResponse {
bool success = 1;
string transaction_id = 2;
}
๐ง Go ์๋ฒ ๊ตฌํ
package main
import (
"context"
"fmt"
"log"
"net"
"google.golang.org/grpc"
pb "path/to/protobuf" // ์ปดํ์ผ๋ ํ๋กํ ๋ฒํผ ํจํค์ง import
)
// ์ค์ ์๋น์ค ๊ตฌํ์ฒด
type paymentServer struct {
pb.UnimplementedPaymentServiceServer
}
// ๊ฒฐ์ ์์ฒญ ์ฒ๋ฆฌ ๋ก์ง
func (s *paymentServer) ProcessPayment(ctx context.Context, req *pb.PaymentRequest) (*pb.PaymentResponse, error) {
// ์ด๊ณณ์ DB ์ฐ๋, ๋น์ฆ๋์ค ๋ก์ง ๋ฑ์ ์ถ๊ฐํ ์ ์์ต๋๋ค.
return &pb.PaymentResponse{
Success: true,
TransactionId: "txn_123456",
}, nil
}
func main() {
// TCP ๋ฆฌ์ค๋ ์์ฑ
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// gRPC ์๋ฒ ์์ฑ ๋ฐ ์๋น์ค ๋ฑ๋ก
s := grpc.NewServer()
pb.RegisterPaymentServiceServer(s, &paymentServer{})
fmt.Println("Payment Service running on port 50051...")
// ์๋ฒ ์คํ
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
๐งพ ์ ํจ๊ณผ๊ฐ ์์์๊น?
gRPC๋ Protobuf๋ฅผ ์ฌ์ฉํด JSON๋ณด๋ค ์ฝ 10๋ฐฐ ๋น ๋ฅธ ์ง๋ ฌํ ์ฑ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
๋ํ HTTP/2 ๊ธฐ๋ฐ ๋ฉํฐํ๋ ์ฑ์ ํตํด ํ๋์ ์ฐ๊ฒฐ๋ก ๋ค์์ ์์ฒญ์ ์ฒ๋ฆฌํ ์ ์์ด, ๋คํธ์ํฌ ์ค๋ฒํค๋์ ์ง์ฐ์ ํฌ๊ฒ ์ค์ผ ์ ์์ต๋๋ค.
API ์ฑ๋ฅ์ ๊ณ ๋ฃจํด, gRPC๋ฅผ ํตํด ์ต์ ํํ ํ,
์ฌ๋ฌ ์ธ์คํด์ค๋ก ๋ถํ๋ฅผ ๋ถ์ฐ์ํค๊ธฐ ์ํด NGINX + ๋ผ์ด๋๋ก๋น(Round Robin) ๋ฐฉ์์ ๋ก๋ ๋ฐธ๋ฐ์๋ฅผ ๋์
ํ์ต๋๋ค.
ํ์ง๋ง ๋ฌธ์ ๊ฐ ๋จ์ ์์์ต๋๋ค.
ํธ๋ํฝ์ด ๋ง์์ง์๋ก ๋์ผํ DB ์ฟผ๋ฆฌ๊ฐ ๋ฐ๋ณต๋๋ฉฐ ์ ์ฒด ์ฑ๋ฅ์ ๋จ์ด๋จ๋ฆฌ๊ณ ์๋ ๊ฒ์
๋๋ค.
โ
ํด๊ฒฐ์ฑ
: Redis๋ฅผ ํ์ฉํ ์บ์ฑ ๋์
์์ฃผ ์กฐํ๋๋ ๋ฐ์ดํฐ(์: ์ต๊ทผ ๊ฒฐ์ ๋ด์ญ)๋ฅผ Redis์ ์บ์ฑํ์ฌ,
๋งค ์์ฒญ๋ง๋ค DB๋ฅผ ๊ฑฐ์น์ง ์๋๋ก ๋ณ๊ฒฝํ์ต๋๋ค.
๐ฆ ์์ ์ฝ๋ (Go + Redis)
package main
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
)
// Redis ๋ช
๋ น ์คํ ์ ์ฌ์ฉํ Context
var ctx = context.Background()
func main() {
// Redis ํด๋ผ์ด์ธํธ ์ด๊ธฐํ
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Redis ์๋ฒ ์ฃผ์
// Password, DB ๋ฑ ์ถ๊ฐ ์ค์ ๊ฐ๋ฅ
})
// Redis์ ๋ฐ์ดํฐ ์ ์ฅ (key: lastTransaction, value: txn_123456)
err := rdb.Set(ctx, "lastTransaction", "txn_123456", 0).Err()
if err != nil {
panic(err) // ์ค๋ฌด์์๋ ๋ก๊ทธ ์ฒ๋ฆฌ ๊ถ์ฅ
}
// Redis์์ ๋ฐ์ดํฐ ์กฐํ
val, err := rdb.Get(ctx, "lastTransaction").Result()
if err != nil {
panic(err)
}
fmt.Println("Cached Transaction:", val)
}
๐ก ์ ํจ๊ณผ์ ์ด์์๊น?
Redis์ ๊ฒฐ๊ณผ๋ฅผ ์บ์ฑํจ์ผ๋ก์จ DB ์์ฒญ ์๋ฅผ ์ฝ 80% ์ค์ผ ์ ์์๊ณ ,
์๋ต ์๊ฐ์ ํ๊ท 50ms โ 5ms ์ดํ๋ก ๊ฐ์ํ์ต๋๋ค.
๐ง ์ค๋ฌด์์ ์์ฃผ ์ฌ์ฉํ๋ Redis ์บ์ฑ ์ ๋ต
| ์ ๋ต | ์ค๋ช | ์์ |
|---|---|---|
| ์ฝ๊ธฐ ์ค์ฌ ๋ฐ์ดํฐ ์บ์ฑ | ์กฐํ ๋น๋๊ฐ ๋์ ๋ฐ์ดํฐ๋ฅผ Redis์ ์ ์ฅํ์ฌ DB ๋ถํ ๊ฐ์ | ์ฌ์ฉ์ ์ ๋ณด, ์ต๊ทผ ๊ฒฐ์ , ์ธ๊ธฐ ์ํ |
| TTL(Time To Live) ์ค์ | ์ผ์ ์๊ฐ ํ ์บ์๋ฅผ ์๋ ๋ง๋ฃ์์ผ ๋ฐ์ดํฐ ์ผ๊ด์ฑ ์ ์ง | 60์ด TTL ์ค์ |
| Lazy-loading (Cache-aside) | ์บ์์ ์์ ๊ฒฝ์ฐ DB์์ ์กฐํํ๊ณ Redis์ ์ ์ฅํ๋ ๋ฐฉ์ | ์ฑ๋ฅ๊ณผ ์ผ๊ด์ฑ ๊ท ํ |
| ์์ธ ์ฒ๋ฆฌ ๋ฐ ์ฅ์ ๋๋น | Redis ์ฅ์ ์ DB๋ก ๋์ฒด ์กฐํ ์ฒ๋ฆฌ (fallback) | if err != redis.Nil ๋ฑ ์ฒดํฌ |
๐ ์บ์ฑ ๋์
์ ์ค๋ฌด ํ
Redis๋ ๋คํธ์ํฌ ๋น์ฉ์ด ๊ฑฐ์ ์๊ณ ์๋๊ฐ ๋น ๋ฅด๊ธฐ ๋๋ฌธ์,
DB ์์ฒญ ์๋ฅผ ์ค์ด๊ณ ์ ์ฒด ์๋ต ์ง์ฐ(latency)์ ํ๊ธฐ์ ์ผ๋ก ์ค์ด๋ ๋ฐ ๋งค์ฐ ํจ๊ณผ์ ์
๋๋ค.
๋จ, ์บ์ฑ์ ์์กดํ ๋ก์ง์ TTL ์ค์ , ๋๊ธฐํ ์ ๋ต, ์บ์ ๋ฌดํจํ ์ ์ฑ ์ ๋ช ํํ ํด์ผ ํฉ๋๋ค.
์ต์ ํ๋ API๋ฅผ ์์ ํ Docker๋ก ์ปจํ
์ด๋ํํ ํ,
Kubernetes ํด๋ฌ์คํฐ์ ๋ฐฐํฌํ๊ณ ์๋ ํ์ฅ(Auto-Scaling)์ ํ์ฑํํ์ต๋๋ค.
์ด ๋จ๊ณ์์ ์ฐ๋ฆฌ๋ ์ํ ํ์ฅ(Horizontal Scaling) ์ ํตํด
ํธ๋ํฝ ๊ธ์ฆ ์ํฉ์์๋ ์๋น์ค๊ฐ ๋ฉ์ถ์ง ์๋๋ก ํ๋ ฅ์ ์ธ ์ธํ๋ผ๋ฅผ ๊ตฌ์ฑํ ์ ์์์ต๋๋ค.
๐งพ Kubernetes Deployment ๊ตฌ์ฑ ์์
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service # ๋ฆฌ์์ค ์ด๋ฆ
spec:
replicas: 10 # ์ด๊ธฐ ๋ณต์ ๋ณธ ์ (HPA์ ์ํด ๋์ ์ผ๋ก ๋ณ๊ฒฝ ๊ฐ๋ฅ)
selector:
matchLabels:
app: payment-service # ํ๋๋ฅผ ์ ํํ ๊ธฐ์ค
template:
metadata:
labels:
app: payment-service
spec:
containers:
- name: payment-service
image: myregistry/payment-service:v1 # ์ปจํ
์ด๋ ์ด๋ฏธ์ง
ports:
- containerPort: 50051 # gRPC ํฌํธ
๐ ์คํ ์ค์ผ์ผ๋ง์ ์ํ ์ถ๊ฐ ๊ตฌ์ฑ (HPA)
Deployment๋ง์ผ๋ก๋ ์๋ ํ์ฅ์ด ํ์ฑํ๋์ง ์๊ธฐ ๋๋ฌธ์,
HorizontalPodAutoscaler(HPA) ๋ฆฌ์์ค๋ฅผ ๋ณ๋๋ก ์์ฑํด์ผ ํฉ๋๋ค.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: payment-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: payment-service
minReplicas: 5
maxReplicas: 100
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
โ
์ ํจ๊ณผ์ ์ด์์๊น?
Kubernetes๋ ํธ๋ํฝ ์ฆ๊ฐ ์ ์๋์ผ๋ก ํ๋ ์๋ฅผ ํ์ฅํด์ฃผ๊ธฐ ๋๋ฌธ์,
ํธ๋ํฝ์ด ๊ฐ์๊ธฐ ๋ชฐ๋ ค๋ ์๋น์ค ์ค๋จ ์์ด ํ๋ ฅ์ ์ผ๋ก ๋์ํ ์ ์์ต๋๋ค.
๐ ์ต์ข
๊ฒฐ๊ณผ: ์ด๋น 100๋ง ์์ฒญ, ๋ค์ดํ์ 0
์ด ๋ชจ๋ ์ต์ ํ ๊ณผ์ ์ ๊ฑฐ์น ๊ฒฐ๊ณผ,
์ฐ๋ฆฌ์ ๋ฐฑ์๋๋ ์ด๋น 1,000,000 ์์ฒญ(RPS) ์ ์์ ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์์๊ณ ,
์ง์ฐ(latency)์ ๋ฎ๊ฒ ์ ์ง๋์์ผ๋ฉฐ, ๋ค์ดํ์์ ๋จ ํ ๋ฒ๋ ๋ฐ์ํ์ง ์์์ต๋๋ค.
โ
๊ณ ๋ฃจํด ์ต์ ํ๋ฅผ ํตํด CPU ๋ถํ๋ฅผ ํฌ๊ฒ ์ค์๊ณ
โ
gRPC ์ ํ์ผ๋ก REST ๋๋น API ์ฑ๋ฅ์ ๋น์ฝ์ ์ผ๋ก ํฅ์์์ผฐ์ผ๋ฉฐ
โ
Redis ์บ์ฑ์ผ๋ก DB ์ฟผ๋ฆฌ ์๋ฅผ ํ๊ธฐ์ ์ผ๋ก ์ค์๊ณ
โ
Kubernetes ์คํ ์ค์ผ์ผ๋ง์ผ๋ก ํผํฌ ํธ๋ํฝ์๋ ๋ฌด์ค๋จ ๋์์ด ๊ฐ๋ฅํด์ก์ต๋๋ค.
์ค์ผ์ผ๋ง์ ๋จ์ํ ์๋ฒ ์๋ฅผ ๋๋ฆฌ๋ ๊ฒ์ด ์๋๋๋ค.
์ง์ ํ ํ์ฅ์ ์์คํ
์ ๋ชจ๋ ๊ณ์ธต์ ์ ๋ฐํ๊ฒ ์ต์ ํํ๋ ๊ฒ์์ ์์๋ฉ๋๋ค.