[Tech +]gRPC Load Test by Locust with k8s

안녕하세요. 반갑습니다! 머지리티에서 Server를 개발하고 있는 DonisRiley 입니다! 

서버팀의 공식 첫 Tech 콘텐츠 작성에 앞서.. 우리 서버팀은 무슨 역할을 하는지 아주 간략하게 소개하고 시작할까 합니다 :)

'Server'라는 단어에서 유추하실 수 있듯 '서브(Serve) + 하는 사람(-er)'의 뜻을 갖고 있습니다. 즉 '서비스를 제공하는 사람' 이라는 뜻이지요. 서버팀에서는 빠르고 안정적이며 지속적으로 확장 가능한 환경을 선행 구축하고, 서비스 장애 등 Risk에 대한 사전 예측과 실제 발생시 이를 최소화하기 위해 존재하는 팀이랍니다. 머지리티 서비스의 안정적인 운영은 서버팀이 책임집니다!

글의 인사말이 조금 길었죠? 서버팀의 첫 콘텐츠 바로 시작해볼게요!




gRPC Load Test by Locust with k8s

더 빠르고 더 안정적인 통신을 찾아서


현재 머지리티 서버 애플리케이션은 go 언어로 개발되었고 gRPC 통신 프로토콜을 사용하는 마이크로서비스로 구성되어있습니다. 기존에는 Nest.js 프레임워크를 활용한 Rest API 통신으로 Client - Server 간 통신을 하였으나 보다 더 빠르고 안정적인 통신을 위해 gRPC를 선택하였습니다. 그 결과 기존 Rest API 에 비해 3배 이상 빠른 결과를 얻을 수 있었습니다.

그러면 어떻게 gRPC 부하(Load) 테스트를 할 수 있는지 알아볼까요?


부하테스트 (Load Test)


부하 테스트는 일반적으로 프로그램에 동시에 액세스하는 여러 사용자를 시뮬레이션하여 시스템 성능을 테스트하고 발생할 수 있는 문제를 예측하기 위한 테스트입니다. 테스트에는 설정한 조건값 또는 임계치에서 부하를 점차 증가시켜 여러 시스템의 기능을 비교하거나 단일 시스템의 기능을 정확하게 측정할 수 있습니다. 또한 실제 시스템 운용에 있어 얼마나 잘 작동하는 지에 대한 측정을 위해 진행하기도 합니다.

부하테스트 체크리스트

  • 어느 정도의 사용자 액세스를 견딜 수 있는가?
  • 수용 범위를 넘어선 경우 확장성에 문제 없는가?
  • 현재 구성된 인프라가 적합한가?
  • 향후 서버 애플리케이션과 관련된 DB, 네트워크 워크로드의 상한을 결정하고 확장 가능한가?
  • 오류, 메모리 누수 등 기타 문제는 없는가?

부하테스트를 위한 툴 조사

대부분의 서비스가 그랬겠지만, 개발 환경에서는 잘만 작동하였으나 실제 환경에서는 예상치 못한 예외 사항들이 발생하곤 합니다. 개발 단계인 코드 레벨에서 모든 예외 상황을 예측하기 위한 테스트를 하기는 사실상 불가능에 가깝다고 말할 수 있습니다.

이러한 이유로 실제 서비스 환경과 최대한 비슷한 조건에서 우리가 사용하는 gRPC API를 테스트하기 위한 여러 툴을 조사했으며 각각 비교해본 결과, 테스트 케이스 기반으로 단일 시스템에서 쉽게 테스팅이 가능한, 특히 Kubernetes(이하 k8s) 환경을 쉽게 테스트할 수 있는  Locust 를 선택하게 되었습니다.

Testing Tool
Locust
JMeter
ghz
그외 GUI Tool
장점
- 비동기 접근 방식 → 단일 시스템에서 수 천명의 동시 사용자를 쉽게 시뮬레이션 가능하다
- Helm chart를 통해 k8s 환경의 부하 분산 테스트가 용이하다
- 테스트 시나리오를 GUI를 통해 작성할 수 있다 (비개발자도 설정 가능)
- 다양한 플러그인을 제공한다
- 단일 API를 간단하게 테스트 할 수 있다
Postman
Insomnia
Kreya.app
BloomRPC
단점
- 테스트 시나리오를 파이썬 스크립트로 작성해야 한다 → python 코딩 경험이 있어야 한다- 스레드 기반 방식으로 각 요청에 대해 별도의 스레드를 할당한다
- 사용자 수가 제한적이다
(하지만, 플러그인으로 스레드 제어가 가능함)
- 테스트 시나리오 기반 테스트에 대한 구현X



