10000개의 데이터를 처리하는데 1시간 20분 정도가 걸리고 있다. 클라이언트와 read timeout 설정이 5분이라서 해당 메서드는 @Async
처리를 하였지만, 여전히 백그라운드에서 로직처리에 1시간 20분이 소요되고 있고, 이 시간동안 작업 요청자는 기다려야 한다.
병목지점을 찾아서 개선해보자.
로직 | 수행시간 |
---|---|
CSV 파일 읽기 및 데이터 프로세싱(1000개 기준) | 8,001 ms |
데이터 유효성 검증(300개 기준) | 60,817 ms |
A 데이터 저장(300개 기준) | 6,834 ms |
A 데이터 암호화(300개 기준) | 6,032 ms |
A 데이터 연관된 B 데이터 저장(300개 기준) | 6,160 ms |
A FeingClient 호출(300개 기준) | 12,372 ms |
B FeingClient 호출(600개 기준) | 12,464 ms |
C FeingClient 호출 | 12,468 ms |
A Kafka 프로듀싱(300개 기준) | 18,073 ms |
현재 데이터 한건당 4건의 select 문이 존재한다. 즉 청크 단위당 1200(4 * 300) 건의 select 문이 존재한다.
❎ 첫번째 시도, 단건 검증 자체를 병렬 처리해보자!
60,817 ms → 8,213 ms
✅ 두번째 시도, findByXXXIsIn를 사용하여 다건 검증을 수행해보자
60,668 ms → 204ms
@DataJpaTest
@ActiveProfiles("local")
public class JpaRepositoryTest {
@Autowired
private ActiveMemberRepository activeMemberRepository;
private static List<String> ids;
@BeforeAll
static void setUp() {
ids = new ArrayList<>();
IntStream.range(0, 100)
.forEach(i -> ids.add(RandomStringUtils.randomAlphanumeric(10)));
}
@Test
void findByXXXTest() { // 1sec 688ms
ids.forEach(id -> {
activeMemberRepository.findByMemberId(id);
});
}
@Test
void findByXXXIsInTest() { // 204ms
activeMemberRepository.findByMemberIdIsIn(ids);
}
}
속도변화가 있었으나 기존에 단건으로 구현된 검증 로직에서 변경사항이 크다는 단점이 있다...😢
응답시간이 외부 서비스에 영향을 받는 네트워크 통신 로직들은 다른 로직과 동시에 진행되도록 처리되도록한다.
데이터 적재 및 프로듀싱(약 36초)이 수행되면서 동시에 A FeingClient 호출, B FeingClient 호출, C FeingClient 호출을 처리하여 외부 서비스 호출에 소요되는 시간을 삭제하는 효과를 얻었다.
List<CompletableFuture<Boolean>> allFutureList = new ArrayList<>();
aDatas.forEach(registrationNumber -> allFutureList.add(executeForSaveLogs(aDatas)));
-- 엄청 오래걸리는 로직
CompletableFuture.allOf(allFutureList.toArray(new CompletableFuture[0])).join();
}
private CompletableFuture<Boolean> executeForSaveLogs(Data data) {
return CompletableFuture.supplyAsync(() -> {
loadCertificateAndScraping(registrationNumber, OrganizationType.STOCK);
}
return true;
}, customExecutor)
.exceptionally(e -> {
// 취소 API 가 생기면 해당 로직이 추가될 수 있다.
return false;
});
}
동기 / 비동기 | 청크 단위 | 소요 시간 |
---|---|---|
동기 | 300개 | 9 m 12.54 s |
비동기 | 200개 | 4 m 18.54 s |
비동기 | 100개 | 1 m 11.34 s |
비약적인 속도 변환가 있었다.
비동기 로직에 300개로 청크되는 단위에도 비동기로 돌릴 수 있었지만, 문제는 요구사항에 하나가 실패하면 전체 롤백이 수행되어져야 하는데, 비동기 처리시에 다른 스레드에서 수행되는 적재 로직들은 transactional 에 의한 롤백이 불가능하다🤔
적재하는 테이블이 많아서 중간에 예외가 발생한 경우, 삭제하는 로직을 추가하는 것이 힘들 것으로 예상한다.
✅ 데이터 유효성 검증시, findByXXX 로 다건 검증 수행
✅ 수행된 후 그 다음로직에 영향이 안 가는 부분은 비동기 처리
✅ 데이터 일괄 적재
청크 단위로 비동기 처리(1000개 계정 생성)
총 소요시간(1000개 기준)
🥳 7 m 25.50 s -> 2m 18.07 s
동일 Transactional 내에서 메서드가 끝나고 commit 과 동시에 flush 하면서 쿼리가 나가는데, chunk 하면 쿼리가 동시에 나가지 않는걸까?
쓰기지연 기능을 통해 flush 시점에 한번에 쿼리가 나가게 된다.
비동기 처리시에 쓰레드 풀(N개)을 지정해놓았을 때, 해당 메서드를 여러번 호출하면 N개 만큼의 쓰레드만 사용될까? 아니면 N * 요청 개수 만큼 사용될까?
적정 쓰레드 풀은 어떻게 책정할 수 있을까?
좋은 정보 감사합니다