배치 작업 어디까지 해봤니?
배치 작업이 무엇인지 정리해보고자 합니다.
서비스를 구축하다 보면 많은 양의 데이터를 일괄적으로 변경하거나 삭제 등의 처리를 해야 할 순간이 옵니다. 이런 작업을 트래픽이 몰리는 시간대에 하게 되면 서비스에 치명적일 수도 있고, 잘못된다면 서비스 전체가 셧다운 되는 악몽을 꾸게 될 수도 있죠.
이때 해야 하는 것이 배치(Batch) 작업입니다. 배치 작업은 데이터를 실시간으로 처리하는 게 아니라, 일괄적으로 모아서 한 번에 처리하는 작업을 의미합니다. 예를 들어 은행의 정산 작업의 경우 배치 작업을 통해 일괄처리를 수행합니다.
이번 글에선 MongoDB + Node.js를 활용한 배치 작업 테스트 결과를 공유하고자 합니다.
테스트 환경
- Cloud: Tencent Cloud CVM
- CPU/RAM: 2core / 2GB
- Database: MongoDB WT 4.2.19
- Runtime: Node.js v14.19.0, NestJs
DB Schema:
1
2
TestA: { name: string }
TestB: { name: string, aId: string }
Take1
간단한 코드입니다. TestA 컬렉션에서 데이터를 가져와 TestB 컬렉션의 document를 하나씩 update 하는 코드입니다.
결과
Heap 메모리 부족으로 프로세스가 죽었어요.
Node.js는 버전별로 사용할 수 있는 기본 메모리 제한이 있습니다.
node --max-old-space-size=<memory>로 메모리 제한을 늘릴 수 있지만, 이것은 근본적인 해결책이 아닙니다. 코드 상에서 메모리 누수가 발생하는 부분을 찾거나 메모리를 많이 잡아먹는 로직이 있는지 먼저 살펴봐야 합니다.
TestA, TestB 컬렉션에 document가 각각 50만 개가 존재했습니다.
50만 개의 데이터를 가져오는 것도 문제지만, 50만 번의 updateOne 호출은 더 큰 문제예요.
이럴 때 사용하면 좋은 MongoDB의 메서드가 bulkWrite입니다. BulkWrite는 DB에 쓰기 작업을 대량으로 해주는 메서드입니다.
Take2
Take1 코드에서 BulkWrite만 추가된 코드입니다.
결과
이번에는 문제없이 50만 개의 document가 update 되었습니다. 처리 시간 1분 19초, CPU 사용률 ~40%.
1분 19초는 나쁘지 않은 속도이지만, 로직이 실행되는 동안 DB CPU가 40%가량 치솟는 것이 문제입니다. update 로직이 조금만 더 복잡해지거나 데이터가 더 많아진다면 문제가 생길 여지가 있어요.
역시 문제는 데이터를 한 번에 처리하려니까 생기는 거였네요.
그렇다면 몇 개씩 나눠서 처리를 한다면 어떨까요?
Take3
반복문이 추가됐습니다. 데이터를 10,000개씩 가져와 쓰기 작업을 하는 코드로 변경했습니다.
결과
실행 시간은 1분 25초로, 50만 개의 데이터를 한 번에 처리하는 것과 비슷한 처리 시간이 나타났습니다.
CPU 사용률을 보면 22% 정도로, Take2의 반절 정도의 사용률을 보여줍니다. 훨씬 안정적이에요.
단점은 나눠서 처리할 데이터양을 정하는 것이 어렵습니다. 테스트 데이터는 용량이 작아 만 개로 지정 가능하지만, 운영 데이터는 훨씬 더 큰 메모리를 사용합니다.
MongoDB는 쿼리에 응답하는 데이터의 최대 메모리가 40MB입니다. 따라서 만 개의 데이터가 40MB를 넘어버린다면, MongoDB는 쿼리에 응답하지 못할 것입니다.
저는 해결 방법을 Stream으로 생각했습니다. DB에서 SELECT를 해올 때 Stream으로 데이터를 가져와서 하나씩 처리한다면, 처리 시간은 오래 걸리겠지만 안정적이겠죠.
Take4
cursor 명령어가 추가됐습니다. cursor는 쿼리에 대한 결과를 document로 반환하지 않고, cursor로 반환하는 명령어입니다.
결과
시간은 11분 55초로 많이 걸렸습니다. 하지만 CPU 사용률은 엄청나게 줄어든 것을 확인할 수 있어요.
결론
Take3과 Take4가 괜찮은 결과를 보였지만, 어느 방법으로 선택할 것인지는 서비스마다 다를 것입니다.
- 리소스를 더 쓰더라도 빠른 실행 시간을 원한다면: Take3 (분할 처리)
- 실행 시간이 오래 걸리더라도 리소스를 절약하길 원한다면: Take4 (Stream 처리)
핵심은 서비스 특성에 맞는 방식을 선택하는 것입니다.











