스프링 버전 5.3.18 기준으로 작성됨

 

이 글에서는 용어를 다음과 같이 줄여서 표현한다.

OpenEntityManagerInViewFilter => 필터

WebApplicationContext => wac

TransactionSynchronizationManager => tsm

EntityManagerFactory => emf

EntityManager => em

EntityManagerHolder => emh

AsyncRequestInterceptor => ari

 

Spring-orm 에서 OSIV 처리를 담당하는 주요 클래스는 org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter 이다. 이 클래스는 다음과 같이 작동한다.

 

이 클래스는 OncePerRequestFilter 타입이다. request가 들어오면 doFilterInternal 에서 다음을 수행한다.

 

step 1. 현재 필터 자신의 인스턴스에 emf 인스턴스가 존재하는지(초기화되어 있는지) 확인한다. 없으면 wac 에서 emf를 찾아서 세팅한다.

- 이 동작은 필터가 받는 첫 요청 처리에서 이루어질 것이다.

 

step 2. tsm에 emf를 키로 리소스 존재 여부를 확인해서, 존재할 경우 participate = true 를 실행한다.

- tsm에 이 리소스가 존재한다는 것은 이미 이 요청에 대해 생성된 em이 존재한다는 뜻이다. 때문에 글자 그대로 기존 요청에 '참여' 함을 표시한다.

 

step 3. 이 요청이 async request이면서 AsyncManager가 ari 를 가지고 있다면, ari 에 em 을 바인딩한다. (bindEntityManager) 그리고 step 6 으로 넘어간다.

 

step 4. 이 요청이 async request 가 아니거나, async 라도 AsyncManager가 ari 를 가지고 있지 않다면, 다음 과정을 수행한다. (일반적인 mvc요청은 이 방식으로 이루어진다.)

1. emf 로 em을 생성한다.

2. 생성한 em 을 가지는 emh 를 생성한다.

3. tsm 에 bindResource(emf, emh) 를 호출한다.

- tsm의 bindResource 는 내부 쓰레드로컬에 맵을 세팅하여 그곳에 리소스를 저장해둔다. 때문에 이미 request에 대해 생성된 em이 존재하는 경우, tsm를 통해 이를 확인할 수 있도록 한다. (step 2)

4. emf, emh 를 가지는 ari를 생성하고, AsyncManager에 ari를 등록한다.

- 이렇게 등록된 ari 가 다음번 async 요청에서 바로 ari 에 em을 바인딩할 수 있게 된다. (step 3)

 

step 5. filterChain.doFilter

 

step 6. finally 에서, participate = false 인 경우 tsm에서 em을 제거하고(unbindResource(emf)), async요청이 아닐 경우 EntityManagerFactoryUtils.closeEntityManager 로 em을 완전히 닫는다.

- '참여' 요청이라면 기존 요청이 존재하므로 여기서 em을 unbind 해서는 안 된다. 때문에 participate를 체크한다.

 

카프카는 왜 빠른가

프로그램 구현 레벨까지 가면 여러가지 이유를 댈 수 있겠지만, 카프카를 고성능으로 만드는 결정적인 이유를 말하라면 네 가지를 들 수 있겠다.

 

1. 시퀀셜 IO

일반적으로 RAM은 랜덤 엑세스를 지원하지만, 디스크는 그렇지 않다. 디스크는 원하는 데이터의 위치한 블록을 찾기 위한 시간(seek time), 블록을 메모리에 카피하는 시간 등의 오버헤드가 존재한다. 데이터가 캐시나 램에 이미 존재하면 이 과정은 생략될 수 있지만, 그렇지 않을 경우(page fault) 이러한 동작을 반복적으로 필요로 한다. 이런 점으로 인해 디스크는 일반적으로 느리다고 인식되곤 한다.

 

용량을 기준으로 볼 때 RAM은 디스크보다 훨씬 비싸다. 대용량 데이터를 핸들링하는 카프카가 그 모든 데이터를 RAM에 올리기는 현실적으로 어려운 일이다. 때문에 카프카는 디스크를 저장소로 사용하여 작동하는데, 대신 시퀀셜 디스크 엑세스를 통해 seek time의 최소화를 도모한다.

 

