Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

강의 OPEN시 수업 정보와 수강생 정보의 Redis 캐싱 및 활용 구현 #62 #66

Merged
merged 26 commits into from
Aug 23, 2023

Conversation

binary-ho
Copy link
Owner

@binary-ho binary-ho commented Aug 23, 2023

What is this Pull Request about? 💬

학생이 출석 시도시, 수강 신청 데이터가 많으면 데이터를 가져오는데에 엄청난 시간이 소요되는 것을 확인했다,
(1000건 동시요청시 10만건에선 아예 600건 이상 타임아웃)
그래서 인덱싱을 통해 해결을 시도하니 약 6.5초만에 전부 성공했다 -> #57

그러나, 6.5초도 여전히 유저 입장에선 여전히 느리다고 생각되어, 다양한 커버링 인덱싱을 고려해보았다. 하지만, 인덱싱을 위해 저장해야 하는 데이터가 아주 많았다. Lecture 객체 전체와 Enrollment 객체의 대부분을 저장해야 했다.

그런데, 잘 생각해보니, 캐싱이 필요한 상황은 강사가 수업을 연 이후였다. 이에, 강의를 열 때 수강생 정보를 캐싱을 해보기로 했다


1. 계기

우리 서비스에서 짧은 시간에 많은 트래픽이 몰리는 기능은 "출석" 기능이다.
출석은 아래와 같은 과정을 통해 이루어진다.

  1. 강사가 수업을 열고 출석 번호를 발급 받는다. (OPEN)
  2. 수강생들은 자신이 수강중이면서 (신청 승인 받음), OPEN 상태인 강의들을 확인한다
  3. 학생은 출석 번호를 입력해 출석을 시도한다.

이떄 강사의 작업을 제외하자면, 학생들은 한번의 출석에서 여러번의 DB I/O를 발생시킨다.

  1. 로그인 하기 - READ
  2. 자신이 출석 가능한 강의 찾기 - READ
  3. 현재 OPEN된 수업의 출석 번호와 자신이 제출한 출석 번호가 맞는지 확인하기 - READ
  4. 맞는 경우 출석하기 - WRITE

3번 과정은 이미 구현할 당시 부터 Redis에 캐싱해 둠으로써 Read과정을 줄였다.

문제는 2번 과정이였다. 수강신청 건수가 10만건이고, 1000명이 동시에 요청할 떄 (동시성 프레임워크) 30초 이상이 소요되었는데, 기본 Time Out 시간에 걸린 것이다. 그리고 총 650건 실패했다.

이 문제를 해결하기 위해 인덱싱을 걸으니, 6.5초대로 개선되었다. 수강신청 건수가 10만건이 아닌 수천건 밖에 안 될 때도 3초 이상이 걸렸다.

이런 응답시간은 유저의 입장에서 답답할 수 밖에 없다.

그래서 캐싱을 도입했다.

2. 캐싱 대상

수업을 OPEN할 시, 원래는 출석번호만 캐싱했다. 이젠, 아래와 같은 데이터를 캐싱한다

  1. 수업 정보 (출석 번호 포함) -> hash를 이용해 저장
  2. 수강생 정보 (학생이 key, 출석 가능한 수업의 집합이 Set)

추가적으로, 강사가 학생의 수강신청을 승인해 줄 때도, 이미 강의가 열려있다면 캐싱해준다.

3. 캐싱 이후의 출석 과정

  1. 수강생들은 출석 가능한 강의를 확인할 때, 캐시 데이터 먼저 확인한다.
  2. 있는 경우 캐싱된 열린 강의 정보에서 정보를 가져온다.
  3. 없는 경우 DB에서 직접 데이터를 가져온다.
  4. 출석 번호를 입력하면 캐싱된 출석 번호와 대조하여 맞는지 확인한다.
  5. 맞은 경우 출석 처리된다.
    • 출석이 가능한 상태에서 새로 수강 승인되는 학생의 경우 승인 이후 바로 캐싱된다.

4. 수업 정보는 같은 트랜잭션, 학생 정보는 비동기 자식 트랜잭션으로 구현한 이유

수업 정보는 수업을 여는 트랜잭션과 같은 트랜잭션 하에 진행된다.

왜냐하면 수업 열기가 실패하는 경우 캐싱은 필요 없고, 수업 정보엔 출석 번호가 포함되는데, 이 출석 번호가 없다면 출석할 수 없기 때문에, 캐싱이 실패했을 때도 수업 열기가 함께 실패해야 하기 때문이다.

