캐싱 가이드라인

이 문서는 GitLab에서 사용 중인 다양한 캐싱 전략, 효과적으로 구현하는 방법 및 다양한 함정에 대해 설명합니다. 이 자료는 훌륭한 캐싱 워크샵에서 추출되었습니다.

캐시란 무엇인가요?

데이터를 더 빠르게 저장하는 장치로, 다음과 같습니다:

  • 컴퓨팅의 여러 영역에서 사용됨.
    • 프로세서에는 캐시가 있고, 하드 디스크에도 캐시가 있으며, 다양한 장치에 캐시가 있습니다!
  • 최종 목적지에 가까운 위치에 있음.
  • 데이터를 단순하게 저장하는 장치.
  • 일시적인 것입니다.

빠른 것은 무엇인가요?

모든 웹 페이지의 목표는 100ms 미만으로 반환하는 것이어야 합니다:

  • 이것은 달성 가능하지만, 현대 어플리케이션에서 캐싱이 필요합니다.
  • 큰 응답은 더 오랜 시간이 걸리며, 캐싱은 일정한 속도를 유지하는 데 중요합니다.
  • 캐시 읽기는 일반적으로 하위 1 ms입니다. 이것이 향상되지 않는 것은 거의 없습니다.
  • 첫 페이지 로드에서 빠른 것만으로 충분하지 않습니다. 초기 경험도 중요하기 때문에, 이것은 완전한 해결책이 아닙니다.
  • 사용자별 데이터는 이 도전과 이 속도 목표를 충족시키기 위한 기존 어플리케이션의 리팩토링에서 가장 큰 도전을 제시합니다.
  • 사용자별 캐시는 여전히 효과적일 수 있지만, 그것은 사용자 간에 공유되는 일반적인 캐시보다 적은 캐시 히트를 가져옵니다.
  • 우리의 목표는 항상 페이지 로드의 대다수를 캐시에서 가져오는 것입니다.

왜 캐시를 사용해야 하나요?

  • 더 빨리 만들기 위해서!
  • IO를 피하기 위해서.
    • 디스크 읽기
    • 데이터베이스 쿼리
    • 네트워크 요청
  • 동일한 결과를 반복해서 다시 계산하는 것을 피하기 위해서:
    • 뷰 렌더링
    • JSON 렌더링
    • Markdown 렌더링
  • 여분 제공하기. 어떤 경우에는 다른 곳의 장애를 감추는 데 캐싱이 도움이 될 수 있습니다. (예: CloudFlare의 “Always Online” 기능)
  • 메모리 소비 감소. Ruby에서 작업을 적게 수행하고 큰 문자열을 가져오기
  • 돈을 절약하기. 특히, 처리기가 RAM에 비해 비싼 클라우드 컴퓨팅에서!

캐싱에 대한 의문점

  • 일부 엔지니어들은 캐싱을 마지막 수단으로 제외하고, 실제 해결책은 기존 코드를 더 빨리 만들도록 개선하는 것이라고 생각합니다.
  • 이것은 시간이 지남에 따라 캐시 만료에 대한 두려움으로 기인할 수 있습니다, 이해할 수 있습니다.
  • 하지만, 캐싱은 여전히 빠릅니다.
  • 참된 성능을 얻기 위해 두 가지 기술 모두를 사용해야 합니다:
    • 예를 들어, 초기 쓰기가 너무 느려서 시간 초과되면 캐싱하는 것은 의미가 없습니다.
    • 그러나 캐싱이 성능 향상을 가져오지 않는 경우는 거의 없습니다.
  • 그러나 완전한 해결책이 몇 달이 걸리는 반면, 캐싱은 하루만에 구현할 수도 있으므로 완전히 캐싱을 사용할 수 있습니다.

GitLab에서의 캐싱

Redis 캐싱에는 단점이 있지만, GitLab 어플리케이션 및 GitLab.com의 캐싱 설정을 효과적으로 활용할 수 있어야 합니다. 우리의 캐시 활용을 위한 예측에 따르면 충분한 여유 공간이 있습니다.

워크플로우

방법론

  1. 최종 사용자에 가까운 곳에서 가능한 한 자주 캐시하십시오.
    • 뷰 렌더링을 캐시하는 것이 훨씬 가장 성능 향상이 좋습니다.
  2. 가능한 한 많은 사용자의 데이터를 최대한 캐시하려고 노력하십시오:
    • 일반적인 데이터는 모든 사람을 위해 캐시할 수 있습니다.
    • 새로운 기능을 구축할 때 이것을 염두에 두고 있어야 합니다.
  3. 캐시 데이터를 최대한 유지하려고 노력하십시오:
    • 만료 기간을 유지하기 위해 중첩된 캐시를 사용하십시오.
  4. 가능한 적은 캐시 요청을 수행하십시오:
    • 이렇게 하면 네트워크 문제로 인한 가변적 지연이 감소합니다.
    • 캐시에서의 각 읽기의 오버헤드가 줄어듭니다.

