weolbu_tech

모든 월급쟁이들이 부자가 되는 꿈을 꾸는 월부의 이야기

Follow publication

월급쟁이부자들의 부하테스트를 위한 k6 도입기

월급쟁이부자들
weolbu_tech
Published in
16 min readSep 18, 2023

안녕하세요. 월급쟁이부자들에서 백엔드 개발을 맡고 있는 디오입니다.

이 글에서는 월급쟁이부자들 백엔드 챕터에서 서버 성능 테스트 도구인 k6와 JMeter를 비교하고 k6를 선택한 이유와 도입한 과정들을 공유합니다.

서버 성능 테스트?

최근 저희 개발팀은 1.5 버전의 리뉴얼 서비스를 런칭하였습니다. 아직 해결하지 못한 Slow Query들이 존재했지만 런칭이후부터 하나씩 빠른속도로 잡아가면서 하루하루를 보내고 있답니다.

월부에서는 프로모션이 열리면 재고가 순식간에 마감되는 상품이 있습니다. 따라서 이렇게 단시간에 폭발적인 트래픽이 들어오는 상황에서 서버가 어느정도 안정적인지, 현재 상태의 처리 한계가 어디까지인지 확인이 필요했습니다.

서버 한계를 확인하기 위해서 사람이 직접 요청을 보내고 확인하는 방법도 있지만 자원이 굉장히 많이 필요로 할 것입니다.

저희는 서버 성능 테스트 도구를 도입해서, 동시 결제가 이루어지는 상황을 최대한 기깝게 만들어내어 서버의 신뢰성과 한계를 정확하게 체크하고 싶었습니다.

서버 성능테스트 도구

JMeter

JMeter는 “아파치재단”에서 Java로 구축한 오픈소스 성능 테스트 도구입니다. 1998년에 처음 출시되었고, 20년이 넘는 시간을 지나 지금까지도 널리 사용되고 있습니다. JMeter는 GUI를 지원함으로써 시각적인 기능을 배치하기 때문에 처음에 배울 때는 더 쉽고, CLI에 익숙하지 않으신분들에게는 친숙하게 느껴집니다. 또한 오랜 시간동안 사용되어 왔기 때문에, 훌륭한 레퍼런스가 축적되어있습니다.

k6

Grafana Labs가 운영하는 오픈소스 성능 테스트 도구로, JMeter 보다는 최근인 2017년에 출시되었습니다. 그런데도 불구하고 k6는 뛰어난 성능과 개발 편의성을 발판으로 빠르게 점유율을 높여나가고 있습니다. CLI에 친숙한 개발자라면 오히려 더 쉽게 설치와 세팅이 가능하고, k6 공식문서도 현대적이면서, 깔끔한 Demo 코드도 지원하고 있기 때문에 빠르게 설치하고 테스트를 할 수 있습니다.

대표적인 두 테스트 도구를 두고서, 여러가지 고민 끝에 월급쟁이부자들 개발팀은 k6를 선택했습니다.

이유는 다음과 같습니다.

  1. 간단한 설치 및 테스트

물론 JMeter도 설치도 쉽고 Homebrew로 빠르게 설치할수도 있습니다. 하지만 Java 설치와 환경변수 세팅이 필요하고 일부 테스트는 플러그인을 추가로 설치해야 한다는 번거러움이 있습니다.

2. 스크립트의 용이성

  • JMeter는 스크립트를 Import/Export 할때 JMeter XML 기반으로 활용이 가능하지만, 어떤 시나리오를 테스트하는건지 파악하기 위해서는 수백라인의 XML을 읽어야하거나 GUI로 Import 했을때 그때서야 어떤 테스트인지 파악할 수 있습니다.
  • k6는 웹개발자에게 친숙한 Javascript로 작성하기 때문에 어떤 시나리오를 테스트하는지 쉽게 확인이 가능합니다.
// 공식문서 Demo Code 

import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
vus: 10, // 10 Users
duration: '30s', //
};

// 1. init Code
export function setup() {
// 2. Setup Code
}


export default function () {
// 3. VU Code
http.get('https://test.k6.io');
sleep(1);
}

3. 성능

k6는 두가지의 대표적인 장점을 가지고 있습니다.

1] 뛰어난 처리로 소문난 Go 언어로 작성

  • k6 라이브러리는 Go 언어로 작성되어있습니다. 이는 곧 더 적은 수의 로드생성기로 많은 사용자를 만들어낼 수 있습니다.