이는 카프카가 데이터를 로그(segments)로 저장하기 때문에 가능한 일이다. 로그는 한 번 쓰여지면 변하지 않는 성질을 지닌다(immutable). 특히 카프카의 데이터는 컨슈머가 읽어도 지워지지 않는다. 데이터는 오로지 맨 끝에 추가될 뿐이다(append-only). 이로 인해 카프카의 데이터는 디스크에 조각(fragments)으로 나뉘어 저장되지 않고, 가능한 연속적인 블록에 저장된다. 때문에 카프카는 원하는 데이터에 순차적으로 접근할 수 있다. 즉 디스크 seek time이 최소화된다.

 

이 시퀀셜 엑세스가 항상 보장되는 것은 아니다. 카프카의 데이터가 위치하는 파일 시스템을 다른 어플리케이션이 함께 사용하는 경우, 다른 어플리케이션으로 인해 디스크 단편화가 발생할 소지는 존재한다. 카프카의 데이터를 가급적 독립된 파일시스템에 유지할 것을 권장하는 이유가 여기에 있다. 하나의 파일 시스템을 카프카가 온전히 홀로 사용하도록 하여 카프카의 시퀀셜 엑세스를 보장해야 한다.

 

또, 디스크 엑세스를 더 빠르게 하기 위해 reading and writing은 OS 페이지 캐시를 최대한 활용하도록 해야 한다. 그래서 카프카가 구동되는 머신은 커널 swapiness를 최대한 작게 설정하여 OS 페이지 캐시가 swap out됨을 최대한 방지하는 것이 좋다.

 

 

2. Zero-Copy

예로부터 데이터를 디스크에서 읽어 네트워크로 전송하는 작업은 아래와 같이 4번의 카피와 4번의 컨텍스트 스위칭을 요구한다:

- 디스크에서 read buffer로 (DMA)

  -> read buffer 에서 application buffer로 (CPU)

    -> application buffer 에서 socket buffer로 (CPU)

      -> socket buffer에서 nic buffer로 (DMA)

 

Zero copy는 이 과정을 대폭 축소한다. 위 과정에서 application 영역은 디스크(read buffer)에서 읽은 데이터를 그대로 socket buffer로 보내기만 할 뿐, 다른 하는 일이 없다. 즉 데이터를 read buffer에서 socket buffer로 곧바로 보낼 수 있으면 좋을 것이다. zero copy는 그대로 탄생했다. 그리고 다음과 같이 동작한다:

- 디스크에서 read buffer로 (DMA)

  -> read buffer에서 socket buffer로 (CPU)

    -> socket buffer에서 nic로 (DMA)

카피 횟수도 줄었지만, 컨텍스트 스위칭이 처음 call에서 한 번, 리턴에서 한 번, 총 2번으로 줄었다.

 

Zero copy는 OS의 sendfile() syscall를 통해 이루어지는데, 이는 OS에서 zero copy를 지원해야 함을 의미한다. 유닉스나 요즘의 웬만한 리눅스 버전은 대부분 지원한다고 한다.

 

그런데 여기서 한 번 더 나아간다. 위 과정에서, read buffer에서 nic로 바로 데이터를 보낼 수 있다면 더욱 좋을 것이다. 이 기능은 nic라 gather operation을 지원해야 한다. 리눅스는 커널 2.4 이후로 이 기능을 지원하기 위해 socker buffer descriptor에 변경이 있었다. 이렇게 하여 최종적으로 zero copy는 다음과 같이 동작한다:

- 디스크에서 read buffer로 (DMA)

  -> read buffer에서 파일 디스크립터 정보(파일 위치, 크기 등)만 socket buffer로 (CPU)

    -> read buffer에서 nic로 (DMA)

Socket buffer는 파일 메타데이터만 가지며, 실제 데이터 카피는 발생하지 않는다. Read buffer에서 nic로의 데이터 카피는 DMA를 통해서만 이루어진다.

 

 

3. Data Batch & Compression

카프카는 데이터 batching과 compression을 지원하여 네트워킹 횟수를 대폭 줄여 성능을 향상한다. 추가로 로그 데이터 디스크 사용량도 크게 감소한다. 이 기능은 카프카 설정에 따라 작동 여부와 그 정도가 달라지지만, 대부분의 경우 사용할 것을 권장한다.

 

 

4. Topic Partitioning

카프카의 데이터는 로그 세그먼트로 저장되는데, 이 데이터는 순차적이기 때문에 다수의 쓰레드가 동시에 접근하는 일은 의미가 없다. 데이터는 최근의 것만 한 번 읽으면 되기 때문에, 아무리 많은 쓰레드가 있어도 경합만 발생할 뿐, 이득이 없다고 할 수 있다. 즉 병렬 처리가 불가능하다.

 

