본문 바로가기
하드웨어/PC 네트워크 서버

[인프라공방] 웹 성능 부하 테스트 (K6, Grafana)

by 3604 2023. 8. 17.
728x90

부하 테스트

 

[인프라공방] 웹 성능 부하 테스트 (K6, Grafana)

부하 테스트 서비스 운영에는 현재 시스템이 어느 정도의 부하를 견딜 수 있는지 확인하고, 한계치에서 병목이 생기는 지점을 파악하고 장애 조치와 복구를 사전에 계획해두는 것이 중요하다.

sasca37.tistory.com

출처: [인프라공방] 웹 성능 부하 테스트 (K6, Grafana) (tistory.com)

서비스 운영에는 현재 시스템이 어느 정도의 부하를 견딜 수 있는지 확인하고, 한계치에서 병목이 생기는 지점을 파악하고 장애 조치와 복구를 사전에 계획해두는 것이 중요하다. 각 시스템의 응답 성능 및 한계치를 알아보는 것을 부하 테스트라고 한다.

 

가용성

가용성이란 시스템이 서비스를 정상적으로 제공할 수 있는 상태를 의미한다. 가령, 한 대의 서버에 문제가 생겨도 사용자는 인지할 수 없도록 하는 것이 가용성을 높이는 것이고, 단일 장애점(SPOF)를 없애고 확장성있는 서비스를 만들어야 한다.

가용성은 uptime 등의 지표로 측정되기도 하며, IDC, 네트워크, 하드웨어, 소프트웨어 등의 장애 혹은 점검기간, 그리고 높은 부하에 따른 타임아웃 등으로 서비스를 이용할 수 없는 경우 가용성이 낮아진다.

 

단일장애점(Single Point Of Failure)

  • 서버 장비, 애플리케이션, 디비 서버 중 하나라도 장애가 날 경우에 서비스가 중단된다. 즉, WAS와 애플리케이션 서버를 여러개 두어도 디비가 하나라면 단일장애점이 될 수 있으며 최악의 경우 서비스 가치 및 신뢰에 큰 손해를 볼 수 있다.

 

이중화

  • 단순히 장비를 여러대 증설할 경우, DB 데이터가 분산되어 사용자가 어느 서비스에 요청하는가에 따라 다른 결과를 응답받게 된다.

 

DNS를 이용한 트래픽 분산

  • DNS 서버는 애플리케이션 서버의 상태를 확인하지 않으므로, 장애가 발생한 서버로 요청을 할 수 있다. 또한 DNS는 일반적으로 캐싱되므로 사용자가 직접 캐시를 날리지 않는 이상 장애가 유지된다.

 

다중화

  • 위와 같은 단일장애점의 문제로 인해 모든 요소를 다중화하는 것이 중요하다. 다중화란 장애가 발생해도 예비 운용 장비로 시스템의 기능을 계속할 수 있도록 (장애 내성) 유지하는 것을 의미한다.
  • 단일장애점을 없애고 무중단, 고가용성 서비스를 위해 다중화가 필요하다. 다만 유휴장비 역시 비용이기 때문에 ROI(Return of Investment, 투자 이익률)을 고려하여 다중화 수준을 정한다.
  • 다중화 대상으론 Server, Load balancer, Network Device 등이 있다. Failover는 active-passive 관계를 의미하며, Replication은 master-slave를 의미한다.

 

성능

그렇다면 성능이 좋다는 것은 어떤 의미일까? 성능 개선에는 암달의 법칙처럼 한계가 생길 수 밖에 없기 때문에 부하의 원인을 파악하여 이를 제거하는 것이 중요하다. 참고로 부하란 처리를 실행하려고해도 실행할 수 없어서 대기하고 있는 프로세스의 수를 의미한다.

  • Users : 얼마나 많은 사람들이 동시에 사용할 수 있는지
  • TPS : 일정시간동안 얼마나 많이 처리할 수 있는지
  • Time : 서비스가 얼마나 빠른지