2] Java 의존성

  • JMeter는 Java에 의존하기 때문에, Java 설치 및 설치를 위한 최소 메모리가 필수적입니다. JMeter 공식문서에서는 Java Heap Memory를 위해 1GB이상을 확보하도록 권장하고 있습니다.
  • 하지만 k6는 javascript 파일을 실행하기 때문에, Java를 설치하지 않아도 되며, 고정으로 할당해야할 메모리에 대한 부담을 가지지 않아도 됩니다.

Java 설치가 필요한 JMeter 대신 k6를 선택한다면 AWS EC2 인스턴스 설치시 메모리 비용을 아낄수 있기 때문에, 최적화된 비용으로 테스트 할 수 있을것이라고 판단했습니다.

4. 의존적인 플러그인

  • JMeter는 GUI 혹은 테스트에 필요한 플러그인을 설치하여야합니다. 테스트 환경이 바뀔때마다 다시 설치해야하는 번거로움이 있을것이라 생각되었습니다.
  • k6는 javascript 라이브러리로 왠만한것들이 존재하며, import 후 바로 사용할 수 있습니다.

위와 같은 이유로 k6가 JMeter보다 저희 환경에 더 유용할것이라는 생각과 판단으로 k6를 최종 결정하였습니다.

도입과정

월부 결제프로세스의 시나리오와 k6 테스트 코드를 예시로 저희의 도입과정을 이야기하려 합니다.

저희 개발팀은 첫 로그인부터 메인화면 결제완료까지 이르는 모든 API를 리스트업하고 시나리오를 그린 다음 시나리오대로 API를 호출하도록 테스트 코드를 작성하여 진행하였습니다.

k6 테스트 시나리오

k6 Options

k6는 테스트 코드에 작성된 option 조건대로 테스트가 수행됩니다.

option은 시나리오에 따라 설정을 직접 할 수 있고, 설정할 수 있는 필드도 많기 때문에 여기에서 읽어보시길 바라며, 저희팀이 간단하게 설정했던 옵션은 아래와 같습니다.

// 모수는 테스트마다 변경하며 진행하였고, 아래 코드는 예시로 작성되어있습니다.

export const options = {
batchPerHost: 10,
scenarios: {
payment_scenario: {
vus: 100,
exec: 'payment_scenario',
executor: 'per-vu-iterations',
iterations: 1
}
}
};
  • batchPerHost

동일한 호스트 이름에 동시/병렬 연결의 최대수를 가상사용자에게 세팅합니다.

  • scenarios

실행할 시나리오를 나열합니다. 각 시나리오는 시나리오의 설정대로 다르게 동작할 수 있습니다. 저희는 결제 시나리오대로 각 사용자별로 딱 한번만 진행하면 되었기에 위와 같이 작성하였습니다.

Scenarios Documents

  • vus

테스트할 가상사용자를 설정합니다.

  • executor

k6에는 테스트 컨셉의 다양한 로드발생기가 있습니다. 아래는 로드 발생기의 리스트와 간단한 설명을 공유드립니다. 자세한 설명은 Executors Documents 에서 읽어보신 후, 용도에 맞게 도입을 해보시기 바랍니다.

저희는 사용자들이 정해진 시간에 상관없이 딱 한번의 결제만 이루어지기 때문에, per-vu-iterations 모듈에 1회 반복으로 설정하여 테스트를 진행하였습니다.

k6 lifeCycle

  • API 응답을 기다려야 하는 문제

시나리오대로 결제 API를 호출려면 사용자의 토큰이 필요했는데, 토큰을 받으려면 “로그인 API”를 호출하여 응답이 오기를 기다려야 합니다. 이 과정을 javascript로 작성하기 때문에, async,await 로 토큰을 응답 받아 처리할 수 있습니다.

async function login() {

let response = await http.asyncRequest('POST', api, JSON.stringify({
'email': `test@naver.com`,
'userPwd': '0000'
}), {
headers: { 'Content-Type' : 'application/json' }
});

return response.json().data.token.accessToken;
}

하지만 응답을 기다리는 과정(await)으로 인해, 3000명의 사용자가 순차적인 테스트를 실행하게 되지만, 실환경에서는 Dynamic하게 각각의 유저가 로그인 ~ 결제를 시도하기 때문에 실환경과 가깝게 테스트 할 수 없었습니다.

이 문제를 해결하기위해 공식문서를 읽다가 k6의 Lifecycle을 발견했습니다.

Lifecycle functions

초기화 코드를 제외하고, 각 단계는 k6 런타임의 특정 시퀀스에 호출되는 라이프사이클 함수가 있습니다.

// 1. init code

export function setup() {
// 2. setup code
}

