backend-tech-forge / benchmark

A enterprise level performance testing solution. Taking inspiration from nGrinder, this project aims to develop a Spring Boot application mirroring nGrinder's functionality as closely as feasible.
MIT License
4 stars 0 forks source link

Redis 기반의 로깅 및 상태 관리를 통한 퍼포먼스 테스트 워크플로우 개선 #81

Closed ghkdqhrbals closed 5 months ago

ghkdqhrbals commented 5 months ago

저는 ConcurrentHashMap, Set 에 주로 현재 퍼포먼스 테스트 중인 정보들을 저장하는데요. 하지만 이를 실시간으로 로그를 확인하기 매우! 어렵다는 점이 존재했습니다. 확인할려면 일일이 디버깅(손버깅) 하는 방법밖에 없더라구요ㅜㅜ. 그리고 로그를 따로 DB 에 저장하는 작업도 추가적으로 필요하죠.

그래서 실시간 퍼포먼스 테스트 중인 로그들을 확인하기 위해서는 다른 기능이 필요했습니다. 해당 기능에 필요한 컨디션은 아래와 같습니다.

1. Race condition 해결 필요

만약 클라이언트가 templateId : 1 을 기반한 퍼포먼스 테스트 요청을 동시에 전송했을 때 템플릿 자체에 mutex lock 이 걸려야 합니다.

2. 간단한 로깅로직 필요

기존 ConcurrentHashMap 에는 로깅코드를 전부 작성해야했습니다. 로그를 확인할려면(또는 실시간으로 확인할려면) 따로 DB 로 영구보관시켜야 했습니다.

3. 로그 유지보수 필요

기존 ConcurrentHashMap 을 통한 데이터 관리 시 유지보수가 쉽지 않았습니다. try finally 로 해당 퍼포먼스 테스트 종료 후 메모리 제거가 필수였습니다ㅜㅜ.

4. 트랜젝션 통합관리 필요

bm-controller <------> bm-agent 의 sse 로직에서 롤백은 간단합니다. onComplete(), onError() 등의 메서드를 통해 이벤트 스트림 로직을 쉽게 관리할 수 있기 때문이죠. 하지만 만약 다른 서비스에서 현재 트랜젝션의 상태를 알고 싶다면?( 예를 들어 현재 bm-agent 의 퍼포먼스 테스트의 상태 진행중/종료/시작전/에러종료 가 무엇인지 ) 중간에 이를 읽어내는건 조금 어렵습니다.

그래서!

Redis 리모트 인메모리 DB 를 사용하려고 합니다. 귀찮은 것을 싫어하는 사람으로써 Redis 에 정보들을 기록하거나 관리하면 어떻게 편해질까요?

  1. Easy logging : 그냥 저장하면 됩니다. 그리고 필요에 따라 나중에 DB 로 자동 마이그레이션 할 수 있도록 할 수도 있습니다!
  2. Easy race condition : 기본적으로 싱글 스레드 동작이라서 race condition 발생할 수 없습니다. 그러니까 딱 기존 ConcurrentHashMap 처럼 동시성 처리를 관리할 수 있는거죠.
  3. Easy TX management : 전체 로직의 상태관리를 리모트에서도 볼 수 있게 됩니다.
  4. Easy maintenance : bm-controller 가 bm-agent 의 작업상태를 직접 로컬 메모리에서 관리했었지만 이제는 Redis 에 저장함으로써 리모트로 작업상태들을 관리 할 수 있습니다.
ghkdqhrbals commented 5 months ago
image

GCP memcache 써볼려고 했지만 비용때문에 K8S 클러스터 내부 직접 로드해서 사용하겠습니다

ghkdqhrbals commented 5 months ago

Bugs Mapping Exception

현재 아래의 객체를 Redis 와 JPA 를 통해 저장합니다. @RedisHash(value= 로 인해 runningTest: 라는 prefix 가 붙어있는 key 로 생성이 됩니다.

@Getter
@ToString
@Builder
@RedisHash(value = "runningTest", timeToLive = 6000)
public class RunningTest {
    @Id
    private Integer templateId;
    private String groupId;
    private String testId;
}

그리고 이 엔티티를 extends CRUD 를 통해 find 하게된다면 아래의 에러가 발생합니다.

org.springframework.data.mapping.MappingException: Parameter org.springframework.data.mapping.Parameter@57ced0b5 does not have a name
...

따라서 이는 key 를 찾을 때 "runningTest:" 라는 prefix 가 자동으로 붙지 않은 상태에서 find 를 하기 때문에 발생한 에러로 보입니다.

그래서 해결책을 찾아보았는데, 단순히 @NoArgsConstructor 어노테이션을 엔티티 상단에 붙이게 되었을 때 정상적으로 동작한다고 하더라구요.

원래 Spring 6.1(Spring Boot 3.1.x) 이전에는 LocalVariableTableParameterNameDiscoverer 를 통해 어떤 객체 생성자 파라미터를 바이트코드에 붙여주었지만, 이제는 Spring 6.1(Spring Boot 3.2.0) 로 업그레이드 되면서 파라미터 이름을 앞에 안붙인다고 합니다. 즉, 생성자를 통해 인스턴스화 시킬 떄 RunningTest(Integer templateId, ...) 의 templateId 파라미터 이름이 식별되지 않는다는거죠. 그렇게 되면 뭐가 문제냐!

엔티티를 생성하고 인스턴스화 시키고 JPA 를 통해서 파라미터 이름(templateId)과 실제 DB 의 칼럼(template_id)을 mapping 할 텐데, 여기서 문제가 발생합니다! 이유는 파라미터 이름이 바이트 코드에서는 식별이 안되기 때문이죠ㅜㅜ. 그래서 Parameter doesn't have a name 이라는 에러가 뜬거에요.

Solution

즉, 이러한 MappingException 은 RedisHash 로 인해 발생한 문제가 아닌 엔티티의 생성자 파라미터 이름의 컴파일 유무에 따라 발생한 에러입니다.

두 가지 방법이 있습니다.

  1. @NoArgsContstructor 를 붙여주면 1차 적으로 mapping exception 은 패싱할 수 있다고 합니다. 이 후 JPA 에서 잘 매핑하는 것 같습니다(이 부분 공부하려면 너무 깊어져서 여기까지만 하겠습니다).
  2. Java 컴파일러에 파라미터 이름도 컴파일하도록 설정하는 방법이 있습니다.

    https://stackoverflow.com/questions/77427865/missing-parameter-name-in-mongodb-repository-causing-mappingexception