다음 3가지와 같은 기준으로 성능을 평가한다. 부하 및 암달의 법칙에 대한 내용은 https://brainbackdoor.tistory.com/117 를 참고하자.

 

사용자

  • 얼마나 많은 사람들이 동시에 사용할 수 있는가는 서비스를 운영하며 늘 화두가 된다. 사용자란, 관점에 따라 다양한 형태로 존재한다.
  • 시스템 관리자의 관점에서는 등록된 사용자와 등록되지 않은 사용자만 존재한다.
  • 서버의 관점에선 로그인 여부로 사용자가 존재한다.
  • 성능 테스터 관점에선 Concurrent User(부하 가능성을 지닌 사용자), Active User(링크를 누르고 결과를 기다리는 유저로 실제 부하를 주고 있는 사용자)로 나뉜다. 해당 사용자들의 비율에 따라 서비스의 성격이 다르므로 이 점을 감안하여 성능테스트를 계획해야 한다. 성능 테스트시 VUser는 Active User와 유사하다.

 

처리량

  • 처리량은 TPS를 의미하며 계산하는 공식은 다음과 같다.
    • 서비스 처리 건수 / 측정 시간
    • 요청 사용자 수 / 평균 응답 시간
    • 동시 사용자 수 / 서비스 요청 간격
  • User 증가 시 TPS는 어느 정도 증가하다가 더 이상 증가하지 않게되며, Time은 일정하게 유지되다 점차적으로 증가한다. 부하가 증가하는 경우(TPS 증가) 지연 시간은 변곡점에 이르기도 하며, 이 경우 시스템 리소스가 누수되고 있는 것은 아닌지 확인해봐야 한다.
  • Time과 달리 TPS는 Scale out 혹은 up을 통해 증가시킬 수 있다. 보통 테스트 시 단순히 응답시간을 기준으로 종료시키지 말고, TPS나 DB, Connection, CPU 등을 종합적으로 확인하고 중단시켜야 한다.

 

시간

  • 사용자에게 있어서 Time은 응답시간만 존재한다. 하지만 시스템 입장에선 사용자 요청에 대한 응답을 받은 후에 웹 페이지에 렌더링해주는 등의 작업을 하는 Think Time이 존재한다.
  • 성능 테스트 시엔 실제 지연시간이 발생하는 구간을 파악하여야 한다.
    • 브라우저와 웹 서버간 구간에서는 정적 리소스, 커넥션, 네트워크 환경 등의 영향을 받을 수 있다.
    • 서버 구간은 DB와 애플리케이션 간의 문제, 프로그램 로직 상의 문제, 서버의 리소스 부족 등을 의심해 볼 수 있다.
    • 네트워크의 이슈의 경우 테스트하는 환경에 따라 달라질 수 있다. 따라서 상위 5프로의 페이지가 사용자의 트래픽을 받는다는 점을 감안하고 튜닝의 대상을 선정한 뒤 출시 전에 최대 응답시간을 파악하고 있어야 한다.

 

부하 테스트 종류

  • 최소한의 부하로 구성된 테스트로, 테스트 시나리오에 오류가 없는지 확인한다. VUser를 1 ~ 2로 구성하여 테스트한다.

  • 서비스의 평소 트래픽과 최대 트래픽 상황에서 성능이 어떤지 확인한다. 애플리케이션 배포 및 인프라 변경(스케일 아웃, 디비 fail over 등)시에 성능 변화를 확인한다.
  • 외부 요인(결제 등)에 따른 예외 상황을 확인한다.

  • 서비스가 극한의 상황에서 어떻게 동작하는지 확인한다. 장기간 부하 발생에 대한 한계치를 확인하고 기능이 정상 동작하는 지 확인한다. 최대 사용자 또는 최대 처리량을 확인한다.
  • 스트레스 테스트 이후 시스템이 수동 개입없이 정상 복구되는지 확인한다.

 

