안정적인 서비스를 오픈하기 위한 조건은 무엇일까요? 좋은 아키텍처도 물론 중요하지만, 트래픽을 견딜 수 있는 부하 테스트가 중요합니다. 이번 블로그 글에서는 부하 테스트 관련 글을 작성하고자 합니다.

이벤트 대응 프로세스

부하 테스트를 알아보기 위해 이벤트 대응 프로세스를 알아보겠습니다. (좋은 영상을 공유해주신 장준엽님 감사합니다! 영상 링크)

event1.png

  • 이벤트 랜딩 페이지 점검 : 부하가 제일 몰리는 구간으로 중요시 봐야하는 구간입니다. 페이지 랜딩 용량으로 7MB 정도(문제가 없는 수준)를 추천합니다.

  • ELB Pre-Warmining 신청 : AWS ELB도 내부적 하드웨어 장비로 미리 늘려 놔야 합니다. 보통 ELB에 IP 할당 2개로 초당 200~300 트래픽이 처리가 가능하며 그 이상의 이벤트 발생시 사전 신청이 필요합니다.

  • 부하 테스트 / 성능지연 원인 분석 : 부하 테스트가 진행되는 단계입니다. 보통 오픈소스 도구를 통해 진행합니다. (아래 내용 계속)

  • 이벤트 모니터링 : 공유해주신 CloudWatch 주요 모니터링 지표입니다. 생소한 지표로 Surge 큐 Length는 ELB 뒷단의 웹서버가 처리하는 못하는 것을 확인하기 위한 지표라 합니다.

    event2.png

이벤트 대응 프로세스 중 부하 테스트 단계를 진행하겠습니다. 과부하 테스트 도구로는 대표적으로 Jmeter, K6, Locust 등 존재하나 이번 글에서는 Loucst를 다룰 예정입니다.

Locust

Locust는 오픈소스 부하 테스트 도구입니다. Locust는 파이썬 코드 기반으로 테스트 케이스가 작성되어 쉬운 스크립팅을 통해 복잡한 테스트 시나리오를 구성할 수 있습니다. (Jmeter 는 Java, k6는 JavaScript) 그리고 Loucst는 마스터-슬레이브 구조로 여러 기기에서 분산 테스트를 진행할 수 있어 대량의 트래픽을 시뮬레이션 할 수 있는 이점이 있으며 웹 UI를 제공하여 테스트 결과에 대해 빠르게 파악할 수 있습니다. 아래 화면은 예시 화면입니다.

webui-running-charts.png

참고로 분산 테스트는 다른 도구들도 다 지원됩니다. 특별히 Locust를 선택한 이유는 필자가 파이썬에 친화적이고 , git repo Star가 가장 많아 자료를 쉽게 찾을 수 있는 점, 웹 UI와 동시에 CLI 가 제공되기 때문이였습니다.

한가지 염두했던 점은 Locust가 파이썬 기반이라 분산 테스트에 효과적으로 테스트할 수 있을까였습니다. 원리를 찾아보니 세부 동작은 gevent라 하여 코루틴 라이브러리로 동작한다고 하네요. 코루틴은 프로그램이 여러 진입점을 가지고 비동기적으로 실행되도록 지원하는 프로그램 구성 요소로 비동기 태스크를 쉽게 구성 및 관리할 수 있고 효과적인 동시성 처리를 관리할 수 있게 만듭니다. 또한, Locust 내부적으로 특정 클라이언트(Locust Class) 를 지원하여 여러 타입의 클라이언트를 동시에 만들어서 실행할 수 있다고 합니다.

Locust 아키텍처를 통해 동작 원리를 파악해보겠습니다.

<a href="https://locust.dev/docs/architecture">https://locust.dev/docs/architecture</a>

https://locust.dev/docs/architecture

특별히 확인할 점은 두가지입니다. Redis 기반으로 잡 스케쥴링을 관리하며, 실제 잡은 워커 노드에 할당하여 진행한다는 점입니다. 워커 노드안에 loucst.execute(), loucst.vaildate() 는 워커 노드 실행시 잡이 수행되는 함수입니다. 각 함수가 순차적으로 진행되는데요. 테스트 시나리오 구성시 워크 노드나 전체 노드에 설치 프로그램이 필요할 수 있는데 setup & teardown(실행시 전체 한번), on_start & on_stop (클라이언트 실행마다 한번) 함수를 통해 시나리오를 구성할 수 있습니다.

배포

EKS에 Locust를 배포하여 테스트해보겠습니다. 테스트 예제 애플리케이션은 지난 아키텍처 글에서 활용한 투표 애플리케이션을 활용하겠습니다.

