- Today
- Total
코기판
grpc client load balancing 구현하기 (grpc 번외편) 본문
안녕하세요,
오늘은 grpc 번외편으로 grpc load balancing에 대한 이야기를 해볼까 합니다.
최근에 회사에서 이 이슈때문에 많은 삽질을 해서 ..
헤매는 분들께 도움이 될만한 부분이 있을 것 같습니다.
최근에 회사에서 Weave사에서 나온 Scope라는 툴을 붙였는데요,
(Scope가 너무 일반명사 같아서 저는 그냥 Weave Scope라고 부릅니다.)
이 Weav Scope라는 툴은, 실시간으로 pod간 연결이나 container 간 연결에 대해 자동으로 지도를 그려주는 역할을 합니다.
즉, network topology를 실시간, 자동적으로 그려주는 모니터링 툴 중 하나입니다.
그런데 막상 이 툴을 붙여보니,
frontend와 backend 연결에서 이상한 점이 보였습니다.
어떤 pod에는 너무 request가 많이 몰려서 backend가 처리해야하는 일감이 넘치고,
반면 어떤 pod은 연결 자체가 가지 않아, 놀고 있는 상황이 펼쳐진 것이죠.
바로 다음과 같은 상황입니다. load balancing이 이루어지지 않는 상황입니다.
사실 조금 당황스러웠습니다.
왜냐하면, 제가 처음에 grpc 테스트를 했을 때는 이런 일이 발생하지 않았거든요
하나의 frontend와 여러 개의 backend replica를 두고,
계속해서 backend 서비스 이름으로 grpc.Dial을 날릴 때, load balancing이 잘 이루어지는 것을 직접 확인했었습니다.
그래서, 아! k8s가 또 똑똑하게 알아서 기본적인 load balancing을 해 주는구나! 라고 생각했었기 때문입니다.
실제로, k8s document를 보아도 기본적인 service proxy에서 기본적인 round robin을 해주는 것을 확인했었는데 말이죠.
사실 제가 생각하는 이상적인 모양은 다음 그림과 같았습니다.
그래서 또 삽질에 삽질을 거듭하니 ..
grpc를 사용할 경우, 권장되는 방법이 따로 있었습니다.
바로 client load balancer를 사용하는 것이었습니다.
사실 제가 이전에 테스트로 load balancing을 확인 한 것이, k8s에 기본적으로 내장되어 있는 kube-proxy를 이용한 방법이었습니다.
자세히 살펴보니 그 때와 지금, 서비스 구현에서 달라진 부분이 존재하더군요.
그 당시에는 frontend로 request가 들어오는 순간, grpc.Dial 명령을 수행하고,
지금은 frontend pod이 처음 생성되는 시점에 필요한 backend에 grpa.Dial을 쏴서 연결을 준비하고,
그 연결 context를 사용하여 필요한 backend 함수를 호출하는 식으로 구현이 바뀌었더군요.
사실 지금 사용하는 방식이 더 효율적인 방식이지요 ^^
clinet load balancer라고 하는 것은 frontend에서 backend로 가능한 모든 연결을 둡니다.
그리고 frontend에서 건강한 pod을 찾아 backend의 함수를 호출하는 겁니다.
결론적으로 제가 예상했던 것과는 다른 그림이 되어야 client load balancing을 사용할 수 있는 것이었습니다.
client load balancing을 사용하기 위해서는 다음 두 조건을 만족해야 합니다.
1. service를 headless 타입으로 로 바꾸어 주어야 합니다.
원래는 frontend에서 backend로 kube-proxy를 타고 접근했다면,
service를 headless로 바꾸어 주어야 frontend to backend가 직접 연결된다고 합니다.
headless 서비스로 바꾸기 위해서는 .spec.clusterIP를 none으로 세팅해주면 됩니다.
아래 코드처럼요.
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
clusterIP: none
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
2. frontend go code에 rounddrobin 모듈을 import하고, grpc.Dial 시 rr(roundrobin) 조건을 함께 명시해주어야 합니다.
코드 출처 : https://medium.com/@ammar.daniel/grpc-client-side-load-balancing-in-go-cd2378b69242
package main
import (
"context"
"log"
"google.golang.org/grpc"
"google.golang.org/grpc/balancer/roundrobin"
pb "google.golang.org/grpc/examples/helloworld/helloworld"
"google.golang.org/grpc/resolver"
)
const (
address = "dns-record-name:443"
defaultName = "world"
)
func main() {
// The secret sauce
resolver.SetDefaultScheme("dns")
// Set up a connection to the server.
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBalancerName(roundrobin.Name))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
// Contact the servers in round-robin manner.
for i := 0; i < 3; i++ {
ctx := context.Background()
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: defaultName})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.Message)
}
중요한 부분은,
"google.golang.org/grpc/balancer/roundrobin" 와 "google.golang.org/grpc/resolver" 를 import 시켜주는 것과
resolver.SetDefaultScheme("dns") //dns schem으로 접근해주는 것,
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBalancerName(roundrobin.Name)) // 마지막에 roundrobin.Name 을 명시해주는 것이 포인트입니다.
제가 주말을 반납해가며 삽질을 했던 부분이
누군가의 시간을 아껴주길 바랍니다 ㅠㅠ
그리고 보실지 모르겠지만 조대협님 항상 너무 감사드립니다 ㅠㅠ
갓대협님 블로그가 저에게 얼마나 큰 도움이 되는지 몰라요.
references :
https://bcho.tistory.com/tag/headless%20service
https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies
https://github.com/coredns/coredns/issues/2815
https://medium.com/@ammar.daniel/grpc-client-side-load-balancing-in-go-cd2378b69242
'Infra' 카테고리의 다른 글
애증의 Locust - load test tool (5) | 2019.10.03 |
---|---|
protobuf란 무엇인가? (gRPC 시리즈 #2) (1) | 2019.05.12 |
gRPC란 무엇인가? (gRPC 시리즈 #1) (5) | 2019.04.21 |
k8s pod autoscaler 개념 (HPA, VPA) (0) | 2019.04.02 |
EKS에 클러스터 오토스케일링(cluster autoscaling) 하기 (4) | 2019.03.29 |