테스트 도구

부하 테스트 도구로는 Apache JMeter, nGrinder, Gatling, Locust, K6 등의 도구가 있다. 테스트 도구는 시나리오 기반의 테스트가 가능해야 한다. 또한 동시 접속자 수, 요청 간격, 최대 Throughput 등 부하를 조정할 수 있어야 하며 부하 테스트 환경이 스케일 아웃을 지원하는 등 충분한 부하를 줄 수 있어야 한다.

 

주의할 점

  • 성능 테스트는 실제 사용자가 접속하는 환경에서 진행해야 한다. 내부 네트워크에서 부하를 발생시킬 경우 응답시간에 차이가 발생할 수 있다.
  • 테스트 DB에 들어 있는 데이터 양이 실제 운영 DB와 동일하여야 한다. 통상 전체 성능의 70프로 이상이 DB에서 좌우된다. 데이터 양이 다르면 쿼리 실행 계획이 달라질 수 있으므로 성능이 다르게 나타날 수 있다.
  • 외부 요인의 경우 시스템과 분리된 별도의 서버를 구성해야한다. 객체를 모킹하는 경우 Http Connection Pool, Connection Thread 등을 미사용하게 되고 IO가 발생하지 않는다. 같은 애플리케이션에서 더미 컨트롤러를 사용하는 경우 테스트 자원과 같이 리소스를 사용하므로 테스트의 신뢰성이 떨어진다.

 

테스트 계획하기

전제 조건 정리

  • 테스트하려는 Target 시스템의 범위를 정한다.
  • 부하 테스트 시 저장될 데이터 건수와 크기를 결정해야 한다. 서비스 이용자 수, 사용자의 행동 패턴, 사용 기간 등을 고려하여 계산한다.
  • 목푯값에 대한 성능 유지기간을 정해야 한다.
  • 서버에 같이 동작하고 있는 다른 시스템, 제약 사항 등을 파악해야 한다.

목푯값 설정

  • 1일 사용자 수 (DAU)를 정하자.
  • 피크 시간대의 집중률을 예상해보자. (최대 트래픽 / 평소 트래픽)
  • 1명 당 1일 평균 접속 혹은 요청 수를 예상해보자.
  • 이를 바탕으로 Throughput(처리율)을 계산하자.
    • Throughput : 1일 평균 rps ~ 1일 최대 rps
      • DAU * 한 명당 1일 평균 접속 수 = 1일 총 접속 수
      • 1일 총 접속 수 / 86,400(하루-초단위) = 1일 평균 rps
      • 1일 평균 rps * (최대 트래픽 / 평소 트래픽) = 1일 최대 rps
    • Latency : 일반적으로 50 ~ 100ms이하로 잡는 것이 좋다.
    • 사용자가 검색하는 데이터의 양, 갱신하는 데이터의 양 등을 파악하자.

  • VUser는 다음과 같이 설정한다.

 

시나리오 대상

  • 접속 빈도가 높은 기능 (홈페이지 등)
  • 서버 리소스 소비량이 높은 기능
    • CPU : 이미지, 동영상 변환, 인증, 파일 압축 및 해제
    • NETWORK : 응답 컨텐츠 크기가 큰 페이지, 이미지 및 동영상 업로드 및 다운로드
    • Disk : 로그가 많은 페이지
  • DB를 사용하는 기능
    • 많은 리소스를 조합하여 결과를 보여주는 페이지
    • 여러 사용자가 같은 리소스를 갱신하는 페이지
  • 외부 시스템과 통신하는 기능
    • 결제 기능, 알림 기능, 인증/인가

k6 부하테스트 적용하기

k6 설치는 공식 문서인 https://k6.io/docs/getting-started/installation/ 를 참고하였다.

 

