-
프로젝트 회고 - 스케줄 자동 공개 성능 최적화Spring 2024. 4. 4. 00:44
이번 프로젝트는, 모든 센터의 수업스케줄을 자동으로 특정 시간에 공개해주는 기능 개선 작업이 있었습니다.
제가 맡은 부분은, 수업 스케줄에 대한 자동공개 설정을 그룹핑해서 연결해주는 알고리즘을 개발하는 부분이었습니다.
그런데..!! 배포 후에 다른 개발자가 개발한 부분에 문제가 발생했고, 해당 개발자가 갑작스레 부재중이 되어서 제가 넘겨 받아 빠르게 문제를 해결하게 되는 상황이 발생했습니다.장애 상황
문제는, 사용자가 자동공개로 지정한 시간,
예를들어 다음과 같이 자동공개 시간을 설정한 경우
- 자동공개 시간 : 10시 30분
- 실제 자동공개 시간 : 10시 30분
위 처럼 되어야 하는데,
- 자동공개 시간 : 10시 30분
- 실제 자동공개 시간 : 10시 33분
위와 같이 제시간에 공개되지 않고 2~3분 뒤에 공개되는 문제가 있었습니다.요구사항
우선 요구사항은 정시에 열려야 한다 였습니다. 이유는, 많은 센터의 회원들이 인기있는 수업의 경우 선착순으로 예약하기 위해 대기중이기 때문입니다.
아키텍처의 문제
우선, 문제를 파악해 봤습니다.
문제가 되었던 아키텍처를 분석해 보니 다음과 같은 모습을 가지고 있었습니다.ASIS Flow
1. EventBridge 에서 Cron 식으로 5분에 한번 Lambda로 Trigger
2. Lambda 에서 Batch Server Trigger
3. Batch 서버에서 대상 목록을 Lesson Server 에 http 로 비동기 호출
4. Lesson Server에서 자동 공개 처리문제 요약
- 너무 많은 컴포넌트 호출로 네트워크 지연 유발
- BatchServer 에서 chunkSize(1,000)만큼 읽은 뒤에, 1건 씩 LessonServer에 처리 요청
- 조회 쿼리의 비효율
- 1건씩 업데이트너무 많은 네트워크 호출
첫 번째 문제는 너무 많은 네트워크 통신이 이루어지고 있다는 점입니다.
네트워크 지연으로 지연시간이 증가할 수 있는 포인트가 너무 여러곳에 존재합니다.우선, 네트워크 지연시간이 발생하지 않도록 할 수 있을까 고민해 봤습니다.
이벤트 브릿지와 람다는 단순히, 스케줄링과 트리거 역할만 해주고 있었는데요.차라리, BatchServer에서 자체적으로 스케줄링하면, 네트워크 호출이 발생하지 않겠다 라는 생각을 했습니다.
그리고, 이벤트 브릿지의 크론식은 정확히 5분마다 동작하도록 설정되어 있었지만, 람다 로그를 보니, 항상 40초에 호출을 하고 있는 문제도 있었습니다.
여기서 생각할 것은 반드시 정각에 실행되어야 하는 것인지, 아니면, 주기적으로만 실행하면 되는지였습니다.
해당 작업은 반드시 정시에 실행되어야 하는 요구사항을 가지고 있기 때문에, 처리 시간의 단축과 정확한 시간에 처리되는 것이 우선이었기에, 네트워크 호출 횟수를 최소화 하자는 방안을 먼저 생각했습니다.
- 이벤트 브릿지와 람다의 역할은 BatchServer 자체에서 @Scheduled 를 사용하는 것으로 전환
- LessonServer에 있던 Update 코드는 BatchServer로 옮기는 작업이렇게 하고 나니 다음과 같이 아주 간단한 아키텍처가 만들어졌습니다.
이걸 보면 무언가, 많은 컴포넌트를 복잡하게 사용한다고 해서, 더 성능이 좋아지는 것은 아니라는 것을 알 수 있습니다.
오히려 성능 감소의 요인이 되었고 장애 포인트와 관리 포인트만 더 늘리게 된 상황이었습니다.계속해서 개선한 내용을 정리해 보자면 다음과 같습니다.
조회 쿼리 개선
문제 요약
- 불필요한 조회 컬럼
- 불필요한 테이블 조인필요한 컬럼은 3개 뿐인데, 모든 컬럼(대략10~20여개)를 조회하고 있었습니다.
그리고, 여러개의 테이블을 JOIN 하는 것을 확인했는데, 제가보니 하나의 테이블만 사용하면 되는 상황이었습니다.메모리만 놓고 봐도, 모든 컬럼을 조회해 오는 것은 메모리를 더 많이 사용하게 되고,
더 많은 데이터를 디스크에서 읽어야 하므로 I/O 비용이 증가하게 됩니다.
그리고, 네트워크 대역폭의 사용량이 더 클 수 밖에 없겠죠? 결과적으로 전체적인 응답시간이 늘어날 수 있습니다.최소한의 테이블만 사용해서 최소한의 컬럼만 조회하도록 수정했습니다.
Update 쿼리 개선
문제 요약
- Single Query 로 2,000~3,000번 update여기서 이제 또 하나의 문제는, update를 Single Query 를 사용해서 하나씩 처리하고 있었는데요.
이렇게 하게되면 다음과 같은 문제가 발생할 수 있습니다.
- 각 쿼리를 처리하기 위한 CPU 및 메모리 리소스 낭비
- 네트워크 지연, 커넥션 오버헤드 증가
- 비록 커넥션은 재사용하지만, 각 쿼리마다 커넥션을 할당받고 반환하는 오버헤드 발생실제 테스트 해보면, Single query와 batch Query의 성능차이를 알 수 있습니다.
그래서 이 부분은 Batch Query를 사용해서 처리했습니다.참고로, 너무 많은 수의 Batch Query는 문제가 될 수 있는데요. Batch Size가 너무 크게 되면, 그 만큼 메모리 사용량도 많이 증가하게 됩니다. 또한, update 를 하게 되면, update 대상 레코드는 모두 lock이 걸리게 되는데요.
이렇게 한번에 update 하는 양이 너무 크다보면, lock 을 오래잡고 있어서, 경합을 유발시키고 이런 경합으로 인해 DeadLock까지 발생할 수 있습니다. 이러면, 시스템의 부하 및 성능저하로 이어지겠죠.그래서 적정 chunk size 및 batch size는 각 시스템의 상황에 맞게 선택할 수 밖에 없는 것 같습니다.
정말 아무도 사용하지 않고, 나 혼자만 사용한다고 하면, MySql의 max_allowed_packet 의 값 만큼 batch query를 만들어서 보내면 되겠죠?max_allowed_packet : MySQL 서버가 한 번에 받아들일 수 있는 패킷의 최대 크기(바이트 단위)
그러면, 이제 테스트를 해봐야겠죠?
얼마만큼 테스트 할까?
신규 개편되서 나가는 기능이었기에 몇건정도가 한번에 처리되어야 할까 정확하게 알기란 쉽지 않았는데요.
여러가지 상황을 고려해 봤을 때, 많아야 몇 천건 일거라고 생각이 됐습니다.
많아야 몇 천건 이라고 판단한 근거는 저희 테이블의 스케줄 갯수, 센터 별 스케줄 갯수 등을 대략적으로 조회한 뒤에 예상을 할 수 있었습니다.앞으로 성장하는 서비스라면, 현재는 많아야 몇천건일지라도, 1만건 정도는 아주 빠른 시간안에 처리가 되어야 한다고 생각했습니다.
실무의 비즈니스란, 데이터를 만드는 것도 쉬운일이 아닌데요.
자동공개설정을 하려면, 여러번 클릭 후 여런번 입력해서 데이터를 하나씩 만들 수 있었습니다.그래서 직접 가공하기로 했습니다.
Test Data 준비
일단, 만들어야 하는 테이블의 not null 컬럼들을 정의하고, 조회조건에 사용되는 컬럼들을 정의했습니다.
그리고 무의미한 값이 들어가도 되는 컬럼, 의미있게 값이 들어가야 하는 컬럼을 구분해서,
이에 맞게 값을 넣었습니다.작업은 스프레드 시트를 활용했습니다.
문자열 연결 함수를 사용한뒤, 1만건을 batch query로 만들어서, 직접 insert해서 테스트 했습니다.
참고로 데이터를 만드는 작업은, 30분도 걸리지 않았답니다.
(보통은 '데이터가 없는데 어떻게 테스트 해'라며 10건 정도 만들어서 테스트 하는 경우도 많이 본 것 같습니다.)이렇게 자유자재로 활용할 수 있게 데이터를 만들고, 테스트를 했습니다.
멱등성, 병렬 처리에 대한 고민
그리고 참고로 제가 조회하는 쿼리는 멱등성이 지켜지지 않은 쿼리였습니다.
ThreadPoolTaskExecutor를 사용해서 병렬처리를 하려고 했지만, 이 때, 조회쿼리가 멱등성이 지켜지지 않다보니,
여러 쓰레드가 처리중인 데이터를 계속해서 불러와서, 1만건이 업데이트 되어야 하는데, 4만건이상 업데이트 되는 상황이 발생되는 걸 볼 수 있었습니다.시간.. 조금 더 지켜보고 개선 할 부분
이 부분을 조금 더 성능 개선 시키고 싶었지만, 시간 관계 상 모든 걸 다 할 순 없었고,
지금까지의 개선사항을 통해 2천~3천 건은 1초도 걸리지 않게 되었습니다.
1만 건의 경우, 1.x초~2.x초가 걸리는 걸 볼 수 있었습니다. 이 부분은 조금 더 후에 처리를 해야 할 것으로 보입니다.
특정 센터는 1초 뒤 또는 2초 뒤에 오픈된다는 이야기이니깐요!
(참고로, 현재는 하루에 자동공개가 20-30번 정도 수행 된다면, 이 때, 가장 많이 처리되는 건수는 0~1번 정도 천건 ~ 3천건 정도가 처리 됩니다.)만약 한번에 처리해야 되는 크기가 더 커진다면, 전체 대상 쿼리를 여러개로 나눠서 병렬처리를 진행해야 할 것 같습니다.
흠.. 지금 생각으로는 센터의 갯수를 나눠서 조회하면 되지 않을까 싶은데, 이렇게 하면 정확하게 분할하는 것은 아니라 더 생각을 해봐야 할 것 같습니다.아! 왜 LIMIT으로 하면 되는데 라고 생각할 수도 있는데요. LIMIT 0-10000, 10001-20000 이런식으로 하게 되면, 결국 LIMIT의 첫번째 숫자인 10001번까지 찾게됩니다. 그러니, 숫자가 커지면 커질수록 결국 성능은 더 느려질 수밖에 없거든요. 분할해서 읽는 것이 아닌, 다음 페이지들은 더 오랜 시간을 들여서 읽겠다와 같기 때문에, LIMIT으로 해결해야겠단 생각을 하지 않았습니다.
장애복구 처리
반드시 정시에 열려야 합니다. 만약 정시에 열리지 않는다면 강성CS가 되겠죠?
이 때, 모든 것을 다 커버할 순 없겠지만, 어느정도 발생 가능한 시나리오를 생각해서 조치를 했습니다.
우선 저희는 너무 많은 비용을 인프라에 쓸 수 없기 때문에, Batch 서버를 한대만 사용중에 있습니다.서버가 한대이니, 다운타임이 존재하겠죠?
자동공개되어야 하는 시간에 배포를 해버리는 경우 서버가 잠시 죽게 됩니다. 이 때가 자동공개 시간이라면, 자동 공개는 되지 않을 것이고 강성 CS로 번질 수 있습니다. 이런 상황에서는 조금이라도 빨리 자동공개를 해줘야 합니다.그래서 생각한 것이, 서버 부팅시에 직전 자동공개가 실행되지 않은 경우 처리해 주자 였습니다.
참고로 배포에 걸리는 시간은 50초 내외로 소요되고, 다운타임은 10초~20초 내외 입니다.간단하게 CommandLineRunner를 활용해서, 서버 부팅시에, 최근 5분 간격 시간에 실행되지 않은 Job이 있는지 확인해서, 실행되지 않은 Job이 있는 경우 처리되도록 코드를 짰습니다.
알림 모니터링
또한, 이 외에, 서버가 비정상적으로 죽어서, 살아나지 않은 경우. 이 경우는 계속 처리되고 있지 않을텐데요.
이 때를 대비해서, 별도 프로젝트에서 모니터링을 해서 알림을 받아야 겠다. 라고 생각했습니다.그래서 다음 처럼 AdminServer에서 RDS에 있는 SpringBatch MetaDataTable을 조회해서, 처리되지 않은 내역을 계속 확인해서 슬랙으로 알림을 보냅니다.
처리되는 방식은 다음과 같습니다.
BatchServer와 동일한 시간에 Scheduling 됩니다.
Thread.sleep(1_000) 을 Loop로 5번 돌면서 확인하게 됩니다. 이 때, 5번 돌때까지 처리되지 않은 내역이 있다면, slack으로 알림 메세지를 발송합니다. 내용에는 수동처리 방법이 기재되어 있는 Confluence 링크를 포함합니다.5번을 도는 이유는, 1초도 안되서 처리 되어야 정상인데, 5초나 걸렸다는 것은, 앞으로 실패할 것이다. 아니면, 현재 실패중이다. 로 볼 수도 있습니다.
알림 감지 시, 수동 처리를 위한 페이지로 바로 이동해서 직접 처리를 해줘야 합니다. SQL 구문을 직접 입력 하는 방법이요.
사실 이 부분도 자동화가 되어야 하지 않을까 싶었습니다. 결국 사람이 하게 되면 실수하게 되거든요.
그리고 수동으로 데이터를 업데이트 하게 되면, SpringBatch MetaDataTable에는 기록이 남지 않게 됩니다.
결국, 이 부분은 정말 서버를 살릴 수 없는 지경에 이르렀을 때, 무언가라도 해야 할 때를 대비하기 위함 이라고 보면 될 것 같습니다.최종 구성도 마치며..
크게 보면, 아키텍처의 변경이 있었고, 작게 보면, 이곳 저곳 코드의 수정도 있었습니다.
작은 것들과 큰것들의 변경을 통해서 많은 성능 개선이 되었는데요.2,000~3,000건을 한번에 자동 공개 할 때, 2~3분 걸리던 것이, 지금은 1초내에 실행되고 있습니다.
참고로, 시작시간과 완료시간은 batch_job_execution 테이블의 START_TIME과 END_TIME 입니다.
아주 빠르게 처리해야 하다보니, 개선 포인트마다의 성능 차이를 알 수 있게 남기진 못한 점이 조금 아쉬웠습니다.
다음번에는 하나하나 비교한 것들을 모두 기록으로 남기면 좋을 것 같네요.!'Spring' 카테고리의 다른 글
엔티티의 생성 및 수정 시간 자동화하기 (0) 2024.04.09 프로젝트 회고 - 수업 스케줄 일괄 변경 배치 처리 (0) 2024.04.08 Spring Version에 맞는 h2Database 설치 (0) 2023.04.16 @Transactional 제대로 알고 쓰기 (0) 2021.11.18 web.xml 에 선언된 context-param, init-param 테스트 (0) 2021.06.30