카프카는 로그 파티셔닝을 통해 이를 개선했다. 같은 토픽이라도 데이터를 나누어(sharding) 저장하고, 각 파티션에는 독립된 쓰레드가 붙어 작업할 수 있도록 함으로써 결과적으로 하나의 토픽을 여러 쓰레드가 나누어 처리할 수 있도록 병렬성을 구현했다.

 

 

@개인적으로 1번과 4번이 카프카의 특이점이라고 생각된다. 나머지는 다른 메시징 어플리케이션에서도 얼마든지 구현할 수 있을 것 같은 느낌이다. 카프카가 결정적으로 차별화를 두는 점은 4번이 결정적이고, 다음이 1번이라고 할 수 있지 않을까. 이 두 특성으로 인해 카프카가 메시징 어플리케이션을 넘어 데이터 스트리밍 플랫폼으로 군림할 수 있게 되었다고 생각된다.

'Kafka' 카테고리의 다른 글

Kafka 설정 listeners vs. advertised.listeners  (1) 2020.09.08

TDD에 대한 기본적인 내용은 인터넷 검색을 통해 충분히 알 수 있다. 그렇게해서 나오는 내용에는 언뜻 특별한 점이 전혀 없는 것 같이 보인다. 처음에는 그저, 구현 후 테스트를 미흡하게 하지 않도록 테스트를 먼저 작성하여 프로그램 결함을 보다 줄이기 위한 방법 쯤으로 생각되었고, 그냥 이런게 있구나 정도로, 교양 지식 정도로 알고 넘어갔었다.

 

그러다 읽은 켄트 백의 TDD는 어쩐지 개발자를 위한 자기계발서를 읽는 듯한 느낌이었다. 개발 과정에서 멘탈을 관리하는 방법 등 직접적인 개발 행위를 벗어난 조언이 섞여 있는 점도 그렇고, 이건 마치 무술 수련서와도 같이, 지식적인 측면보다는 개발 수련법을 제시하는 것 같다. 책에서 어떤 정답을 제시하고 그대로만 하면 모든 개발에 적용할 수 있는 그런 과학적인 종류의 지식을 소개하지 않는다. 책의 내용은 "앞으로 이렇게 이렇게 연습하세요, 그럼 이러이러한 장점이 있습니다" 정도에서 끝이고, 정말 여기서 무언가를 얻으려면 꾸준히 실천해야만 한다.

 

지금까지 TDD를 적용해보며 느낀 점들을 나열해보면,

 

1. 구현 먼저? 테스트 먼저? 테스트도 구현인가?

- 정석적인 OO설계와 배치된다고 느껴지는 부분이 있어, 어디에 중심을 두어야할지 판단이 서지 않는 기분이 간혹 느껴졌다. 내가 아는 OO의 정석? 이라고 한다면, 도메인에서 필요한 주요 오퍼레이션(메시지)을 도출하고 그 메시지를 수신하기 적당한 객체를 선정한다(책임의 할당). 그리고 책임을 수행하기 위해 필요한 정보를 그 객체가 가지는데, 책임의 일부가 다른 객체에게 있다고 판단되는 경우 책임을 나눌 다른 객체를 정의하여 책임을 위임한다. 위임은 역시 메시지를 전달하는 방식으로 이루어지며, 이렇게 객체 간 협력이 완성된다... 이런 식인데, TDD는 일단 다짜고짜 테스트를 작성한다. 심지어 객체 클래스를 정의하기도 전에 테스트를 작성해서 컴파일 에러를 일으키라 한다. OO에서는 바로 구현에 뛰어드는 행위는 금물이라 했다. 왜냐하면 모든걸 구현 중심으로 생각하게 되기 쉽고, 이는 메시지와 책임, 협력을 우선시하는 객체 지향 모델링에 방해가 될 수 있어서이다. 그런데 TDD는 간단히 눈에 보이는 구현을 일단 중시하는 것 같아 내가 알고 있는 OO 설계와는 배치되는게 아닌가 생각했다.

 