테스트 전제 조건

  • 대상 시스템 범위
    • 메인 페이지, 로그인 페이지, 경로 검색 페이지
  • 목푯값 설정
    • latency : 500ms
    • throughput
      • DAU : 7,300,000(명) (2020년 하루 평균 대중교통 이용자 수)
      • 평균 접속 수 : 3회 (출근, 퇴근, 환승 등)
      • 1일 총 접속 수 : 21,900,000회
      • 1일 평균 rps : 353회 (1일 총 접속 수 / 86,400)
      • 1일 최대 rps : 3530회 (출, 퇴근 시간 - 10배 예상)
  • VUser
    • T = (3 * 0.13) + 0.5 = 0.44
    • VUser = (3530 * 0.44) / 3 = 517명 (최대), 52명 (평균)

 

k6 설치

 
<code />
 
$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
$ echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
$ sudo apt-get update
$ sudo apt-get install k6
  • 다음과 같이 k6 부하테스트를 설치하자. apt-key는 apt가 패키지를 인증하는 데 사용하는 키 목록을 관리하는 패키지이다.
  • adv --recv-keys를 사용하면 고급 옵션을 gpg로 전달하게 된다. 키 서버에서 키를 신뢰할 수 있는 키목록 모음으로 직접 다운로드 된다.
  • mac의 경우 brew install k6 로 설치 가능하다.

 

smoke test

smoke 테스트는 최소 부하로 구성된 테스트로 VUser를 1~2를 두어 시나리오의 온전성 검사를 위해 실행한다.

 
<code />
 
import http from 'k6/http';
import { check, group, sleep, fail } from 'k6';

export let options = {
    vus: 1, // 1 user looping for 1 minute
    duration: '10s',

    thresholds: {
        http_req_duration: ['p(99)<1500'], // 99% of requests must complete below 1.5s
    },
};

const BASE_URL = 'https://sasca-subway.kro.kr/';
const USERNAME = 'sasca37@naver.com';
const PASSWORD = 'qwer1234';

export default function ()  {
    accessMain();
    let loginRes = loginWithToken();
    accessPath(loginRes);
};

function accessMain() {
    let mainPage = http.get(`${BASE_URL}`);
    check(mainPage, {
        'Main Page Access Successfull': (res) => res.status === 200
    });
}

function loginWithToken() {
    var payload = JSON.stringify({
        email: USERNAME,
        password: PASSWORD,
    });

    var params = {
        headers: {
            'Content-Type': 'application/json',
        },
    };

    let loginRes = http.get(`${BASE_URL}/login/token`, payload, params);

    check(loginRes, {
        'Login With Token Successfull' : (res) => res.json('accessToken') !== '',
    });

    sleep(1);

    return loginRes;
}

function accessPath(loginRes) {
    let authHeaders = {
        headers: {
            Authorization: `Bearer ${loginRes.json('accessToken')}`,
        },
    };

    let pathRes = http.get(`${BASE_URL}/path`, authHeaders);
    check(pathRes, {
        'Access Path with Login Successfull': (res) => res.status === 200
    });

    sleep(1);
}

 

load test

load 테스트는 서비스의 평소 트래픽과 최대 트래픽 상황에서 성능이 어떤지 확인한다. 애플리케이션 배포 및 인프라 변경(스케일 아웃, 디비 fail over 등)시에 성능 변화를 확인한다.

 
<code />
 
import http from 'k6/http';
import { check, group, sleep, fail } from 'k6';

export const options = {
    stages: [
        { duration: '5m', target: 52 },
        { duration: '10m', target: 52 },
        { duration: '3m', target: 517 },
        { duration: '2m', target: 517 },
        { duration: '3m', target: 52 },
        { duration: '10m', target: 52 },
        { duration: '5m', target: 0 },
    ]
};

const BASE_URL = 'https://sasca-subway.kro.kr/';
const USERNAME = 'sasca37@naver.com';
const PASSWORD = 'qwer1234';

export default function ()  {
    accessMain();
    let loginRes = loginWithToken();
    accessPath(loginRes);
};

