일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- CU
- 노마드코더
- transaction
- Rails
- Cache
- Python
- 사업
- 레일즈
- 북클럽
- django
- trouble shooting
- redis
- 레일즈 캐시
- rails cache
- 경제
- Race Condition
- HTTP
- redis transaction
- 노개북
- API
- 투자
- 재태크
- memcached
- 주식
- 노마드코드
- iamport
- Watcha pedia
- restful
- 아임포트
- Today
- Total
Stay hungry, Stay foolish
index 페이지 로딩 속도 개선기 (Rails Cache) 본문
현재 운영하고 있는 서비스의 인덱스 페이지가 많이 아팠습니다. 페이지를 렌더링 하는 시간이 프로덕션 기준 7s (로컬에서는 10s 를 넘겼습니다 ㅠㅠ) 를 넘어가는 끔찍한 상태였습니다.
저 정도 로딩 속도면 유저분들 다 도망갔어도 할 말 없습니다... 근본적으로 개선을 하긴 해야겠다 마음먹었습니다.
음.. 근데, 어디가 문제지...?
해결과정
route53, nginx loadbalancer 부터 다 뜯어봐야 하는 건가... 싶어 절망할 뻔했으나 일단 눈에 보이는 것부터 체크하기로 했습니다.
무식하지만 정직하게 체크했습니다. 컴포넌트를 하나씩 빼서 그 부분만 렌더링 시키며 속도를 확인해 보는 방식으로 말이죠.
이런 식으로 컴포넌트를 하나씩 꺼내면서 속도가 유독 느리다 싶은 부분을 확인하다 보니 문제는 비교적 가까운 곳에 단순하게 있더군요.
일단 문제는 크게 세 가지였습니다
1. 불필요한 쿼리 산재
먼저 불필요한 쿼리는 개발 당시에는 필요하던 쿼리였습니다.
대표적으로 맨 위 이미지인 job category 를 조회하는 컴포넌트만 꺼내서 페이지를 로딩하는데 4초가 넘게 걸리더군요
해당 job category 가 만들어질 당시에는 전체 클래스 숫자가 적어서, 대다수의 job category 에 클래스가 존재하지 않았습니다. 그렇다 보니 유저 입장에서 빈 필터를 체크해서 어 클래스가 하나도 없네? 하는 것보다는 클래스가 존재하는 job category 만 보여주도록 쿼리가 존재했던 거고요. 하지만 현재에 와서는 모든 job category에 클래스들이 존재했고 굳이 해당 쿼리를 살려둘 필요가 없었습니다.
해당 쿼리는 날리고 20개 남짓한 job category 는 단순하게 전부 호출시켰습니다.
이렇게만 해줬는데도 해당 컴포넌트의 렌더링 시간이 4.7s -> 0.5s 로 대폭 감소했습니다.
2. caching 미적용
살펴보니 현재 제품에 inmemory db 를 이용한 cache 는 sidekiq message queue 작업을 위한 것뿐이었습니다.
memcached 를 환경설정까지는 해놓으셨지만 현재는 사용되는 부분이 없더라고요.
그래서 index페이지에 존재하는 정적인 partial 페이지들은 rails collection caching 을 사용해 memcached에 올리기로 했습니다.
( 사실 cache 를 적용시키고 추후 새로운 문제들을 만들어 내면서, 다음부터는 무조건 제대로 알 고쓰자...라는 마인드가 생기는 계기가 되었습니다.)
rails caching에 대해 제대로 공부하지 않고 적용시킨 대가는 아래에서 다시 말씀드리겠습니다... 🥲
어쨌거나 위의 1, 2번 문제만 개선했는데도 70%가 넘게 속도가 개선되는 걸 볼 수 있었습니다 ^ㅇ^
3. 로그 모니터링에 지속적으로 보이던 N+1 쿼리들
대표적으로 보이던 N+1중에 하나입니다. 사실 보면서 알고 있었음에도 개선해야 될게 하도 많아 엄두가 안 나고 있었던 와중에 index 페이지를 개선하며 같이 개선했습니다.
N+1 쿼리도 이곳저곳 산재해 있었지만 가장 자주 보이던 위의 경우를 살펴보면,
클래스 카드 컴포넌트에서 review 존재 유무를 체크하고, review count를 하는 과정에서 불필요하게 N+1 쿼리가 호출되는 경우였습니다. 왜인지는 모르겠으나 review 존재 유무 체크 후 review count를 위한 helper 호출 함수 속에서 한 번 더 review 존재 체크, review.sum, review.count 그리고 reviews_count helper method를 따로 한번 더 호출해서 동일한 쿼리를 5번씩 호출하고 있던 상황이었죠.
일단 근본적으로 만들어놓은 helper 함수 한개만 사용하도록 코드를 수정하고 해당 쿼리는 pre_load 시켜서 반복적으로 쿼리가 호출되지 않게 만들어줬습니다.
(-> rails N+1 쿼리 해결법 )
결과
어려운 작업은 없었지만 결과는 드라마틱하긴 했습니다.
(드라마틱하게 또 다른 문제를 만들었다는것만 빼면 말이죠...)
로컬 서버 기준 10.x s -> 2.x s 까지 80% 가까이 속도가 개선 되었습니다.
프로덕션에서도 7.6 s -> 1.7s 까지 속도가 개선된 걸 확인할 수 있었습니다.
또다른 문제 발생...
개선 몇일 후 cs 한 건이 들어왔습니다. 멘토님이 클래스 프로필 카드를 변경했는데 적용이 안된다는것...
뭔가 직감적으로 캐시를 적용시켜놓은게 문제가 됐을거란 생각이 들었습니다.
역시나 cache를 적용시키지 않은 다른 페이지에서는 이미지가 정상 반영되었고, chrome inspect를 키고 확인해보니 변경 전 이미지를 호출하고 있는게 보이더군요. rails 공식문서와 다른 자료들을 꼼꼼히 다시 확인해봤습니다.
문제의 핵심은 제가 적용시켜놓은 cache가 정확하게 뭘 memcached에 올려놓았는지, 해당 cache가 expired 되고 fetch 가 언제 되는지 명확하게 이해하지 않고 냅다 적용시켜놔서 생긴 문제였죠 😭
마지막으로 제가 이 기회에 제대로 학습할 수 있었던 Rails 의 Cache 에 대해 같이 학습하며 마무리 해보겠습니다.
Rails Cache
먼저 제가 사용한 Rails 의 collection cache는
<%= render partial: "classes_card",
collection: @meetings, as: :meeting,
layout: "layouts/grid",
locals: {
ref: "classes_list",
grid_column: "col-50 desktop-20",
},
cached: true
%>
이렇게 작성 할 수 있는데요.
이걸 풀어서 보면 아래와 같다고 할 수 있습니다.
<% @meetings.each do |meeting| %>
<% cache meeting do %>
<%= render partial: "classes_card",
layout: "layouts/grid",
locals: {
ref: "classes_list",
grid_column: "col-50 desktop-20",
}
%>
<% end %>
<% end %>
자 이건 rails 공식문서에서 설명하고 있는 Russiandoll caching 과 같다고 할 수 있습니다.
<% @users.each do |user| %>
<% cache user do %>
...
<%= render user.posts %>
...
<% end %>
<% end %>
=> 이렇게 cache를 거는 user 객체를 상속받는 다른 객체 (post)가 존재할때 그 객체인 post 가 변경 된다 해서 올려놓은 cache 가 변하지는 않습니다. 해당 cache 는 user 객체에 걸려있기 때문에 user 객체에 변경이 있을 때에만 기존 cache 가 expired 되고 fetch 된다고 볼 수 있죠.
따라서 위와 같은 문제를 해결해 주기 위해
class User < ApplicationRecord
has_many :post
end
class Post < ApplicationRecord
belongs_to :User, touch: true
end
post 변경시 User 모델에도 변경이 생기도록 위의 코드처럼 touch 를 모델에 추가하거나
post 객체를 업데이트 하는 로직안에 User에 time stamp 를 찍어주는 코드를 추가하던지 해야합니다.
일단 해당 이슈를 해결하기 위해 필요했던 핵심은
view cache 가 expired 되는 시점.
→ collection cache 에서 collection 을 걸어 놓은 객체들에 변경이 있을 때 cache 가 fetch 된다.
+++
그리고 한가지 더 cs가 들어오기전에 확인 된것이 있었는데, 클래스 카드의 마감시간이 변경되지 않는 것이었습니다.
해당 문제는
-> 클래스 카드의 마감까지 남은 시간은 모델과는 연관 없는 static한 요소이므로 cache의 변경이 적용 되지 않았던 것이었어요.
Rails에서는 cache 에 조건문을 줄 수 있는 방법이 있습니다.
따라서 아래 코드와 같이 클래스 상태가 종료인 클래스에 한해서만 cache를 적용하도록 해주었습니다.
<% @meetings.each do |meeting| %>
<% cache_if meeting.ended?, meeting do %>
<%= render partial: "classes_card",
layout: "layouts/grid",
locals: {
ref: "classes_list",
grid_column: "col-50 desktop-20",
}
%>
<% end %>
<% end %>
이번 이슈로 얻게 된 교훈...
뭐든 새로 적용해보는 기술들 기존에 적용 되어 있던 기술들 잘 알지 못하면 안쓰니만 못하다 😇
왜 되고 왜 안되는지 꼭! 꼭! 알고 쓰도록 하자!
<참고자료>
'트러블슈팅' 카테고리의 다른 글
Zoom ThirdParty API 트러블 슈팅과 회고 (0) | 2022.05.14 |
---|