부하테스트 환경 구성


테스트 케이스를 작성하기 위한 Locust 환경을 설정합니다. Locust는 테스트 케이스를 파이썬으로 작성하기 때문에 파이썬 설치 및 gRPC 관련 의존 라이브러리를 설치해야 합니다.


파이썬 gRPC 관련 의존 라이브러리 설치

# 파이썬 업그레이드 python >= 3.6
> python3 -m pip install --upgrade pip

# grpcio 설치
> python3 -m pip install grpcio

# grpcio-tools 설치
> python3 -m pip install grpcio-tools


테스트 대상 gRPC의 protobuffer 컴파일

# proto 파일 컴파일

python3 -m grpc_tools.protoc -I=. --python_out=. --grpc_python_out=. proto/v1/user/user.proto
python3 -m grpc_tools.protoc -I=. --python_out=. --grpc_python_out=. proto/v1/feed/feed.proto

* 튜토리얼이 궁금하신 분들은 우측 페이지 클릭 ▶ Python gRPC Basics tutorial



gRPC를 위한 테스트 케이스 파일 작성

import sys
import grpc
import inspect
import time
import gevent

# Libs
from locust.contrib.fasthttp import FastHttpUser
from locust import task, events, constant, between
from locust.runners import STATE_STOPPING, STATE_STOPPED, STATE_CLEANUP, WorkerRunner
from proto.v1.user import user_pb2
from proto.v1.user import user_pb2_grpc
from proto.v1.feed import feed_pb2
from proto.v1.feed import feed_pb2_grpc

def stopwatch(func):

    def wrapper(*args, **kwargs):
        # 기능 이름 가져오기
        previous_frame = inspect.currentframe().f_back
        _, _, task_name, _, _ = inspect.getframeinfo(previous_frame)

        start = time.time()
        result = None
        try:
            result = func(*args, **kwargs)
        except Exception as e:
            total = int((time.time() - start) * 1000)
            events.request_failure.fire(request_type="TYPE",
                                        name=task_name,
                                        response_time=total,
                                        response_length=0,
                                        exception=e)
        else:
            total = int((time.time() - start) * 1000)
            events.request_success.fire(request_type="TYPE",
                                        name=task_name,
                                        response_time=total,
                                        response_length=0)
        return result

    return wrapper


class GRPCMyLocust(FastHttpUser):
    host = 'https://grpc.bla-bla-비밀.com:443'
    wait_time = between(1, 5)
    # 저희는 TLS gRPC통신을 하기때문에 pem파일을 별도 추가하여 설정하였습니다.
    creds = grpc.ssl_channel_credentials(open('./cert.pem', 'rb').read())
    access_token = ""
    try:
        replace_host = host.replace('https://', '')
        channel = grpc.secure_channel(replace_host, creds)
        stub = user_pb2_grpc.UserStub(channel)
        response = stub.DefaultLogin(user_pb2.DefaultLoginRequest(user_id='donis', password='1234'))
        # 토큰 발급후 저장
        access_token = response.data.access_token
    except (KeyboardInterrupt, SystemExit):
        sys.exit(0)

    def on_start(self):
        # @task 하기전에 실행해야할 것이 있다면 여기 작성해주세요.
        pass

    def on_stop(self):
        # @task 종료후 실행해야할 것이 있다면 여기 작성해주세요.
        pass

    @task
    @stopwatch
    def grpc_client_task(self):
        try:
            # gRPC 통신시에는 앞에 'http://' 또는 'https://' 프로토콜명을 붙이지 않기 때문에 replace 해줍니다.
            replace_host = self.host.replace('https://', '')
            channel = grpc.secure_channel(replace_host, self.creds)
            feed_stub = feed_pb2_grpc.FeedServiceStub(channel)

            metadata = (('authorization', self.access_token),)
            request = feed_pb2.FeedTimelineRequest(user_id='donis', sort_type='RECENT', last_feed_id='0')
            response = feed_stub.GetFeedTimeline(request=request, metadata=metadata)
            # print(response)
        except (KeyboardInterrupt, SystemExit):
            sys.exit(0)