여기서 혼란을 느껴 구글링해보니 스택오버플로에서 누군가 정확히 나와 같은 혼란을 느껴 질문했다. 인터페이스를 먼저 작성해야 하느냐, 테스트를 먼저 작성해야 하느냐고. 그리고 여기의 답변이 내 혼란을 잠재워줬다. 그 내용은, 테스트를 작성하는 일과 인터페이스를 작성하는 일은 같은 일이지, 나눌 수 있는 일이 아니라는 것이다. 답변에서 든 단순한 예로, 계산기 어플리케이션을 작성할 때 먼저 떠오르는 기능은 어떤 것인가? 그것이 덧셈이라고 하면, 간단히 다음과 같은 테스트를 먼저 작성할 수 있을 것이다.

Calcurator calc = new Calcurator();
int sum = calc.add(3, 4);
assertTrue(sum == 7);

add의 명세와 의도는 명확하다. 두 숫자값을 취하여 그 합을 반환한다. 이 add가 바로 퍼플릭 인터페이스의 오퍼레이션, 즉 메시지가 된다. 이렇게 출발한다. 사용자가 계산기에 기대하는 다른 오퍼레이션들을 테스트로 하나 하나 추가해가며 하나의 인터페이스가 완성되고, 각 테스트들은 어플리케이션에 기대하는 하나의 작고 구체적인 실행 가능한 기능이 된다. 퍼블릭 인터페이스는 사용자가 기대하는 동작(책임)을 수행하는 것이어야 하며, 테스트는 인터페이스의 상호작용을 정의하는 첫 번째 스텝이다. 이렇게 하면 인터페이스에 불필요한 기능이 추가되거나 자신의 책임이 아닌 코드가 들어가는 일이 잘 발생하지 않는다. 구현에 앞서 메시지를 생각한 덕분이다.

 

 

2. 테스트를 아주 작게 나누는 일은 OO에서 책임을 분리하는 일과 같은 맥락이다

- 한 번에 한 기능에 대한 짧은 테스트를 작성하고 테스트 간 의존성을 두지 않게 함으로써(테스트 격리), 테스트 대상 기능이 자연스럽게 단일 책임 원칙을 준수하게 만드는 효과가 있음을 느꼈다.

 

 

3. 완성 형태를 먼저 그리게 된다

- 테스트 대상 기능이 최종적으로 어떤 형태로 완성될 것이고, 어떤 형태로 사용될 것인지를 먼저 테스트 작성 시 그려놓게 된다. 이는 내가 늘 생각해온 올바른 개발 방식에 거의 정확히 부합하는 느낌이다. 난 늘 코드는 그 코드의 최종 사용자의 입장에 서서, 사용자가 이 완성본을 볼 때, 그리고 실제 사용할 때 어떤 느낌일지를 유념하며 작성해야 한다고 생각한다. 그 과정의 어려움을 먼저 생각해서는 안 된다. 그런 것은 일단 저 멀리 던져두고, 내가 개발하는 프로그램이 궁극적으로 어떤 형태를 띄고, 그 기능의 아웃풋이 어떤 모양이기를 원하는지 생각해두고 개발하는 것이 바람직하다고 생각한다. 그리고 그 품질 기준이 높아야한다. 개발자가 개발 과정의 힘듦을 먼저 생각하면 그 고통을 두려워하여 그건 하지 못하는 일로 단정짓게 됨을 피하기 위해서이다. 그래서는 안 된다.

 

 

4. 내부 값에 의존하는 테스트는 언제나 틀린가?