투표 애플리케이션 배포 참고

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# git clone  
git clone https://github.com/HanHoRang31/blog-share.git
cd blog-chare/k8s-app/vote-app
tree

# 네임스페이스 생성 후 변경
kubectl create ns vote

# 서비스 배포 
kubectl apply -f .

# ExternaDNS 추가 (없으면 생략) 
## 각자 자신의 도메인 정보 입력
MyDOMAIN1=<각자 자신의 nginx 도메인 지정>
MyDOMAIN1=vote.hanhorang.link
MyDOMAIN2=result.hanhorang.link
kubectl annotate service vote "external-dns.alpha.kubernetes.io/hostname=$MyDOMAIN1."
kubectl annotate service result "external-dns.alpha.kubernetes.io/hostname=$MyDOMAIN2."

Locust 배포

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# repo 설치
helm repo add deliveryhero  https://charts.deliveryhero.io/ 
helm repo update 
helm repo list 
---
NAME            URL                            
deliveryhero    https://charts.deliveryhero.io/ 

# 매개 변수 확인 
helm show values deliveryhero/locust > values.yaml
---
loadtest:
  # loadtest.name -- 이 부하 테스트에 사용될 리소스 및 설정의 이름
  name: example
  # loadtest.locust_locustfile -- locustfile의 이름
  locust_locustfile: main.py
  # loadtest.locust_locustfile_path -- locustfile의 경로 (끝에 슬래시 제외)
  locust_locustfile_path: "/mnt/locust"
  # loadtest.locust_locustfile_configmap -- locustfile을 포함하는 configmap의 이름 (기본값은 예제 locustfile 사용)
  locust_locustfile_configmap: "example-locustfile"
  # loadtest.locust_lib_configmap -- 라이브러리를 포함하는 configmap의 이름 (기본값은 예제 라이브러리 사용)
  locust_lib_configmap: "example-lib"
  # loadtest.locust_host -- 부하 테스트할 대상 호스트
  locust_host: https://www.google.com
  # loadtest.pip_packages -- 설치할 추가 파이썬 pip 패키지의 목록
  pip_packages: []
  # loadtest.environment -- 마스터와 워커 모두에게 적용될 환경 변수
  environment: {}
    # VAR: VALUE
.
.
worker:
  # worker.image -- 사용자 정의 도커 이미지를 포함한 태그
  image: ""
  # worker.logLevel -- 로그 레벨. INFO 또는 DEBUG 가능
  logLevel: INFO
  replicas: 1
  # worker.pdb.enabled -- worker 파드에 대한 PodDisruptionBudget를 생성할지 여부
  pdb:
    enabled: false
  hpa:
    enabled: false
    minReplicas: 1
    maxReplicas: 100
    targetCPUUtilizationPercentage: 40

차트 매개 변수는 깃허브 링크에서 확인이 가능합니다. 차트 변수로 특별하게 볼점은 다음과 같습니다.

  • 테스트 스크립트는 loadtest 변수 (locust_locustfile_configmap) 설정이나 경로를 지정한다음 볼륨을 통해 직접 삽입합니다.(loadtest.locust_locustfile_path) 를 통해 가져옵니다.
  • 워커노드에 대한 과부하 설정은 worker에서 진행합니다.

과부하 테스트

해당 블로그 글에서는 투표 애플리케이션에 대해 과부하 테스트 스크립트를 configmap으로 작성하여 업데이트하여 진행하겠습니다. 과부하 테스트 스크립트는 다음과 같습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Execute in root of repository, and maybe this file has existed.
cat <<EOF > locustfile.py && cat locustfile.py
from locust import HttpUser, task, between
import random

class UserBehavior(HttpUser):
    wait_time = between(1, 2.5)

    @task(1)
    def get_home_page(self):
        self.client.get("http://result.hanhorang.link/")

    @task(2)
    def post_vote(self):
        vote = ['a', 'b'] # a represents 'Cats', b represents 'Dogs'
        self.client.post("http://vote.hanhorang.link/",
                         data={'vote': random.choice(vote)})
EOF

코드는 간단하다. wait_time 변수 설정으로 1초 ~2.5초 사이의 무작위 대기 시작을 가진 후 @task(1) 과 @task(2) 작업을 1:2 비율로 작업을 수행합니다. @task(1)은 투표 결과화면(밑 화면 오른쪽) 을 조회하고 @task(2)는 dog, cat 투표를 무작위 진행(밑 화면 왼쪽)합니다.

archi24.png

스크립트를 구성했으면 config를 수정하고 locust를 배포하겠습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 스크립트 배포 
kubectl create configmap eks-loadtest-locustfile --from-file ./locustfile.py