캐싱이 어떤 것에 이점을 제공하는지 확인하기

추가되는 캐시가 “가치 있는”가요? 이것이 측정하기 어려울 수 있지만, 고려할 수 있는 사항은 다음과 같습니다:

  • 캐시된 데이터의 크기는 얼마나 큰가요?
    • 이것은 어떤 종류의 캐시 저장소를 사용해야 하는지(예: 큰 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할 때 주의할 사항 몇 가지:
      • tail -f log/development.log | grep "cache hits"
      • tail -f log/development.log | grep "Rendered "
  • 올바른 위치에서 찾고 있다면:
    • 원인을 찾을 때까지 코드 섹션을 제거하거나 주석 처리하십시오.
    • binding.pry를 사용하여 라이브 요청에서 조사해보세요. 이것은 Foreground 웹 프로세스가 필요합니다.

확인

  • 특히 다음 대시보드의 Grafana:
  • 로그
    • Grafana 차트가 필요로 하는 것을 커버하지 못할 때는 Kibana를 대신 사용하십시오.
  • 기능 플래그:
    • 캐시를 추가할 때 거의 항상 기능 플래그를 사용하는 것이 좋습니다.
    • 켜고 끄고를 번갈아가며 Grafana의 꾸불꾸불한 선들을 확인하세요.
    • 캐시가 따뜻해짐에 따라 처음에는 응답 시간이 늘어날 것으로 예상됩니다.
    • 그 효과는 플래그를 100%로 실행할 때까지 명확하지 않습니다.
  • 성능 막대:
    • 이를 로컬에서 사용하고 Redis 목록에서 캐시 호출을 확인하세요.
    • 또한 이를 프로덕션 환경에서 사용하여 캐시 키가 예상대로 작동하는지 확인하세요.
  • Flamegraphs:
    • 페이지 뒤에 ?performance_bar=flamegraph를 추가하세요.

캐시 레벨

고수준

  • HTTP 캐싱:
    • ETag 및 만료 시간을 사용하여 브라우저에게 자체 캐시된 버전을 제공하도록 지시합니다.
    • 이것은 여전히 Rails에 영향을 주지만 뷰 레이어는 생략됩니다.
  • 반대 프록시 캐시의 HTTP 캐싱:
    • 위와 같지만 public 설정이 있습니다.
    • 브라우저 대신, 이는 반대 프록시(예: NGINX, HAProxy, Varnish)에 캐시된 버전을 제공하도록 지시합니다.
    • 후속 요청은 결코 Rails에 영향을 주지 않습니다.
  • HTML 페이지 캐싱:
    • HTML 파일을 디스크에 작성합니다.
    • 웹 서버(예: NGINX, Apache, Caddy)가 Rails를 건너뛰고 HTML 파일 자체를 제공합니다.
  • 뷰 또는 액션 캐싱
    • Rails가 전체 렌더링된 뷰를 캐시 저장소에 작성하고 다시 제공합니다.
  • 조각 캐싱:
    • Rails 캐시 저장소에 뷰의 일부를 캐시합니다.
    • 캐시된 부분은 뷰에 렌더링됩니다.

저수준

  1. 메서드 캐싱:
    • 동일한 메서드를 여러 번 호출하지만 값을 한 번만 계산합니다.
    • Ruby 메모리에 저장됩니다.
    • @article ||= Article.find(params[:id])
    • strong_memoize_attr :method_name
  2. 요청 캐싱:
    • 웹 요청의 기간 동안 키에 대해 동일한 값을 반환합니다.
    • Gitlab::SafeRequestStore.fetch
  3. 읽기-쓰기 SQL 캐싱:
    • 데이터베이스 앞에 위치한 캐시.
    • Rails는 동일한 쿼리에 대해 요청 내에서 이를 수행합니다.
  4. 특이한 캐시
  5. 한 가지 사용 사례에 대한 매우 특정한 캐시.

Rails에 내장된 캐싱 도우미

이것은 Rails 가이드에서 잘 문서화되어 있습니다.

  • HTML 페이지 캐싱 및 액션 캐싱은 더 이상 기본적으로 포함되어 있지 않지만 여전히 유용합니다.
  • Rails 가이드에서는 HTTP 캐싱을 조건부 GET라고 부릅니다.
  • Rails의 캐시 저장소에 대해 두 가지 매우 중요하고 거의 동일한 메서드를 기억하세요:
    • 거의 모든 곳에서 사용할 수 있는 cache와 거의 유사한 별칭인:
    • Rails.cache.fetch.
  • cache에는 뷰 파일을 수정할 때 변하는 “템플릿 트리 다이제스트”가 포함되어 있습니다.

Rails 캐시 옵션

expires_in

이것은 캐시 항목의 TTL(Time To Live)을 설정하고 가장 유용하며 가장 일반적으로 사용되는 캐시 옵션입니다. 이것은 대부분의 Rails 캐싱 도우미에서 지원됩니다.

race_condition_ttl

이 옵션은 동일한 시간에 키에 대한 여러 가지 미캐시된 요청을 방지합니다. 키가 만료된 것을 처음 찾은 프로세스가 해당 금액으로 TTL을 증가시키고 새 캐시 값을 설정합니다.

키가 매우 무거운 부하를 받을 때 여러 동시 라이트를 방지하기 위해 사용되지만 10초와 같이 낮은 값으로 설정해야 합니다.

언제 HTTP 캐싱을 사용해야 하는가

전체 응답이 캐시할 수 있는 경우 조건부 GET 캐싱을 사용하세요:

  • 공개 캐시를 사용하지 않을 때 개인 정보 누출 위험이 없습니다. 사용자가 브라우저에서 볼 수 있는 것을 그 사용자만 캐시합니다.
  • 폴링되는 엔드포인트의 경우 특히 유용합니다.
  • 좋은 예:
    • 우리가 업데이트를 폴링하는 토론 목록. updated_at 값을 etag로 사용합니다.
    • API 엔드포인트.

view 또는 action 캐싱을 사용하는 시점

이것은 더 이상 레일즈(Rails)의 핵심에서 지원되지 않는다:

  • 레일즈 핵심에서 이에 대한 지원이 제거되었다.
  • 보통 리버스 프록시 캐싱이나 조건부 GET 응답을 살펴보는 것이 더 좋다.
  • 그러나 디스크에 쓰지 않고 HTML 페이지 캐싱을 흉내 내는 비교적 간단한 방법을 제공하며, 이는 클라우드 환경에서 유용하다.
  • 캐시 저장소에 상당히 큰 마크업 청크를 저장한다.
  • 우리는 이에 대한 사용자 정의 구현을 API에서 사용할 수 있으며, cache_action에서 더 유용하다.

fragment 캐싱을 사용하는 시점

언제나!

  • 레일즈에서 사용하는 가장 유용한 캐싱 유형으로, 뷰의 섹션, 전체 부분, 부분 집합을 캐시할 수 있어서 널리 사용된다.
  • 부분 집합의 렌더링은 cached: true를 사용하는 것을 목표로 설계되어야 한다.
  • 부분 내부가 아닌 렌더링 호출 주변을 캐시하는 것이 빠르지만, 그러면 템플릿 트리 다이제스트가 손실되어 해당 부분을 업데이트할 경우 캐시가 자동으로 만료되지 않는다.
  • 루프 내에 캐시 호출을 넣는 등 여러 캐시 호출을 도입하는 것에 주의해야 한다. 때로는 피할 수 없지만, 부분 집합 캐싱과 같은 이를 피하는 옵션이 있다.
  • 뷰 렌더링 및 JSON 생성은 느리기 때문에 가능한 곳마다 캐시해야 한다.

method 캐싱을 사용하는 시점

  • 인스턴스 변수 또는 StrongMemoize를 사용하라.
  • 동일한 값이 요청 내에서 여러 번 필요할 때 유용하다.
  • 동일한 키에 대해 여러 번 캐시 호출을 방지하는 데 사용될 수 있다.
  • 테스트 스위트에서 종종 나타나는데, 값이 reload를 호출할 때까지 변경되지 않는 ActiveRecord 객체에서 문제를 일으킬 수 있다.

request 캐싱을 사용하는 시점

  • 메서드 캐싱과 유사한 사용 패턴이지만 여러 메서드를 통해 사용될 수 있다.
  • 요청 기간 동안 무언가를 저장하는 표준화된 방법이다.
  • (GitLab 구현에서) 조회가 캐시 조회와 유사하기 때문에 양쪽에 동일한 키를 사용할 수 있다. 이것이 Gitlab::Cache.fetch_once의 동작 방식이다.

가능한 단점

  • Gitlab::Cache::JsonCacheGitlab::SafeRequestStore를 사용하여 캐시된 객체에 새로운 속성을 추가하는 것은 캐시 데이터가 새로운 속성에 적합한 값을 가지지 않을 수 있어서 (지금까지의 사건 참조) 잘못된 데이터 문제로 이어질 수 있다.

SQL 캐싱을 사용하는 시점

레일즈는 요청에서 동일한 쿼리에 대해 자동으로 이를 사용하므로 그 경우에는 동작이 필요하지 않다.

  • 그러나 identity_cache와 같은 젬을 사용하는 경우: 여러 요청에 걸쳐 쿼리를 캐싱하는 것이 다른 목적이 있다.
  • Article.find(params[:id])와 같이 단일 객체 조회에는 사용하지 않는 것이 좋다.
  • 때로는 결과를 사용하지 못하는 경우가 있는데, 이는 읽기 전용 객체를 제공하기 때문이다.
  • 또한 관계를 캐시할 수 있으며, 필터링이나 다른 방식으로 정렬하는 데 관심이 없지만 특정 목록을 반환하려는 상황에서 유용하다.

새로운 캐시를 사용하는 시점

다른 옵션을 고갈시켰고 실제로 어렵다면, 정말 어색한 것을 캐시해야 하는 경우에 맞춤 솔루션을 살펴보자:

  • GitLab에서의 예제로는 RepositorySetCache, RepositoryHashCache, AvatarCache 등이 있다.
  • 가능하면 사용자 정의 캐시 구현을 만들지 않는 것이 좋다. 일관성을 해치기 때문이다.
  • 굉장히 효과적일 수 있다. 예를 들어 merged_branch_names 주변의 캐싱은 RepositoryHashCache을 사용한다.

캐시 만료

Redis가 키를 만료하는 방법

간단히 말하자면, 가장 오래된 것은 새로운 것으로 대체된다:

  • Redis를 LRU 캐시로 구성하는 데 도움이 되는 기사.
  • 다양한 캐시 만료 전략에 대한 많은 옵션이 있다.
  • 기능적으로 Memcached와 유사한 allkeys-lru를 원할 것이다.
  • Redis 4.0 이후에는 allkeys-lfu를 사용할 수 있다, 이는 유사하지만 다르다.
  • 지금은 Redis가 직접 메모리를 회수할 수 있도록 UNLINK을 통해 명시적으로 삭제할 것이기 때문에 DEL 대신에 UNLINK를 사용한다는 점에 주목하자.
    • 이것은 키를 삭제로 표시하고 나중에 실제로 삭제된다는 것을 빨리 성공적인 값을 반환하지만 표시됨으로 표시한다.

레일즈가 키를 만료하는 방법

  • 레일즈는 명시적 삭제보다 TTL 및 캐시 키 만료를 사용하는 것을 선호한다.
  • 캐시 키에는 기본적으로 뷰에서 fragment 캐싱할 때 템플릿 트리 다이제스트가 포함되어 있어서 템플릿의 모든 변경 사항이 자동으로 캐시를 만료시킨다는 것을 보장한다.
    • 그러나 이는 도우미에서는 정확하지 않다는 경고가 있다.
  • 레일즈에는 ActiveRecord 객체에서 두 가지 캐시 키 메서드가 있다: cache_key_with_versioncache_key. 5.2 버전 이후에는 기본적으로 첫 번째가 사용되며, 이전부터의 표준 동작이다. 기본적으로 이 메서드는 키에 updated_at 타임스탬프를 포함한다.

캐시 키 구성 요소

application.log에서 발견된 예시:

cache(@project, :tag_list)
views/projects/_home_panel:462ad2485d7d6957e03ceba2c6717c29/projects/16-20210316142425469452/tag_list
  1. 뷰 이름 및 템플릿 트리 다이제스트 views/projects/_home_panel:462ad2485d7d6957e03ceba2c6717c29
  2. 모델 이름, ID 및 updated_atprojects/16-20210316142425469452
  3. 우리가 전달한 심볼을 문자열로 변환 tag_list

찾아보기

  • 사용자별 데이터
    • 가장 중요합니다!
    • 이것은 항상 명확한 것은 아닙니다, 특히 뷰에서는.
    • 캐시하려는 영역에서 사용된 모든 헬퍼 메서드를 조사해야 합니다.
  • 시간별 데이터, 예를 들어 “Billy가 8분 전에 이것을 게시했습니다”.
  • 업데이트되는 레코드지만 updated_at 필드를 트리거하지 않는 경우
  • Rails 헬퍼는 템플릿 다이제스트를 뷰의 키에 포함시키지만, 이는 헬퍼 등의 다른 곳에서는 발생하지 않습니다.
  • Grape::Entity는 API 레이어에서 효과적인 캐싱을 굉장히 어렵게 만듭니다. 나중에 자세히 다루겠습니다.
  • 뷰에서의 fragment 캐시 헬퍼 안에서 break 또는 return을 사용하지 마십시오 - 이는 캐시 항목을 절대로 작성하지 않습니다.
  • 이전 데이터를 반환할 수 있는 캐시 키의 아이템 순서 변경:
    • 예를 들어, 두 값이 nil을 반환할 수 있고 이들을 교환하는 경우.
    • 대신 { project: nil }과 같은 해시를 사용하십시오.
  • Rails는 배열의 멤버에 #cache_key를 호출하여 키를 찾지만, 해시의 값에 대해 호출하지는 않습니다.