대규모 데이터를 배치로 처리할 때에 알아두면 좋은 내용
대규모 데이터를 배치로 처리할 때에 알아두면 좋은 내용
배치가 뭐고 왜 쓰는거지?
간단하게 말하자면 데이터나 작업 등을 자동으로 처리하는 방법이다.
사용 용도는 다음과 같다.
- 대용량 데이터 처리 : 실시간으로 하기 곤란한 대용량 데이터 처리
- 반복 작업 : 주기적으로 수행되기 때문에 자동으로 되면 좋은 것
- 복잡하거나 리소스가 많이 드는 작업 : 시간이 오래 걸리거나 리소스 소모가 심하면 사용량이 적을 때에 배치로 처리 가능
즉, 얘는 개발자가 직접 처리하지 않고 뭉탱이로 처리할 수 있는 용도이고 장점은
- 대용량 처리 가능
- 작업 용이
- 자동화
- 부하 분산
등이 있을 것이다.
대용량 데이터 사용 시 배치 주의점
배치를 사용하면 대용량 데이터를 처리할 수 있다.
그런데... 만약 그 대상이 되는 데이터가 진짜 짱많은면 어떨까?
생각을 해보면
- 배치도 결국 데이터를 처리하기 위해서는 리소스를 잡아먹는다.
- 그리고, 뭉탱이로 처리하기 위해 보통은 처리하는 데이터를 Transaction 으로 감싸서 처리한다.
- 만약 Spring 에서 저 많은 데이터를 로드하고 처리하려고 한다면?
- OOM 이 발생할 가능성이 매우 높다.
- 그리고, 그렇지 않다고 해도 Transaction 때문에 DB에 부하가 가고 시스템 장애의 원인이 될 수 있다.
해결 방법
결국은 간단하게(말로는) 해결할 수 있어 보인다.
한번에 다 해서 문제라면 그냥 따로 나눠서 하면 되기 때문.
- 페이징 기법 활용
- 병렬 처리
- 합쳐서 처리
1. 페이징 기법 활용
한번에 가져오는게 문제라면?
-> 여러번에 걸쳐 들고오면 된다.
그냥 간단히 말하면 게시글 볼 때 페이지 나누는 거라고 생각하면 된다.
당연하겠지만 offset limit 은 좋지 않고, Cursor 방식을 활용하면 좋다.
마지막으로 처리한 데이터 이후로부터 들고오는 방식이라고 생각하면 되는데, 이 글을 참고해 봐도 좋을듯.
방식을 설명해 보자면, 한 10000개 정도의 데이터를 가져오려 할 때
- Transaction 시작
- 1000개를 읽어옴
- 1000개를 처리함
- Transaction 커밋
- Transaction 시작
- 1000번째 데이터를 통해 그 다음 1000개를 읽어옴
- 반복...
이러면 된다.
중간에 실패한다면 그 부분만 다시 시도하면 될 것이다.
2. 병렬 처리
한번에 가져오는게 문제라면?
-> 여러명이 들고오면 된다.
이거는 이제 여러 배치 서버가 같이 일한다고 생각하면 된다.
나누는 것 자체는 위랑 같지만, 혼자서 여러번 하는것과 한꺼번에 나눠서 하는것의 차이다.
근데 그게 방법이 여러개가 있는데
- 멀티스레딩
- 여러 스레드가 나눠서 처리하는 방식
- 장점
- 메모리를 공유하기 때문에 데이터를 주고받기 쉽고, 그 때문에 I/O 대기가 많은 작업에 효율적이다.
- 단점
- 메모리 문제가 터질 수 있다.
- 메모리 동기화 문제 발생 가능
- 멀티프로세싱
- 여러 프로세스가 나눠서 처리하는 방식
- 장점
- 메모리 문제가 생길 여지가 적다.
- 동기화 문제도 발생하지 않는다.(애초에 공유를 못하니)
- 단점
- 메모리 공유 자체가 안됨.
- 프로세스를 매번 만들고 내려야 한다.
- 분산처리
- 여러 서버에서 배치 작업 수행
- 장점
- 스케일아웃이 매우 자유롭다.
- 데이터가 많으면 사실상 이게 정답.
- 단점
- 구성과 관리가 난감하다.
- 하나가 아예 죽어버리거나 각각 서버 간의 통신 등.. 고민할 점이 너무 많다.
일단은 셋의 차이는 이렇다.
정말 데이터가 많다면 사실상 분산처리가 강제되기 때문에 이 내용에서는 이거를 생각하면 될듯.
그러면 이게 어떻게 할까? 똑같이 10000개의 데이터를 처리한다고 하면
10개의 서버가 있다고 가정한다.
- 1번 서버에서 1~1000 번 데이터 처리
- 2번 서버에서 1001번 ~ 2000번 데이터 처리
- 10개 서버에서 수행
이러면 간단히 처리된다.
여기서 저거 어떤 값을 처리할지를 어떻게 아냐? 하면
- Range 파티셔닝
- 범위를 애초에 나눠서 처리
- 위의 방식대로 1
1000 / 10012000 .... 이렇게 - 장점
- Index 효율이 좋다.
- 범위를 나눠서 처리하면 인덱스 내에서 순서대로 위치하기 때문에 모듈러보다 훨씬 효율적이다.
- 직관적
- Index 효율이 좋다.
- 단점
- 데이터 쏠림
- 나눠둔 범위에 데이터가 얼마나 있을지 모른다.
- 사전 분석 필요
- 어떤 범위를 어떻게 나눠서 배치 서버에서 수행하게 할지 고민해 봐야 한다.
- 데이터 쏠림
- 모듈로 파티셔닝
- 서버 개수로 나머지를 구해서 처리
- ID가 1로 끝나면 1번 서버가 처리, 2로 끝나면 2번 서버가 처리 이런 식으로 가는거
- 장점
- 데이터 쏠림이 적다.
- 얘는 위의 범위를 나누는 것에 비해 데이터가 나눠져 있을 여지가 많다.
- 간단한 구현 가능
- 어떤 범위로 나눠야 할지 깊은 고민하지 않고 서버 수로 구하면 된다.
- 데이터 쏠림이 적다.
- 단점
- Index 효율이 낮다.
- 당연하지만 index를 타도 숫자가 모듈로로 인해 나뉘어 있어 더 많은 데이터의 로드가 필요하기 떄문에 Range 방식에 비하면 훨씬 비효율적이다.
- Index 효율이 낮다.
하는 방식이 있다.
DB 부하를 줄이는 것이 좋고 데이터 분포가 균일하다면 Range를, 아니면 Modulo 를 선택하면 될 것이다.
합쳐서 처리
사실 분산 처리의 경우는 합쳐서 처리하는 경우가 많다.
서버를 여러 대 운용하면서 각 서버가 잘 나눠서 처리하는 것이다.
예를 들어 10개의 서버에서 100000개 데이터를 처리해야 한다면
- 1번 서버
- 1~10000개의 데이터 처리
- 1000개씩 나눠서 10번 처리
- 2번 서버
- 10001 ~ 20000개의 데이터 처리
- 1000개씩 나눠서 10번 처리
요런 식으로 가면 된다.
모듈로를 사용한다면
- 1번 서버
- 1, 11, 21 ... 해서 1000개까지 처리하고 다음으로
- 2번 서버
- 2, 12, 22 .. 해서 1000개까지 처리하고 다음으로
이런 식으로 가면 될 것이다.
어떤 것이 성능이 좋을지를 잘 고민하고 쓰면 좋을 것 같다.
실패 시 재시도?
그러면 저런 작업 중에 일부분이 실패하면 어떻게 할까?
보통은 배치에서는 작업 단위로 Transaction 을 건다.
그렇기 때문에 어떤 작업이 실패하면 그 부분에 해당하는 것들만 롤백될 것이다.
이 경우 재시도를 하면 되는데, 여기서도 좀 애매한게 있다.
- 재시도 횟수 고려
- 당연하지만 실패했으니 재시도를 통해 처리해야 하는데, 그게 계속 실패하면 뭔가 일시적인게 아니라 계속되는 실패일 수도 있다.
- 그러면 이거를 계속 돌리는 것 자체가 리소스의 낭비일 것이다.
- Threshold 등을 걸어, 재시도 횟수를 제한하는 것도 괜찮을 것이다.
- 멱등성을 보장해야 한다.
- 매우 중요한 내용인데, 배치에서 실패했을 떄 재시도를 하는 경우 원했던 결과의 멱등성을 보장해야 한다.