diff --git a/.gitignore b/.gitignore index 04d67791..a16f4947 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ +.DS_Store + +.env +*.pem + +src/performanceTest/results/* jacocoReport/ HELP.md diff --git a/src/performanceTest/README.md b/src/performanceTest/README.md new file mode 100644 index 00000000..0a76b920 --- /dev/null +++ b/src/performanceTest/README.md @@ -0,0 +1,67 @@ +# PerforanceTest + +You can refer to the [k6 documentation](https://k6.io/) for additional information. + +## Getting Started + +This performance test runs on Docker Engine. + +### 1. Start Docker Engine on your host + +### 2. Copy the `bastion.pem` File + +Copy the `bastion.pem` file to the `/src/performanceTest/initdb` directory. + +This file is a public key used to access AWS RDS for database initialization. + +### Configure Environment Variables + +Set the required environment variables, or create a `.env` file within the `/src/performanceTest/` directory. + +Here is an example `.env` file with placeholders: + +```dotenv +MYSQL_HOST= +MYSQL_PORT= +MYSQL_USERNAME= +MYSQL_PASSWORD= +MYSQL_SCHEMA= +BASTION_HOST= +BASTION_USERNAME= + +HOST= + +# Monitoring Configuration (Optional) +GRAFANA_HOST= +K6_PROMETHEUS_RW_SERVER_URL= +K6_PROMETHEUS_RW_USERNAME= +K6_PROMETHEUS_RW_PASSWORD= +``` + +### 4. Run the following commands + + +Move to the `src/performanceTest` directory + +You can change the `ENTRYPOINT` variable to run different test files: + + +```shell +# To run the `a_test.js` file +ENTRYPOINT=a_test.js docker-compose up + +# To run the `b_test.js` file +ENTRYPOINT=b_test.js docker-compose up +``` + + +## Running K6 Scripts Independently + +If you want to run a K6 script **without Docker** and **without initializing the database**, you can use the following command: + + +```sh +# Install k6 on your host + +HOST=http://... k6 run scripts/healthCheck.js +``` \ No newline at end of file diff --git a/src/performanceTest/docker-compose.yml b/src/performanceTest/docker-compose.yml new file mode 100644 index 00000000..a945742e --- /dev/null +++ b/src/performanceTest/docker-compose.yml @@ -0,0 +1,39 @@ +version: '3' +services: + initdb: + build: + context: . + dockerfile: ./initdb/Dockerfile + volumes: + - ./initdb/initdb.sh:/initdb.sh + - ./initdb/sqls:/sqls + - ./initdb/bastion.pem:/bastion.pem + environment: + MYSQL_HOST: ${MYSQL_HOST} + MYSQL_PORT: ${MYSQL_PORT} + MYSQL_USERNAME: ${MYSQL_USERNAME} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + MYSQL_SCHEMA: ${MYSQL_SCHEMA} + BASTION_HOST: ${BASTION_HOST} + BASTION_USERNAME: ${BASTION_USERNAME} + + k6: + depends_on: + initdb: + condition: service_completed_successfully + image: grafana/k6:latest + volumes: + - ./scripts:/scripts + - ./results:/results + environment: + HOST: ${HOST} + GRAFANA_HOST: ${GRAFANA_HOST} + K6_PROMETHEUS_RW_SERVER_URL: ${K6_PROMETHEUS_RW_SERVER_URL} + K6_PROMETHEUS_RW_USERNAME: ${K6_PROMETHEUS_RW_USERNAME} + K6_PROMETHEUS_RW_PASSWORD: ${K6_PROMETHEUS_RW_PASSWORD} + K6_PROMETHEUS_RW_TREND_AS_NATIVE_HISTOGRAM: true + OUTPUT_HTML_DIR: "/results" + command: run /scripts/${ENTRYPOINT} -o experimental-prometheus-rw + + + \ No newline at end of file diff --git a/src/performanceTest/initdb/Dockerfile b/src/performanceTest/initdb/Dockerfile new file mode 100644 index 00000000..d72daddf --- /dev/null +++ b/src/performanceTest/initdb/Dockerfile @@ -0,0 +1,9 @@ +FROM ubuntu:22.04 + +# 패키지 설치 및 스크립트 복사 +RUN apt-get update -y +RUN apt-get install -y openssh-client mysql-client +# RUN chmod 600 /bastion.pem + +# 스크립트 실행 +CMD ["/bin/bash", "/initdb.sh"] \ No newline at end of file diff --git a/src/performanceTest/initdb/initdb.sh b/src/performanceTest/initdb/initdb.sh new file mode 100755 index 00000000..4e05557a --- /dev/null +++ b/src/performanceTest/initdb/initdb.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +MYSQL_HOST="$MYSQL_HOST" +MYSQL_PORT="$MYSQL_PORT" +MYSQL_USERNAME="$MYSQL_USERNAME" +MYSQL_PASSWORD="$MYSQL_PASSWORD" +MYSQL_SCHEMA="$MYSQL_SCHEMA" +BASTION_HOST="$BASTION_HOST" +BASTION_USERNAME="$BASTION_USERNAME" +TUNNELING_PORT=30010 + +echo "Running initdb.sh" + +chmod 600 /bastion.pem +ssh -i bastion.pem -CNf -L $TUNNELING_PORT:$MYSQL_HOST:$MYSQL_PORT $BASTION_USERNAME@$BASTION_HOST -o StrictHostKeyChecking=no + +mysql -h 127.0.0.1 -P $TUNNELING_PORT -u $MYSQL_USERNAME -p$MYSQL_PASSWORD -D $MYSQL_SCHEMA < /sqls/cleanup.sql +mysql -h 127.0.0.1 -P $TUNNELING_PORT -u $MYSQL_USERNAME -p$MYSQL_PASSWORD -D $MYSQL_SCHEMA < /sqls/initdata.sql \ No newline at end of file diff --git a/src/performanceTest/initdb/sqls/cleanup.sql b/src/performanceTest/initdb/sqls/cleanup.sql new file mode 100644 index 00000000..0feaaa52 --- /dev/null +++ b/src/performanceTest/initdb/sqls/cleanup.sql @@ -0,0 +1,10 @@ +USE ticketingdb; + +SET foreign_key_checks = 0; + +TRUNCATE user; +TRUNCATE event; +TRUNCATE bookmark; +TRUNCATE reservation; + +SET foreign_key_checks = 1; \ No newline at end of file diff --git a/src/performanceTest/initdb/sqls/initdata.sql b/src/performanceTest/initdb/sqls/initdata.sql new file mode 100644 index 00000000..9c0ba7bd --- /dev/null +++ b/src/performanceTest/initdb/sqls/initdata.sql @@ -0,0 +1,4 @@ +USE ticketingdb; + +INSERT INTO event (title, date, reservation_start_time, reservation_end_time, max_attendees, current_reservation_count) VALUES ('Concert', NOW(), NOW(), NOW(), 10, 0); +INSERT INTO event (title, date, reservation_start_time, reservation_end_time, max_attendees, current_reservation_count) VALUES ('Concert', NOW(), NOW(), NOW(), 10, 0); \ No newline at end of file diff --git a/src/performanceTest/scripts/healthCheck.js b/src/performanceTest/scripts/healthCheck.js new file mode 100644 index 00000000..05c21aad --- /dev/null +++ b/src/performanceTest/scripts/healthCheck.js @@ -0,0 +1,17 @@ +import { check } from "k6"; +import Request from "./lib/request.js"; +import hooks from "./lib/hooks.js"; + +export const handleSummary = hooks.handleSummary + +export const options = { + vus: 10, + duration: '5s', + }; + +export default function () { + const req = new Request() + const res = req.helthCheck() + check(res, { "status == 200": (r) => r.status == 200 }); + check(res, { "body == OK": (r) => r.body == "OK" }); +} \ No newline at end of file diff --git a/src/performanceTest/scripts/lib/config.js b/src/performanceTest/scripts/lib/config.js new file mode 100644 index 00000000..eacf8325 --- /dev/null +++ b/src/performanceTest/scripts/lib/config.js @@ -0,0 +1,13 @@ +const removeLastSlash = (str) => { + if (str) { + return str[str.length - 1] == "/" ? str.slice(0, -1) : str + } + return str +} +export default { + START: new Date().valueOf(), + HOST: removeLastSlash(__ENV.HOST), + GRAFANA_HOST: removeLastSlash(__ENV.GRAFANA_HOST), + OUTPUT_HTML_DIR: __ENV.OUTPUT_HTML_DIR || './results', + DASHBOARD_DELAY: 1000 * 30 // 30 SEC +} \ No newline at end of file diff --git a/src/performanceTest/scripts/lib/generator.js b/src/performanceTest/scripts/lib/generator.js new file mode 100644 index 00000000..ef846672 --- /dev/null +++ b/src/performanceTest/scripts/lib/generator.js @@ -0,0 +1,21 @@ + +function uuid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +const getPrefix = () => { + return `K6-${uuid().substring(0, 10)}`; +} + +const User = () => ({ + name: `${getPrefix()}-name`, + email: `${getPrefix()}@email.com`, + password: `${getPrefix()}-password`, +}); + +export default { + User +} \ No newline at end of file diff --git a/src/performanceTest/scripts/lib/helpers.js b/src/performanceTest/scripts/lib/helpers.js new file mode 100644 index 00000000..32548bb1 --- /dev/null +++ b/src/performanceTest/scripts/lib/helpers.js @@ -0,0 +1,3 @@ +const getRandomByRange = (max) => Math.floor(Math.random() * max); + +export const getOneFromList = (list) => list[getRandomByRange(list.length)]; diff --git a/src/performanceTest/scripts/lib/hooks.js b/src/performanceTest/scripts/lib/hooks.js new file mode 100644 index 00000000..86aaf1d0 --- /dev/null +++ b/src/performanceTest/scripts/lib/hooks.js @@ -0,0 +1,38 @@ +import { htmlReport } from "https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js"; +import { textSummary } from "https://jslib.k6.io/k6-summary/0.0.1/index.js"; +import config from "./config.js"; + +export default { + handleSummary: function(data) { + const htmlPath = `${config.OUTPUT_HTML_DIR}/result.html` + + + let Dahboard = '' + if (config.GRAFANA_HOST) { + const timeRange = `from=${config.START - config.DASHBOARD_DELAY}&to=${new Date().valueOf() + config.DASHBOARD_DELAY}` + const query = `&orgId=1&refresh=10s&${timeRange}` + Dahboard = ` + ------------------------DASHBOARD---------------------------- + HTML : ${htmlPath} + + K6 : ${config.GRAFANA_HOST}/d/01npcT44k/official-k6-test-result?${query} + + CLUSTER: ${config.GRAFANA_HOST}/d/85a562078cdf77779eaa1add43ccec1e/kubernetes-compute-resources-namespace-pods?${query}&var-datasource=default&var-cluster=&var-namespace=default + + SPRING : ${config.GRAFANA_HOST}/d/OS7-NUiGz/spring-boot-statistics?${query} + + MYSQL : ${config.GRAFANA_HOST}/d/549c2bf8936f7767ea6ac47c47b00f2a/mysql-exporter?${query} + ------------------------------------------------------------- + + + ` + } + return { + [htmlPath]: htmlReport(data), + stdout: ` +${Dahboard} +${textSummary(data, { indent: " ", enableColors: true })} + `, + }; + } +} \ No newline at end of file diff --git a/src/performanceTest/scripts/lib/request.js b/src/performanceTest/scripts/lib/request.js new file mode 100644 index 00000000..6f16f477 --- /dev/null +++ b/src/performanceTest/scripts/lib/request.js @@ -0,0 +1,91 @@ +import http from "k6/http"; +import { sleep } from "k6"; +import config from "./config.js"; + +export default class Request { + constructor(baseURL = config.HOST) { + this.baseURL = baseURL + this.Token = null + this.SLEEP = 2 + } + + beforeHook() { + // do nothing + } + + afterHook() { + sleep(this.SLEEP) + } + + setToken(token) { + if (token.substring(0, 7) === 'Bearer '){ + // set this.token = token without 'Bearer ' + this.Token = token.substring(7); + } + else { + this.Token = token; + } + } + getToken() { + return this.Token; + } + + getHeaders() { + const headers = { + 'Content-Type': 'application/json', + } + if (this.getToken() != null) { + headers['Authorization'] = `Bearer ${this.getToken()}`; + } + return headers + } + + getParams() { + return { + headers: this.getHeaders(), + } + } + + + helthCheck() { + this.beforeHook() + const res= http.get(this.baseURL, this.getParams()) + this.afterHook() + return res + } + + getEvents() { + this.beforeHook() + const res = http.get(`${this.baseURL}/events/`); + this.afterHook() + return res + } + signup(body) { + this.beforeHook() + const res = http.post(`${this.baseURL}/users/signup`, JSON.stringify(body), this.getParams()); + this.afterHook() + return res + } + signin(body) { + this.beforeHook() + const res = http.post(`${this.baseURL}/users/signin`, JSON.stringify({ email: body.email, password: body.password }), this.getParams()); + this.setToken(res.json()['Authorization']) + this.afterHook() + return res + } + signout() { + this.setToken(null) + } + access_token_info() { + this.beforeHook() + const res = http.get(`${this.baseURL}/users/access_token_info`, this.getParams()); + this.afterHook() + return res + } + createReservation(body) { + this.beforeHook() + const res = http.post(`${this.baseURL}/reservations`, JSON.stringify(body), this.getParams()); + this.afterHook() + return res + } +} \ No newline at end of file diff --git a/src/performanceTest/scripts/reservationCheck.js b/src/performanceTest/scripts/reservationCheck.js new file mode 100644 index 00000000..75ada6af --- /dev/null +++ b/src/performanceTest/scripts/reservationCheck.js @@ -0,0 +1,22 @@ +import { check } from "k6"; +import Request from "./lib/request.js"; +import generator from "./lib/generator.js"; +import hooks from "./lib/hooks.js"; +import { getOneFromList } from "./lib/helpers.js"; + + +export const handleSummary = hooks.handleSummary + +export default function () { + const req = new Request() + + const user = generator.User() + req.signup(user) + req.signin(user) + + const event = getOneFromList(req.getEvents().json()) + + const res = req.createReservation({ eventId: event.id }) + check(res, { "status == 200": (r) => r.status == 200 }); + check(res, { "created.eventId == event.id": (r) => r.json().eventId == event.id }); +} diff --git a/src/performanceTest/scripts/signinCheck.js b/src/performanceTest/scripts/signinCheck.js new file mode 100644 index 00000000..b21a7749 --- /dev/null +++ b/src/performanceTest/scripts/signinCheck.js @@ -0,0 +1,20 @@ +import { check } from "k6"; +import Request from "./lib/request.js"; +import generator from "./lib/generator.js"; +import hooks from "./lib/hooks.js"; + +export const handleSummary = hooks.handleSummary + +export default function () { + const req = new Request() + + const user = generator.User() + + req.signup(user) + req.signin(user) + + const res = req.access_token_info().json() + + check(res, { "status == 200": (r) => r.status == 200 }); + check(res, { "res has userId key": (r) => r.json().userId > 0 }); +}