PPAK

[Hazelcast] Distributed Computing (Predicate) 본문

infra

[Hazelcast] Distributed Computing (Predicate)

PPakSang 2023. 9. 28. 16:26

이번 포스팅에서는 Hazelcast를 프로젝트에서 사용하면서 정리한 내용을 간단하게 적고, 내가 겪은 Hazelcast 관련 문제에 대한 상황과 해결(?)한 방법을 설명하고자 한다.

 

본 포스팅에서는 Hazelcast 환경을 구축하는 방법, 클러스터를 배포하는 방법을 다루지 않는다.

 

Hazelcast는 IMDG를 지원하는 분산 메모리 시스템이다. Redis 역시 클러스터링 기술을 통해 IMDG를 지원하지만 대용량 트래픽 환경에서 Hazelcast가 더 유리한 점이 많다고 한다. (아래 특징을 통해 확인)

 

다만, Redis에 비해 늦게 출시되고 사용자가 적은탓인지 레퍼런스가 매우 부족했고, 공식문서를 살펴보며 대부분 개발했다.

 

아래에 자료조사를 통해 얻게된 Hazelcast의 특징에 대해서 기술하겠지만, 내가 생각하는 가장 큰 특징은

1. 쿠버네티스 환경에 배포하기가 편리 -> 트래픽에 동적으로 대응이 가능하다.

2. Distributed Computing 수행 -> Hazelcast 클러스터 내 노드는 모두 동등하고(redis와 구분되는 차이점) 따라서 병렬 연산처리 성능이 우수하다. (내부적으로 SQL, Predicate 연산 지원)

3. Hazelcast는 Java로 작성됐고 여러 플랫폼 언어로 제어가 가능하다. -> Java로 작성된 소프트웨어들은 기본적으로 디버깅할 때 정말 유리하다고 생각한다. (물론 클러스터쪽 연산은 디버깅이 힘들다)

 

단점은

1. 쿠버네티스 환경을 구축하기 힘들다 -> 쿠버네티스 그 자체에 대한 러닝 커브를 이야기하는데, 이건 어쩔 수 없는 것이라고 생각한다.

2. 메모리를 많이 사용한다 -> 동등한 노드를 유지하기 위해서 모든 데이터의 사본을 노드 간 동기화하는 과정을 거친다고 한다.

 

Hazelcast 

우선 Hazelcast 공식 문서 를 살펴봐도 좋다.

 

Hazelcast has high scalability and data distribution in a clustered environment.

 

컴퓨터 1대의 메모리로는 전체 애플리케이션을 운영하기 부족

여러 대의 컴퓨터 메모리를 묶어서 하나의 메모리 처럼 사용하면?(→ IMDG)

  • 데이터는 항상 서버 측 RAM 메모리에 저장되어 빠른 속도를 갖는다.
  • 단일 또는 다중 서버에 장애가 발생할 경우, 자동으로 데이터를 복구하기 위해 여러대의 머신에 복수 개의 사본이 저장된다
  • 각각의 노드들은 기능적으로 똑같이 구성되고, p2p 방식으로 동작한다.
  • 데이터 모델은 객체 지향적이고 비관계형이다.
  • 서버의 CPU와 RAM 용량을 동적으로 추가 제거할 수 있다.
  • 데이터는 헤이즐캐스트에서 관계형 또는 NoSQL 데이터베이스를 사용해서 유지할 수 있다.
  • JAVA MAP API를 통해 분산된 Key-Value 스토어에 액세스할 수 있다.

 

Disadvantages

It has high memory consumption.

Implementation of a memory-sharing system is difficult.

It runs in a virtualization environment.

 

쿠버네티스를 통해 클러스터를 구축할 때의 아키텍처


Distrubuted Computing 관련 문제

Hazelcast 클러스터(서버)를 구축하면 응용 프로그램 쪽에서는 쿠버네티스와 커넥션을 만들고 통신하는 형태로 동작한다.

이때, (공식문서 내에서)클러스터를 Member라고 하고, 응용 프로그램 쪽을 Client라고 한다. 

 

서비스 요구사항 중 Redis SortedSet처럼 클러스터 내부에서 연산을 직접 수행하고 결괏값을 응용단에 넘겨주는 방식이 필요했다. 이를 Hazelcast에서는 SQL 혹은 Predicate를 통해 지원하는데 Client쪽에서 이를 작성해서 Member에 전달하면 Member는 노드 집합에서 해당 연산을 통해 얻은 결괏값을 받아서 돌려준다.

 

원인

기본적으로 데이터를 저장할 때는 Client 내부 Serializer를 사용하는데, 이때는 큰 문제가 없다. Member는 전달된 Byte 데이터를 저장해 두었다가 조회 요청이 오면 Key 기반으로 찾아주면 그만이다.

Client에서 Key와 Data 클래스에 Serializable 인터페이스를 구현하는 것을 제외하곤 별다른 처리를 할 필요가 없다.