- 어떤 객체의 인스턴스가 정확히 작동하는지 여부를 인스턴스의 내부 상태로 확인하는 것은 일반적으로 bad practice라고 배웠다. 테스트는 객체의 행동을 확인해야 하지, 그 내부 상태에 의존해서는 안 된다는 것이다. 테스트를 위해 런타임에 인스턴스의 내부 상태를 직접 확인해야만 한다면 이는 내부 구현에 의존하는 일이고, 이런 경우 설계가 옳지 않았을 확률이 높다고 한다. 그런데 여기에 한 가지 예외 케이스를 두고 싶다. 테스트하려는 기능이 인스턴스를 생성하는 것이라면? 생성된 인스턴스가 의도한 값을 정확히 가지고 있는지를 확인해야만 한다면 어떨까? 한번은 커스텀 xsd를 작성하고 스프링 커스텀 빈 데피니션 파서를 작성하여 사용자가 xml에 정의한대로 원하는 빈을 생성해주는 기능을 개발하고 있었다. 생성된 빈은 설정 xml에 정의된 값을 내부에 가져야만 하며, 이 변수는 역시 private field였다. 이것 자체가 이미 하나의 기능이기 때문에, 테스트 대상이 되어야 한다고 생각되었다. 그런데 생성된 인스턴스의 값 확인은 리플렉션을 사용하여 private field에 접근하는 방법 외에는 떠오르지 않았다. 이처럼 테스트하려는 기능 자체가 private field 값을 세팅하는 것이라면, 이 테스트는 그 값을 확인해야 옳지 않을까? 이 private field를 사용하는 메서드를 호출하여 그 결과를 통해 필드값이 정확히 설정되었음을 간접적으로 확인하는 방법도 있기는 했다. 허나 이 방법은 그 과정에 다른 기능 테스트를 포함하는 것이고, 결국 테스트 격리를 깨는 행위로 보여 결국 리플렉션을 사용했다. 결과적으로 의도한 테스트는 잘 되었지만, 정말 이 방법 뿐이었을지, 애초에 인터페이스 설계가 잘못된 것은 아니었을지 두고 두고 나를 괴롭혔다.

 

 

5. TDD는 설계를 포함한다

- 하면 할수록, TDD는 설계에 관련 것이라는 느낌에 확신이 들었다. 사실 Test Driven Developement 라는 이름이 말하듯, TDD는 구현에 국한되는 것이 아닌, 개발이라는 행위 전체를 아우르는 방법론이다. 하지만 TDD는 언뜻 보면 '기능을 작은 단위로 나누어 테스트를 먼저 작성하여 빠르게 피드백을 받아 어플리케이션을 구축하는', 즉 빠르게 테스트를 수행하여 안정적인 기능을 구축하기 위한 방법쯤으로 보인다. 처음에는 나도, 개발 다 해놓고 테스트를 작성하려면 번거롭고 고통스러우니, 그냥 그 때 그 때 부지런히 테스트해서 개발하자는 뜻으로 해석했다. 하지만 직접 실천해보니 테스트를 작성하며 자연히 오퍼레이션이 정의되고, 퍼블릭 인터페이스가 구성되는 과정을 보았고, TDD는 어플리케이션을 설계하는 또다른 경로(path)라고 느꼈다. OO를 처음 제대로 공부하기 시작하면서는 실제 구현에서 첫 발을 어떻게 떼야 할지 더 어려워지는 느낌을 받았었는데, TDD가 그것을 아주 가볍게 만들어주었다. 아직 한참 부족하지만 적어도 TDD에서 이것 하나는 확실히 취한 수 있는 이점이라고 생각되었다.

 

 

 

두 설정 모두 모두 카프카 브로커 서버가 바인딩하는 주소를 나타낸다.

 

listeners

- 카프카 브로커가 내부적으로 바인딩하는 주소.

 

advertised.listeners

- 카프카 프로듀서, 컨슈머에게 노출할 주소. 설정하지 않을 경우 디폴트로 listners 설정이 적용된다.

 

카프카는 분산 시스템이다. 클라이언트(프로듀서, 컨슈머)는 분산된 파티션에 접근하여 write/read 를 수행한다. 카프카가 클러스터로 묶인 경우, 카프카 리더만이 write/read 요청을 받는데, 클라이언트는 클러스터의 브로커 중 누가 리더인지 알아야 하기 때문에 write/read 요청에 앞서 해당 파티션의 리더가 누구인지 알 수 있는 메타데이터를 요청한다. 이 메타데이터 요청은 클러스터의 브로커 중 아무나 받아서 응답할 수 있다. 메타데이터 요청을 받은 브로커는 요청된 파티션의 리더가 어떤 브로커인지와 그 브로커에게 접근할 수 있는 엔드포인트를 반환한다. 그러면 클라이언트는 이 반환된 메타데이터를 가지고 실제 요청을 수행한다.

 

머신 OS에 직접 설치되어 구동되는 카프카의 경우 이 엔드포인트는 단순히 호스트 주소가 되는데, 그렇지 않고 가상 환경이나 클라우드와 같이 복잡한 네트워크 환경에서 구동되는 경우 상황이 달라진다.

 

