Post

배치 작업 어디까지 해봤니?

배치 작업이 무엇인지 정리해보고자 합니다.

배치 작업 어디까지 해봤니?

서비스를 구축하다 보면 많은 양의 데이터를 일괄적으로 변경하거나 삭제 등의 처리를 해야 할 순간이 옵니다. 이런 작업을 트래픽이 몰리는 시간대에 하게 되면 서비스에 치명적일 수도 있고, 잘못된다면 서비스 전체가 셧다운 되는 악몽을 꾸게 될 수도 있죠.

이때 해야 하는 것이 배치(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

2022-11-26-image1

간단한 코드입니다. TestA 컬렉션에서 데이터를 가져와 TestB 컬렉션의 document를 하나씩 update 하는 코드입니다.

결과

2022-11-26-image2

Heap 메모리 부족으로 프로세스가 죽었어요.

Node.js는 버전별로 사용할 수 있는 기본 메모리 제한이 있습니다.

2022-11-26-image3

node --max-old-space-size=<memory>로 메모리 제한을 늘릴 수 있지만, 이것은 근본적인 해결책이 아닙니다. 코드 상에서 메모리 누수가 발생하는 부분을 찾거나 메모리를 많이 잡아먹는 로직이 있는지 먼저 살펴봐야 합니다.

TestA, TestB 컬렉션에 document가 각각 50만 개가 존재했습니다.

2022-11-26-image4

50만 개의 데이터를 가져오는 것도 문제지만, 50만 번의 updateOne 호출은 더 큰 문제예요.

이럴 때 사용하면 좋은 MongoDB의 메서드가 bulkWrite입니다. BulkWrite는 DB에 쓰기 작업을 대량으로 해주는 메서드입니다.


Take2

2022-11-26-image6

Take1 코드에서 BulkWrite만 추가된 코드입니다.

결과

2022-11-26-image7

이번에는 문제없이 50만 개의 document가 update 되었습니다. 처리 시간 1분 19초, CPU 사용률 ~40%.

1분 19초는 나쁘지 않은 속도이지만, 로직이 실행되는 동안 DB CPU가 40%가량 치솟는 것이 문제입니다. update 로직이 조금만 더 복잡해지거나 데이터가 더 많아진다면 문제가 생길 여지가 있어요.

역시 문제는 데이터를 한 번에 처리하려니까 생기는 거였네요.

2022-11-26-image8

그렇다면 몇 개씩 나눠서 처리를 한다면 어떨까요?


Take3

2022-11-26-image9

반복문이 추가됐습니다. 데이터를 10,000개씩 가져와 쓰기 작업을 하는 코드로 변경했습니다.

결과

2022-11-26-image10

실행 시간은 1분 25초로, 50만 개의 데이터를 한 번에 처리하는 것과 비슷한 처리 시간이 나타났습니다.

2022-11-26-image11

CPU 사용률을 보면 22% 정도로, Take2의 반절 정도의 사용률을 보여줍니다. 훨씬 안정적이에요.

단점은 나눠서 처리할 데이터양을 정하는 것이 어렵습니다. 테스트 데이터는 용량이 작아 만 개로 지정 가능하지만, 운영 데이터는 훨씬 더 큰 메모리를 사용합니다.

MongoDB는 쿼리에 응답하는 데이터의 최대 메모리가 40MB입니다. 따라서 만 개의 데이터가 40MB를 넘어버린다면, MongoDB는 쿼리에 응답하지 못할 것입니다.

저는 해결 방법을 Stream으로 생각했습니다. DB에서 SELECT를 해올 때 Stream으로 데이터를 가져와서 하나씩 처리한다면, 처리 시간은 오래 걸리겠지만 안정적이겠죠.


Take4

2022-11-26-image12

cursor 명령어가 추가됐습니다. cursor는 쿼리에 대한 결과를 document로 반환하지 않고, cursor로 반환하는 명령어입니다.

결과

2022-11-26-image13

시간은 11분 55초로 많이 걸렸습니다. 하지만 CPU 사용률은 엄청나게 줄어든 것을 확인할 수 있어요.


결론

Take3과 Take4가 괜찮은 결과를 보였지만, 어느 방법으로 선택할 것인지는 서비스마다 다를 것입니다.

  • 리소스를 더 쓰더라도 빠른 실행 시간을 원한다면: Take3 (분할 처리)
  • 실행 시간이 오래 걸리더라도 리소스를 절약하길 원한다면: Take4 (Stream 처리)

핵심은 서비스 특성에 맞는 방식을 선택하는 것입니다.


참고 자료

This post is licensed under CC BY 4.0 by the author.