# locust 배포 
helm upgrade --install locust deliveryhero/locust \
 --set loadtest.name=eks-loadtest \
 --set loadtest.locust_locustfile_configmap=eks-loadtest-locustfile \
 --set loadtest.locust_locustfile=locustfile.py \
 --set worker.hpa.enabled=true \
 --set worker.hpa.minReplicas=5

# 서비스 ALB 배포 
cat <<EOF > locust-ingress.yaml && cat locust-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    kubernetes.io/ingress.class: alb
  finalizers:
  - ingress.k8s.aws/resources
  labels:
    app: ingress
  name: ingress-locust-dashboard
  namespace: default
spec:
  defaultBackend:
    service:
      name: locust
      port:
        number: 8089
EOF

kubectl apply -f locust-ingress.yaml

배포를 완료하면 ingress ADDRESS 를 입력하여 locust 웹 UI로 접속하여 테스트를 진행하겠습니다.

왼쪽 처음 화면에서 매개 변수을 입력(밑 사진)하여 테스트를 진행하자. 매개변수는 다음과 같습니다.

  • Number of Users : 동시 사용자 수
  • Spawn Rate: 빠르게 추가되는 수치, 1로 설정했으므로 초당 1명씩 사용자를 추가한다.
  • Host: 이는 부하 테스트의 대상이 되는 웹 사이트나 애플리케이션의 URL

테스트를 진행하면 config에서 설정한 스크립트 구성에서의 task 비율만큼 post와 get 함수가 1:2 비율로 호출됨을 알 수 있습니다.

locust1.png

과부하 테스트를 진행하겠습니다, 밑의 결과 화면을 보면 다음과 같이 사용자가 1000 명에서 응답 시간이 증가함을 알 수 있습니다. 많게는 50000ms로 50초에 해당되어 실제 운영시 추가 작업이 필요함을 알 수 있는데요, Task 함수의 응답시간을 보면 Post 함수에서 지연시간이 발생하는 것을 알 수 있습니다.

locust2.png

locust3.png

가장 먼저 처리한 조치로 POST 응답시간을 최소화시키기 위해 해당 기능 파드인 vote, worker, redis 파드를 증가시켜 봤지만 파드 사용량이 분산되었을 뿐 처리 결과는 똑같았습니다. 다음 단계로는 파드 내 로그를 직접 확인해서 조치해야겠네요.

과부하 테스트(2)

실제 과부하 테스트를 한다면 처음 페이지에서 이벤트 페이지까지의 트래픽이 구성됩니다. 예를 들면, 할인행사시, e-마켓 메인페이지에서 할인행사 이벤트 페이지까지로 생각하시면 이해하기 편하실 것 같습니다. 과부하 테스트에서도 해당 단계의 테스트가 필요합니다.

앞 서 투표 예플리케이션에서는 랜덤으로 투표 1,2를 찍는 것을 테스트했는데요. 랜덤이라 분산되는 시나리오였습니다. 여기서는 투표 1,2를 랜덤으로 찍고 결고화면까지 가는 것을 코드로 구성하겠습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
cat <<EOF > locustfile.py && cat locustfile.py
from locust import HttpUser, task, between, SequentialTaskSet
import random

class UserBehavior(SequentialTaskSet):

    @task
    def post_vote(self):
        vote = ['a', 'b'] # a represents 'Cats', b represents 'Dogs'
        self.client.post("http://vote.hanhorang.link/",
                         data={'vote': random.choice(vote)})

    @task
    def get_home_page(self):
        self.client.get("http://result.hanhorang.link/")

class MyUser(HttpUser):
    tasks = [UserBehavior]
    wait_time = between(1, 2.5)
EOF

주요 변경사항은 코드 내 클래스 구성입니다. Myuser 클래스 구성으로 순차적으로 작업 코드를 구성하였습니다. 이렇게 된다면 Locust Client 는 먼저 랜덤으로 A,B를 투표하고 결과 페이지를 조회하는 시나리오로 진행하게 됩니다.

부하 테스트 팁

오늘은 오픈소스 Locust를 통해 부하테스트를 다뤘습니다. 부하테스트를 진행하니 성능지연 원인 분석도 확인이 필요한 단계로 생각되네요. AWS 공식 문서를 찾아보면 EKS에서 부하 테스트 팁을 확인할 수 있습니다. 거의 파드 라이프사이클, HPA, CA 구성 관련 팁이 있네요. 향후 구성시 고려해봐야겠습니다. 다음 글은 부하테스트에 이어서 성능지연에 관한 원인 분석 글을 다뤄볼 예정입니다.