이 경우, 카프카는 내부 브로커들 간의 통신을 위한 엔드포인트와 외부 클라이언트를 위한 엔드포인트를 구분할 필요가 생긴다. 네트워크 환경에 따라 내부의 서버 간 접근에 사용되는 호스트 주소와 외부에서 접근하기 위한 주소가 달리지기 때문이다. 그리고 성능 및 기타 비용을 고려할 때, 내부에서는 plaintext로, 외부에서는 SSL로 통신하도록 하는 등의 구분이 필요할 수 있다.

 

lisnters 와 advertised.listeners 는 그 구분을 위해 사용되는 설정이다.

 

카프카 공식 사이트에서 가져온 아래 설정 예시는 다양하게 구분된 엔드포인트를 보여준다:

listener.security.protocol.map=CLIENT:SASL_PLAINTEXT,REPLICATION:PLAINTEXT,INTERNAL_PLAINTEXT:PLAINTEXT,INTERNAL_SASL:SASL_PLAINTEXT
advertised.listeners=CLIENT://cluster1.foo.com:9092,REPLICATION://broker1.replication.local:9093,INTERNAL_PLAINTEXT://broker1.local:9094,INTERNAL_SASL://broker1.local:9095
listeners=CLIENT://192.1.1.8:9092,REPLICATION://10.1.1.5:9093,INTERNAL_PLAINTEXT://10.1.1.5:9094,INTERNAL_SASL://10.1.1.5:9095

 

 

모든 설정은 key-value 로 매핑된다. CLIENT 라는 key 의 내부 바인딩은 192.1.1.8:9092 로, 외부에서 접근하기 위한 엔드포인트는 cluster1.foo.com:9092 가 되며, 사용되는 프로토콜은 SASL_PLAINTEXT 를 사용한다.

 

내부 사용을 위한 엔드포인트로 보이는 INTERNAL_PLAINTEXT 는 내부 바인딩으로 10.1.1.5:9094 를, 외부에서의 접근은 broker1.local:9094 를 사용하며 PLAINTEXT 로 통신한다.

 

출처: 

cwiki.apache.org/confluence/display/KAFKA/KIP-103%3A+Separation+of+Internal+and+External+traffic

 

'Kafka' 카테고리의 다른 글

카프카는 왜 빠른가  (0) 2021.11.26

spring-data-redis의 LettuceConnectionFactory에 setEnableTransactionSupport(boolean) 메서드가 있다.

 

기본값은 false인데, true로 설정하면 트랜잭션을 사용할 수 있게 된다. 그런데 이는 트랜잭션을 지원한다는 의미이지, JDBC처럼 트랜잭션 처리를 알아서 해준다는 의미가 아니다. 

 

레디스에서 트랜잭션을 처리는 MULTI-EXEC 를 사용하여 이루어지는데, 당연하게도 한 트랜잭션은 하나의 커넥션이 MULTI 에서 EXEC 까지 모두 수행해야 한다. setEnableTransactionSupport 는 그것을 돕는다.

 

이 값이 false 이면, MULTI-SET-EXEC 를 호출하면 다음과 같이 동작한다:

getConnection: 커넥션A 획득

MULTI

close

getConnection: 커넥션 B 획득

SET

close

getConnection: 커넥션 C 획득

EXEC

close

 

MULTI 를 포함하여 모든 오퍼레이션이 다른 커넥션에서 호출된다. 마지막 EXEC 에서는 'EXEC without MULTI' 에러가 떨어진다. MULTI 없이 EXEC를 호출했다는 의미이다. 다른 커넥션에서 호출했었으니 하나의 트랜잭션으로 이어지지 않는다.

 

setEnableTransactionSupport 이 true 이면 위 오퍼레이션은 다음과 같이 동작한다:

getConnection: 커넥션A 획득

MULTI

getConnection: 커넥션A 획득

SET

getConnection: 커넥션A 획득

EXEC

 

이와 같이 MULTI에서부터 EXEC 까지의 오퍼레이션을 한 커넥션에 묶어준다. 위 동작의 배경은 LettuceConnection을 가지고 있는 RedisConnectionHolder를 생성하여 스프링의 TransactionSynchronizationManager에 리소스로 바인딩하는 방식을 취한다. JDBC 트랜잭션 관리와 마찬가지로 쓰레드에 자신이 사용할 커넥션을 묶어두는 것이다.

'Redis' 카테고리의 다른 글

Spring-Lettuce 커넥션 풀링 시 shareNativeConnection  (3) 2020.09.02
Lettuce 센티넬 failover 테스트  (0) 2020.09.01