# 실패 비율이 넘으면 locust 종료
def checker(environment):
    while not environment.runner.state in [STATE_STOPPING, STATE_STOPPED, STATE_CLEANUP]:
        time.sleep(1)
        if environment.runner.stats.total.fail_ratio > 0.2:
            print(f"fail ratio was {environment.runner.stats.total.fail_ratio}, quitting")
            environment.runner.quit()
            return


# 초기 세팅
@events.init.add_listener
def on_locust_init(environment, **_kwargs):
    if not isinstance(environment.runner, WorkerRunner):
        gevent.spawn(checker, environment)
  • locust.py @Task 파일 작성 ( gRPC )


k8s 환경에서 부하 분산 테스트 실행


머지리티 서버의 서비스들은 Container로 구성되어있고 k8s로 Container를 관리하고 있습니다. 
Locust의 경우 k8s에서 쉽게 배포할 수 있도록 Helm으로 패키지화되어 있어  k8s 환경에서 부하테스트가 용이합니다.


Locust 테스트 케이스 Docker 이미지 작성

# Dockerfile
FROM locustio/locust:2.10.1
RUN pip3 install grpcio
RUN pip3 install grpcio-tools

COPY . /home/locust/.
  • Docker Build시 배포될 인프라 아키텍쳐에 맞게 빌드해야 합니다. 작성자는 MacBook M1을 사용하고 있어 아래와 같이 명시하여 빌드하였습니다.
docker docker buildx build --platform linux/amd64 -t locust-grpc:0.0.1 .


Docker 이미지를 repository에 등록

  • 빌드 된 이미지를 AWS ECR이나 Dockerhub 등에 푸시합니다.
docker push public.ecr.aws/블라블라/locust-grpc:0.0.1


Helm 차트 사용

  • helm chart로 부하테스트의 클러스터 설치 환경을 구성해 놓으면 필요할 때마다 손쉽게 locust 클러스터를 설치, 삭제할 수 있습니다.
  • Delivery Hero에서 제공하는 deliveryhero/locust 를 helm repository에 추가후 업데이트합니다.
helm repo add deliveryhero ""
helm repo update


설정 파일 values.yaml 작성 후 locust helm chart를 설치합니다.

# values.yaml
loadtest:
name: grpc-loadtest
locust_locustfile: ../../home/locust/locustfile.py
master:
image: public.ecr.aws/블라블라/locust-grpc:0.0.1
worker:
image: public.ecr.aws/블라블라/locust-grpc:0.0.1
hpa:
enabled: true
minReplicas: 5
helm upgrade --install locust deliveryhero/locust -f values.yaml

deliveryhero/locust 설치 완료!


locust 접속 화면


  • Locust에서는 Host 입력시 반드시 http:// 또는 https:// 로 시작하는 주소를 넣어야합니다. 
    (이런 이유로 locust.py 테스트 코드 작성에서 host주소를 replace하는 로직을 넣었습니다)


결론 : 부하테스트 결과


MacBook에서 Locust로 테스트 진행 결과


  • MacBook( CPU M1, Memory 16GB )에서는 User가 100에서 더 안올라가더군요.


On-Premise k8s를 구성하여 Locust로 테스트를 진행한 결과

  • Worker Node( CPU 6core, Memory 32GB )가 1개인 k8s에서 750 User까지 테스트 해볼 수 있었습니다.





관련 테스트 결과는 콘텐츠 하단부에 첨부파일로 업로드 해드렸으니 궁금하신 분들께서는 확인하셔도 좋을 것 같습니다!


오늘은 gRPC 테스트에 대해 다뤄보았는데요, 좀 더 많은 다른 서비스에서 gRPC를 활용하고 이 방법을 통해 테스트 및 기술 검증을 할 수 있었으면 좋겠습니다. 본 콘텐츠 관련 질문이나 의견, 글에 오류가 발견되었다면 media@mergerity.com에 문의주세요!


감사합니다.



참고


테스트 결과

Contact us

일반문의 - official@mergerity.com

투자문의 - ir@mergerity.com
언론보도 – media@mergerity.com
파트너쉽 – partnership@mergerity.com

Social Media

3F, 211, Hakdong-ro, Gangnam-gu, Seoul, Republic of Korea  

 02-545-2091 | official@mergerity.com


Contact us

일반문의 - official@mergerity.com

투자문의 - ir@mergerity.com
언론보도 – media@mergerity.com
파트너쉽 – partnership@mergerity.com

Contact Info

official@mergerity.com

3F, 211, Hakdong-ro, Gangnam-gu, 

Seoul, Republic of Korea