function accessMain() {
    let mainPage = http.get(`${BASE_URL}`);
    check(mainPage, {
        'Main Page Access Successfull': (res) => res.status === 200
    });
}

function loginWithToken() {
    var payload = JSON.stringify({
        email: USERNAME,
        password: PASSWORD,
    });

    var params = {
        headers: {
            'Content-Type': 'application/json',
        },
    };

    let loginRes = http.get(`${BASE_URL}/login/token`, payload, params);

    check(loginRes, {
        'Login With Token Successfull' : (res) => res.json('accessToken') !== '',
    });

    sleep(1);

    return loginRes;
}

function accessPath(loginRes) {
    let authHeaders = {
        headers: {
            Authorization: `Bearer ${loginRes.json('accessToken')}`,
        },
    };

    let pathRes = http.get(`${BASE_URL}/path`, authHeaders);
    check(pathRes, {
        'Access Path with Login Successfull': (res) => res.status === 200
    });

    sleep(1);
}

 

stress test

strss test 는 서비스가 극한의 상황에서 어떻게 동작하는지 확인한다. 장기간 부하 발생에 대한 한계치를 확인하고 기능이 정상 동작하는 지 확인한다. 최대 사용자 또는 최대 처리량을 확인한다.

 
<code />
 
import http from 'k6/http';
import { check, group, sleep, fail } from 'k6';

export const options = {
    stages: [
        { duration: '2m', target: 200 },
        { duration: '4m', target: 200 },
        { duration: '2m', target: 400 },
        { duration: '4m', target: 400 },
        { duration: '2m', target: 600 },
        { duration: '10m', target: 600 },
        { duration: '5m', target: 0 },
    ]
};

const BASE_URL = 'https://sasca-subway.kro.kr/';
const USERNAME = 'sasca37@naver.com';
const PASSWORD = 'qwer1234';

export default function ()  {
    accessMain();
    let loginRes = loginWithToken();
    accessPath(loginRes);
};

function accessMain() {
    let mainPage = http.get(`${BASE_URL}`);
    check(mainPage, {
        'Main Page Access Successfull': (res) => res.status === 200
    });
}

function loginWithToken() {
    var payload = JSON.stringify({
        email: USERNAME,
        password: PASSWORD,
    });

    var params = {
        headers: {
            'Content-Type': 'application/json',
        },
    };

    let loginRes = http.get(`${BASE_URL}/login/token`, payload, params);

    check(loginRes, {
        'Login With Token Successfull' : (res) => res.json('accessToken') !== '',
    });

    sleep(1);

    return loginRes;
}

function accessPath(loginRes) {
    let authHeaders = {
        headers: {
            Authorization: `Bearer ${loginRes.json('accessToken')}`,
        },
    };

    let pathRes = http.get(`${BASE_URL}/path`, authHeaders);
    check(pathRes, {
        'Access Path with Login Successfull': (res) => res.status === 200
    });

    sleep(1);
}

 

Grafana 모니터링

influx db 설치

 
<code />
 
$ sudo apt install influxdb
  • influx db는 8086포트를 점유하므로 AWS 퍼블릭 망 보안그룹에서 8086포트를 open 시키자.

grafana 설치

 
<code />
 
$ sudo apt install grafana
  • grafana 는 3000포트를 점유한다. 마찬가지로 보안그룹에서 3000포트를 open 시키자.

  • configuration - datasource 에서 influxDB를 추가하고, 대시보드 - import 에서 2587을 입력하고 datasource로 influxdb를 설정하고 import 하자.
 
<code />
 
$ k6 run --out influxdb=http://localhost:8086/myk6db smoke.js
  • 다음과 같이 실행하면 grafana에서 부하테스트를 모니터링할 수 있다.

 


REFERENCES

https://brainbackdoor.tistory.com/117

https://edu.nextstep.camp/

728x90
반응형