이미 공개한 글이지만 회사 블로그를 홍보할 겸 여기에 늦게나마 공개합니다.
동명의 글이 Google Cloud Platform에도 있으니 여기서는 여태까지 한 삽질과 교훈에 집중한다.
첫 시도 ngrinder
처음에는 ngrinder로 부하 테스트 환경을 구축하려 했다. 몇 달 전에 부하 테스트를 진행할 때 잠시 쓴 적이 있었기 때문에 굳이 다른 솔루션을 찾을 이유가 없었다. 하지만 결국 후회하고 다른 솔루션으로 넘어갔는데 그 이유를 중요한 순으로 꼽자면
- 로컬 개발환경과 실제 환경이 차이가 많다. 로컬에서는 JUnit 기반으로 개발과 디버깅이 가능하다. 하지만 이렇게 작성한 코드를 ngrinder에 넣으려 하면 외부 라이브러리가 문제가 된다.
.jar등 패키지 파일을 업로드하는 방식이 아니라 Groovy 스크립트 따로 스크립트에서 사용하는 라이브러리 따로 업로드를 해야 하는데 상당히 번거롭다. - 웹 UI를 통해 설정한 내용이 내장 데이터베이스에 바이너리로 들어가기 때문에 ngrinder 데이터를 관리하기가 힘들다.
- 개발이 활발하지 않다. 주력 개발자가 Naver를 떠났다는 이야기도 있긴 한데 아무튼 커밋 히스토리를 보면 개발이 정체되어 있는 건 분명하다.
- 설계가 진보적이지 않다. 예를 들어 현재 쓰레드의 ID를 시스템이 직접 계산해서 주입하지 않고 개발자가 주어진 코드 스니펫을 Copy & Paste 해야 하는 이유를 모르겠다.
등이 있다. 이런 까닭에 좀더 간단한 솔루션을 찾아보았다.
대안
몇 가지 대안을 살펴보았는데
- Artillery는 테스트 스크립트를 yaml로 기술하기 때문에 얼핏 쉬워보이지만 이런 식의 접근 방법은 매번 실망만 안겨주었다. 조금만 테스트 시나리오가 복잡해지면 일반적인 코딩보다 설정 파일이 훨씬 짜기가 어렵고 이해하기도 어렵다.
config: target: 'http://my.app.dev' phases: - duration: 60 arrivalRate: 20 defaults: headers: x-my-service-auth: '987401838271002188298567' scenarios: - flow: - get: url: "/api/resource" - Gatling은 아직 분산 서비스를 지원하지 않아서 제외했다. 팀 내에 Scala 개발경험이 있는 사람이 극소수인 점도 문제였다.
Locust로 정착
이런 까닭에 Locust로 넘어왔다. 장점은
- 파이썬 스크립트로 시나리오를 작성하니 내부에 개발인력이 충분하다.
- 로컬환경과 실제 부하테스팅 환경이 동일하다. 즉, 디버깅하기 쉽다.
- Locust 데이터를 Dockerize하기 쉽다.
한마디로 ngrinder에서 아쉬웠던 점이 모두 해결됐다. 반면 ngrinder에 비해 못한 면도 많긴 하다.
- 통계가 세밀하지 않다.
- 테스트 시나리오를 세밀하게 조정하기 힘들다.
현재로썬 그때그때 가볍게 시나리오를 작성해서 가볍게 돌려보는 게 중요하지 세밀함은 그리 중요하지 않아서 Locust가 더 나아 보인다. 시나리오는 몰라도 통계의 경우, DataDog 같은 모니터링 시스템에서 추가로 정보를 제공받기 때문에 큰 문제도 아니긴 하다.
결과물
Locust on Kubernetes
GoogleCloudPlatform/distributed-load-testing-using-kubernetes에 있는 소소코드를 참고로 작업하면 된다. 단지 Dockerfile의 경우, 테스트 스크립트만 바뀌고 파이썬 패키지는 변경사항이 없는 경우에도 파이썬 스크립트 전체를 새로 빌드하는 문제가 있다.
# Add the external tasks directory into /tasks
ADD locust-tasks /locust-tasks
# Install the required dependencies via pip
RUN pip install -r /locust-tasks/requirements.txt
그러므로 이 부분을 살짝 고쳐주면 좋다.
ADD locust-tasks/requirements.txt /locust-tasks/requirements.txt
RUN pip install -r /locust-tasks/requirements.txt
ADD locust-tasks /locust-tasks
ngrinder on Kubernetes
ngrinder를 Kubernetes v1.4.0 위에서 돌리는데 사용한 설정은 다음과 같다. 참고로 dailyhotel/ngrinder-data는 ngrinder의 데이터만 따로 뽑아서 관리하는 도커 이미지이다.
Controller
apiVersion: v1
kind: Service
metadata:
name: ngrinder
labels:
app: ngrinder
tier: middle
dns: route53
annotations:
domainName: "ngrinder.test.com"
spec:
ports:
# the port that this service should serve on
- name: port80
port: 80
targetPort: 80
protocol: TCP
- name: port16001
port: 16001
targetPort: 16001
protocol: TCP
- name: port12000
port: 12000
targetPort: 12000
protocol: TCP
- name: port12001
port: 12001
targetPort: 12001
protocol: TCP
- name: port12002
port: 12002
targetPort: 12002
protocol: TCP
- name: port12003
port: 12003
targetPort: 12003
protocol: TCP
- name: port12004
port: 12004
targetPort: 12004
protocol: TCP
- name: port12005
port: 12005
targetPort: 12005
protocol: TCP
- name: port12006
port: 12006
targetPort: 12006
protocol: TCP
- name: port12007
port: 12007
targetPort: 12007
protocol: TCP
- name: port12008
port: 12008
targetPort: 12008
protocol: TCP
- name: port12009
port: 12009
targetPort: 12009
protocol: TCP
selector:
app: ngrinder
tier: middle
type: LoadBalancer
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: ngrinder
spec:
replicas: 1
template:
metadata:
labels:
app: ngrinder
tier: middle
spec:
containers:
- name: ngrinder-data
image: dailyhotel/ngrinder-data:latest
imagePullPolicy: Always
volumeMounts:
- mountPath: /opt/ngrinder-controller
name: ngrinder-data-volume
- name: ngrinder
image: ngrinder/controller:latest
resources:
requests:
cpu: 800m
ports:
- containerPort: 80
- containerPort: 16001
- containerPort: 12000
- containerPort: 12001
- containerPort: 12002
- containerPort: 12003
- containerPort: 12004
- containerPort: 12005
- containerPort: 12006
- containerPort: 12007
- containerPort: 12008
- containerPort: 12009
volumeMounts:
- mountPath: /opt/ngrinder-controller
name: ngrinder-data-volume
volumes:
- name: ngrinder-data-volume
emptyDir: {}
Agents
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: ngrinder-agent
spec:
replicas: 5
template:
metadata:
labels:
app: ngrinder-agent
tier: middle
spec:
containers:
- name: ngrinder-agent
image: ngrinder/agent:latest
imagePullPolicy: Always
resources:
requests:
cpu: 300m
args: ["ngrinder.test.com:80"]