spring-data-redis, Lettuce 사용 시, non-blocking, non-transactional 오퍼레이션은 커넥션 풀을 설정해도 하나의 포트로만 통신한다.

 

이 동작은 LettuceConnectionFactory 의 shareNativeConnection 값에 따라 달라진다. 이 값은 네이티브 커넥션을 공유해서 사용할지 여부를 가리킨다. 기본값이 true 이기에 non-blocking, non-transactional 오퍼레이션은 하나의 커넥션으로만 통신한다. MULTI 를 통해 트랜잭션을 시작하면 새로운 dedicated 커넥션이 할당되어 이것으로 통신한다.

 

언뜻 보기에 이 값을 false로 두고 커넥션 풀을 사용하여 오퍼레이션에 사용되는 커넥션을 분산하면 레디스 처리량이 올라가 성능이 좋아질 것 같지만, 레디스 자체가 싱글 쓰레드로 작동하기 때문에 실제로는 그렇지 않다고 한다. 스프링 부트 github에 부트 레벨에서 이 값을 false 로 설정할 수 있도록 해달라는 issue가 있었는데, 이와 같은 이유로 거부되었다:

 

github.com/spring-projects/spring-boot/issues/14196

 

Allow the ability to set shareNativeConnection to false for LettuceConnectionFactory · Issue #14196 · spring-projects/spring-b

By default, the LettuceConnection will use the sharedConnection for almost all non-tx or non-blocking operations. I have found the exclusive use of the shared connection to not allow very high thro...

github.com

 

이런 이유로, 스탠드얼론 레디스를 사용할 때 트랜잭션을 사용하지 않는다면 커넥션 풀을 사용할 이유가 없다.

 

 

출처:

docs.spring.io/spring-data/data-redis/docs/current/reference/html/#reference

'Redis' 카테고리의 다른 글

Spring-Lettuce setEnableTransactionSupport  (0) 2020.09.02
Lettuce 센티넬 failover 테스트  (0) 2020.09.01

spring-data-redis 1.7.111RELEASE

lettuce-3.5.0.Final

Redis 5.0

 

1 master - port 6379

3 replicas - port 6380, 6381, 6382

3 sentinels - port 26379, 26380, 26381

 

빈 설정

 

첫 연결 수립

로그 상단을 보면 아래와 같이, 3 개의 센티넬 주소 중 하나로 접근한다고 하는데, 여러 번 시도했지만 항상 첫 번째 주소인 26379 포트로만 접근했다. 순서대로 접근 시도하면서 센티넬이 응답하지 않는 경우 다음 센티넬로 넘어간다. 정상적으로 응답하는 센티넬을 찾으면 나머지 센티넬로는 요청을 보내지 않았다.

Trying to get a Sentinel connection for one of: [RedisURI [host='127.0.0.1', port=26379], RedisURI [host='127.0.0.1', port=26380], RedisURI [host='127.0.0.1', port=26381]]

 

로그 하단에서는 SENTINEL 커맨드를 통해 마스터 호스트를 얻는다:

Decoded Command [type=SENTINEL, output=ValueListOutput [output=[127.0.0.1, 6379], error='null']] ...

 

마스터로의 연결 수립

 

마스터 fails

마스터와 연결이 끊어져도 failover가 수행되기 전까지는 계속 연결 시도한다.

 

마스터와 연결이 끊어지자, 다시 센티넬로 연결하여 새로운 마스터 정보를 조회하는데, failover가 완료되기 전까지는 기존 마스터 정보를 반환한다.

 

Failover 완료 후

센티넬이 failover를 완료하고 새로운 마스터 주소(포트 6381)를 반환한다.

 

새 마스터로의 연결 정상 수립.

 

 

NOTE:

- 센티넬이 죽으면 다음 센티넬로 연결한다.

- quorum 값을 2로 하여 3 개의 센티넬이 구동했다가 2 개의 센티넬이 죽으면 failover가 수행되지 못한다. 1 개의 센티넬만 죽으면 failover는 수행되었다. 센티넬의 상태와 상관없이 vote 수를 채우면 failover에는 문제가 없는 것 같다.

1. 디폴트

정적 자원 핸들링에는 디폴트로 ResourcehttpRequestHandler가 사용된다.

디폴트로, 아래 경로의 자원을 '/' URI 패턴으로 제공한다.

classpath:/static

classpath:/public

classpath:/resources

classpath:/META-INF/resources

 