그런데, 학생 정보는 Event를 이용해 캐싱했다.
그 이유는 이 방법이 내 의도와 가장 잘 맞으면서도 간단했기 때문이다.
보통 의존성을 끊는 방법으로 Event가 쓰이곤 하는데, 내가 쓴 이유는 수강생 캐싱이 아래와 같이 동작하길 바랬기 때문이다.

  1. 강사의 강의 Open은 캐싱과 무관하게 빠르게 응답 받아야 한다.
    강사는 강의를 Open하고 출석 번호를 발급 받는 것이 API를 호출하면서 기대하는 바이기 때문이다
  2. 강의 Open이 실패하면 함께 실패해야 한다.
  3. 그러나 수강생 캐싱이 실패하는 경우 강의 Open까지 실패할 필요는 없다.

그래서 Event를 이용하면 위 구현이 쉬웠다.

Event를 발행해 처리하고, 비동기적으로 처리되게 하였으며, 새로운 트랜잭션을 만들되, 강의 Open이 실패하는 경우 이루어지지 않도록 쉽게 처리할 수 있었다.

5. 구현과 설정에 대한 고민

5.1 현재 동시성 문제가 발생하는 부분이 있다

나는 학생별 강의 데이터를 저장할 때, 하나인 경우 String 이후엔 Set으로 구현하고 있었다. 이는 실수가 없는 한 학생이 동시에 출석 가능한 강의는 1개이고 (대부분이 값을 1개만 가짐) 메모리를 아껴야 하기 때문에 이렇게 구현했는데, Type별로 가져오는 방식이 달라, 타입을 확인하고 가져오는 사이에 타입이 변하여 에러가 발생했다. 1만건당 1 ~ 2건 꼴이지만, 누락되면 사실상 학생은 그 강의에 아예 출석이 불가능하므로, 누락되선 안 된다. 이에 대한 고민은 여기 새로운 이슈에서 -> #65

5.2 eviction 정책은 volitle ttl 로 한다.

앱에서 다양한 종류의 캐싱을 하지만, 가장 큰 용량을 차지하는 것은 이 수강생 정보이다. 문제는 수강생 정보는 사용되지 않은 데이터 일수록 오히려 사용 확률이 높다는 점이다 이는 기존의 캐싱 정책들과 반대이다
왜냐하면 한번 출석한 학생은 오히려 또 출석할 일이 없고, 출석하지 않은 학생이 새로 출석할 가능성이 높기 떄문이다. 그래서 레디스에 이 상황에 100% 맞는 적절한 정책이 없다. (보통은 자주 쓰이는 데이터를 더 살려 놓는다)
그나마 적절해 보이는 것이 volitle-ttl이다. (정책이 아예 없는 경우엔 새로운 저장이 안 된다.) 그래서 volitle-ttl을 선택했다.
문제는 앱에서 캐싱하는 다양한 데이터 중에 캐싱된 데이터가 수강생 데이터가 제일 사라져도 문제가 없는 데이터란 것이고, 다른 데이터들은 사라지는 경우 문제가 된다.

이제까지 사라져선 안 되는 데이터들은 제한 시간이 실제 비즈니스적으로도 존재하는 데이터들이여서 비즈니스적인 제한 시간과 redis exprire time을 똑같이 맞췄다. 이젠, 이 데이터들의 exprire time을 2배로 늘리고, 따로 유효 시간을 저장하는 방식을 고려해봤다.
그러니까 위험을 대비해 저장 용량을 늘리게 되는 것이다.
이 방법은 기존의 일반적인 캐싱과 같은 상황에선 비싼 메모리 자원을 더 소모하는 것임으로 문제가 되겠지만, 우리 서비스에선 이런 방식으로 추가 메모리를 사용해 저장하는 자료가 적음으로 고려할만해 보인다. 만약 용량이 그리 넉넉하지 않았더라면, 유효시간을 대폭 줄이는 방법으로 바꿨을 것이다. -> 또다른 이슈가 필요하다

3. 강의를 OPEN 중에 새로운 학생이 승인될 수 있다

(출석 가능한 학생이 동적으로 생겨날 수 있음)
-> 강사가 학생을 승인했을 때, 강의가 이미 열려있다면 레디스에 캐싱한다.

4. redis transaction support

redis template에서 관련 설정을 true로 설정해야 /@transactional 을 붙인 메서드에서 RedisTemplate를 사용할 때, 우리가 평소에 DB를 사용할 때와 같이 동작한다. (하나의 트랜잭션으로 묶이고, 롤백시 롤백 등)

6. 적용 결과

image

  1. 인덱스 X 수행 : 30초 이상 (Time Out) + 650건 이상 실패
  2. 인덱스를 활용 : 약 6.4638 초 소요 + 실패 0건
  3. 캐싱 활용 : 약 0.1434초 소요
  4. 이벤트 수신 외의 모든 테스트 구현 및 변화에 맞게 변경

7. 추가된 새로운 객체들

  1. OpenLecture : 현재 열려있는 강의를 의미한다
  2. Attendee : 수강생을 의미한다

@binary-ho binary-ho merged commit 989945a into develop Aug 23, 2023
@binary-ho binary-ho deleted the feature/student-caching-#62 branch September 13, 2023 07:38
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant