목차
1. 문제 상황
2. 요구 사항 분석
3. 스케줄링 전략 선정
- 테스트 환경
- 스케줄러 전략 간단 요약
- 스케줄러 전략별 장단점
- 스케줄러 전략 상세 설명 및 테스트 결과
- 스케줄러 전략 3번 선택
4. 개선 필요 사항
✍️ 사전 지식
용어
- 당행 : 사용자로부터 이체 요청을 받고, 이체 입금을 진행하는 은행
- 타행 : 이체 과정에서 출금을 진행하는 은행
- 당행 이체 : 입금 은행과 출금 은행이 동일한 이체
- 타행 이체 : 입금 은행과 출금 은행이 다른 이체
- OutBox 데이터 : 이벤트나 메시지를 DB에 있는 아웃박스에 저장해서 DB 트랜잭션의 일부로 발행하는 패턴을 Transactinal OutBOx라고 합니다. 서로 다른 서버의 트랜잭션을 관리하고 할 때, eventual consistency를 보장하기 위해 DB 테이블을 메세지 큐로 사용하는 방식을 사용합니다. 이때 DB 테이블에 있는 이벤트 데이터를 OutBox 데이터라고 본 글에선 칭했습니다.
- OutBox polling : DB에 있는 아웃박스를 폴링해서 메시지를 발행하는 방식입니다. Transactional OutBox 패턴에 사용되는 Polling 발행기 패턴입니다.
Transactional OutBox 패턴 Ref : https://thebook.io/007035/0185/
Polling 발행기 패턴 Ref : https://thebook.io/007035/0186/
테스트 환경
- 구체적인 타행 이체 상황 : 타행 이체 중에서도 계좌 A에서 계좌 B로 연속 이체하는 상황을 테스트 했습니다. 주식 트레이드 시스템 처럼 단위 시간 동안 수많은 타행 이체가 일어나는 상황을 가정했습니다.
✍️ 문제 상황
이전 글(#2 타행 이체 기능 성능 개선기, 데이터 정합성)에서 DB polling을 구현하는 방식으로 스프링의 @Scheduled 메소드를 이용했습니다. 메소드 안에서는 매 timedelay마다 1건의 이벤트를 처리했습니다. 성능과 무관하게 원하는 대로 작동하는 로직을 만드는 것이 저의 목표였어서 이와 같이 구현했지만, 구현 후에는 성능에 대해 의심해볼 필요가 있었습니다.
실제로 nGrinder로 성능 테스트를 한 결과 이체 출금-입금 시간 간격이 33.3초가 나왔습니다. 출금 후 적어도 33.3초 후에 입금이 된다는 뜻이였죠. 목표 VUser보다 훨씬 적은 4명으로 했음에도 이렇게 적은 시간이 나왔다면, 목표 VUser가 100명일 때는 훨씬 느릴 것입니다.
✍️ 요구 사항 분석
타행 이체 서비스에서 사용되는 '이체 입금 요청 API'와 '이체 입금 완료 응답 API'는 멱등성 있게 작동해야 한다는 은행간 규약이 존재했습니다(참조: #2 타행 이체 기능 성능 개선기, 데이터 정합성). 또한 추후 유지 보수성을 생각하면 scale out에 잘 맞아야 하며, 이체 출금-입금 시간 간격을 0.8초 이내로 가져오는게 좋습니다(목표로 설정한 MTT는 0.8였습니다. 하지만 이 MTT는 이체를 요청한 client 입장에서의 데이터이므로 이체 출금-입금 시간과 다릅니다. 그럼에도 사용자 관점에서 0.8초가 사용자 경험에 좋은 영향을 주는 응답 속도이므로 이 또한 0.8초가 되는 것이 좋을 것이라고 판단했습니다).
스케줄러가 고려해야할 사항
- 멱등성 : 이체 입금 요청 API와 이체 입금 완료 응답 API는 멱등성 있게 작동해야함.
- Scale out : 유지 보수성
- 0.8초 : 이체 출금-입금 시간 간격을 0.8초 이내로 가져오기.
✍️ 스케줄링 전략 선정
스케줄링 전략에는 크게 4가지가 있었습니다. 각 스케줄링 전략의 특징에 대해 먼저 분석한 뒤 저희 요구 사항과 맞는 스케줄링 방법을 선정하겠습니다.
ps. 노션으로 만든 스케줄링 전략 분석 표가 이뻐서 한번 가져왔습니다 ㅎㅎ
테스트 환경
- VPC
- 앱 서버 : Naver Cloud Platform, [Standard] 2vCPU, 8GB Mem [g2], 1대
- DB : mysql 8.0.22
- nGrinder agent : Naver Cloud Platform, [Standard] 2vCPU, 8GB Mem [g2]
- nGrinder controller : Naver Cloud Platform, [Standard] 2vCPU, 8GB Mem [g2]
스케줄러 전략 간단 요약
- ReadOnceSendOnce : @Transactional가 붙은 스케줄러 메서드에서 이벤트 하나만 락 걸고 읽어와서 처리
ReadMultipleSendOnce: @Transactional이 붙지 않은 스케줄러 메서드에서 여러 row에 읽어온 후 각 row를 비동기로 처리함. 비동기 처리 시 락걸고 처리.- ReadOnceSendMultiple: @Transactional가 붙은 스케줄러 메서드에서 여러 row에 락 걸고 읽어와서 차례로(동기) row에 해당하는 로직 처리
- ReadOnceSendMultipleV2: @Transactional가 붙은 스케줄러 메서드, 여러 row에 락 걸고 읽어와서 각 row 관련 로직 처리(비동기)
스케줄러 전략별 장단점
스케줄러 전략 종류 | 장점 | 단점 |
ReadOnceSendOnce |
|
|
|
|
|
ReadOnceSendMultiple |
|
|
ReadOnceSendMultipleV2 |
|
|
스케줄러 전략 상세 설명 및 테스트 결과
1. (현재) TransferReadOnceSendOnceScheduler
[특징]
- @Transactional가 붙은 스케줄러 메서드에서 하나만 락 걸고 읽어와서 처리
- 스케줄러 : 동기
- 이체 메소드 : 동기
- Read once, send once 전략
- 상대 API가 멱등성 보장 해도 되고 안해도 됨
- 1초에 몇개의 이벤트를 평균적으로 처리할 수 있는가 : (Async 스레드 개수 * was 개수) / max(로직 처리 시간, fixedDelay)
[성능 측정 결과]
<설정>
- schduler timeDelay = 10ms
<결과>
- tps : 64.0
- 이체 처리 평균 시간 33.3초(그림1, 2 참조)
2. TransferReadMultipleSendOnceScheduler
[특징]
@Transactional이 붙지 않은스케줄러 메서드에서 여러 row에 읽어온 후 각 row를 비동기로 처리함. 비동기 처리 시 락걸고 처리. 이미 lock이 걸려있다면 다른 스레드가 처리중이므로 스레드는 자원 반납.스케줄러 : 동기이체 메소드 : 비동기Read multiple, send Once 전략상대 API가 멱등성 보장 해도 되고 안해도 됨n개의 이벤트 row가 있고, was가 m개 있다면 스케줄러가 m개 있기 때문에 최대 한번에 mn번 row를 읽고 mn번 스레드 컨텍스트 스위칭이 일어남. 하지만 n개의 row만 관련 로직을 처리하기 때문에 (m-1)*n개의 row는 관련 로직을 처리하지 못하고 그 결과 성능이 낭비되는 문제가 발생.애플리케이션 코드에서 병목이 일어남
[성능 측정 결과]
<설정>
- threadPoolSize = 10
- schduler timeDelay = 1000ms
<결과>
- tps : 51.0
- 이체 처리 평균 시간 6.9초
3. TransferReadOnceSendMultipleScheduler
[특징]
- @Transactional가 붙은 스케줄러 메서드여러 row에 락 걸고 읽어와서 차례로 row에 해당하는 로직 처리(동기)
- 스케줄러 : 동기
- 이체 메소드 : 동기
- Read multiple sometimes, send at least Once 전략
- 상대 API가 멱등성을 보장해줘야함
- n개 읽어온 후 n-1번째 까지 처리 성공했는데 n번째에서 처리 실패하면 1~n까지 다시 처리해야하는 문제 발생
- 각 입금 요청 이벤트 처리(NIO 동반)이 동기로 작동하기에 이체 완료 소요 시간이 비교적 클 것으로 예상
동기로 각 outbox를 처리하기 때문에 cpu 효율이 낮아져서 tps가 낮게 나옴. 대신 이체 처리 속도는 빨라짐.
[성능 측정 결과]
<설정>
- schduler timeDelay = 100ms
<결과>
- tps : 38.0
- 이체 처리 평균 시간 0.231초
4. TransferReadOnceSendMultipleSchedulerV2
[특징]
- @Transactional가 붙은 스케줄러 메서드, 여러 row에 락 걸고 읽어와서 각 row 관련 로직 처리(비동기).
- 스케줄러 : 동기
- 이체 메소드 : 비동기
- Read multiple sometimes, send at least Once 전략
- 상대 API가 멱등성을 보장해줘야함
- n개 읽어온 후 n-1번째 까지 처리 성공했는데 n번째에서 처리 실패하면 1~n까지 다시 처리해야하는 문제 발생
[성능 측정 결과]
<설정>
- schduler timeDelay = 100ms
<결과>
- tps : 45.0
- 이체 처리 평균 시간 0.332초
스케줄링 전략 4번 선택
4가지 스케줄링 전략 중 4번을 선택했습니다. 1,2번 전략은 상대 API가 멱등성을 보장해도 되고 안해도 되는 send only once 전략이어서 현 설계에 사용될 수 있지만 이체 완료 소요 속도가 느린 것이 큰 단점이였습니다. 3번과 4번은 모두 상대 API가 멱등성을 보장해야하만 하는 전략이기에 현 설계와 부합했으며, 이체 완료 소요 속도 또한 0.231, 0.332초로 목표 기준치인 0.8초 보다 낮았습니다.
물론 스케줄링 전략 4번은 'n개 읽어온 후 n-1번째 까지 처리 성공했는데 n번째에서 처리 실패하면 1~n까지 다시 처리해야하는 문제 발생' 문제가 있었습니다. 하지만 이는 은행 서비스에서 큰 문제가 아니라고 판단했습니다. 실제로 이체 입금을 진행할 때 입금 은행에 문제가 생겨서 k번째 입금이 실패되는 일은 비교적 적다고 판단했습니다. 또한 실패가 일어나더라도 이체 입금 요청 API가 멱등성을 보장하기에 데이터 일관성 문제도 없을 것으로 결론 지었습니다.
✍️ 개선 필요 사항
인프라 개선 필요
- 스케줄러 내 각 이체 입금 이벤트 건이 동기로 작동하는 3번 스케줄링 방식이 비동기로 작동하는 4번 스케줄링 방식 보다 빠름. (0.231s < 0.332s) NIO가 일어나기에 비동기가 더 빨라야 하지만 동기 방식이 더 빨랐음. 더 나은 테스트 데이터를 얻기 위해 인프라 개선이 필요하다고 판단함.
'TIL > 개발 칼럼' 카테고리의 다른 글
#5 타행 이체 기능 성능 개선기, 속도(이체 입금 요청 API 처리 전략) (2) | 2023.04.08 |
---|---|
#4 타행 이체 기능 성능 개선기, 속도(인프라 개선) (0) | 2023.04.08 |
#2 타행 이체 기능 성능 개선기, 데이터 정합성(아웃박스 패턴) (1) | 2023.04.07 |
#1 타행 이체 기능 성능 개선기, 프로젝트 소개 (0) | 2023.04.07 |
야크 털은 어디까지 깎아야 할까? JWT, 비대칭키, RSA, 유클리드 호제법, 정수론 (0) | 2022.11.10 |