캐싱 지침
이 문서는 GitLab에서 사용 중인 다양한 캐싱 전략, 효율적인 구현 방법 및 다양한 주의 사항에 대해 설명합니다. 이 내용은 훌륭한 캐싱 워크숍에서 추출되었습니다.
캐시란 무엇인가요?
데이터에 대한 빠른 리포지터리로, 여러 영역에서 사용됩니다.
- 프로세서에는 캐시가 있고, 하드 디스크에도 캐시가 있으며, 여러 가지에 캐시가 있습니다!
- 종종 데이터를 최종 목적지에 가까이 두는 것입니다.
- 데이터에 대한 간단한 리포지터리입니다.
- 일시적입니다.
빠르다는 것은 무엇을 의미하나요?
모든 웹 페이지의 목표는 100ms 미만으로 반환하는 것이어야 합니다.
- 이것은 달성 가능하지만, 현대 애플리케이션에서 캐싱이 필요합니다.
- 더 큰 응답은 더 오래 걸리며, 캐싱은 일정한 속도를 유지하는 데 중요합니다.
- 캐시 읽기는 일반적으로 하위 1밀리초 미만입니다. 이로 인해 거의 모든 것이 개선됩니다.
- 초기 경험도 중요하므로 반복적인 페이지 로드에서 빠르다고 좋은 것은 아닙니다.
- 사용자별 데이터는 이를 어렵게 만들며, 이 속도 목표를 충족하기 위해 기존 애플리케이션을 개선하는 데 가장 큰 어려움을 제시합니다.
- 사용자별 캐시는 효과적일 수 있지만, 사용자 간에 공유되는 일반적인 캐시보다 적은 캐시 히트를 가져옵니다.
- 우리는 항상 페이지 로드의 대다수를 캐시에서 끌어오기를 목표로 합니다.
왜 캐시를 사용해야 하나요?
- 더 빠르게 만들기 위해!
- IO를 피하기 위해:
- 디스크 읽기
- 데이터베이스 쿼리
- 네트워크 요청
- 동일한 결과를 여러 번 다시 계산하는 것을 피하기 위해:
- 뷰 렌더링
- JSON 렌더링
- Markdown 렌더링
- 여분의 제공. 경우에 따라 캐싱은 CloudFlare의 “항상 온라인” 기능과 같이 다른 곳의 실패를 가리는 데 도움이 될 수 있습니다.
- 메모리 사용량을 줄이기 위해. 루비에서의 처리를 줄이고 큰 문자열만 가져오기
- 돈을 절약하기 위해. 특히 프로세서가 RAM에 비해 비싼 클라우드 컴퓨팅에서는 더욱 그렇습니다.
캐싱에 대한 의문점
- 일부 엔지니어들은 캐싱을 마지막 수단으로만 사용해야 한다고 반대하며, 이를 해킹으로 간주하고 진정한 해결책은 기본 코드를 더 빠르게 개선하는 것이라고 생각합니다.
- 이는 캐시 만료에 대한 두려움으로부터 유발될 수 있습니다.
- 그러나 캐싱은 여전히 더 빠릅니다.
- 참한 성능을 달성하려면 두 가지 기술을 모두 사용해야 합니다:
- 예를 들어 초기 쓰기 작업이 너무 느려서 시간 초과되는 경우 캐싱하는 것은 의미가 없습니다.
- 그러나 캐싱이 성능 향상을 가져오지 않는 경우는 드뭅니다.
- 그러나 완전한 솔루션에 몇 달이 걸리는 경우에도 캐싱을 빠른 해킹으로 완전히 사용할 수 있습니다.
GitLab에서의 캐싱
Redis 캐싱에 단점이 있지만, GitLab 애플리케이션 내부와 GitLab.com에서 캐싱 설정을 적극적으로 활용해도 좋습니다. 저희의 캐시 활용을 위한 예측에 따르면 충분한 여유 공간이 있다는 것을 보여줍니다.
워크플로우
방법론
- 가능한 한 최종 사용자에게 가능한 가까운 위치에서 캐시 사용.
- 뷰 렌더링을 캐싱하는 것이 훨씬 성능 향상이 됩니다.
- 가능한 한 많은 사용자의 데이터를 최대한 많이 캐시:
- 일반적인 데이터는 모든 사람을 위해 캐시할 수 있습니다.
- 새로운 기능을 만들 때 이 사항을 염두에 두어야 합니다.
- 캐시 데이터를 최대한 유지하려고 노력:
- 만료 후에도 최대한 많은 캐시된 데이터를 유지하기 위해 중첩 캐시를 사용합니다.
- 가능한 한 적은 요청을 캐시에 하기:
- 네트워크 문제로 인한 가변적인 대기 시간을 줄입니다.
- 캐시에서의 각 읽기의 오버헤드를 줄입니다.
캐싱의 이점을 식별
캐시가 추가되는 것이 “가치 있는지” 확인해야 합니다. 이를 메트릭하는 것은 어려울 수 있지만 다음을 고려할 수 있습니다:
- 캐시된 데이터의 크기는 어떻습니까?
- 이것은 큰 HTML 응답을 RAM이 아닌 디스크에 저장하는 등 특정 유형의 캐시 리포지터리를 사용해야 할 수 있습니다.
- 캐싱으로 인해 얼마나 많은 I/O, CPU 및 응답 시간이 절약되는가?
- 캐시된 데이터가 크지만 렌더링하는 데 걸리는 시간이 짧을 경우, 큰 텍스트 청크를 페이지에 덤프하는 것과 같이 이를 캐싱하는 것이 가장 좋을 수 있습니다.
- 이 데이터는 얼마나 자주 액세스되나요?
- 자주 액세스되는 데이터를 캐싱하는 것이 효과가 크다.
- 이 데이터는 얼마나 자주 변경되나요?
- 캐시가 다시 읽히기 전에 캐시가 회전되면 이 캐시는 실제로 유용한 것인가요?
도구
조사
- 성능 막대는 로컬 및 프로덕션에서 조사할 때 첫 번째 단계입니다. 비싼 쿼리, 과도한 Redis 호출 등을 찾아보세요.
- Flamegraph 생성: URL에
?performance_bar=flamegraph
를 추가하여 시간이 소비되는 메서드를 찾아보세요. - Rails 로그 자세히 살펴보기:
- 부분적 렌더링의 렌더 시간에 주목하세요.
- 응답 시간만 메트릭하려면
jq
를 사용하여 JSON 로그를 구문 분석할 수 있습니다:tail -f log/development_json.log | jq ".duration_s"
tail -f log/api_json.log | jq ".duration_s"
-
development.log
를 꼬리치기할 때 주의해야 할 사항 몇 가지:tail -f log/development.log | grep "cache hits"
tail -f log/development.log | grep "Rendered "
- 올바른 곳을 찾았다면:
- 원인을 찾을 때까지 코드 부분을 제거하거나 주석 처리합니다.
- 라이브 요청에서 둘러보려면
binding.pry
를 사용합니다. 이를 위해서는 전경 웹 프로세스가 필요합니다.
검증
- 특히 다음 대시보드에서 Grafana:
- 로그
- Grafana 차트가 필요로 하는 상황에 대비해 Kibana를 대신 사용하세요.
- 피처 플래그:
- 캐시를 추가할 때 거의 항상 피처 플래그를 사용하는 것이 좋습니다.
- 이를 0% 및 100%로 토글하고 Grafana에서 변동성있는 선을 확인하세요.
- 캐시가 워밍될 때 초기에 응답 시간이 늘어나는 것에 대비하세요.
- 효과는 100%에서 플래그를 실행하여야만 명확해집니다.
- 성능 막대:
- 로컬에서 사용하고 Redis 디렉터리에서 캐시 호출을 찾습니다.
- 캐시 키가 예상대로인지 프로덕션에서도 사용하세요.
- Flamegraph:
- 페이지에
?performance_bar=flamegraph
를 추가하세요.
- 페이지에
캐시 레벨
상위 수준
- HTTP 캐싱:
- ETag 및 만료 시간을 사용하여 브라우저에 자체 캐시된 버전을 제공하도록 지시합니다.
- 여전히 Rails를 탭니다만, 뷰 레이어를 건너뛸 수 있습니다.
- 역방향 프록시 캐시의 HTTP 캐싱:
- 이는 브라우저를 대신하여 역방향 프록시(예: NGINX, HAProxy, Varnish)에 캐시된 버전을 제공하는 것입니다.
- 다음 요청에서는 결코 Rails에 닿지 않습니다.
- HTML 페이지 캐싱:
- 디스크에 HTML 파일을 쓰기
- 웹 서버(예: NGINX, Apache, Caddy)가 Rails를 건너뛰고 HTML 파일 자체를 제공합니다.
- 뷰 또는 액션 캐싱
- Rails가 전체 렌더링된 뷰를 캐시 리포지터리에 기록하고 다시 제공합니다.
- 프래그먼트 캐싱:
- Rails 캐시 리포지터리에 뷰 부분을 캐시합니다.
- 캐시된 부분이 렌더링될 때 뷰에 삽입됩니다.
Low level
-
Method caching:
- 동일한 메서드를 여러 번 호출하나 값은 한 번만 계산됩니다.
- Ruby 메모리에 저장됩니다.
@article ||= Article.find(params[:id])
strong_memoize_attr :method_name
-
Request caching:
- 웹 요청의 기간 동안 키에 대해 동일한 값을 반환합니다.
Gitlab::SafeRequestStore.fetch
-
Read-through 또는 write-through SQL caching:
- 데이터베이스 앞단에 위치한 캐시입니다.
- Rails는 동일한 쿼리에 대해 동일한 요청 내에서 이 작업을 수행합니다.
- 새로운 캐시 방법.
- 특정 사용 사례를 위한 초특화 캐시.
Rails 내장 캐싱 도우미
이 내용은 Rails 가이드에서 상세히 설명되어 있습니다.
- HTML 페이지 캐싱 및 액션 캐싱은 더 이상 기본 포함되지 않았지만 여전히 유용합니다.
- Rails 가이드에서는 HTTP 캐싱을 “조건부 GET”이라고 부릅니다.
- Rails의 캐시 스토어에서 두 가지 매우 중요하고 거의 동일한 메서드를 기억하세요:
- 뷰에서 사용되는
cache
, 거의Rails.cache.fetch
의 별칭입니다. -
cache
에는 “템플릿 트리 다이제스트”가 포함되어 있어 뷰 파일을 수정할 때마다 변경됩니다.
- 뷰에서 사용되는
Rails 캐시 옵션
expires_in
이 옵션은 캐시 항목의 Time To Live (TTL)을 설정하며, 가장 유용하고 자주 사용되는 캐시 옵션입니다. 대부분의 Rails 캐싱 도우미에서 지원됩니다.
race_condition_ttl
이 옵션은 동일한 키에 대해 여러 개의 캐시 misses를 방지합니다. 키가 만료된 첫 번째 프로세스는 TTL을 이 값만큼 증가시키고, 그런 다음 새로운 캐시 값을 설정합니다.
이 옵션은 캐시 키가 매우 빈번하게 사용될 때 여러 동시 쓰기를 방지하는 데 사용되지만, 10초와 같은 낮은 값을 설정해야 합니다.
HTTP 캐싱을 사용해야 하는 경우
전체 응답이 캐시될 수 있는 경우 조건부 GET 캐싱을 사용합니다:
- 공용 캐시를 사용하지 않을 때 개인 정보 노출 위험이 없습니다. 사용자가 볼 수 있는 것만 캐시됩니다.
- 폴링을 받는 포인트에서 특히 유용합니다.
- 좋은 예시:
- 업데이트를 위해 폴링하는 토론 디렉터리.
updated_at
값에 대한etag
를 사용합니다. - API 엔드포인트.
- 업데이트를 위해 폴링하는 토론 디렉터리.
가능한 단점
- 사용자 및 API 라이브러리는 캐시를 무시할 수 있습니다.
- 때때로 Chrome이 캐시를 이상하게 처리하는 경우가 있습니다.
- 개발 모드에서 존재를 잊고 변경 사항이 표시되지 않으면 화가 날 수 있습니다.
- 이론적으로 조건부 GET 캐싱을 어디서나 사용하는 게 의미가 있다고 보지만, 실제로는 때로 이상한 문제를 일으킬 수 있습니다.
뷰 또는 액션 캐싱을 사용해야 하는 경우
Rails 세계에서는 이제 이 기능을 많이 사용하지 않습니다:
- 이 기능은 Rails 코어에서 제거되었습니다.
- 보통 역방향 프록시 캐싱이나 조건부 GET 응답을 살펴보는 것이 더 좋습니다.
- 그러나 디스크에 쓰지 않고 HTML 페이지 캐싱을 모방하는 상당히 간단한 방법을 제공하므로 클라우드 환경에서 유용합니다.
- 캐시 스토어에 상당히 큰 마크업 청크를 저장합니다.
- 사용 사례가 더 중요한 API에서 사용되는 한 우리는 사용 가능한지라도 이 기능의 사용이 유용합니다.
cache_action
에는 실제로 이에 해당하는 사용자 정의 구현이 있습니다.
단편 캐싱을 사용해야 하는 경우
항상 사용하세요!
- 아마 Rails에서 가장 유용한 캐싱 유형으로, 뷰의 섹션, 전체 파셜, 파셜 모음을 캐시할 수 있습니다.
- 파셜의 렌더링을 주의 깊게 고려하여
cached: true
를 사용하도록 작성되어야 합니다. - 파셜 주위에 캐시를 추가하는 것이 파셜 내부에 캐시를 넣는 것보다 빠릅니다. 그러나 이렇게 하면 템플릿 트리 다이제스트를 잃어버리게 되어 해당 파셜을 업데이트하면 캐시가 자동으로 만료되지 않습니다.
- 루프 안에 캐시 호출을 놓는 등 많은 캐시 호출을 도입하는 것은 조심해야 합니다. 때로는 피할 수 없지만 부분 모음 캐싱과 같은 옵션이 있습니다.
- 뷰 렌더링과 JSON 생성은 느리며 가능한 곳마다 캐시해야 합니다.
메서드 캐싱을 사용해야 하는 경우
- 인스턴스 변수 또는
StrongMemoize
를 사용하세요. - 동일한 값이 요청 내에서 여러 번 필요할 때 유용합니다.
- 동일한 키에 대해 여러 번의 캐시 호출을 방지하는 데 사용할 수 있습니다.
- 값이 변경되지 않은 ActiveRecord 객체에서
reload
를 호출하기 전까지 값이 변경되지 않는 문제를 일으킬 수 있습니다. 이는 보통 테스트 스위트에서 발생합니다.
요청 캐싱을 사용해야 하는 경우
- 메서드 캐싱과 유사한 사용 패턴을 가지지만 여러 메서드에 걸쳐 사용할 수 있습니다.
- 요청의 기간 동안에 저장하는 표준화된 방법입니다.
- (GitLab 구현에서처럼) 조회가 캐시 조회와 유사하기 때문에 두 가지에 동일한 키를 사용할 수 있습니다. 이것이
Gitlab::Cache.fetch_once
의 작동 방식입니다.
가능한 단점
-
Gitlab::Cache::JsonCache
및Gitlab::SafeRequestStore
를 사용하여 캐시된 객체에 새로운 속성을 추가하는 경우 캐시 데이터가 새로운 속성에 적합한 값을 가지지 않아 캐시된 데이터에 문제가 발생할 수 있습니다. 이에 대한 예시는 지난 문제에서 확인할 수 있습니다.
SQL 캐싱을 사용해야 하는 경우
Rails는 동일한 쿼리에 대해 요청 내에서 자동으로 이 작업을 수행하므로 그 use case에 대해 추가 작업이 필요하지 않습니다.
- 그러나
identity_cache
와 같은 젬을 사용하면 여러 요청에 걸쳐 쿼리를 캐시할 수 있습니다. -
Article.find(params[:id])
와 같이 개별 객체 조회에는 사용하지 마세요. - 때로는 결과를 사용하는 것이 불가능할 수 있으며, 읽기 전용 객체를 제공합니다.
- 관계도 캐시될 수 있어 필터링이나 정렬을 다르게 사용할 필요가 없는 상황에서 유용합니다.
신제품 캐시를 사용해야 하는 경우
기타 옵션을 모두 고려한 뒤에 매우 복잡한 캐시가 필요한 경우 커스텀 솔루션을 검토할 시간입니다:
- GitLab에는
RepositorySetCache
,RepositoryHashCache
,AvatarCache
와 같은 예시가 있습니다. - 가능하다면 일관성을 유지하기 위해 사용자 정의 캐시 구현을 만들지 않는 것이 좋습니다.
- 매우 효과적일 수 있습니다. 예를 들어 RepositoryHashCache를 사용한
merged_branch_names
주변의 캐싱입니다.
캐시 만료
Redis가 키를 만료하는 방법
간단히 말하면, 가장 오래된 것은 새로운 것으로 대체됩니다:
- LRU 캐시로 Redis를 구성하는 유용한 문서입니다.
- 다양한 캐시 제거 전략을 위한 여러 옵션이 있습니다.
- 기능적으로 Memcached와 유사한
allkeys-lru
를 원할 것입니다. - Redis 4.0부터 allkeys-lfu를 사용할 수 있습니다만, 약간 다릅니다.
- 이제 우리는
DEL
대신UNLINK
를 사용하여 모든 명시적인 삭제를 처리하고 Redis가 자체적으로 메모리를 회수할 수 있도록 합니다.- 이는 키를 삭제로 표시하고 나중에 실제로 삭제합니다.
Rails가 키를 만료하는 방법
- Rails는 명시적인 삭제보다 TTL 및 캐시 키 만료를 선호합니다.
- 캐시 키에는 뷰에서 단편 캐싱을 할 때 기본적으로 템플릿 트리 다이제스트가 포함되어 있어 템플릿 변경이 자동으로 캐시를 만료시킵니다.
- 하지만 도우미에서는 이런 기능이 없으니 주의하십시오.
- Rails에는 ActiveRecord 객체에 대한 두 가지 캐시 키 메서드가 있습니다:
cache_key_with_version
및cache_key
. 첫 번째는 5.2 버전부터 기본적으로 사용되며 이전의 표준 동작 방식이고 키에updated_at
타임스탬프를 포함합니다.
캐시 키 컴포넌트
application.log
에서 발견된 예시:
cache(@project, :tag_list)
views/projects/_home_panel:462ad2485d7d6957e03ceba2c6717c29/projects/16-2021031614242546945
2/tag_list
- 뷰 이름과 템플릿 트리 다이제스트
views/projects/_home_panel:462ad2485d7d6957e03ceba2c6717c29
- 모델 이름, ID 및
updated_at
값projects/16-20210316142425469452
- 문자열로 변환된 전달한 심볼
tag_list
찾아보세요
- 사용자별 데이터
- 이것이 가장 중요합니다!
- 이것은 항상 명백하지 않으며 특히 뷰에서는 명확하지 않습니다.
- 원하는 영역에서 사용되는 모든 도우미 메소드를 순회해야 합니다.
- “빌리가 8분 전에 이것을 게시했습니다.”와 같은 시간별 데이터.
- 레코드가 업데이트되지만
updated_at
필드가 변경되지 않는 경우 - Rails 도우미는 템플릿 다이제스트를 뷰의 키에 결합하지만, 이는 헬퍼와 같은 다른 곳에서는 발생하지 않습니다.
-
Grape::Entity
는 API 레이어에서 효율적인 캐싱을 매우 어렵게 만듭니다. 이에 대해서는 나중에 더 알아보겠습니다. - 뷰의 단편화된 캐시 도우미 내에서
break
또는return
을 사용하지 마세요. 캐시 항목을 쓰지 않습니다. - 오래된 데이터를 반환할 수 있는 캐시 키 내에서 항목을 재정렬하세요:
-
nil
을 반환할 수 있는 두 값을 가지고 순서를 바꾸는 것과 같은 경우. - 대신
{ project: nil }
과 같은 해시를 사용하세요.
-
- Rails는 키를 찾기 위해 배열의 멤버에 대해
#cache_key
를 호출하지만, 해시의 값에 대해서는 호출하지 않습니다.