ASIS
1. A서비스 실행
2. B서비스 확인 (헬스체크)
(3). B서비스 비정상일 경우 B서비스로 못넘어가게 팝업창
4. A서비스 로딩 완료
문제점
B서비스가 비정상일때 고객이 무조건 B서비스 헬스체크 타임아웃만큼 기다려야 함
해결방안
1. Async로 B서비스 헬스체크
2. B서비스 헬스체크 캐싱
고객이 몰릴때 몰린 고객 만큼 헬스체크가 필요하기 때문에 2번이 더 효율적임
(1,2 동시에 적용할 필요는 없었음)
고려사항
A서비스 직전 프론트에 따라 B서비스의 url이 다름 (직전에 어떤 프론트였는지는 파라미터로 알수있음)
B서비스의 정상여부를 알기위해서는 2개의 서로다른 헬스체크 수행필요
구현
단순하게 @Casheable을 추가할수도 있지만 캐시가 만료되었을때 특정 고객 한명은 대기를 해야하기 때문에 서버가 주기적으로 헬스체크를 해서 캐싱함
├── controller
│ └── HealthCheckController.java # 없어도 됨, 테스트용
├── model
│ ├── HealthCheckResult.java # 헬스체크 결과 모델
│ └── ServiceHealthStatus.java # 서비스 헬스 상태 모델
├── service
│ └── HealthCheckScheduler.java # 헬스체크 스케줄러
├── cache
│ └── HealthCheckCache.java # 헬스체크 상태 캐시
스케줄러는 따로 패키지를 만들지않고 service로 넣음
HealthCheckResult
public class HealthCheckResult {
private final String checkName; // 헬스체크 이름
private boolean isHealthy; // 헬스 상태
private long checkedAt; // 체크 시간
public HealthCheckResult(String checkName, boolean isHealthy, long checkedAt) {
this.checkName = checkName;
this.isHealthy = isHealthy;
this.checkedAt = checkedAt;
}
public String getCheckName() {
return checkName;
}
public boolean isHealthy() {
return isHealthy;
}
public synchronized void update(boolean isHealthy, long checkedAt) {
this.isHealthy = isHealthy;
this.checkedAt = checkedAt;
}
public long getCheckedAt() {
return checkedAt;
}
}
헬스체크 할때마다 update 메소드를 통해 업데이트 해준다
concurrenthashmap처럼 업데이트만 synchronized로 하고 read는 동기화처리를 하지 않는다 (굳이 최신정보가 필요하진 않음)
ServiceHealthStatus
public class ServiceHealthStatus {
private final String serviceName;
private final HealthCheckResult healthCheck1;
private final HealthCheckResult healthCheck2;
public ServiceHealthStatus(String serviceName, HealthCheckResult healthCheck1, HealthCheckResult healthCheck2) {
this.serviceName = serviceName;
this.healthCheck1 = healthCheck1;
this.healthCheck2 = healthCheck2;
}
public HealthCheckResult getHealthCheck1() {
return healthCheck1;
}
public HealthCheckResult getHealthCheck2() {
return healthCheck2;
}
public boolean isHealthy() {
// 2개의 헬스체크가 healthy 일때 정상
return healthCheck1.isHealthy() && healthCheck2.isHealthy();
}
}
두개의 healthcheck에 대한 상태를 보존
두 상태 모두 isHealthy일 경우 healthy임
HealthCheckCache
public class HealthCheckCache {
private static final ConcurrentHashMap<String, ServiceHealthStatus> cache = new ConcurrentHashMap<>();
static {
cache.put("B",
new ServiceHealthStatus(
"B",
new HealthCheckResult("healthcheck1", true, 0),
new HealthCheckResult("healthcheck2", true, 0)
));
cache.put("C",
new ServiceHealthStatus(
"C",
new HealthCheckResult("healthcheck1", true, 0),
new HealthCheckResult("healthcheck2", true, 0)
));
cache.put("D",
new ServiceHealthStatus(
"D",
new HealthCheckResult("healthcheck1", true, 0),
new HealthCheckResult("healthcheck2", true, 0)
));
}
public static void updateServiceHealthStatus(String serviceName, HealthCheckResult healthCheck1, HealthCheckResult healthCheck2) {
// 안씀
ServiceHealthStatus status = new ServiceHealthStatus(serviceName, healthCheck1, healthCheck2);
cache.put(serviceName, status);
}
public static ServiceHealthStatus getServiceHealthStatus(String serviceName) {
return cache.getOrDefault(
serviceName,
new ServiceHealthStatus(
serviceName,
new HealthCheckResult("unknownservice", false, 0),
new HealthCheckResult("unknownservice", false, 0)
)
);
}
public static ConcurrentHashMap<String, ServiceHealthStatus> getAllServiceHealthStatuses() {
return cache;
}
}
다중 스레드에서는 동시성 문제가 있기 때문에 ConcurrentHashMap 사용. (근데 구현하고 보니 굳이 필요없다)
처음에는 각 상태를 true로 설정한다
cache.put 메소드를 반복호출하게 되면 매번 새로운 객체가 생성되기 때문에 메모리에 영향을 준다 (무시할만하긴하다)
따라서 처음에 모든 서비스에 대한 헬스체크 상태를 생성하고 헬스체크 상태 업데이트 시 각 객체 안의 값을 업데이트 한다
HealthCheckScheduler
@Component
public class HealthCheckScheduler {
Map<String, String> getHealthCheck1Url = Map.ofEntries(
entry("B", "http://localhost:8081"), // 실제론 url 각각 다름
entry("C", "http://localhost:8081"),
entry("D", "http://localhost:8081")
);
Map<String, String> getHealthCheck2Url = Map.ofEntries(
entry("B", "http://localhost:8081"),
entry("C", "http://localhost:8081"),
entry("D", "http://localhost:8081")
);
@Scheduled(fixedRate = 5000) // 5초마다 실행 @EnableScheduling 필요
public void performHealthChecks() {
String[] services = {"B", "C", "D"};
for (String service : services) {
performServiceHealthCheck(service);
}
}
private void performServiceHealthCheck(String serviceName) {
CompletableFuture<Boolean> healthCheck1Future = CompletableFuture.supplyAsync(() -> checkHealth1(serviceName));
CompletableFuture<Boolean> healthCheck2Future = CompletableFuture.supplyAsync(() -> checkHealth2(serviceName));
// 각 헬스체크 항목 병렬 실행
boolean isCheck1Healthy = healthCheck1Future.join();
boolean isCheck2Healthy = healthCheck2Future.join();
// 캐시에 업데이트
ServiceHealthStatus serviceHealthStatus = HealthCheckCache.getServiceHealthStatus(serviceName);
serviceHealthStatus.getHealthCheck1().update(isCheck1Healthy, System.currentTimeMillis());
serviceHealthStatus.getHealthCheck2().update(isCheck2Healthy, System.currentTimeMillis());
System.out.println(serviceName + "상태 : " + serviceHealthStatus.isHealthy()); // log.debug 어쩌고
}
private boolean checkHealth1(String serviceName) {
String url = getHealthCheck1Url.get(serviceName);
return performHttpCheck1(url);
}
private boolean checkHealth2(String serviceName) {
String url = getHealthCheck2Url.get(serviceName);
return performHttpCheck2(url);
}
private boolean performHttpCheck1(String url) {
try {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofSeconds(2))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
return response.statusCode() == 200;
} catch (Exception e) {
return false;
}
// 헬스체크 fail 됐을때 log.warn
}
private boolean performHttpCheck2(String url) {
try {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofSeconds(2))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
return response.statusCode() == 200;
} catch (Exception e) {
return false;
}
// 헬스체크 fail 됐을때 log.warn
}
}
CompletableFuture를 통해 비동기로 (각 서비스 개수 * 헬스체크필요한수) 만큼 헬스체크를 수행한다 (@Async를 써도 됨)
- 실제 구현에서는 async안씀
실제 구현도 checkHealth1와 checkHealth2가 소스가 똑같다
수정사항이 발생했을때 (예. check1과 check2가 timeout이 달라짐) 소스 전체를 고쳐야 하기 때문에 중복 소스라도 메소드를 나눴다
실제구현은 HttpRequest를 보내는 부분이 다른 class의 public method로 선언되어 있어 stub을 통해 쉽게 테스트 코드 작성이 가능하다
HealthCheckController
@RestController
public class HealthCheckController {
@GetMapping("/health-check/{serviceName}")
public String triggerHealthCheck(@PathVariable String serviceName) {
return HealthCheckCache.getServiceHealthStatus(serviceName).isHealthy() ? "정상" : "비정상";
}
}
'개발업무 > 개발' 카테고리의 다른 글
Java SimpleDateFormat YYYY vs yyyy (0) | 2025.01.12 |
---|---|
docker network (0) | 2024.07.21 |
Apache web server request body 로깅 (0) | 2024.05.30 |
Apache Server 설치 (0) | 2024.05.29 |
Java 대용량 엑셀 다운로드 (SXSSF) (0) | 2024.04.10 |