export default function (data) {
// 3. Virtual User Scenario code
}

export function teardown(data) {
// 4. teardown code
}
  • setup()

k6 런타임시 최초 한번 호출되며 데이터를 전처리하거나, 가상 사용자간 데이터 공유가 필요할때 사용합니다.

  • default function()

일반적인 테스트 시나리오 코드를 이 함수에 작성하며, k6 옵션에 따라 반복 호출됩니다.

  • teardown()

예상한 결과를 얻었는지 확인하고 테스트가 완료되었음을 알리는 웹훅 함수입니다.

저희는 이 k6 lifeCycle을 활용하여 setup() 함수에서 미리 테스트할 사용자의 로그인 토큰을 생성해놓고 default function() 함수로 토큰을 전달하여 이 함수에서는 오직 시나리오대로 API를 Dynamic하게 호출하였습니다.

그 결과 실환경과 최대한 비슷한 환경을 만들 수 있었고, 아래는 저희가 테스트하고자 하는 방향과 예시 코드입니다.

import http from 'k6/http';

export const options = {
// ...
};

export function setup() {
// 토큰생성 함수 호출
const tokens = generateTokens(requests);

return { tokens };
}

export function payment_scenario({ tokens }) {
// 생성한 토큰들을 각 가상사용자 시퀀스를 이용하여 할당
const token = tokens[__VU - 1];

// 시나리오 API Start

mainProductApi(token); // 메인페이지의 상품 관련 Api

productDetailApi(__VU, productId); // 상품의 상세정보 관련 Api

paymentDetailApi(__VU, productId); // 상품 결제상세창 관련 Api

prePaymentApi(__VU, token, productId); // 상품 점유 Api

paymentApi(__VU, token, productId); // 상품 결제 Api

paymentWebhook(__VU, token, productId); // 상품 최종결제 정산 Api
}

/**
* k6 http 모듈을 활용한 POST 요청 예시
*/
function paymentApi(vu, token, productId) {
const api = `${host}/payment`;
http.asyncRequest('POST', api, JSON.stringify({
"productId": productId,
"pg": "kcp.IP2RK",
"payMethod": "card",
}), {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type' : 'application/json',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36'
}
});
}

export function teardown(data) {
// 정산확인 Api 호출
paymentWebhook(__VU, token, productId);
}

테스트 결과 및 개선과정

저희팀은 로그인하여 메인페이지 렌더링 되는 시점부터 결제까지 이어지는 모든 Api를 리스트업해서 작성한 테스트 코드를 통해 1000명의 가상 사용자를 만들어 두 번의 테스트를 진행했습니다. 첫 시도와 두번째 시도의 결과는 아래와 같았습니다.

문제는 바로 나타났습니다.

AWS CloudWatch를 통해서 데이터베이스 지표를 추적한 결과, DB에서 Slow Query로 인한 Latency가 발생하여 웹서버의 응답이 순식간에 느려졌고, 결국 MaxConnectionTimeout이 발생하며 장애가 터져나왔습니다.

결국 1000명중 제대로 결제가 되어 DB에 남은 데이터는 50%에 불과했습니다.

아마 이대로 프로모션을 진행했다면.. 상상만 해도 끔찍한 일이 벌어졌을것입니다. (복선)

1. SlowQuery 개선

저희는 위 테스트 결과를 토대로 Slow Query가 발생하는 Api를 추적하여 개선작업을 진행하였고, 아래는 같은 환경에서 다시 실행한 테스트 결과입니다.

결과는 첫 테스트때보다 안정적인 결과를 얻게 되었습니다.

2. 성능 한계 테스트

SlowQuery를 개선 후 순간적으로 1000명의 결제 트랜잭션을 안정적으로 처리 할 수 있는것을 확인했지만, 현재의 컴퓨팅 자원으로 최대 한계 지점을 테스트 할 필요가 있습니다. 이로 인해 얻는 효과는

1] 프로모션 일정에 따라 최적화된 비용으로 서버 스케일링 할 수 있다.

  • 5000명이 한번에 결제하는 인기 많은 상품의 프로모션
  • 300명정도가 한번에 결제하는 비인기 상품의 프로모션

2] 응답처리가 늦어져 문제가 발생시, 현재의 트래픽을 고려한 서버 스케일링 산정을 계산할 수 있다.

3] 한계점에서 처리가 늦어지는 원인을 파악할 수 있다.

  • 성능 개선의 여지가 남아있을수 있습니다.

4] 개발자의 심적 편안함

