목차
1. [문제 상황] 동기 보다 비동기가 느린 현상
2. [원인 분석] 로컬에서만 일어나는 NIO
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로 연속 이체하는 상황을 테스트 했습니다. 주식 트레이드 시스템 처럼 단위 시간 동안 수많은 타행 이체가 일어나는 상황을 가정했습니다.
✍️ [문제 상황] 동기 보다 비동기가 느린 현상
ReadOnceSendMultiple(동기) 방식이 ReadOnceSendMultipleV2(비동기) 방식보다 빠른 문제가 있었습니다(0.231 < 0.331). ReadOnceSendMultipleV2는 비동기로 작업을 처리하기에 모든 작업이 순차적으로 진행되는 동기 방식보다 빠를 것으로 예상됐기에 해당 결과가 더 납득이 안됐습니다.
ReadOnceSendMultiple: @Transactional가 붙은 스케줄러 메서드여러 row에 락 걸고 읽어와서 차례로(동기) row에 해당하는 로직 처리
ReadOnceSendMultipleV2: @Transactional가 붙은 스케줄러 메서드, 여러 row에 락 걸고 읽어와서 각 row 관련 로직 처리(비동기)
출처 : #3 타행 이체 기능 성능 개선기, 아웃박스 스케줄러 전략 선정 및 구현
✍️ [원인 분석] 로컬에서만 일어나는 NIO
ReadOnceSendMultiple(동기)와 ReadOnceSendMultipleV2(비동기) 방식이 어떻게 작동하는지 DB I/O, Network IO 관점에서 하나씩 trace 해보기로 했습니다.
타행 이체 순차 다이어그램을 살펴봤을 때 문제가 될 만한 곳은 <그림1>의 5곳이였습니다. 1,4번은 NIO였고 2,3,5번은 DB I/O이기 때문이죠. 두 종류의 작업 중 동기, 비동기 작업에 유의미한 영향을 줄 작업이 무엇일지 알기 위해 NIO와 DB I/O의 부하량을 검색해봤습니다.
위 표는 Systems Performance: Enterprise and the Cloud 책에서 정리한 작업 별 소요 시간 비교 표입니다. 해당 표에 의하면 NIO중 가장 작은 SF to NYC NIO는 4년(상대적인 수치)인 반면, SSD I/O는 최대 6일입니다. 즉 NIO는 DB I/O 보다 약 243배(4*365/6) 느립니다. 이에 저는 NIO가 제대로 되고 있는지 살펴보는게 좋다고 판단했습니다. 그후 문제가 없다면 DB I/O를 살펴보는 순서죠.
NIO 관점에서 저의 인프라 구조도를 봤습니다. 저의 인프라에선 현재 타행(이체 입금 은행) 역할을 당행이 모두 수행하고 있었습니다. '서버 환경에서 로직 자체가 제대로 돌아가는가'를 먼저 중요시했기에 타행을 따로 띄우지 않았었습니다. 하지만 이는 NIO를 고려하지 않은 테스트 환경이였습니다.
현재 설계에선 이체 입금 요청과 이체 입금 완료 응답이 모두 로컬 호스트 호출을 하기 때문에 NIO가 실질적으로 진행되지 않고 굉장히 빠르게 실행되는 것이라고 생각했습니다. 오히려 비동기 방식은 thread switching이 많이 일어나기에 동기 방식보다 느린 현상을 보인 것이라고 결론 지었습니다.
✍️ [해결] 당행과 타행으로 테스트 서버 분리
타행을 완전히 다른 서버에 띄우고 MySQL DB도 타행 전용으로 띄웠습니다! 두 은행은 모두 같은 코드로 돌아갑니다. 같은 코드이기에 같은 API 규약을 지키게 되어 API 멱등성 문제도 없게되죠:) 로컬 호스트로 호출하는 것이 문제였는지 테스트를 통해 확인해봅시다!
ReadOnceSendMultiple(동기) 결과
동기 방식의 성능 테스트 결과입니다! 평균 이체 처리 시간이 무려 39.74초가 나왔습니다! 저희가 예상한 결과대로 NIO를 하게되니 로컬 호스트로 호출하는 것 보다 시간이 100배 이상 소모됐습니다. 예상대로 로컬 호스트로 호출하는 것이 문제였었습니다.
TPS가 더 높게 나온 이유(TPS 38.0 -> 90.4)는 이전과 달리 API 호출이 서로 다른 서버에서 일어나기 때문이라고 판단했습니다. 서버가 1개일 때(한 서버가 당행과 타행 역할을 같이 할 경우)는 그 서버가 타행 이체 API, 이체 입금 요청 API, 이체 입금 완료 응답 API의 호출을 모두 처리했기에 nGrinder agent가 호출하는 타행 이체 API에게 배당될 톰캣 스레드가 비교적 적었을 것입니다. 당행과 타행의 서버를 분리한 경우엔 당행은 타행 이체 API, 이체 입금 완료 응답 API만 HTTP 요청을 받기에 nGrinder agent로 부터 더 많은 요청을 받을 수 있게 된 것이죠.
ReadOnceSendMultiple(비동기) 결과
반은 예상하고 반은 예상하지 못한 테스트 결과가 나왔습니다. 비동기 방식의 평균 이체 처리 시간이 35.5초가 나왔습니다. 평균 이체 처리 시간은 예상대로 동기 방식보다 짧게 나왔지만 35.5초는 목표 수치인 0.8초 보다 너무 큰 격차가 나는 시간이였습니다. 이제는 NIO가 아닌 DB I/O를 봐야할 차례가 된 듯 합니다. 이와 관련해서는 다음 글에서 설명 드리겠습니다.
ps. 미리 스포를 하자면 비동기로 작동하도록 설계했지만 DB 작업으로 인해 동기로 작동하고 있었습니다.
✍️ 고민해봐야할 것
1. 35.5초가 걸리는 현재 타행 이체 구조
'TIL > 개발 칼럼' 카테고리의 다른 글
#6 타행 이체 기능 성능 개선기, 속도(gap lock으로 인한 insert 병목 해결) (1) | 2023.04.20 |
---|---|
#5 타행 이체 기능 성능 개선기, 속도(이체 입금 요청 API 처리 전략) (2) | 2023.04.08 |
#3 타행 이체 기능 성능 개선기, 속도(아웃박스 스케줄러 전략 선정 및 구현) (0) | 2023.04.08 |
#2 타행 이체 기능 성능 개선기, 데이터 정합성(아웃박스 패턴) (1) | 2023.04.07 |
#1 타행 이체 기능 성능 개선기, 프로젝트 소개 (0) | 2023.04.07 |