아래 글은 Toss Slash 24 세션 참가 후 느낀 소감과 발표 내용을 단순 개조식으로 변환한 내용입니다. 자세한 내용은 아래 유튜브 영상에서 확인하실 수 있습니다.
https://www.youtube.com/watch?v=5I7ehDTvnWA
24년 9월 12일에 좋은 기회가 되어 Toss에서 개최하는 컨퍼런스 Slash 24에 다녀왔다. 총 10개의 세션을 들었는데, 그 중에서도 가장 인상 깊었던 세션이 '대규모 사용자 기반의 마이데이터 서비스 안정적으로 운영하기'라는 세션이었다. 발표의 내용과 구성, 발화자의 딕션, ppt, 전달력 등 최근 들었던 발표 중에 가장 깔끔하지 않았나 싶다.
발표에 따르면 70,000 TPS 트래픽이 발생하는 서비스를 안정적으로 운영하는 노하우에 대한 내용이다. 이를 문제의 정의부터 해결 과정에서의 기술적 의사결정을 중심으로 설명하고 있다.
크게 장애 대응, 시스템 부하, 피크 트래픽 제어 3가지 토픽으로 나누어 설명하는데, 장애 대응 파트가 그 중에서도 가장 인상깊었다. 현재 업무를 하면서도 나에게 제어권이 없는 대외 기관의 장애나 지연에 어떻게 대응해야 하는가는 참 어려운 문제다. 장애 대응 파트가 'target system의 장애나 지연을 어떻게 handling 할 것인가?'에 대한 좋은 사례로 보였다.
결론만 보자면 circuit breaker를 기존 오픈소스(resilience4j)를 활용해서 구축해두었는데, 동일 application group 내의 단일 pod 단위로 적용되어 해당 group이 일관되게 circuit breaker가 open되지 못하고, 그 수치도 제대로 측정되지 않는다는 문제점이 있었다. 이를 Coordinator 서버라는 계층을 추가해 해결하였다. 이를 통해 동일 group의 application 중 80% 가량이 일관되게 circuit breaker가 open되어 target system에 걸리는 부하나 내부 시스템의 자원을 낭비하는 상황을 줄여주었다.
위 과정까지는 개인적으로 구현해보면 좋은 공부가 되겠다는 생각이 들었다. 해당 지점 이후부터 남다르게 느껴졌던 점은, 조직의 배포 방식 특수성까지 고려하여 구현했다는 점과 유의미한 트래픽 통계치까지 산출했다는 점이다. canary 배포 방식(심지어 다른 발표에서 본 것처럼 sticky canary라는 토스만의 배포 방식도 있었다)에 알맞은 설계를 통해 유의미한 데이터를 도출할 수 있는 기반을 마련하고, 해당 데이터를 통해 향후 시스템 개선사항을 찾아낼 검증 데이터들을 수치화했다는 점에서 놀라웠다. 기존 시스템과의 coordinating 까지도 고려한 것이 아닐까하는 생각이 들 정도였다.
원문을 거의 그대로 옮긴 수준이지만 내가 나중에 볼 때 편할 방식인 개조식으로 정리해보았다. 발표는 storytelling이 중요하지만 머리 속에 넣을 때는 개조식으로 정리하는게 나한텐 더 도움이 되는 것 같다.
마이데이터란?
토스에서 금융 데이터의 플랫폼 역할
- 여러 금융기관에 흩어져 있는 개인의 신용정보를 편리하게 조회할 수 있는 서비스
- 토스앱을 열었을 때 가장 먼저 확인하실 수 있는 홈 화면은 마이데이터를 대표적으로 경험해 볼 수 있는 서비스
- 홈 화면에 들어오면 마이데이터를 통해 연결한 다양한 금융자산과 잔액 거래 내역 등을 확인할 수 있음
- 이 밖에도 신용관리 및 대출과 같은 토스의 다양한 금융 서비스들이 마이데이터를 이용
상황
- 자연스럽게 마이데이터 서버로 들어오는 트래픽의 양도 늘어나게 됨
- 현재 토스의 마이데이터 서버는 약 7만 TPS가 발생
- 대용량 트래픽이 발생하고 있다 보니 나비효과처럼 아주 작은 변화가 일어나도 큰 문제를 일으킬 수 있는 환경에 놓임
오늘 살펴볼 주제
- 장애 대응
- 토스 시스템 부하
- 피크 트래픽 제어
장애대응
- 상황
- 현재 토스의 마이데이터 서비스는 7개 업권에 해당하는 수백 개의 제공 기관과 API 통신을 주고받음
- 여러 기관과 시스템 간의 복잡한 상호작용으로 인해서 상대적으로 장애가 발생할 가능성이 높은 환경에 놓여져 있음(소프트웨어 시스템은 언제든 장애가 발생할 수 있고 그 장애가 쉽게 전파되는 특성을 가지고 있음)
- 만약 제공 기관에서 장애가 발생하면 그 장애는 마이데이터 서버로 전파되고 다시 그 장애가 유저나 다른 토스의 시스템으로 전파
- 통신하는 기관의 수가 많은 만큼 대응하는 데 리소스도 함께 늘어나게 됨
- 문제 해결
- 방향
- API 호출이 제공기관 시스템 임계치를 넘어서 성능에 영향을 주면 제공 기관에 장애 발생
- 기관으로 향하는 트래픽을 모니터링하여서 유저당 일정 수 이상의 API 호출이 발생하지 않도록 관리
- 제공 기관으로 향하는 트래픽 차단해서 제공기관의 시스템이 복구될 수 있도록 도움
- 기존 시스템
- 서킷 브레이커를 구현한 resilience4j 오픈소스 솔루션 사용
- 기관 단위로 서킷 브레이커 적용
- 장애 발생 시 서킷이 오픈되고 클라이언트에게 점검 중이라는 문구 노출
- 한계점
- 서버 단위로 서킷 브레이커 적용
- 예시
- 모든 서버가 A은행으로 향하는 트래픽이 발생하고, 이 때 특정 서버에서 호출한 API만 실패했다고 가정
- 전체 시스템 관점에서는 실패율이 10%지만, 실패가 발생한 서버 입장에서 보면 실패율은 100%이므로 서킷이 오픈됨
- 하지만 호출이 없었거나 최소한의 실패율에 대한 표본도 수집하지 못한 서버는 서킷이 오픈되지 못함
- 서버마다 서킷의 상태가 달라지게 되면 시스템이 일반적인 동작을 하지 못하는 문제가 발생
- 그래서 코디네이터 시스템을 개발해야겠다고 생각
- 신규 시스템(코디네이터 시스템)
- 위 문제점을 해결하기 위해 코디네이터 시스템을 디자인
- 개발자의 직접적인 개입 없이도 트래픽을 제어하고 모니터링하는 시스템
- 두 가지 옵션
- 마이데이터 서버에 기능 구현
- 여러 시스템 간의 의존성으로 상대적으로 장애 발생 가능성 높음
- 만약 장애가 발생한 서버가 코디네이터 기능 수행하던 서버였다면 해당 서버의 장애로 인해 전체 시스템 기능이 중단되게 됨
- 별도 서버에 기능 구현
- 시스템 운영과 자동화 관련 기능을 온전히 분리해 유연하고 확장 가능한 시스템을 만들자고 결론
- 마이데이터 서버에 기능 구현
- 위 문제점을 해결하기 위해 코디네이터 시스템을 디자인
- 방향
- 코디네이터 서버
- ready
- 서버가 처음 쿠버네티스 클러스터에 배포될 때 코디네이터 서버에게 establish 요청을 보냄
- 코디네이터 서버는 요청이 온 서버를 마이데이터 클러스터 목록에 편입시키고 서버 아이디를 발급
- processing
- 지속적으로 마이데이터 서버로부터 heartbeat 요청이 오는지 확인
- 마이데이터 서버는 발급받은 서버 아이디를 이용해 분산 환경에서 unique API txId를 생성해 제공 기관을 호출
- 제공 기관 호출 후 성공/실패 여부 카운트해서 버퍼에 쌓아두었다가 kafka로 전송
- 코디네이터 서버도 kafka로부터 데이터를 consume해오고, 분 단위로 성공/실패 통계 데이터 생성
- 마이데이터 서버도 10초에 한 번씩 코디네이터 서버에게 서킷이 오픈된 기관 목록이 있는지 API 요청
- circuit open
- 실패율이 임계치가 넘어간 기관이 있다면 일정 시간 동안 서킷 오픈
- 이 결과를 다음 10초까지 로컬에 캐싱해두고, 장애가 나는 기관으로 향하는 트래픽 차단
- circuit close
- 코디네이터 서버는 가장 먼저 배포된 서버와 가장 마지막에 배포된 서버 2대를 master로 지정
- 서킷이 오픈된 기관 목록을 요청할 때 master 서버를 대상으로만 응답을 주어, A은행에 장애가 났는지 모르게 함
- 마스터 서버 2대는 장애가 발생한 시간 동안 API 호출이 발생하고, 표본을 수집하게 됨
- 1분 뒤 장애 해소되었다면 정상적으로 서킷은 close 됨
- 다음 사이클에 모든 서버가 로컬 캐시 리프레시 되고, 장애가 발생했던 서버로 다시 트래픽을 발생시킴
- why 2 master server?
- canary 배포 방식 때문
- 점진적 배포 방식으로 일시적으로 서버 댓수가 2배가 됨
- old-version -> new-version 트래픽 전환
- 점진적 배포 방식으로 일시적으로 서버 댓수가 2배가 됨
- old-version or new-version 어느쪽으로 트래픽이 가더라도 최소 한 대의 서버는 표본을 수집하기 위해 가장 처음과 마지막에 배포된 서버가 master 서버를 담당
- canary 배포 방식 때문
- reporting
- 트래픽 통계치를 이용해 지난주와 비교해 이번 주 트래픽 증감 수치를 유의미하게 감지 가능
- 개발자의 확인이 필요한 정도의 증감 수치가 있다면 사내 메신저로 알림
- ready
토스 시스템 부하 개선
- 상황
- 마이데이터 API들 중에는 한 번의 마이데이터 서버 API 호출에 n번의 제공 기관 API 호출이 발생하는 API들이 존재
- 대표적으로 자산 등록 API(여러 번의 인증 과정과 자산 정보 호출)
- 제공 기관 의존성이 있는 API의 경우 제공 기관의 응답 속도에 따라서 전체 API 실행 시간이 결정
- 마이데이터 공식 스펙은 타임아웃 20초지만 예외 케이스 존재
- 20초가 지났지만 성공 응답이 온 케이스
- 순차 API 호출 케이스
- 마이데이터 API들 중에는 한 번의 마이데이터 서버 API 호출에 n번의 제공 기관 API 호출이 발생하는 API들이 존재
- 문제
- 마이데이터 서버의 응답 속도는 비대칭적으로 증가
- 자산 등록 API 응답 지연 사례
- 유저로부터 자산 등록 요청 시 인증 기관을 통한 인증 확인, 자산 정보를 가져오기 위한 토큰 발급 요청
- 토큰을 이용해 기관으로부터 자산 목록 호출(잔액, 거래내역 등 상세 정보 fetch)
- 총 77초(토큰 발급 28초, 자산 목록 조회 25초, 상세 정보 조회 24초)
- 1분의 waiting time -> 유저 부정적 경험
- 문제 해결
- 방향
- 웹소켓
- polling
- 웹소켓 결정
- 대용량 트래픽이 발생하는 서버에 잦은 폴링으로 부하를 주는 것보다 효율적이라고 판단
- 기존 시스템
- 당장은 성공 화면을 보여줄 수 없지만 유저가 화면을 이탈한 이후에라도 모든 API가 성공한다면 이후에 다시 유저가 토스 앱에 진입했을 때 성공적으로 연결된 자산을 확인 가능
- (실제로 77초가 걸린 유저도 당시에는 이 앱을 이탈하였지만 이후에 다시 토스 앱에 진입했을 때 API가 모두 성공해서 등록된 자산을 확인 가능)
- 긴 API 처리 시간으로 인해 토스 인프라 공용 리소스 점유 시간이 길어짐
- 토스 앱 - 네트워크 장비 및 게이트웨이 - 마이데이터 서버
- 네트워크 응답 대기 시 커넥션 유지
- 서버 응답 지연 시 토스 트래픽 입구에 가용성 문제 발생
- 실제로 마이데이터 서버는 트래픽이 높기 때문에 제공 기관 응답 지연 시 토스 시스템도 지연
- 신규 시스템(웹소켓)
- 앱 실행 시점
- 토스 앱 실행 시 웹소켓 서버와 커넥션 수립
- 해당 커넥션은 서버 푸시가 필요한 다양한 토스 서비스가 공유해서 사용
- 자산 등록 요청 시점
- 유저가 자산 등록 API 요청 시 마이데이터 서버는 API를 early return하고, 즉시 공용 리소스 해제
- 클라이언트는 웹소켓으로부터 서버 푸시가 오는지 기다림
- 마이데이터 서버 비동기 자산 등록 시작
- 자산 등록(마이데이터 서버)
- 비동기 호출 시작
- 완료된 기관이 생길때마다 웹소켓을 통해서 서버 푸시로 클라이언트에게 통지(화면에서 동적으로 표현할 수 있게 됨)
- 결과
- 공용 리소스 점유 시간 감소(3,300ms -> 58ms)
- 앱 실행 시점
- 방향
피크 트래픽 제어
- 상황
- 2가지 타입의 API
- 유저 액션에 의해 발생하는 유저호출
- 7일에 한 번씩 유저 자산을 토스에서 갱신할 수 있는 배치 호출
- (하루 중 6시간 동안 제공 기관에서 허용한 시간대에만 호출 가능)
- 2가지 타입의 API
- 문제
- 호출 허용 시간대는 짧지만 유저의 수가 많으므로 제공 기관에 높은 트래픽 발생
- 문제 해결
- 방향
- 7일 동안 API를 균일하게 호출
- 방법
- 유저의 사용 패턴
- 유저 호출이 많을 때는 배치 호출을 낮춤, 적을 때는 배치 호출을 높임
- (월급일, 평일 증권 거래 시간 등)
- 호출 유저 수 계산
- A은행 예시로, 하루 실행 유저 수 100만명, 배치 호출 실행대 01~07시
- 하루 시작 시점에 오늘 호출해야 하는 배치 호출 대상 유저의 수 계산
- A은행 호출 시간대에 1분간 실행해야 하는 대상 유저 수 다시 계산
- 이를 통해 제공 기관의 배치 호출 허용 시간대를 분단위로 설정 가능
- 배치 호출은 유저 1명당 n번의 API 호출 발생하므로, 동시 호출 수를 최소화하기 위해 1분간 실행해야 할 유저를 다시 100ms으로 나눠서 실행
- 코루틴 사용
- n개의 기관이 존재하고, 기관마다 호출해야 하는 유저의 수도 다름
- Command 객체로 유저의 범위 표현
- 1분간 n개의 기관을 실행해야 하므로 별도의 코루틴에서 병렬로 실행
- processCommand 메서드에서 2개의 코루틴 실행
- 첫번째는 일정한 간격으로 DB에서 대상자 정보 조회, 두번째는 대상자 정보를 다시 100ms 단위로 나누어 실행
- 2개의 코루틴을 CoroutineChannel을 통해 연결
- 비동기 프로그램이 코루틴 간 데이터를 주고 받을 수 있는 통신 메커니즘
- produceOperator 메서드는 DB에서 대상자 정보를 1초마다 소분해서 가져와 채널로 produce
- 1분 간 가져와야 하는 수가 4만개가 넘으면 슬로우 쿼리가 발생해 영향이 있으므로 소분해서 가져옴
- 60초동안 produce 완료 시 채널은 정상적으로 close
- consumeOperater가 채널로부터 operator 객체를 받아서 100ms 마다 소분해 실행
- coroutine scope 분리 코드 - structured concurrency 특성을 이용해 비동기적으로 실행
- 이후 produce 쪽에서 채널이 클로즈 되게 되면 consumer 쪽에도 for문이 닫히면서 1분간의 배치 호출이 종료
- 유저의 사용 패턴
- 결과
- 7일 동안 발생하는 트래픽의 총량은 같지만 특정일에 과도하게 트래픽이 몰려서 장애가 나는 것을 방지
- 장애 발성 가능성을 낮추고 시스템 안정성을 높임
- 방향
'끄적끄적' 카테고리의 다른 글
향로님의 토크 세미나 돌아보기(스펙터 라운지) (1) | 2024.04.26 |
---|