아래는 동일한 서버 스펙으로 1000명부터 5000명까지의 가상 사용자별 테스트 진행 내역입니다. 지표를 보면 3000명의 동시 결제 테스트부터 비정상적인 상태를 가진 결제내역들이 나타나기 시작했습니다.

비정상 결제상태의 원인은 아래 그래프에서는 안보이지만, ECS의 CPU가 100%를 유지하면서 정상적인 처리를 하지 못했던것으로 확인되었습니다. 그렇다는건 DB 보다는 웹서버의 튜닝 혹은 스케일링 작업이 필요하다는것을 의미합니다.

하지만 저희는 현재 컴퓨팅자원으로 최대 약 2000명의 동시 결제를 보장할 수 있다는 정확한 데이터를 얻었고, 프로모션 대비를 위한 프로세스를 정립할 수 있었습니다.

3. 실전편 1 : “첫술에 배 부르랴”

k6 테스트를 통한 서버 튜닝 작업 후 처음 열리는 프로모션이였기 때문에 오픈시간전에 미리 서버 스케일링 작업을 해두고서 만반의 준비를 하였습니다. 아래는 그때 당시의 지표입니다.

그래프에서 보이는것처럼 DB CPU가 과부하되고 지연현상이 발생하였습니다. 이유가 무엇이였을까요?

“잘못된 방향의 테스트 시나리오”

문제의 원인은 테스트 시나리오가 실제 사용자의 흐름과 달랐던 점입니다.

k6 테스트 시나리오
실제 사용자 흐름
  • 객은 저희 월부닷컴의 홈페이지를 미리 접속하여 프로모션의 상품을 확인하고 다른 컨텐츠들을 탐험한다.
  • 이때, 테스트 시나리오에는 확인할 수 없었던 마이페이지에서 SlowQuery들이 터져나오면서 문제가 지속적으로 발생

테스트 시나리오는 오직 로그인 → 결제 까지의 단방향 프로세스에 대한 테스트였지만, 저희 월부닷컴은 결제외에도 수많은 컨텐츠들이 있다는것까지 고려하지 못했습니다. 그 결과, 메인페이지조차 제대로 열리지 않았고, 제 때 결제하지 못한 고객들이 발생하였습니다.

4. 실전편 2 : “원하지 않는 결과는 성공으로 가는 과정”

지금까지 월부가 수많은 시도를 하면서 한번도 실패하지 않았습니다. 그 이유는 월부는 시도하고, 원하는 결과가 나오지 않으면 복기하고 또 시도하면서 결국 성공할때까지 수많은 실패에도 다시 도전하는 조직이었기 때문입니다.

<월급쟁이부자들, 너바나 대표>

첫번째 실전을 통해서 넓은 시각의 테스트 시나리오가 필요하다는 교훈을 얻었습니다.

저희 개발팀은 다음날에도 있을 프로모션을 위해 다양한 시나리오를 다시 구상하고, 넒은 시각으로 SlowQuey를 체크, 개선하며 두번째 만반의 준비를 마쳤습니다. (진짜)

이날의 프로모션은 첫날만큼 강력한 상품은 아니었지만, 그래도 최대 25프로 언저리를 기록할만큼 안정적인 지표를 보여주었고, 저희는 만족할만한 원하는 결과를 얻게 되었습니다.

마무리하며

무엇을 깨달았나?

k6를 쉽게 도입하고, 극한으로 빠르게 테스트를 진행할 수 있다는 걸 체감할 수 있었던 경험이었으나, 그 안에서의 다양하고 실전같은 시나리오, 넓은 시각이 중요하다는 값비싼 교훈을 얻었습니다.

더 고민해볼 점?

k6는 이 기능 외에도 많은 기능을 제공합니다.

이를 활용한 더 멋진 시나리오 테스트를 시도해볼수 있을것 같습니다.

  • Lifecycle의 teardown() 함수를 통한 지표분석 활용
  • Cloud k6를 활용한 시각화
  • interval Test를 통한 더 실전과 가까운 시나리오
  • 테스트 자동화

마지막으로

저희 월부 조직은 고객의 편의, 좋은 컨텐츠를 최우선으로 생각하고 수많은 실패에도 다시 부딪히고 용감하게 도전하고 있습니다. 다음에도 멋진 도전과 그 흔적을 공유드리겠습니다.

읽어주셔서 감사합니다.

written by Dio (서희륜)

email: dio@weolbu.com

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

weolbu_tech
weolbu_tech

Published in weolbu_tech

모든 월급쟁이들이 부자가 되는 꿈을 꾸는 월부의 이야기

Responses (1)

Write a response