diff --git a/.github/workflows/gke-cd.yaml b/.github/workflows/gke-cd.yaml index 98870c38..a30442e6 100644 --- a/.github/workflows/gke-cd.yaml +++ b/.github/workflows/gke-cd.yaml @@ -72,7 +72,7 @@ jobs: run: chmod +x ./gradlew - name: Build and test project - run: ./gradlew build + run: ./gradlew clean build - name: Build docker image and push run: bash ./script/img_push_multi_arch.sh -u ${{ secrets.DOCKERHUB_USERNAME }} -t ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/bm-agent/src/main/java/org/benchmarker/bmagent/consts/PreftestConsts.java b/bm-agent/src/main/java/org/benchmarker/bmagent/consts/PreftestConsts.java index 0d08e3a6..2e62e751 100644 --- a/bm-agent/src/main/java/org/benchmarker/bmagent/consts/PreftestConsts.java +++ b/bm-agent/src/main/java/org/benchmarker/bmagent/consts/PreftestConsts.java @@ -3,7 +3,30 @@ import java.util.Arrays; import java.util.List; +/** + * Performance Test Constants + */ public interface PreftestConsts { - List percentiles = Arrays.asList(50D, 90D, 95D,99D,99.9D); + + /** + * Required when measuring percentile of TPS and MTTFB(Mean Time To First Byte) + */ + List percentiles = Arrays.asList(50D, 90D, 95D, 99D, 99.9D); + + /** + * If performance test has error rate exceed {@link PreftestConsts#errorLimitRate}, instantly + * shutdown all scheduler and emit event to bm-controller + */ + Double errorLimitRate = 50D; + + /** + * ErrorLimit Lookup process will be started after this time + */ + int errorLimitCheckDelay = 10; + + /** + * ErrorLimit Lookup process period + */ + int errorLimitCheckPeriod = 5; } diff --git a/bm-agent/src/main/java/org/benchmarker/bmagent/consts/SseManageConsts.java b/bm-agent/src/main/java/org/benchmarker/bmagent/consts/SseManageConsts.java index 26fc0044..accf9de6 100644 --- a/bm-agent/src/main/java/org/benchmarker/bmagent/consts/SseManageConsts.java +++ b/bm-agent/src/main/java/org/benchmarker/bmagent/consts/SseManageConsts.java @@ -1,5 +1,12 @@ package org.benchmarker.bmagent.consts; +/** + * SSE constants + */ public interface SseManageConsts { + + /** + * SSE timeout constant + */ Long SSE_TIMEOUT = 600000L; } diff --git a/bm-agent/src/main/java/org/benchmarker/bmagent/consts/SystemSchedulerConst.java b/bm-agent/src/main/java/org/benchmarker/bmagent/consts/SystemSchedulerConst.java index 7d502532..df15e3c4 100644 --- a/bm-agent/src/main/java/org/benchmarker/bmagent/consts/SystemSchedulerConst.java +++ b/bm-agent/src/main/java/org/benchmarker/bmagent/consts/SystemSchedulerConst.java @@ -1,13 +1,28 @@ package org.benchmarker.bmagent.consts; +/** + * System Scheduler constants + */ public interface SystemSchedulerConst { /** * System scheduler will run in this ID */ Long systemSchedulerId = -100L; + + /** + * System scheduler's name + */ String systemUsageSchedulerName = "cpu-memory-usage-update"; + + /** + * @deprecated since 2024-03-28 + */ Integer connectControllerTimeout = 10; // seconds + + /** + * @deprecated since 2024-03-28 + */ Integer connectionFailedLimit = 50; diff --git a/bm-agent/src/main/java/org/benchmarker/bmagent/controller/AgentApiController.java b/bm-agent/src/main/java/org/benchmarker/bmagent/controller/AgentApiController.java index d023f350..17b8efe3 100644 --- a/bm-agent/src/main/java/org/benchmarker/bmagent/controller/AgentApiController.java +++ b/bm-agent/src/main/java/org/benchmarker/bmagent/controller/AgentApiController.java @@ -1,7 +1,9 @@ package org.benchmarker.bmagent.controller; -import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.Map; import java.util.Set; import lombok.RequiredArgsConstructor; @@ -21,11 +23,15 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; - +/** + * Main RESTAPI endpoint + * + *

+ * bm-controller will send request to here for performance testing + *

+ */ @RestController @RequiredArgsConstructor @RequestMapping("/api") @@ -46,7 +52,8 @@ public class AgentApiController { @PostMapping("/groups/{group_id}/templates/{template_id}") public SseEmitter manageSSE(@PathVariable("template_id") Long templateId, @PathVariable("group_id") String groupId, - @RequestParam("action") String action, @RequestBody TemplateInfo templateInfo) { + @RequestParam("action") String action, @RequestBody TemplateInfo templateInfo) + throws IOException { log.info(templateInfo.toString()); if (action.equals("start")) { @@ -54,7 +61,7 @@ public SseEmitter manageSSE(@PathVariable("template_id") Long templateId, AgentStatus.TESTING).orElseThrow(() -> new RuntimeException("agent is not ready")); return sseManageService.start(templateId, groupId, templateInfo); } else { - sseManageService.stop(templateId); + sseManageService.stopSign(templateId); return null; } } @@ -70,16 +77,17 @@ public ResponseEntity> getSchedulersStatus() { } @GetMapping("/status") - public AgentInfo getStatus() { - HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); - String scheme = request.getScheme(); // http or https - String serverName = request.getServerName(); - int serverPort = request.getServerPort(); - Set longs = scheduledTaskService.getStatus().keySet(); + public AgentInfo getStatus() throws UnknownHostException { + log.info("Check current status"); + InetAddress localHost = InetAddress.getLocalHost(); + String serverAddress = localHost.getHostAddress(); + String scheme = "http"; // Assuming it's always HTTP when accessed locally + int serverPort = 8081; // Assuming default port is 8080 - String agentServerUrl = scheme + "://" + serverName + ":" + serverPort; + Set longs = scheduledTaskService.getStatus().keySet(); - return AgentInfo.builder() + String agentServerUrl = scheme + "://" + serverAddress + ":" + serverPort; + AgentInfo info = AgentInfo.builder() .templateId(longs) .cpuUsage(agentStatusManager.getCpuUsage()) .memoryUsage(agentStatusManager.getMemoryUsage()) @@ -87,6 +95,8 @@ public AgentInfo getStatus() { .serverUrl(agentServerUrl) .status(agentStatusManager.getStatus().get()) .build(); + log.info(info.toString()); + return info; } } diff --git a/bm-agent/src/main/java/org/benchmarker/bmagent/initializer/Initializer.java b/bm-agent/src/main/java/org/benchmarker/bmagent/initializer/Initializer.java index 817bfc96..661d888e 100644 --- a/bm-agent/src/main/java/org/benchmarker/bmagent/initializer/Initializer.java +++ b/bm-agent/src/main/java/org/benchmarker/bmagent/initializer/Initializer.java @@ -13,6 +13,13 @@ import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; +/** + * The Initializer class is responsible for initializing various components of the + * application when it starts up. + * + *

+ * The Initializer will start system scheduler which recording CPU / MEMORY usage in every seconds + */ @Component @Profile("!test") @Slf4j @@ -26,7 +33,6 @@ public class Initializer implements CommandLineRunner, ApplicationContextAware { @Override public void run(String... args) throws Exception { - log.info("init"); // cpu, memory usage checker scheduledTaskService.startChild(SystemSchedulerConst.systemSchedulerId, SystemSchedulerConst.systemUsageSchedulerName, agentStatusManager::updateStats, 0, 1, diff --git a/bm-agent/src/main/java/org/benchmarker/bmagent/pref/HttpSender.java b/bm-agent/src/main/java/org/benchmarker/bmagent/pref/HttpSender.java index 605f2c70..72ec4cce 100644 --- a/bm-agent/src/main/java/org/benchmarker/bmagent/pref/HttpSender.java +++ b/bm-agent/src/main/java/org/benchmarker/bmagent/pref/HttpSender.java @@ -4,11 +4,10 @@ import java.net.MalformedURLException; import java.net.URL; +import java.text.DecimalFormat; import java.time.Duration; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; -import java.util.Comparator; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -18,6 +17,9 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.benchmarker.bmagent.AgentStatus; +import org.benchmarker.bmagent.consts.PreftestConsts; +import org.benchmarker.bmagent.pref.calculate.IResultCalculator; +import org.benchmarker.bmagent.pref.calculate.ResultCalculator; import org.benchmarker.bmagent.service.IScheduledTaskService; import org.benchmarker.bmagent.status.AgentStatusManager; import org.benchmarker.bmagent.util.WebClientSupport; @@ -28,8 +30,19 @@ /** * High load HTTP sender + *

+ * This class represents a high load HTTP sender responsible for sending multiple requests to a + * target server. It contains functionality to 1) manage request sending, 2) calculate statistics + * such as Transactions Per Second (TPS) and Mean Time To First Byte (MTTFB), and 3) cancel ongoing + * requests. + *

+ *

+ * All request will be running in {@link CompletableFuture} with common-pool. And threads will be + * created according to the number of {@code vuser} + *

* * @author Gyumin Hwangbo + * @since 2024-03-30 */ @Slf4j @Getter @@ -40,6 +53,8 @@ public class HttpSender { private final AgentStatusManager agentStatusManager; + private IResultCalculator resultCalculator = new ResultCalculator(); + private DecimalFormat decimalFormat = new DecimalFormat("#.##"); public HttpSender(ResultManagerService resultManagerService, IScheduledTaskService scheduledTaskService, AgentStatusManager agentStatusManager) { @@ -53,6 +68,8 @@ public HttpSender(ResultManagerService resultManagerService, private AtomicInteger totalRequests = new AtomicInteger(0); private AtomicInteger totalSuccess = new AtomicInteger(0); private AtomicInteger totalErrors = new AtomicInteger(0); + private Double tpsAvg = 0D; + private Double mttfbAvg = 0D; // Response time, TPS private AtomicInteger tps = new AtomicInteger(0); private ConcurrentHashMap tpsMap = new ConcurrentHashMap<>(); @@ -60,6 +77,8 @@ public HttpSender(ResultManagerService resultManagerService, private ConcurrentHashMap statusCodeCount = new ConcurrentHashMap<>(); private List> futures; private Boolean isRunning = true; + private Boolean isErrorExceed = false; + private Boolean isStopped = false; /** * Major implementation sending multiple requests to target server @@ -72,7 +91,8 @@ public HttpSender(ResultManagerService resultManagerService, * * @param templateInfo {@link TemplateInfo} */ - public void sendRequests(SseEmitter sseEmitter, TemplateInfo templateInfo) throws MalformedURLException { + public void sendRequests(SseEmitter sseEmitter, TemplateInfo templateInfo) + throws MalformedURLException { URL url = new URL(templateInfo.getUrl()); RequestHeadersSpec req = WebClientSupport.create(templateInfo.getMethod(), @@ -102,8 +122,12 @@ public void sendRequests(SseEmitter sseEmitter, TemplateInfo templateInfo) throw long endTime = startTime + duration.toMillis(); for (int j = 0; j < templateInfo.getMaxRequest(); j++) { - // 만약 running 이 아니거나 시간이 끝났다면, - if (!isRunning || System.currentTimeMillis() > endTime) { + // 테스트 시간 종료 + if (System.currentTimeMillis() > endTime){ + break; + } + // 만약 running 이 아니거나 시간이 끝났다면, 에러율이 너무 높다면 + if (!isRunning || isErrorExceed) { agentStatusManager.updateAgentStatus(AgentStatus.READY); break; } @@ -113,6 +137,7 @@ public void sendRequests(SseEmitter sseEmitter, TemplateInfo templateInfo) throw statusCodeCount.merge(statusCode, 1, Integer::sum); if (resp.statusCode().is2xxSuccessful()) { totalSuccess.incrementAndGet(); + tps.incrementAndGet(); } else { totalErrors.incrementAndGet(); } @@ -125,7 +150,6 @@ public void sendRequests(SseEmitter sseEmitter, TemplateInfo templateInfo) throw .truncatedTo(ChronoUnit.SECONDS); mttfbMap.merge(currentTime, elapsedTime, (oldValue, newValue) -> (oldValue + newValue) / 2); - tps.incrementAndGet(); totalRequests.incrementAndGet(); } @@ -134,12 +158,29 @@ public void sendRequests(SseEmitter sseEmitter, TemplateInfo templateInfo) throw // Need to calculate & save TPS and MTTFB in every 1 second. scheduledTaskService.startChild(Long.valueOf(templateInfo.getId()), "recorder", () -> { + LocalDateTime currentTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS); // save current tps & reset - tpsMap.put(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS), - Double.valueOf(tps.get())); + tpsMap.put(currentTime, Double.valueOf(tps.get())); + saveAverage(currentTime); tps.set(0); // initial }, 0, 1, java.util.concurrent.TimeUnit.SECONDS); + // error observer + scheduledTaskService.startChild(Long.valueOf(templateInfo.getId()), "error-observer", + () -> { + int requests = totalRequests.get(); + int errors = totalErrors.get(); + if (requests != 0 && errors != 0) { + // if error rate exceed 50%, order future to stop! + if ((double) errors / requests * 100 > PreftestConsts.errorLimitRate) { + log.warn("Template-{}, error rate exceed {}", templateInfo.getId(), + PreftestConsts.errorLimitRate); + isErrorExceed=true; + } + } + }, PreftestConsts.errorLimitCheckDelay, PreftestConsts.errorLimitCheckPeriod, + java.util.concurrent.TimeUnit.SECONDS); + // CompletableFuture 종료까지 대기 CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); } @@ -151,30 +192,7 @@ public void sendRequests(SseEmitter sseEmitter, TemplateInfo templateInfo) throw * @return The calculated percentile value */ public Map calculateTpsPercentile(List percentile) { - Map result = new HashMap<>(); - - List tpsList = this.sortedTpsMap(); - int size = tpsList.size(); - for (Double p : percentile) { - int index = (int) Math.ceil((p / 100) * size) - 1; - if (index < 0) { - return Map.of(0D, 0D); // 예외처리: 인덱스가 음수인 경우 - } - result.put(p, tpsList.get(index)); - } - - return result; - } - - public List sortedTpsMap() { - Map tpsSnapshot = new ConcurrentHashMap<>(tpsMap); - return tpsSnapshot.values().stream().sorted(Comparator.reverseOrder()) - .toList(); // <-- Here STOP! - } - - public List sortedMttfbMap() { - Map tpsSnapshot = new ConcurrentHashMap<>(mttfbMap); - return tpsSnapshot.values().stream().sorted().toList(); // <-- Here STOP! + return resultCalculator.percentile(tpsMap, percentile, true); } /** @@ -183,20 +201,35 @@ public List sortedMttfbMap() { * @param percentile The percentile to calculate (e.g., 90 for 90th percentile) * @return The calculated percentile value */ - public Map calculateMttfbPercentile(List percentile) { - Map result = new HashMap<>(); - - List mttfbList = this.sortedMttfbMap(); - int size = mttfbList.size(); - for (Double p : percentile) { - int index = (int) Math.ceil((p / 100) * size) - 1; - if (index < 0) { - return Map.of(0D, 0D); // 예외처리: 인덱스가 음수인 경우 + public Map calculateMttfbPercentile(List percentile) { + return resultCalculator.percentile(mttfbMap, percentile, false); + } + + /** + * Save current TPS, MTTFB average + * + * @param currentTime LocalDateTime + */ + public void saveAverage(LocalDateTime currentTime) { + Double currentTps = (double) tps.get(); + Long currentMttfb = mttfbMap.get(currentTime); + + if (tpsAvg == 0) { + tpsAvg = currentTps; + } else { + currentTps = resultCalculator.average(tpsAvg, currentTps); + tpsAvg = Double.parseDouble(decimalFormat.format(currentTps)); + } + if (currentMttfb != null) { + Double doubleMTTFB = Double.valueOf(currentMttfb); + if (mttfbAvg == 0) { + mttfbAvg = doubleMTTFB; + } else { + Double average = resultCalculator.average(mttfbAvg, doubleMTTFB); + mttfbAvg = Double.parseDouble(decimalFormat.format(average)); } - result.put(p, Double.valueOf(mttfbList.get(index))); } - return result; } /** diff --git a/bm-agent/src/main/java/org/benchmarker/bmagent/pref/ResultManagerService.java b/bm-agent/src/main/java/org/benchmarker/bmagent/pref/ResultManagerService.java index f23c391b..4e645d99 100644 --- a/bm-agent/src/main/java/org/benchmarker/bmagent/pref/ResultManagerService.java +++ b/bm-agent/src/main/java/org/benchmarker/bmagent/pref/ResultManagerService.java @@ -10,13 +10,12 @@ * ResultManagerService for managing TestResult * *

- * After bmagent send multiple HTTP request to target server, calculated delays and others things need - * to be added in resultHashMap + * After bmagent send multiple HTTP request to target server, calculated delays and others things + * need to be added in resultHashMap *

* *

- * resultHashMap will be used by {@link ISseManageService} to send - * TestResult to client + * resultHashMap will be used by {@link ISseManageService} to send TestResult to client *

* * @see ISseManageService @@ -45,7 +44,8 @@ public CommonTestResult find(Long id) { /** * Save TestResult to resultHashMap - * @param id {@link Long} + * + * @param id {@link Long} * @param object {@link CommonTestResult} */ @Override @@ -55,6 +55,7 @@ public void save(Long id, CommonTestResult object) { /** * Remove TestResult from resultHashMap + * * @param id {@link Long} */ @Override diff --git a/bm-agent/src/main/java/org/benchmarker/bmagent/pref/calculate/IResultCalculator.java b/bm-agent/src/main/java/org/benchmarker/bmagent/pref/calculate/IResultCalculator.java new file mode 100644 index 00000000..fb6b3732 --- /dev/null +++ b/bm-agent/src/main/java/org/benchmarker/bmagent/pref/calculate/IResultCalculator.java @@ -0,0 +1,38 @@ +package org.benchmarker.bmagent.pref.calculate; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * This interface support useful methods for calculating average, percentile. + * + * @author gyumin hwangbo + * @since 2024-03-30 + */ +public interface IResultCalculator { + + /** + * Calculates the average of a set of values. + * + * @param values The values to calculate the average of. + * @param T The type of values. Must extend Number and implement Comparable. + * @return The average of the provided values as a Double. + * @throws IllegalArgumentException if no values are provided. + */ + > Double average(T... values); + + + /** + * Calculates percentiles from a map of results. + * + * @param results The map containing the results with their associated date and time. + * @param percentile The list of percentiles to calculate. + * @param reverse Determines whether to calculate percentiles in reverse order. + * @param T The type of values in the map. Must extend Number and implement + * Comparable. + * @return A map containing the calculated percentiles along with their associated values. + */ + > Map percentile(Map results, + List percentile, Boolean reverse); +} diff --git a/bm-agent/src/main/java/org/benchmarker/bmagent/pref/calculate/ResultCalculator.java b/bm-agent/src/main/java/org/benchmarker/bmagent/pref/calculate/ResultCalculator.java new file mode 100644 index 00000000..bac39cc8 --- /dev/null +++ b/bm-agent/src/main/java/org/benchmarker/bmagent/pref/calculate/ResultCalculator.java @@ -0,0 +1,57 @@ +package org.benchmarker.bmagent.pref.calculate; + +import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class ResultCalculator implements IResultCalculator { + + @SafeVarargs + @Override + public final > Double average(T... values) { + if (values == null || values.length == 0) { + throw new IllegalArgumentException("At least one value must be provided."); + } + + // calculate values average + double sum = values[0].doubleValue(); + for (int i = 1; i < values.length; i++) { + sum += values[i].doubleValue(); + } + double average = sum / values.length; + return Double.valueOf(average); + } + + @Override + public > Map percentile( + Map results, List percentile, Boolean reverse) { + + Map snapshot = new ConcurrentHashMap<>(results); + if (snapshot.size() == 0){ + return Map.of(); + } + List values; + if (!reverse) { + values = snapshot.values().stream().sorted(Comparator.reverseOrder()).toList(); + } else { + values = snapshot.values().stream().sorted().toList(); + } + + Map result = new HashMap<>(); + int size = values.size(); + for (Double p : percentile) { + int index = (int) Math.ceil((p / 100) * size) - 1; + if (index < 0) { + result.put(p, values.get(0)); + } else { + result.put(p, values.get(index)); + } + } + + return result; + } + +} diff --git a/bm-agent/src/main/java/org/benchmarker/bmagent/service/ISseManageService.java b/bm-agent/src/main/java/org/benchmarker/bmagent/service/ISseManageService.java index 2afa9802..5c03cc04 100644 --- a/bm-agent/src/main/java/org/benchmarker/bmagent/service/ISseManageService.java +++ b/bm-agent/src/main/java/org/benchmarker/bmagent/service/ISseManageService.java @@ -1,5 +1,6 @@ package org.benchmarker.bmagent.service; +import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.benchmarker.bmagent.consts.SseManageConsts; @@ -10,12 +11,13 @@ * Interface for the SSE service */ public interface ISseManageService extends SseManageConsts { + Map sseEmitterHashMap = new ConcurrentHashMap<>(); /** * Start a new SSE emitter for the given id * - * @param id Long + * @param id Long * @param templateInfo TemplateInfo * @return SseEmitter */ @@ -31,8 +33,16 @@ public interface ISseManageService extends SseManageConsts { /** * Send a message to the SSE emitter for the given id * - * @param id Long + * @param id Long * @param message string */ void send(Long id, Object message); + + /** + * + * + * @param id + * @throws IOException + */ + void stopSign(Long id) throws IOException; } diff --git a/bm-agent/src/main/java/org/benchmarker/bmagent/sse/CurrentRunner.java b/bm-agent/src/main/java/org/benchmarker/bmagent/sse/CurrentRunner.java new file mode 100644 index 00000000..1d4b78dc --- /dev/null +++ b/bm-agent/src/main/java/org/benchmarker/bmagent/sse/CurrentRunner.java @@ -0,0 +1,22 @@ +package org.benchmarker.bmagent.sse; + +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.benchmarker.bmagent.pref.HttpSender; +import org.benchmarker.bmcommon.dto.TemplateInfo; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class CurrentRunner { + private HttpSender httpSender; + private LocalDateTime startAt; + private String groupId; + private TemplateInfo templateInfo; +} diff --git a/bm-agent/src/main/java/org/benchmarker/bmagent/sse/SseManageService.java b/bm-agent/src/main/java/org/benchmarker/bmagent/sse/SseManageService.java index 80dd7008..edc94890 100644 --- a/bm-agent/src/main/java/org/benchmarker/bmagent/sse/SseManageService.java +++ b/bm-agent/src/main/java/org/benchmarker/bmagent/sse/SseManageService.java @@ -6,7 +6,6 @@ import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; @@ -35,7 +34,7 @@ public class SseManageService extends AbstractSseManageService { private final IScheduledTaskService scheduledTaskService; private final ResultManagerService resultManagerService; private final AgentStatusManager agentStatusManager; - private HashMap httpSender = new HashMap<>(); + private HashMap runner = new HashMap<>(); /** * Start the SseEmitter for the given id and return the SseEmitter @@ -53,42 +52,52 @@ public SseEmitter start(Long id, String groupId, TemplateInfo templateInfo) { // when the client disconnects, complete the SseEmitter alwaysDoStop(id, emitter); - if (sseEmitterHashMap.containsKey(id)) { + if (sseEmitterHashMap.get(id) != null) { log.debug("SSE already exists for template ID: {}", id); return null; } // Save the SseEmitter to the map + LocalDateTime startTime = LocalDateTime.now(); sseEmitterHashMap.put(id, emitter); + HttpSender httpSender = new HttpSender(resultManagerService, scheduledTaskService, + agentStatusManager); - httpSender.put(id, new HttpSender(resultManagerService, scheduledTaskService, agentStatusManager)); - HttpSender htps = httpSender.get(id); - - LocalDateTime now = LocalDateTime.now(); - List percentiles = PreftestConsts.percentiles; + runner.put(id, + CurrentRunner.builder() + .httpSender(httpSender) + .groupId(groupId) + .startAt(startTime) + .templateInfo(templateInfo).build()); // 1초마다 TestResult 를 보내는 스케줄러 시작 scheduledTaskService.start(id, () -> { LocalDateTime curTime = LocalDateTime.now(); - Map tpsP = htps.calculateTpsPercentile(percentiles); - Map mttfbP = htps.calculateMttfbPercentile(percentiles); - CommonTestResult data = getCommonTestResult(groupId,templateInfo, htps, now, curTime, tpsP, mttfbP); + CommonTestResult data = getCommonTestResult(groupId, templateInfo, httpSender, + startTime, curTime); + data.setFinishedAt("-"); + resultManagerService.save(id, data); send(id, resultManagerService.find(id)); }, 0, 1, TimeUnit.SECONDS); - // async + non-blocking 필수 CompletableFuture.runAsync(() -> { try { - htps.sendRequests(emitter, templateInfo); + httpSender.sendRequests(emitter, templateInfo); LocalDateTime finished = LocalDateTime.now(); - Map tpsP = htps.calculateTpsPercentile(percentiles); - Map mttfbP = htps.calculateMttfbPercentile(percentiles); - CommonTestResult data = getCommonTestResult(groupId,templateInfo, htps, now, finished, tpsP, mttfbP); + CommonTestResult data = getCommonTestResult(groupId, templateInfo, httpSender, + startTime, + finished); data.setFinishedAt(finished.toString()); - data.setTestStatus(AgentStatus.TESTING_FINISH); + if (httpSender.getIsErrorExceed()) { // error rate exceeded + data.setTestStatus(AgentStatus.STOP_BY_ERROR); + } else if (!httpSender.getIsRunning()) { // bm-controller send stop sign + data.setTestStatus(AgentStatus.STOP); + } else { // performance test finished + data.setTestStatus(AgentStatus.TESTING_FINISH); + } send(id, data); emitter.complete(); } catch (MalformedURLException e) { @@ -107,13 +116,11 @@ public SseEmitter start(Long id, String groupId, TemplateInfo templateInfo) { * @param htps * @param start * @param cur - * @param tpsP - * @param mttfbP * @return CommonTestResult */ - private CommonTestResult getCommonTestResult(String groupId,TemplateInfo templateInfo, HttpSender htps, - LocalDateTime start, LocalDateTime cur, Map tpsP, - Map mttfbP) { + private CommonTestResult getCommonTestResult(String groupId, TemplateInfo templateInfo, + HttpSender htps, LocalDateTime start, LocalDateTime cur) { + List percentiles = PreftestConsts.percentiles; return CommonTestResult.builder() .groupId(groupId) .startedAt(start.toString()) @@ -126,13 +133,12 @@ private CommonTestResult getCommonTestResult(String groupId,TemplateInfo templat .method(templateInfo.getMethod()) .totalUsers(templateInfo.getVuser()) .totalDuration(Duration.between(start, cur).toString()) - .MTTFBPercentiles(mttfbP) - .TPSPercentiles(tpsP) + .MTTFBPercentiles(htps.calculateMttfbPercentile(percentiles)) + .TPSPercentiles(htps.calculateTpsPercentile(percentiles)) .testStatus(agentStatusManager.getStatus().get()) .finishedAt(cur.toString()) - // TODO temp - .mttfbAverage("0") - .tpsAverage(0) + .mttfbAverage(htps.getMttfbAvg().toString()) + .tpsAverage(htps.getTpsAvg()) .build(); } @@ -151,8 +157,32 @@ public void stop(Long id) { this.send(id, "SSE completed"); emitter.complete(); } - httpSender.get(id).cancelRequests(); - scheduledTaskService.shutdown(id); + + runner.get(id).getHttpSender().cancelRequests(); + scheduledTaskService.shutdown(id); // but shutdown + resultManagerService.remove(id); + agentStatusManager.updateAgentStatus(AgentStatus.READY); + } + + @Override + public void stopSign(Long id) throws IOException { + SseEmitter emitter = sseEmitterHashMap.remove(id); + CurrentRunner curRunner = runner.get(id); + CommonTestResult commonTestResult = this.getCommonTestResult(curRunner.getGroupId(), + curRunner.getTemplateInfo(), curRunner.getHttpSender(), + curRunner.getStartAt(), LocalDateTime.now()); + commonTestResult.setTestStatus(AgentStatus.STOP); + + if (emitter != null) { + emitter.send(commonTestResult); + log.info("remove sse emitter"); + this.send(id, "SSE completed"); + emitter.complete(); + } + + curRunner.getHttpSender().cancelRequests(); + runner.remove(id); + scheduledTaskService.shutdown(id); // but shutdown resultManagerService.remove(id); agentStatusManager.updateAgentStatus(AgentStatus.READY); } diff --git a/bm-agent/src/test/java/org/benchmarker/bmagent/controller/AgentApiControllerTest.java b/bm-agent/src/test/java/org/benchmarker/bmagent/controller/AgentApiControllerTest.java index dae9be5b..c4cc49f3 100644 --- a/bm-agent/src/test/java/org/benchmarker/bmagent/controller/AgentApiControllerTest.java +++ b/bm-agent/src/test/java/org/benchmarker/bmagent/controller/AgentApiControllerTest.java @@ -104,7 +104,7 @@ public void testStopSSE() throws IOException { // then // sseManageService.stop() 메서드가 호출되었는지 검증 - verify(sseManageService, times(1)).stop(eq(templateId)); + verify(sseManageService, times(1)).stopSign(eq(templateId)); } @Test diff --git a/bm-agent/src/test/java/org/benchmarker/bmagent/pref/calculate/IResultCalculatorTest.java b/bm-agent/src/test/java/org/benchmarker/bmagent/pref/calculate/IResultCalculatorTest.java new file mode 100644 index 00000000..72207c5e --- /dev/null +++ b/bm-agent/src/test/java/org/benchmarker/bmagent/pref/calculate/IResultCalculatorTest.java @@ -0,0 +1,65 @@ +package org.benchmarker.bmagent.pref.calculate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class IResultCalculatorTest { + + private IResultCalculator resultCalculator; + + @BeforeEach + void setup() { + resultCalculator = mock(IResultCalculator.class); + } + + @Test + @DisplayName("평균 계산 테스트") + public void testAverage() { + // given + when(resultCalculator.average(1.0, 2.0, 3.0, 4.0)).thenReturn(2.5); + + // when + Double result = resultCalculator.average(1.0, 2.0, 3.0, 4.0); + + // then + assertEquals(2.5, result); + } + + @Test + @DisplayName("퍼센타일 계산") + public void testPercentile() { + // given + Map results = new HashMap<>(); + List percentiles = Arrays.asList(50D, 90D); // 50th, 90th index + LocalDateTime now = LocalDateTime.now(); + for (int i = 1; i <= 10; i++) { + results.put(now, (double) i); + now = now.plusSeconds(1); + } + + // expected results + Map expectResult = Map.of(50D, 5D, 90D, 9D); + Map expectResultReverse = Map.of(50D, 5D, 90D, 1D); + when(resultCalculator.percentile(results, percentiles, false)).thenReturn(expectResult); + when(resultCalculator.percentile(results, percentiles, true)).thenReturn(expectResultReverse); + + // when + Map result = resultCalculator.percentile(results, percentiles, false); + Map resultReverse = resultCalculator.percentile(results, percentiles, true); + + // then + assertThat(result).isEqualTo(expectResult); + assertThat(resultReverse).isEqualTo(expectResultReverse); + } +} \ No newline at end of file diff --git a/bm-agent/src/test/java/org/benchmarker/bmagent/pref/calculate/ResultCalculatorTest.java b/bm-agent/src/test/java/org/benchmarker/bmagent/pref/calculate/ResultCalculatorTest.java new file mode 100644 index 00000000..261196d3 --- /dev/null +++ b/bm-agent/src/test/java/org/benchmarker/bmagent/pref/calculate/ResultCalculatorTest.java @@ -0,0 +1,65 @@ +package org.benchmarker.bmagent.pref.calculate; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.text.DecimalFormat; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ResultCalculatorTest { + + private IResultCalculator resultCalculator; + + @BeforeEach + void setup(){ + resultCalculator = new ResultCalculator(); + } + + @Test + @DisplayName("평균 계산") + void test1(){ + // when + Double average = resultCalculator.average(1, 2, 3, 4); + + // then + assertThat(average).isEqualTo(2.5D); + } + + @Test + @DisplayName("퍼센타일 계산") + void test2(){ + // given + Map results = new HashMap<>(); + List percentiles = Arrays.asList(50D, 90D); // 50th, 90th index + LocalDateTime now = LocalDateTime.now(); + for (int i = 1; i <= 10; i++) { + results.put(now, i); + now = now.plusSeconds(1); + } + + // when + Map percentile = resultCalculator.percentile(results, percentiles, true); + + // then + Map expectResult = Map.of(50D, 5, 90D, 9); + assertThat(percentile).isEqualTo(expectResult); + + double value = 123.456789; + + // Format the double value to have up to two decimal places + DecimalFormat df = new DecimalFormat("#.##"); + String formattedValue = df.format(value); + + // Parse the formatted string back to double + double result = Double.parseDouble(formattedValue); + + System.out.println("Formatted value: " + result); + } + +} \ No newline at end of file diff --git a/bm-common/build.gradle b/bm-common/build.gradle index f0f7224e..0b1039ca 100644 --- a/bm-common/build.gradle +++ b/bm-common/build.gradle @@ -23,6 +23,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/bm-common/src/main/java/org/benchmarker/bmagent/AgentStatus.java b/bm-common/src/main/java/org/benchmarker/bmagent/AgentStatus.java index ae3cc91d..ea0fe01c 100644 --- a/bm-common/src/main/java/org/benchmarker/bmagent/AgentStatus.java +++ b/bm-common/src/main/java/org/benchmarker/bmagent/AgentStatus.java @@ -1,5 +1,5 @@ package org.benchmarker.bmagent; public enum AgentStatus { - READY, TESTING_START, TESTING, TESTING_FINISH, CANCELED, STOP_BY_ERROR, UNKNOWN + READY, TESTING_START, TESTING, TESTING_FINISH, CANCELED, STOP_BY_ERROR, STOP, UNKNOWN } diff --git a/bm-common/src/main/java/org/benchmarker/bmcommon/dto/CommonTestResult.java b/bm-common/src/main/java/org/benchmarker/bmcommon/dto/CommonTestResult.java index 3e158d93..5c125783 100644 --- a/bm-common/src/main/java/org/benchmarker/bmcommon/dto/CommonTestResult.java +++ b/bm-common/src/main/java/org/benchmarker/bmcommon/dto/CommonTestResult.java @@ -58,8 +58,7 @@ public class CommonTestResult { private String mttfbAverage; @JsonProperty("mttfb_percentiles") - private Map MTTFBPercentiles; - + private Map MTTFBPercentiles; @JsonProperty("tps_average") private double tpsAverage; diff --git a/bm-common/src/main/java/org/benchmarker/bmcommon/dto/PrepareInfo.java b/bm-common/src/main/java/org/benchmarker/bmcommon/dto/PrepareInfo.java deleted file mode 100644 index ca0e1eda..00000000 --- a/bm-common/src/main/java/org/benchmarker/bmcommon/dto/PrepareInfo.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.benchmarker.bmcommon.dto; - -import java.util.Map; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter -@Builder -@AllArgsConstructor -@NoArgsConstructor -public class PrepareInfo { - - private String url; - private String method; - private Map headers; - private Map body; - - public PrepareInfo random(){ - this.url = "url"; - this.method = "method"; - this.headers = Map.of("key", "value"); - this.body = Map.of("key", "value"); - return this; - } -} diff --git a/bm-common/src/main/java/org/benchmarker/bmcommon/util/RandomUtils.java b/bm-common/src/main/java/org/benchmarker/bmcommon/util/RandomUtils.java index 6a138dea..7239b8cf 100644 --- a/bm-common/src/main/java/org/benchmarker/bmcommon/util/RandomUtils.java +++ b/bm-common/src/main/java/org/benchmarker/bmcommon/util/RandomUtils.java @@ -27,10 +27,10 @@ public static CommonTestResult generateRandomTestResult() { .totalUsers(1) .totalDuration("1s") .mttfbAverage("1ms") - .MTTFBPercentiles(new HashMap() {{ - put(50D, randDouble(0, 100)); - put(95D, randDouble(0, 100)); - put(99D, randDouble(0, 100)); + .MTTFBPercentiles(new HashMap() {{ + put(50D, randLong(0, 100)); + put(95D, randLong(0, 100)); + put(99D, randLong(0, 100)); }}) .tpsAverage(randDouble(0, 100)) .TPSPercentiles(new HashMap() {{ diff --git a/bm-controller/build.gradle b/bm-controller/build.gradle index 5bd0125c..6ab47ae3 100644 --- a/bm-controller/build.gradle +++ b/bm-controller/build.gradle @@ -98,6 +98,9 @@ dependencies { implementation 'com.squareup.okhttp3:mockwebserver:4.9.1' implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + implementation 'org.springframework.cloud:spring-cloud-starter-kubernetes-client' + + implementation 'org.springframework.boot:spring-boot-starter-validation' asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' @@ -107,6 +110,7 @@ dependencies { testImplementation 'org.testcontainers:postgresql:1.19.3' testImplementation "org.testcontainers:junit-jupiter:1.19.3" + implementation 'org.postgresql:postgresql' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/bm-controller/src/main/java/org/benchmarker/bmcontroller/common/error/ErrorCode.java b/bm-controller/src/main/java/org/benchmarker/bmcontroller/common/error/ErrorCode.java index 1d98c633..2287a2e7 100644 --- a/bm-controller/src/main/java/org/benchmarker/bmcontroller/common/error/ErrorCode.java +++ b/bm-controller/src/main/java/org/benchmarker/bmcontroller/common/error/ErrorCode.java @@ -15,6 +15,7 @@ public enum ErrorCode { * 400 */ BAD_REQUEST(400, "잘못된 요청입니다."), + ALREADY_RUNNING(400, "해당 테스트를 이미 진행중입니다"), INVALID_JSON(400, "잘못된 JSON 형식입니다."), PASSWORD_NOT_MATCH(400, "패스워드를 정확히 입력해주세요."), USER_NOT_SAME_GROUP(400, "그룹이 다른 사용자입니다."), diff --git a/bm-controller/src/main/java/org/benchmarker/bmcontroller/preftest/controller/PerftestController.java b/bm-controller/src/main/java/org/benchmarker/bmcontroller/preftest/controller/PerftestController.java index e1227fc0..640e316f 100644 --- a/bm-controller/src/main/java/org/benchmarker/bmcontroller/preftest/controller/PerftestController.java +++ b/bm-controller/src/main/java/org/benchmarker/bmcontroller/preftest/controller/PerftestController.java @@ -70,8 +70,11 @@ public ResponseEntity send(@PathVariable("group_id") String groupId, agentServerManager.removeTemplateRunnerAgent(Long.valueOf(templateId)); log.info("stop to " + serverUrl); - } else { - + }else{ + if (perftestService.isRunning(groupId, templateId)){ + log.warn("template is already running"); + throw new GlobalException(ErrorCode.ALREADY_RUNNING); + } serverUrl = agentServerManager.getReadyAgent().orElseThrow(() -> new GlobalException(ErrorCode.INTERNAL_SERVER_ERROR)).getServerUrl(); @@ -90,8 +93,8 @@ public ResponseEntity send(@PathVariable("group_id") String groupId, perftestService.removeRunning(groupId,templateId); if (action.equals("stop")) { - log.info("Test completed! {}", action); - messagingTemplate.convertAndSend("/topic/" + userId, "test started!"); + log.info("Test completed! {}", templateId); + messagingTemplate.convertAndSend("/topic/" + userId, "test complete"); } }) .subscribe(event -> { diff --git a/bm-controller/src/main/java/org/benchmarker/bmcontroller/preftest/service/PerftestService.java b/bm-controller/src/main/java/org/benchmarker/bmcontroller/preftest/service/PerftestService.java index 2445dd4e..7fb484ca 100644 --- a/bm-controller/src/main/java/org/benchmarker/bmcontroller/preftest/service/PerftestService.java +++ b/bm-controller/src/main/java/org/benchmarker/bmcontroller/preftest/service/PerftestService.java @@ -43,6 +43,14 @@ public void removeRunning(String groupId, Integer templateId) { ; } + public Boolean isRunning(String groupId, Integer templateId){ + Set templates = runningTemplates.get(groupId); + if (templates != null && templates.contains(templateId)) { + return true; + } + return false; + } + /** * Execute a performance test request to the bm-agent API and receive intermediate results via * Server-Sent Events (SSE). diff --git a/bm-controller/src/main/java/org/benchmarker/bmcontroller/preftest/websocket/WebSocketConfig.java b/bm-controller/src/main/java/org/benchmarker/bmcontroller/preftest/websocket/WebSocketConfig.java index 6c000896..e2b6f0fa 100644 --- a/bm-controller/src/main/java/org/benchmarker/bmcontroller/preftest/websocket/WebSocketConfig.java +++ b/bm-controller/src/main/java/org/benchmarker/bmcontroller/preftest/websocket/WebSocketConfig.java @@ -18,7 +18,9 @@ public void configureMessageBroker(MessageBrokerRegistry config) { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { - registry.addEndpoint("/gs-guide-websocket"); + registry.addEndpoint("/gs-guide-websocket") + .setAllowedOrigins("http://localhost:8080") + .withSockJS(); } } \ No newline at end of file diff --git a/bm-controller/src/main/java/org/benchmarker/bmcontroller/prerun/DataLoader.java b/bm-controller/src/main/java/org/benchmarker/bmcontroller/prerun/DataLoader.java index e8f4da48..e5a7e38e 100644 --- a/bm-controller/src/main/java/org/benchmarker/bmcontroller/prerun/DataLoader.java +++ b/bm-controller/src/main/java/org/benchmarker/bmcontroller/prerun/DataLoader.java @@ -5,6 +5,8 @@ import static org.benchmarker.bmcontroller.user.constant.UserConsts.USER_GROUP_DEFAULT_NAME; import jakarta.transaction.Transactional; +import java.time.Duration; +import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map.Entry; @@ -27,13 +29,16 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.CommandLineRunner; -import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.context.annotation.Profile; import org.springframework.http.ResponseEntity; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; /** * After the application starts, this class will be executed to add the default user to the @@ -43,8 +48,10 @@ */ @Component @Slf4j +@Profile("production") @RequiredArgsConstructor public class DataLoader implements CommandLineRunner { + private final SimpMessagingTemplate messagingTemplate; private final UserRepository userRepository; private final UserGroupRepository userGroupRepository; @@ -114,28 +121,45 @@ public void run(String... args) throws Exception { } } - // get current agent from eureka discovery - List instances = discoveryClient.getInstances("bm-agent"); - // 각 인스턴스의 URL을 사용하여 요청을 보냄 - for (ServiceInstance instance : instances) { - try{ - String serverUrl = instance.getUri().toString(); - AgentInfo agentInfo = WebClient.create(serverUrl) + List podNames = new ArrayList<>(); + + podNames = discoveryClient.getInstances("bm-agent").stream() + .map((serviceInstance -> { + return serviceInstance.getUri().toString(); + })).toList(); + + Flux.fromIterable(podNames) + .parallel() + .runOn(Schedulers.parallel()) + .flatMap(instanceUrl -> { + return WebClient.create(instanceUrl) .get() .uri("/api/status") .retrieve() .bodyToMono(AgentInfo.class) - .block(); + .timeout(Duration.ofSeconds(1)) + .onErrorResume(e -> { + log.error("Error occurred while fetching data from {}", instanceUrl, e); + return Mono.empty(); + }) + .map(agentInfo -> { + agentInfo.setServerUrl(instanceUrl); // 수정된 부분 + return agentInfo; + }); + }) + .sequential() + .doOnNext(agentInfo -> { + if (agentInfo != null) { + log.info("agentInfo {}", agentInfo.toString()); + agentServerManager.add(agentInfo.getServerUrl(), agentInfo); + } + }) + .blockLast(); - agentInfo.setServerUrl(serverUrl); - agentServerManager.add(serverUrl, agentInfo); - }catch (Exception e){ - noOp(); - } - } messagingTemplate.convertAndSend("/topic/server", agentServerManager.getAgentsUrl().values()); - }, 0, 500, TimeUnit.MILLISECONDS); + + }, 0, 1000, TimeUnit.MILLISECONDS); } diff --git a/bm-controller/src/main/java/org/benchmarker/bmcontroller/prerun/DataLoaderKubernetes.java b/bm-controller/src/main/java/org/benchmarker/bmcontroller/prerun/DataLoaderKubernetes.java new file mode 100644 index 00000000..3f8139e4 --- /dev/null +++ b/bm-controller/src/main/java/org/benchmarker/bmcontroller/prerun/DataLoaderKubernetes.java @@ -0,0 +1,218 @@ +package org.benchmarker.bmcontroller.prerun; + +import static org.benchmarker.bmcontroller.common.util.NoOp.noOp; +import static org.benchmarker.bmcontroller.user.constant.UserConsts.USER_GROUP_DEFAULT_ID; +import static org.benchmarker.bmcontroller.user.constant.UserConsts.USER_GROUP_DEFAULT_NAME; + +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.Configuration; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1Pod; +import io.kubernetes.client.openapi.models.V1PodList; +import io.kubernetes.client.util.Config; +import jakarta.transaction.Transactional; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.benchmarker.bmagent.AgentInfo; +import org.benchmarker.bmcontroller.agent.AgentServerManager; +import org.benchmarker.bmcontroller.scheduler.ScheduledTaskService; +import org.benchmarker.bmcontroller.user.model.User; +import org.benchmarker.bmcontroller.user.model.UserGroup; +import org.benchmarker.bmcontroller.user.model.UserGroupJoin; +import org.benchmarker.bmcontroller.user.model.enums.GroupRole; +import org.benchmarker.bmcontroller.user.model.enums.Role; +import org.benchmarker.bmcontroller.user.repository.UserGroupJoinRepository; +import org.benchmarker.bmcontroller.user.repository.UserGroupRepository; +import org.benchmarker.bmcontroller.user.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +/** + * After the application starts, this class will be executed to add the default user to the + * database. + * + * @see CommandLineRunner + */ +@Component +@Slf4j +@Profile("kubernetes") +@RequiredArgsConstructor +public class DataLoaderKubernetes implements CommandLineRunner { + + private final SimpMessagingTemplate messagingTemplate; + private final UserRepository userRepository; + private final UserGroupRepository userGroupRepository; + private final UserGroupJoinRepository userGroupJoinRepository; + private final PasswordEncoder passwordEncoder; + private final ScheduledTaskService scheduledTaskService; + private final AgentServerManager agentServerManager; + + @Value("${admin.id}") + private String adminId; + @Value("${admin.password}") + private String adminPassword; + @Autowired + private DiscoveryClient discoveryClient; + + @Override + @Transactional + public void run(String... args) throws Exception { + // 데이터베이스에 초기 사용자 추가 + Optional adminUser = userRepository.findById(adminId); + Optional defaultGroup = userGroupRepository.findById(USER_GROUP_DEFAULT_ID); + adminUser.ifPresentOrElse( + user -> { + noOp(); + }, + () -> userRepository.save(adminUser())); + defaultGroup.ifPresentOrElse( + userGroup -> { + noOp(); + }, + () -> userGroupRepository.save(defaultAdminGroup())); + List findJoin = userGroupJoinRepository.findByUserId(adminId); + if (findJoin.isEmpty()) { + Optional group = userGroupRepository.findById(USER_GROUP_DEFAULT_ID); + group.ifPresent(userGroup -> userGroupJoinRepository.save( + UserGroupJoin.builder() + .user(adminUser()) + .userGroup(defaultAdminGroup()) + .role(GroupRole.LEADER) + .build() + )); + } + + // remove & add agent in every seconds + scheduledTaskService.start(-100L, () -> { + // agent health check + Iterator> iterator = agentServerManager.getAgentsUrl() + .entrySet().iterator(); + while (iterator.hasNext()) { + Entry next = iterator.next(); + try { + ResponseEntity agentInfo = WebClient.create(next.getKey()) + .get() + .uri("/api/status") + .retrieve() + .toEntity(AgentInfo.class) + .block(); + + assert agentInfo != null; + if (agentInfo.getStatusCode().is2xxSuccessful()) { + next.setValue(Objects.requireNonNull(agentInfo.getBody())); + } else { + iterator.remove(); + } + } catch (Exception e) { + iterator.remove(); + } + } + + List podNames = new ArrayList<>(); + + try { + // Kubernetes 클라이언트 설정 + ApiClient client = Config.defaultClient(); + Configuration.setDefaultApiClient(client); + + // CoreV1Api 객체 생성 + CoreV1Api api = new CoreV1Api(); + + V1PodList podList = api.listNamespacedPod("default", null, null, null, null, + "app=agent-service", null, null, null, null, null, null); + // Pod 목록 순회 + for (V1Pod pod : podList.getItems()) { + // Pod 이름 출력 + String podIP = pod.getStatus().getPodIP(); + podNames.add("http://" + podIP + ":8081"); + } + + } catch (ApiException e) { + System.err.println( + "Exception when calling CoreV1Api#listServiceForAllNamespaces"); + e.printStackTrace(); + } catch (Exception e) { + System.err.println("Exception: " + e.getMessage()); + e.printStackTrace(); + } + +// podNames = discoveryClient.getInstances("bm-agent").stream() +// .map((serviceInstance -> { +// return serviceInstance.getUri().toString(); +// })).toList(); + + Flux.fromIterable(podNames) + .parallel() + .runOn(Schedulers.parallel()) + .flatMap(instanceUrl -> { + return WebClient.create(instanceUrl) + .get() + .uri("/api/status") + .retrieve() + .bodyToMono(AgentInfo.class) + .timeout(Duration.ofSeconds(1)) + .onErrorResume(e -> { + log.error("Error occurred while fetching data from {}", instanceUrl, e); + return Mono.empty(); + }) + .map(agentInfo -> { + agentInfo.setServerUrl(instanceUrl); // 수정된 부분 + return agentInfo; + }); + }) + .sequential() + .doOnNext(agentInfo -> { + if (agentInfo != null) { + log.info("agentInfo {}", agentInfo.toString()); + agentServerManager.add(agentInfo.getServerUrl(), agentInfo); + } + }) + .blockLast(); + + messagingTemplate.convertAndSend("/topic/server", + agentServerManager.getAgentsUrl().values()); + + }, 0, 1000, TimeUnit.MILLISECONDS); + + + } + + private UserGroup defaultAdminGroup() { + return UserGroup.builder() + .id(USER_GROUP_DEFAULT_ID) + .name(USER_GROUP_DEFAULT_NAME) + .build(); + } + + private User adminUser() { + return User.builder() + .id(adminId) + .password(passwordEncoder.encode(adminPassword)) + .email("admin@gmail.com") + .emailNotification(false) + .slackNotification(false) + .slackWebhookUrl("admin-webhook-url") + .role(Role.ROLE_ADMIN) + .build(); + } +} \ No newline at end of file diff --git a/bm-controller/src/main/java/org/benchmarker/bmcontroller/template/controller/TestTemplateApiController.java b/bm-controller/src/main/java/org/benchmarker/bmcontroller/template/controller/TestTemplateApiController.java index c98a3c55..1bf22006 100644 --- a/bm-controller/src/main/java/org/benchmarker/bmcontroller/template/controller/TestTemplateApiController.java +++ b/bm-controller/src/main/java/org/benchmarker/bmcontroller/template/controller/TestTemplateApiController.java @@ -1,6 +1,8 @@ package org.benchmarker.bmcontroller.template.controller; import com.fasterxml.jackson.core.JsonProcessingException; +import jakarta.validation.Valid; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.benchmarker.bmcontroller.common.error.ErrorCode; @@ -15,9 +17,14 @@ import org.benchmarker.bmcontroller.user.service.UserContext; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; - -import java.util.List; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @Slf4j @RestController @@ -31,7 +38,7 @@ public class TestTemplateApiController { @PostMapping("/template") @PreAuthorize("hasAnyRole('USER')") - public ResponseEntity createTemplate(@RequestBody TestTemplateRequestDto reqTestTemplate) + public ResponseEntity createTemplate(@Valid @RequestBody TestTemplateRequestDto reqTestTemplate) throws JsonProcessingException { if (reqTestTemplate.getMethod().equalsIgnoreCase("GET")) { diff --git a/bm-controller/src/main/java/org/benchmarker/bmcontroller/template/controller/dto/TestTemplateRequestDto.java b/bm-controller/src/main/java/org/benchmarker/bmcontroller/template/controller/dto/TestTemplateRequestDto.java index c6bcf3c0..81354ed2 100644 --- a/bm-controller/src/main/java/org/benchmarker/bmcontroller/template/controller/dto/TestTemplateRequestDto.java +++ b/bm-controller/src/main/java/org/benchmarker/bmcontroller/template/controller/dto/TestTemplateRequestDto.java @@ -2,8 +2,10 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; import java.util.Map; import lombok.AllArgsConstructor; import lombok.Builder; @@ -26,16 +28,18 @@ public class TestTemplateRequestDto { @NotBlank private String url; @NotBlank + @Pattern(regexp = "^(?i)(GET|PUT|DELETE|POST|PATCH)$", message = "HTTP method must be GET, PUT, DELETE, POST, or PATCH") private String method; private String body; - @NotNull + @Min(value = 1, message = "Virtual users must be at least 1") + @Max(value = 500, message = "Virtual users must be at most 500") private Integer vuser; - @NotNull + @Min(value = 1) + @Max(value = 1000000) private Integer maxRequest; // maxDuration is in seconds - @NotNull + @Max(600) private Integer maxDuration; - @NotNull private Integer cpuLimit; private Map headers; diff --git a/bm-controller/src/main/resources/application.yaml b/bm-controller/src/main/resources/application.yaml index 7eb19480..9b18b5f7 100644 --- a/bm-controller/src/main/resources/application.yaml +++ b/bm-controller/src/main/resources/application.yaml @@ -33,7 +33,7 @@ spring: hibernate: use-new-id-generator-mappings: true order_inserts: true - ddl-auto: validate + ddl-auto: update format_sql: true jdbc: time_zone: UTC diff --git a/bm-controller/src/main/resources/static/css/styles.css b/bm-controller/src/main/resources/static/css/styles.css index ccb5ff7d..a06477e5 100644 --- a/bm-controller/src/main/resources/static/css/styles.css +++ b/bm-controller/src/main/resources/static/css/styles.css @@ -221,4 +221,27 @@ button[type="submit"]:hover { width: 20px; /* 원의 크기 늘리기 */ height: 20px; margin-left: 15px; /* 원과 텍스트 간격 늘리기 */ +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.spinner { + display: inline-block; + width: 10px; + height: 10px; + border: 6px solid rgba(0, 0, 0, 0.3); + border-radius: 50%; + border-top-color: #007bff; + animation: spin 1s ease-in-out infinite; +} +.badge-wrapper { + display: flex; /* 내부 요소를 가로로 정렬 */ + align-items: center; /* 수직 중앙 정렬 */ +} + +.badge-wrapper span { + margin-right: 5px; /* 텍스트와 로딩 원 사이 간격 조정 */ } \ No newline at end of file diff --git a/bm-controller/src/main/resources/templates/fragments/agentStatus.html b/bm-controller/src/main/resources/templates/fragments/agentStatus.html index fcdc426f..9d2b5453 100644 --- a/bm-controller/src/main/resources/templates/fragments/agentStatus.html +++ b/bm-controller/src/main/resources/templates/fragments/agentStatus.html @@ -11,13 +11,13 @@
diff --git a/bm-controller/src/main/resources/templates/fragments/commonHeader.html b/bm-controller/src/main/resources/templates/fragments/commonHeader.html index 329ffbf9..b958fb61 100644 --- a/bm-controller/src/main/resources/templates/fragments/commonHeader.html +++ b/bm-controller/src/main/resources/templates/fragments/commonHeader.html @@ -3,8 +3,9 @@ TITLE - - + + + diff --git a/bm-controller/src/main/resources/templates/group/info.html b/bm-controller/src/main/resources/templates/group/info.html index 023d7708..aaa5f23a 100644 --- a/bm-controller/src/main/resources/templates/group/info.html +++ b/bm-controller/src/main/resources/templates/group/info.html @@ -1,12 +1,12 @@ -
+
-
-
+
+

Information about Group

@@ -91,39 +91,39 @@

Add Template

- +
- +
- +
- +
- +
- +
- +
diff --git a/bm-controller/src/main/resources/templates/home.html b/bm-controller/src/main/resources/templates/home.html index c10a4ff9..f0251fd9 100644 --- a/bm-controller/src/main/resources/templates/home.html +++ b/bm-controller/src/main/resources/templates/home.html @@ -1,7 +1,15 @@ - -
+ + + PerformanceTest Home + + + + + + +
diff --git a/bm-controller/src/main/resources/templates/template/info.html b/bm-controller/src/main/resources/templates/template/info.html index 63dc26d4..b45659d4 100644 --- a/bm-controller/src/main/resources/templates/template/info.html +++ b/bm-controller/src/main/resources/templates/template/info.html @@ -42,24 +42,13 @@

Test Template Body

Test Results

+
- - - - - - - - - - - - - - + + @@ -86,7 +75,7 @@

Test Results

- + @@ -110,12 +99,9 @@

Test Results

diff --git a/bm-controller/src/test/java/org/benchmarker/bmcontroller/preftest/service/PerftestServiceTest.java b/bm-controller/src/test/java/org/benchmarker/bmcontroller/preftest/service/PerftestServiceTest.java index c802ab4f..8bbc6a4d 100644 --- a/bm-controller/src/test/java/org/benchmarker/bmcontroller/preftest/service/PerftestServiceTest.java +++ b/bm-controller/src/test/java/org/benchmarker/bmcontroller/preftest/service/PerftestServiceTest.java @@ -2,10 +2,16 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.benchmarker.bmcommon.dto.CommonTestResult; import org.benchmarker.bmcommon.dto.TemplateInfo; import org.benchmarker.bmcommon.util.RandomUtils; import org.benchmarker.bmcontroller.MockServer; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.codec.ServerSentEvent; import org.springframework.web.reactive.function.client.WebClient; @@ -38,5 +44,130 @@ void testExecutePerformanceTest() { .thenCancel() .verify(); + } + + @Test + @DisplayName("테스트 저장 및 진행상황 확인") + void testExecute() { + String groupId = "1"; + Integer templateId = 1; + + // expect + HashSet expectTemplate = new HashSet<>(); + expectTemplate.add(templateId); + + PerftestService perftestService = new PerftestService(); + perftestService.saveRunning(groupId,templateId); + + // 저장 확인 + ConcurrentHashMap> runningTemplates = perftestService.getRunningTemplates(); + Boolean running = perftestService.isRunning(groupId, templateId); + + assertThat(runningTemplates.get(groupId)).isEqualTo(expectTemplate); + assertThat(running).isTrue(); + + // 중복저장 시도 + perftestService.saveRunning(groupId,templateId); + } + @Test + @DisplayName("테스트 진행저장 및 확인") + void testExecute2() { + // given + String groupId = "1"; + Integer templateId = 1; + PerftestService perftestService = new PerftestService(); + + // expect + HashSet expectTemplate = new HashSet<>(); + expectTemplate.add(templateId); + + // when + perftestService.saveRunning(groupId,templateId); + + // then-1 + ConcurrentHashMap> runningTemplates = perftestService.getRunningTemplates(); + assertThat(runningTemplates.get(groupId)).isEqualTo(expectTemplate); + + // then-2 + Boolean running = perftestService.isRunning(groupId, templateId); + assertThat(running).isTrue(); + + // 중복저장 시도 + perftestService.saveRunning(groupId,templateId); + } + + @Test + @DisplayName("테스트 중복저장 시 noOp") + void testExecute3() { + // given + String groupId = "1"; + Integer templateId = 1; + PerftestService perftestService = new PerftestService(); + perftestService.saveRunning(groupId,templateId); + + // expect + HashSet expectTemplate = new HashSet<>(); + expectTemplate.add(templateId); + + // when + perftestService.saveRunning(groupId,templateId); + + // then-1 + ConcurrentHashMap> runningTemplates = perftestService.getRunningTemplates(); + assertThat(runningTemplates.get(groupId)).isEqualTo(expectTemplate); + + // then-2 + Boolean running = perftestService.isRunning(groupId, templateId); + assertThat(running).isTrue(); + + // 중복저장 시도 + perftestService.saveRunning(groupId,templateId); + } + + @Test + @DisplayName("테스트 정상제거 시 null 반환") + void testExecute4() { + // given + String groupId = "1"; + Integer templateId = 1; + PerftestService perftestService = new PerftestService(); + perftestService.saveRunning(groupId,templateId); + + // expect + HashSet expectTemplate = new HashSet<>(); + expectTemplate.add(templateId); + + // when + perftestService.removeRunning(groupId,templateId); + + // then-1 + ConcurrentHashMap> runningTemplates = perftestService.getRunningTemplates(); + assertThat(runningTemplates.get(groupId)).isNull(); + + } + + @Test + @DisplayName("테스트 10개 save 및 1개 삭제 및 잔여 9개 확인") + void testExecute6() { + // given + String groupId = "1"; + PerftestService perftestService = new PerftestService(); + List templates = new ArrayList<>(); + + // when + for (int i=0;i<10;i++){ + perftestService.saveRunning(groupId,i); + templates.add(i); + } + perftestService.removeRunning(groupId,1); + + + // then + ConcurrentHashMap> runningTemplates = perftestService.getRunningTemplates(); + assertThat(runningTemplates.get(groupId).size()).isEqualTo(9); + assertThat(runningTemplates.get(groupId).contains(1)).isFalse(); + + + } } \ No newline at end of file diff --git a/kubernetes/SpringRole.yaml b/kubernetes/SpringRole.yaml new file mode 100644 index 00000000..66a6c7a0 --- /dev/null +++ b/kubernetes/SpringRole.yaml @@ -0,0 +1,8 @@ +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: service-discovery-client +rules: + - apiGroups: [""] # "" indicates the core API group + resources: ["services", "pods", "configmaps", "endpoints"] + verbs: ["get", "watch", "list"] \ No newline at end of file diff --git a/kubernetes/deploy/agent-deploy.yaml b/kubernetes/deploy/agent-deploy.yaml index 352051a3..e6dbe99d 100644 --- a/kubernetes/deploy/agent-deploy.yaml +++ b/kubernetes/deploy/agent-deploy.yaml @@ -1,9 +1,10 @@ apiVersion: apps/v1 -kind: Deployment +kind: StatefulSet metadata: - name: agent-deployment + name: agent-statefulset spec: - replicas: 1 + replicas: 3 + serviceName: agent-service selector: matchLabels: app: agent-service @@ -13,9 +14,13 @@ spec: app: agent-service spec: containers: - - env: + - name: bm-agent + imagePullPolicy: Always + image: ghkdqhrbals/bm-agent:latest + ports: + - containerPort: 8081 + env: - name: SERVER_PORT value: "8081" - image: ghkdqhrbals/bm-agent:latest - name: bm-agent - restartPolicy: Always \ No newline at end of file + - name: eureka_client_serviceUrl_defaultZone + value: "http://eureka-service:8761/eureka/" \ No newline at end of file diff --git a/kubernetes/deploy/controller-deploy.yaml b/kubernetes/deploy/controller-deploy.yaml index 1b54990b..af83c98c 100644 --- a/kubernetes/deploy/controller-deploy.yaml +++ b/kubernetes/deploy/controller-deploy.yaml @@ -12,32 +12,48 @@ spec: labels: app: controller-service spec: + restartPolicy: Always + serviceAccountName: my-service-account containers: - - env: - - name: SERVER_PORT - value: "8080" - - name: spring_datasource_url - value: "jdbc:postgresql://benchmark-db:5433/test" - - name: spring_datasource_hikari_password - valueFrom: - secretKeyRef: - name: db-secret - key: password - - name: spring_datasource_hikari_username - valueFrom: - secretKeyRef: - name: db-secret - key: username - - name: token_secret - valueFrom: - secretKeyRef: - name: token-secret - key: secret - - name: token_expiration_time - valueFrom: - secretKeyRef: - name: token-secret - key: exp - image: ghkdqhrbals/bm-controller:latest - name: bm-controller - restartPolicy: Always \ No newline at end of file + - image: ghkdqhrbals/bm-controller:latest + imagePullPolicy: Always + name: bm-controller + env: + - name: spring_profiles_active + value: kubernetes + - name: SERVER_PORT + value: "8080" + - name: eureka_client_serviceUrl_defaultZone + value: "http://eureka-service:8761/eureka/" + - name: spring_datasource_url + value: "jdbc:postgresql://benchmark-db:5433/test" + - name: spring_datasource_hikari_password + valueFrom: + secretKeyRef: + name: db-secret + key: password + - name: admin_id + valueFrom: + secretKeyRef: + name: admin-secret + key: username + - name: admin_password + valueFrom: + secretKeyRef: + name: admin-secret + key: password + - name: spring_datasource_hikari_username + valueFrom: + secretKeyRef: + name: db-secret + key: username + - name: token_secret + valueFrom: + secretKeyRef: + name: token-secret + key: secret + - name: token_expiration_time + valueFrom: + secretKeyRef: + name: token-secret + key: exp \ No newline at end of file diff --git a/kubernetes/deploy/eureka-deploy.yaml b/kubernetes/deploy/eureka-deploy.yaml new file mode 100644 index 00000000..d3131720 --- /dev/null +++ b/kubernetes/deploy/eureka-deploy.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: eureka-deployment +spec: + replicas: 1 + selector: + matchLabels: + app: eureka-service + template: + metadata: + labels: + app: eureka-service + spec: + containers: + - env: + - name: SERVER_PORT + value: "8761" + image: ghkdqhrbals/bm-eureka:latest + name: bm-eureka + restartPolicy: Always \ No newline at end of file diff --git a/kubernetes/ingress/ingress.yaml b/kubernetes/ingress/ingress.yaml index 4e3a92f5..131b3a10 100644 --- a/kubernetes/ingress/ingress.yaml +++ b/kubernetes/ingress/ingress.yaml @@ -1,24 +1,33 @@ -apiVersion: v1 -kind: Pod +apiVersion: networking.k8s.io/v1 +kind: Ingress metadata: - labels: - run: mynginx - name: mynginx + name: front-api-ingress + namespace: default + annotations: + nginx.ingress.kubernetes.io/rewrite-target: /$1 + cert-manager.io/issuer: "letsencrypt-prod" + cert-manager.io/acme-challenge-type: http01 spec: - containers: - - image: nginx:1.16 - name: mynginx - resources: {} - restartPolicy: Always ---- -apiVersion: v1 -kind: Service -metadata: - name: nginxsvc -spec: - ports: - - port: 80 - protocol: TCP - targetPort: 80 - selector: - run: mynginx \ No newline at end of file + ingressClassName: nginx + tls: + - hosts: + - www.high-load.org + secretName: hbgm-tls-cert + rules: + - host: "www.high-load.org" + http: + paths: + - path: /eureka(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: eureka-service + port: + number: 8761 + - path: /(.*) + pathType: ImplementationSpecific + backend: + service: + name: controller-service + port: + number: 8080 \ No newline at end of file diff --git a/kubernetes/ingress/issuer.yaml b/kubernetes/ingress/issuer.yaml new file mode 100644 index 00000000..14d30e9f --- /dev/null +++ b/kubernetes/ingress/issuer.yaml @@ -0,0 +1,14 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: letsencrypt-prod +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: ghkdqhrbals@gmail.com + privateKeySecretRef: + name: letsencrypt-prod + solvers: + - http01: + ingress: + ingressClassName: nginx \ No newline at end of file diff --git a/kubernetes/ingress/routes.yaml b/kubernetes/ingress/routes.yaml deleted file mode 100644 index 1b88d78e..00000000 --- a/kubernetes/ingress/routes.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: example -spec: - ingressClassName: nginx - rules: - - host: www.high-load.org - http: - paths: - - pathType: Prefix - backend: - service: - name: controller-service - port: - number: 80 - path: / \ No newline at end of file diff --git a/kubernetes/service/controller-service.yaml b/kubernetes/service/controller-service.yaml index c036277f..94974f10 100644 --- a/kubernetes/service/controller-service.yaml +++ b/kubernetes/service/controller-service.yaml @@ -10,7 +10,10 @@ spec: protocol: TCP port: 8080 targetPort: 8080 - nodePort: 30000 # Specify the NodePort value - type: NodePort # Expose the service with a NodePort + - name: websocket + protocol: TCP + port: 8081 + targetPort: 8080 + type: ClusterIP # Expose the service with a NodePort selector: app: controller-service \ No newline at end of file diff --git a/kubernetes/service/eureka-service.yaml b/kubernetes/service/eureka-service.yaml new file mode 100644 index 00000000..29ed05ec --- /dev/null +++ b/kubernetes/service/eureka-service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: eureka-service + name: eureka-service +spec: + ports: + - name: http + protocol: TCP + port: 8761 # This is the port that the service listens on + targetPort: 8761 # This is the port that the container listens on + type: ClusterIP + selector: + app: eureka-service diff --git a/script/img_push_multi_arch.sh b/script/img_push_multi_arch.sh index e5901b1f..d8a32182 100644 --- a/script/img_push_multi_arch.sh +++ b/script/img_push_multi_arch.sh @@ -52,8 +52,15 @@ docker buildx inspect --bootstrap # Build and push Docker images for multiple platforms echo "Build the docker image with multi platform support" -docker buildx build --platform ${BM_PLATFORM} -t ${BM_USER}/bm-controller:${BM_VERSION} -f ./bm-controller/Dockerfile --push . -docker buildx build --platform ${BM_PLATFORM} -t ${BM_USER}/bm-agent:${BM_VERSION} -f ./bm-agent/Dockerfile --push . + +cd bm-controller +pwd +docker buildx build --platform ${BM_PLATFORM} -t ${BM_USER}/bm-controller:${BM_VERSION} . --push +cd ../bm-agent +pwd +docker buildx build --platform ${BM_PLATFORM} -t ${BM_USER}/bm-agent:${BM_VERSION} . --push +cd .. +pwd # Push the docker image to docker hub images=$(docker images --format "{{.Repository}}" | grep "^${BM_USER}") diff --git a/script/img_push_multi_arch_local.sh b/script/img_push_multi_arch_local.sh new file mode 100644 index 00000000..3f9d8e52 --- /dev/null +++ b/script/img_push_multi_arch_local.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +DEFAULT_USER="ghkdqhrbals" +DEFAULT_PLATFORM="linux/amd64,linux/arm64" +DEFAULT_VERSION="latest" + +# Parse command-line arguments +while [[ $# -gt 0 ]]; do + case "$1" in + -u|--username) + BM_USER="$2" + shift + ;; + -p|--platform) + BM_PLATFORM="$2" + shift + ;; + -v|--version) + BM_VERSION="$2" + shift + ;; + *) + echo "Invalid argument: $1" + exit 1 + ;; + esac + shift +done + +BM_USER=${BM_USER:-$DEFAULT_USER} +BM_PLATFORM=${BM_PLATFORM:-$DEFAULT_PLATFORM} +BM_VERSION=${BM_VERSION:-$DEFAULT_VERSION} + +# Check if required arguments are provided +if [ -z "$BM_USER" ] || [ -z "$BM_PLATFORM" ] || [ -z "$BM_VERSION" ] ; then + echo "Usage: $0 [-u|--username ] [-p|--platform ] [-v|--version ] " + exit 1 +fi + +# This is a script to push with multiple platform images to docker hub +echo "Logging in to Docker Hub (LOCAL)" +docker login + +echo "PWD" +pwd + + +# Use Docker Buildx +echo "Using Docker Buildx" +docker buildx create --use +docker buildx inspect --bootstrap + +# Check if ./gradlew build succeeded +if ! ./gradlew clean build; then + echo "Gradle build failed. Exiting." + exit 1 +fi + +# Build and push Docker images for multiple platforms +echo "Build the docker image with multi platform support" + +# if you don't have eureka image, run below script +#cd eureka +#pwd +#docker buildx build --platform ${BM_PLATFORM} -t ${BM_USER}/bm-eureka:${BM_VERSION} . --push + +cd bm-controller +pwd +docker buildx build --platform ${BM_PLATFORM} -t ${BM_USER}/bm-controller:${BM_VERSION} . --push +pwd +cd ../bm-agent +docker buildx build --platform ${BM_PLATFORM} -t ${BM_USER}/bm-agent:${BM_VERSION} . --push +cd .. +pwd + +# Push the docker image to docker hub +images=$(docker images --format "{{.Repository}}" | grep "^${BM_USER}") +echo "Image deploy to docker hub" +for image in $images; do + echo "${image}" + docker push "${image}" +done
Test IDStarted AtFinished AtURLMethodTotal RequestsTotal ErrorsTotal SuccessTotal UsersTotal DurationMTTFB AverageTPS AverageTest Status속성
mttfb percentilevaluevalue(ms)