HazelcastInstance hazelcastInstance;

void test() {
    IMap map = hazelcastInstance.getMap(MAP_NAME);
    map.set(key, value);
    Object value = map.get(key);
}

 

문제는 SQL 혹은 Predicate를 사용해서 클러스터쪽 연산을 유도하는 경우에 발생한다.

 

Predicate을 사용해 Comparator, Sorting 등 연산을 수행하는데 클래스 정보가 필요

-> 단순 Byte데이터를 저장하고 조회해서 갖다주는 것과는 다르게 Class를 역직렬화해서 Member 쪽에서 연산을 수행해야한다.

 

Client는 이미 클래스 정보를 가지고 있기 때문에 내부 연산에는 문제가 없지만, Member는 클래스 정보가 없기 때문에 ClassNotFound 예외가 발생한다.

 

@Test
void classNotFoundException() {
    IMap<Key, Value> map = hazelcastInstance.getMap(MAP_NAME);
    IntStream.rangeClosed(1, 100)
            .forEach(i -> {
                map.put(new Key(i), new Value(i));

    Predicate<Key, Value> idFilter = Predicates.equal("id", "1");
    PagingPredicate<Key, Value> predicate = Predicates.pagingPredicate(
            idNoFilter, new Comparator(), 5);

    assertThatThrownBy(() -> map.values(predicate)).isInstanceOf(ClassNotFoundException.class);
}

해결 방법

이 부분에서 삽질을 조금 했는데, 레퍼런스가 다소 부족하고 같은 예외 케이스에 대한 질문이 여러 군데서 올라오긴 했는데 대부분 직접 서버 인스턴스를 생성하는 환경에 대한 예제고, 동일한 환경(쿠버네티스 클러스터 환경)에서의 레퍼런스를 찾지를 못했다.

 

그래도 근본적인 원인은 Member 쪽에서 Class 정보를 찾을 수 없기 때문에 어떻게 런타임에 Member 쪽에 Client 정보를 주입하는 것에 주안점을 맞추고 방법을 찾았다.

 

여러 가지 방법이 보였는데

첫 번째로는 LRU Map 을 사용해서 Hazelcast에서 기본적으로 제공하는 기능을 사용해서 논리적으로도 푸는 방법도 있었는데, 이건 기존의 Map 관리 방식과 상이해져서 우선 보류를 했다. 이처럼 주어진 기능을 사용해서 논리적으로 해결 가능하면 그 방법도 좋다고 생각한다. (Custom Config는 관리 소요를 올리기 때문에,,)

 

두 번째로는 클러스터를 배포할 때 Client Jar 파일을 함께 말아 넣는 것인데, 이 방법은 쿠버네티스 배포 과정도 복잡하게 할 뿐만 아니라 Client, Member 간 결합이 너무 강해질 것 같아서 우선 배제했다.

 

세 번째로는 Hazelcast 에서 런타임에 클래스 정보를 Member 쪽으로 주입할 수 있는 방법을 사용하는 것이다.

https://docs.hazelcast.com/imdg/4.2/clusters/deploying-code-on-member 

 

클러스터를 배포하는 hazelcast.yml 파일에 아래와 같이 user-code-deployment를 허용해주고, whitelist(package prefix)를 추가한다(필수)

Hazelcast Client Instance를 생성하는 과정에서 Member에 전달할 Class 정보를 주입한다.

-> Client 실행 시 Class 정보가 Member 쪽으로 전송되고, 캐싱된다.

ClientConfig config = new ClientConfig();
ClientUserCodeDeploymentConfig clientUserCodeDeploymentConfig = new ClientUserCodeDeploymentConfig();
clientUserCodeDeploymentConfig.addClass(SomeClass.class);
clientUserCodeDeploymentConfig.setEnabled(true);
config.setUserCodeDeploymentConfig(clientUserCodeDeploymentConfig);

 

단점

클래스 정보가 캐싱되는 방식인데, 캐싱된 데이터를 해제하거나 덮어쓸 방법이 없다.

따라서, hazelcast cluster에 저장되는 클래스 정보가 변경되면(공백 하나도 허용X) 아래 중 하나를 수행해야한다.

클러스터 캐시에 클래스 정보를 덮어쓰기 하지 않는 것과 관련해 이슈가 있는데, 덮어쓰는 방법은 아직까지도 지원하지 않는 것 같다.

  1. 클러스터를 재시작
  2. 클래스 이름을 변경해서 배포해야한다

클래스 정보가 자주 변경되지 않는 경우에 효과적인 해결 방법이라고 생각하고 정말 코어 영역의 데이터라면 당장은 클래스 이름을 변경하는 방법으로 우회해서 처리가 가능하기도 하다.

 

더 좋은 해결 방법이 있다면 공유해 주시면 감사하겠습니다!

Comments