- AWS, 온프레미스 방식을 이용해도 되지만. 간단하게 도커, 도커 컴포즈를 이용하여 설치하도록 할 예정이다.
- 주키퍼3, 카프카3 형태의 클러스터 설치 방법을 공유할 예정이다.
주키퍼 = 과반수 방식을 유지해야하므로 홀수 구성 필요(Elasticsearch 클러스터 구성과 유사)
카프카 = 과반수 방식이 아니라 반드시 3대를 만들 필요는 없지만, 리플리케이션 팩터 수를 3을 충족시키기 위해 최소 3대의 클러스터 구조로 구성한다
1. 도커 설치 방법
# docker 리포지토리에 접근하기 위한 키 생성 설정
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
# 패키지 매니저가 docker 설치 시, 설치 위치를 알기 위한 repository 추가
sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"# 위에서 추가한 repository를 위해서 업데이트!
sudo apt update
# docker 설치
sudo apt install docker-ce
sudo systemctl status docker
- 메시지 큐(Message Queue)는 프로세스 또는 프로그램 간에 데이터를 교환할 때 사용하는 통신 방법 중에 하나로, 메시지 지향 미들웨어(Message Oriented Middleware:MOM)를 구현한 시스템을 의미한다. 메시지 지향 미들웨어란 비동기 메시지를 사용하는 응용 프로그램들 사이에서 데이터를 송수신하는 것을 의미한다. 여기서 메시지란 요청, 응답, 오류 메시지 혹은 단순한 정보 등의 작은 데이터가 될 수 있다.
1. 메시지 전송 시 생산자(Producer)로 취급되는 컴포넌트가 메시지를 메시지 큐에 추가한다.
2. 해당 메시지는 소비자(Consumer)로 취급되는 또 다른 컴포넌트가 메시지를 검색하고 이를 사용해 어떤 작업을 수행할 때까지 메시지 큐에 저장된다.
3. 각 메시지는 하나의 소비자에 의해 한 번만 처리될 수 있는데, 이러한 이유로 메시지 큐를 이용하는 방식을 일대일 통신이라고 부른다.
Message Queue는 언제쓰나요?
메시지 큐는 소비자(Consumer)가 실제로 메시지를 어느 시점에 가져가서 처리하는 지는 보장하지 않는다.
언젠가는 큐에 넣어둔 메시지가 소비되어 처리될 것이라고 믿는 것이다.
이러한 비동기적 특성 때문에 메시지 큐는 실패하면 치명적인 핵심 작업보다는 어플리케이션의 부가적인 기능에 사용하는 것이 적합하다.
1. 비동기 작업이 필요한 서비스
이메일전송 나는 이미 이메일을 전송했고, 실제 받는 사람이 읽을 때까지 시간은 걸리겠지만, 해당 작업이 완료처리 될 것을 우리는 알고있다. 바로 실시간으로 처리되지 않아도 서비스에 크게 문제 없는 이런 작업에 MQ를 사용할 수 있다. 즉, MQ는 어느 정도의 응답 지연이 허용되며, 어플리케이션의 핵심 기능은 아닌 경우에 사용하는 것이 적합하다.
2. 시스템 간 통신
서버 간 데이터를 주고 받거나 작업을 요청할 때, 시스템 장애를 생각해야한다.
MQ를 사용할 경우 이러한 처리를 간편하게 할 수 있다.
\
- P는 C에 직접 요청하는것이 아닌, MQ에 메세지를 전달한다.
- C는 MQ를 구독하고 MQ로부터 데이터를 수신하여 처리한다.
- C가 데이터를 수신할 수 없는 상황이더라도, 데이터는 보장된다 (C가 복구된 후 다시 수신 가능)
- C의 데이터처리 TPS에 맞는 속도로 데이터를 처리할 수 있다. (구현이 쉬워짐)
3. 서버 부하가 많은 작업 (2번과 같은 구조)
- 이미지 처리, 비디오 인코딩, 빅데이터 등 대용량 데이터 처리와 같은 작업은 메모리, CPU를 많이 사용한다.
이러한 작업은 동시에 처리할 수 있는 양이 한정적이므로 MQ를 사용하여 서버가 처리할 수 있는 양을 가져와 처리할 수 있다.
4. 부하분산
- 부하를 분산 하여 처리할 수 있다. 여러 C(Customer)를 배치해, 원하는 메세지(데이터)의 처리가 가능하다.
- 해당 구조는 수평구조이기 때문에 수평적 확장에 유리하다 (C를 늘리는 구조)
message queue 장점
비동기(Asynchronous)
메시지 큐는 생산된 메시지의 저장, 전송에 대해 동기화 처리를 진행하지 않고, 큐에 넣어 두기 때문에 나중에 처리할 수 있다.
여기서, 기존 동기화 방식은 많은 메시지(데이터)가 전송될 경우 병목이 생길 수 있고, 뒤에 들어오는 요청에 대한 응답이 지연될 것이다.
낮은 결합도(Decoupling)
생산자 서비스와 소비자 서비스가 독립적으로 행동하게 됨으로써 서비스 간 결합도가 낮아진다.
확장성(Scalable)
생산자 서비스 혹은 소비자 서비스를 원하는 대로 확장할 수 있기 때문에 확장성이 좋다.
탄력성(Resilience)
소비자 서비스가 다운되더라도 어플리케이션이 중단되는 것은 아니다. 메시지는 메시지 큐에 남아 있다. 소비자 서비스가 다시 시작될 때마다 추가 설정이나 작업을 수행하지 않고도 메시지 처리를 시작할 수 있다.
보장성(Guarantees)
메시지 큐는 큐에 보관되는 모든 메시지가 결국 소비자 서비스에게 전달된다는 일반적인 보장을 제공한다.
A Cursor offers the same results as a List, except it fetches data lazily using an Iterator.
(Cursor는 Iterator를 사용하여 Lazy하게 데이터를 가져오고 이는 List와 동일한 결과를 제공한다)
간단 정리 : 대량 데이터(대량 ROW)를 가져올때 사용하는 방법
- 약 1천만건의 데이터를 List에 담아서 어떠한 처리를 한다고 가정했을때 기존 방법은 OOM 발생 > CURSOR사용하면 해결 가능
1. cursor를 사용할때 동작방식의 차이
- 기존 방식의 동작 방법
1. DAO Mapper 인터페이스 선언을 바탕으로, Mybatis가 동적으로 생성한 코드로 DB작업을 준비
2. DAO Mapper를 통해 DB작업이 진행되면 알맞은 드라이버 (ex: JDBC)나 풀을 통하여 작업을 수행
3. 2.의 작업이 완료될때까지 코드 블로킹 (통상 service 코드 블로킹)
4. DB작업이 끝나면 spring 으로 돌아옴 (서비스로)
- Cursor를 이용했을 때 동작 방법
1. DAO Mapper 인터페이스 선언을 바탕으로, Mybatis가 동적으로 생성한 코드로 DB작업을 준비
2. DAO Mapper를 통해 DB작업이 진행되면 알맞은 드라이버 (ex: JDBC)나 풀을 통하여 작업을 수행
3. 트랜잭션을 시작, cursor의 경우 2의 작업이 cursor로 iteration을 반복할 수 있는 상태가 되면 모든 데이터가 받아지지 않더라도 DAO interface를 통해 cursor를 반환한다. (반환한 cursor를 서비스에서 처리 후 > 다시 반복) 4. DB 커넥션이 유지되는 동안 필요한 작업을 수행. cursor가 데이터셋의 끝에 도달할때까지 반복 가능
5. 트랜잭션 종료 (커넥션 종료)
3번에 따라 JVM 메모리에 한번에 모든 결과를 올려둘 필요가 없으므로, 충분한 시간만 주어진다면 조회 데이터 수가 많더라도 OOM 없이 데이터를 모두 읽어서 처리할 수 있다.
2. 사용 방법
기존 코드를 다음과 같이 개선하여 사용할 수 있다.
- 기존 Mapper 코드
List<TestDto> selectTest();
- 개선 Mapper 코드
Cursor<TestDto> selectTest();
- 기존 서비스 코드
@Service
@RequiredArgsConstructor
public classtestService{
@Autowired
private TestMapper testMapper;
public voidtest() {
List<TestDto> list = testMapper.selectTest();
...
....
}
}
- 개선 서비스 코드
@Service
@RequiredArgsConstructor
public classtestService{
@Autowired
private TestMapper testMapper;
@Transactional
public voidtest() {
try( Cursor<TestDto> list = testMapper.selectTest()) {
for (TestDto dto : list) {
//....//..
}
} catch (Exception e) {
// ..
}
}
}
왜 이렇게 개선되어야 할까? 이는 앞서 말한 동작방식을 보면 알 수 있다.
Cursor는 한줄씩 데이터를 처리할 수 있게 해준다고 앞에서 언급했는데, 이는 즉 데이터 처리가 끝나면 다음 줄을 읽어와야하는것을 의미한다.
따라서 전체 데이터를 모두 순회 할때까지 DB 연결이 유지되어야 한다는 걸 의미한다.
정리해보면
1. 해당 서비스 메소드에@Transactional을 달아 트랜잭션 상태를 유지시킨다
2. Service 메소드를 벗어나기전 Cursor를 써야하는 작업을 모두 마쳐야한다.
3. cursor와 fetchsize의 관계
네트워크 통신보다 메모리에 있는 내용을 처리하는 속도가 훨씬 빠름 따라서 얼만큼 처리할 데이터를 메모리에 올려놓는지 적절히 조율이 필요함
통신 빈도를 줄인다 - 통신 한번에 받아올 데이터의 양이 늘어난다(캐시를 많이 해야하므로 JVM 메모리를 많이 먹는다)
통신 빈도를 늘린다 - 통신 한번에 받아올 데이터의 양이 줄어든다(캐시를 적게 해도 되므로 JVM 메모리를 적게 먹는다)