예로, classpath:/static/myface.html 이 있을 때,

http://example.com/myface.html 경로로 접근할 수 있다.

 

 

2. 커스터마이징

주의: 설정 커스터마이징 시 디폴트 설정은 사라진다. 따로 설정하지 않으면 더이상 '/' 패턴으로 정적 자원 접근이 불가능하다.

 

프로퍼티 방식

spring.mvc.static-path-pattern=/mystatic/**

spring.resources.static-locations=classpath:/mystatic/,file:/shared/mystatic/

 

URI 패턴, 자원 위치 모두 콤마로 구분하여 2개 이상 설정할 수 있다.

 

위 설정은 아래와 같이 매핑된다

http://example.com/mystatic/myface.html => classpath:/mystatic/myface.html (없으면 파일시스템 /shared/mystatic/myface.html)

 

 

위와 같이, 클래스패스가 아닌 일반 파일시스템 접근에는 'classpath' 대신 'file:' 을 사용한다. (윈도우는 'file://')

 

자바 방식

위 설정을 자바 방식으로 하면 아래와 같다.

@Configuration 
public class WebConfig implements WebMvcConfigurer {

	@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { 
    		registry.addResourceHandler("/mystatic/**")
        		.addResourceLocations("classpath:/mystatic/", "file:/shared/mystatic/");
	}    
}

 

WAR 파일의 경우, webapp/mystatic/ 에 자원이 있다면, 아래와 같이 'classpath:'를 빼고 설정한다.

spring.mvc.static-path-pattern=/mystatic/**

spring.resources.static-locations=/mystatic/

 

 

Windows 10

IntelliJ IC2019.3 기준

 

1. 프로젝트 루트\.idea\

1.1. workspace.xml

- 최근 변경된 파일, 열려있는 파일, 사용자가 지정한 각종 윈도우의 위치, 사이즈 등 사용자의 세션 정보

- 설정파일에서 쓰이는 프로젝트ID (예: 1g1MbsIX7nO62asWlrlyedlPXqr)

- RunManager (un configuration) 정보

 

1.2. gradle.xml

- 그레이들 home, 버전, 그레이들 구동 jvm 버전 및 기타 그레이들 정보

 

1.3. jarRepositories.xml

- maven central 등 원격 jar 저장소 정보

 

1.4. misc.xml

- 프로젝트의 JDK 이름, 타입, 언어 레벨, 빌드 output path 등

 

 

2. user path\.Idea{인텔리J 버전}\ (예: C:\Users\user\.IdeaIC2019.3\config\workspace)

2.1. config\workspace\{1.1. workspace.xml 의 프로젝트ID }.xml (예: 1g1MbsIX7nO62asWlrlyedlPXqr.xml)

- 최근 변경 파일, 레이아웃 등 1.1. workspace.xml 과 비슷한 정보가 있는 것 같으나, 정확한 역할은 모르겠음

- 프로젝트 뷰, 모듈 UI 구조 설정 정보

 

2.2. system\workspace\{프로젝트명.{해시값}.xml (예: remote-file-transfer.938a031e.xml)

- 프로젝트(모듈) 빌드 클래스패스, 그레이들 설정 등

 

 

'Env' 카테고리의 다른 글

IntelliJ 스프링부트 app 실행  (0) 2020.08.14

1. Project Structure -> Project -> Project SDK 설정 (원하는 런타임 Java)

2. Project Structure -> Modules -> main/resources 영역 Resoueces 마킹

3. Settings -> Build Tools -> Gradle -> Build and run 에서 'Build and run using' - IntelliJ IDEA 로 변경

 

3번 설정은 그레이들 프로젝트의 경우 디폴트로 Gradle 로 맞추어져 있다. 때문에 스프링부트 어플리케이션의 main 메서드 실행 시 그레이들이 사용되는데, 이를 IntelliJ IDEA 로 바꾸면 인텔리J의 자체 최적화를 거쳐 더 빠른 실행 속도를 누릴 수 있다.

 

허나 이 설정의 단점은, 로컬 테스트 환경이 인텔리J에 의존하게 된다는 점이다. Gradle은 로컬 뿐만 아니라 다른 런타임에서도 동일한 실행 결과를 보장하겠지만. 인텔리J는 아닐 수 있다.

'Env' 카테고리의 다른 글

IntelliJ 프로젝트 설정 저장파일 (Windows)  (0) 2020.08.14

+ Recent posts