redis를 활용한 조회수 기능 개선기

redis를 활용한 조회수 기능 개선기

Tags
데이터베이스
redis
Published
Jan 31, 2022
Property
💡
본 글의 코드는 실제와는 다르게 임의로 수정, 각색되어 있습니다.

배경

서버에 부담 안가는 이벤트는 없는걸까
서버에 부담 안가는 이벤트는 없는걸까
연휴시즌을 맞아 대규모 이벤트를 하는 도중 서비스가 다운되는 것으로 문제는 시작되었다.
이 정도 규모의 PV가 처음이기도 했고, 단순히 페이지에 접속만 하면 되는 이벤트였기 때문에 이유를 알지 못했다.
쿼리 캐싱은 물론, 페이지 캐싱도 하고 있었으며 캐싱에는 아무 문제가 없는 것으로 확인이 되었다.
 

원인이 무엇일까

해당 이벤트 페이지에는 페이지 조회수 기능 이라는 암살자가 있었다.
페이지의 조회수를 알기 위해서 페이지에 접근할 때마다 조회수를 업데이트해주는 기능이다.
그리고 이 조회수는 단순히 MySQL update로 관리되고 있었다.
이 코드는 쇼핑몰 솔루션의 레거시였고, 그 동안 기획전 이벤트 페이지에는 순간적으로 유저가 몰리지 않았었기 때문에 문제가 드러나지 않았던 것이다.
 

좀 더 자세히 알아보자

  • DB I/O가 너무 많았다.
    • 순간 최대 WriteIOPS 1.79K를 기록했다.
      모니터링 당시 CPU 크레딧(BurstBalance)은 100%에 가까운 상태였다.
      (그래프를 보니) 서버는 크레딧을 급속도로 소모한 뒤, 장렬하게 사망하였다.
  • 그래서 해당 테이블의 인덱스 레코드에 데드락이 발생했다.
    • 조회수 관련 쿼리는 다음과 같았다.
      UPDATE board SET view_count = view_count + 1 WHERE no = ? 
      이 쿼리를 풀면 다음과 같다.
      BEGIN;
       SELECT @var := view_count FROM board WHERE no=? LOCK IN SHARE MODE;
       UPDATE board SET view_counter = @var + 1 WHERE no=?;
      COMMIT;
      1) view_count 컬럼 값을 읽기 위해 해당 record에 Shared Lock을 건다.
      2) 게시물의 조회수를 업데이트하기 위해 해당 record에 Exclusive Lock이 걸린다.
      2) Excluseive Lock 상태에서는 다른 작업에서 Exclusive Lock을 걸 수 없다. 이 상태에서 다른 작업이 들어오게 되면, 데드락이 발생한다.
      순간적으로 많은 유저가 접근하였을 때 데드락이 더욱 쉽게 일어난다.
      row lock: 테이블의 row에 걸리는 lock record lock: index에 걸리는 lock
       

해결방법

💡
조회수 기능을 배치 처리를 하도록 개선하자
조회수 기능은 즉시 반영이 필요할까?
조회수는 실시간성이 보장되어야 하는 민감한 정보는 아니었다.
조회수 업데이트 작업을 10분에 1회 가량 배치로 처리해준다면 매우 크게 부하를 분산할 수 있다.
 

고려사항

  • 당시 서비스에서는 캐시 DB로 memcached와 redis를 사용하고 있었다.
    • → 단순 카운터로는 둘 다 선택이 가능했다.
      redis가 메뉴얼이 잘 나와있었고 추후 지원이 잘 될 것으로 보았다.
      또한 데이터 타입이 다양하기 때문에 확장성 측면에서 redis를 선택하였다. (set 자료형이 있다!)
  • Codeigniter에 내장된 redis 라이브러리는 기능이 적고 메뉴얼이 거의 없었다.
    • → phpredis라는 라이브러리로 레디스를 이용하였다.
 

구현

redis view counter 라는 키워드로 검색해보니 많은 예제들을 확인할 수 있었다.
💡
Redis 공식 문서에는 명령어의 시간 복잡도가 함께 나와있는데, 이를 고려하여 key를 설계하면 좋다.
  1. redis key별(게시물 no별) 조회수를 증가시킨다.
    1. 레디스에서는 이를 카운터 패턴이라고 한다.
      key: 게시물 글 번호, value: 조회수
      redis> incr view_counter:board:item:145
  1. cronjob으로 10분에 1번 조회수를 flush한다.
    1. # 조회수 적용
      */10 * * * * /usr/bin/php /home/crewbi/html/app/index.php view_counter_flush
      당시 서비스는 cron을 사용하여 잡 스케쥴링을 진행하고 있어서, cron script를 작성해주었다.
      Spring에서는 Spring Scheduler를 사용하여 간단히 대체할 수 있을 것 같다.
 

+ 하는 김에 다른 기능에도 적용해보자

(중복 없이) 해당 상품을 조회한 사람을 기록하고 싶다면, 조회 시마다 set add를 해주면 된다.
key: 상품번호, value: 상품을 조회한 유저들의 id
redis> sAdd view_counter:good:item:13201
redis> sAdd view_counter:good:item:13201
1) 391212
2) 606115
3) 314221
4) 12421
5) 606523
6) 605434
해당 상품을 조회한 유저의 아이디가 담긴 set이 저장된다.
 

레디스의 캐시 전략

캐시 전략에 대하여 잘 정리 된 글들이 많다.
본 글에서처럼 캐시에 먼저 데이터를 쓰고, 약간의 지연 후에 데이터를 모아서 다시 DB에 쓰는 캐싱 방식을 Write-Back이라고 한다.
 

마치며..

redis는 이 외에도 다양한 용도로 활용 가능하다.
다양한 사례를 살펴보고 서비스를 운영할 때 부하 분산이 필요한 곳에 적용해주자.
점진적으로 적용이 가능하기 때문에 빠르게 성장하고 있는 서비스에 적용하기 매우 용이하다.
 

참고