5P by GN⁺ | ★ favorite | 댓글 1개
  • 캐시는 데이터베이스 부하를 줄이기 위해 들어오지만, Redis처럼 쓰기 쉬운 도구는 시간이 지나며 영속 저장소처럼 의존되기 쉬움
  • 문제는 Redis의 영속성 기능이 아니라, 처음에는 휘발성 캐시로 도입된 구성 요소가 애플리케이션의 핵심 상태와 엮이는 운영 흐름임
  • memcached는 공식 정의부터 분산 메모리 객체 캐싱 시스템이고 디스크 저장을 전제로 하지 않아, 상태 없는 캐시 워크로드로 다루기 쉬움
  • 여러 memcached 인스턴스는 서버가 아니라 클라이언트가 URL 목록과 키 해시로 나누며, 장애 노드는 해셔에서 빠졌다가 나중에 재연결을 시도함
  • “데이터베이스가 느리다”는 이유로 캐시부터 추가하기보다, 먼저 느린 쿼리와 누락된 인덱스를 확인해야 함

Redis가 캐시에서 저장소로 변하는 순간

  • 인프라를 운영하다 보면 “캐시가 필요하다”는 요구가 자주 나오고, 익숙하고 기능이 많은 Redis가 먼저 떠오르기 쉬움
  • Redis 홈페이지는 AI 앱용 실시간 컨텍스트 엔진인 Redis Iris를 전면에 내세우지만, Redis가 수익을 내야 하는 회사라는 점에서는 이해 가능한 방향임
  • Redis를 배포하고 연결 문자열을 넘기면 처음에는 신뢰할 수 있는 캐시처럼 동작함

몇 달 후 발생하는 문제

  • 시간이 지나면 cache.set("key", "value")INSERT INTO table VALUES ('key', 'value')보다 훨씬 단순해, 사람들이 Redis를 다음처럼 취급하기 시작함
    • 항상 존재하는 구성 요소로, 데이터를 보존하는 장소. 사실상의 데이터베이스
    • REmote DIctionary Server를 휘발성 캐시가 아닌 영구 저장소로 인식하게 됨
  • 당신도, 당신의 운영자팀 동료들도 이 사실을 모르며, 캐시를 휘발성으로 가정할 것 이라고 생각하기 때문에 알림(alerting) 시스템도 이를 감지하지 못함
    • 업그레이드, 노드 이전, 또는 RAID0 서버 HDD 트레이가 빠지는 사고 등 Redis에 무언가를 했을 때 비로소 문제가 드러남
  • 핵심 문제는 Redis에 영속성 기능이 없다는 점이 아니라, 캐시로 도입된 Redis를 사람들이 캐시처럼 다루지 않는다는 가정의 어긋남에 있음
  • 뒤늦게 의존성을 발견하면 Redis가 애플리케이션에 너무 깊게 엮여 제거하기 어렵고, 결국 “애완동물”처럼 유지보수하고 모니터링해야 함

memcached가 캐시 역할에 더 직접적인 이유

  • memcached는 “무료 오픈소스, 고성능, 분산 메모리 객체 캐싱 시스템”이며, 데이터베이스 부하를 줄여 동적 웹 애플리케이션을 빠르게 하기 위한 범용 캐시임
  • Django처럼 플러그형 캐싱을 지원하는 프레임워크에서는 캐싱 백엔드를 바꿀 수 있음
  • Redis보다 기능이 적어도 memcached를 선택할 만한 이유는 운영 특성이 단순하기 때문임
    • 다운타임 처리가 쉬움: 클라이언트 라이브러리가 연결 예외를 무시하는 경우가 많고, 단순 get은 서버가 내려가도 기본값이나 None을 반환할 수 있음
    • memcached는 클러스터링 기능이 내장되어 있지 않아 오히려 클러스터링이 편리함
      • 클라이언트 라이브러리에 여러 URL을 설정하면 키 해시로 대상 인스턴스를 고름
      • 클라이언트 호출이 인스턴스 다운을 감지하면 해셔에서 노드를 제거하고, 일정 시간이 지난 뒤 자동으로 재연결을 시도함
    • 영속성 부담이 구조적으로 줄어듦: memcached는 디스크에 저장하지 않아 상태 없는 워크로드로 원하는 곳에 스케줄링하기 좋음
  • Redis로도 비슷한 운영 방식을 만들 수는 있지만, memcached의 아키텍처는 이런 방향에 더 가까워 캐시로 다루기 직관적
  • memcached는 비교적 단순한 애플리케이션이고, 약 64MB 캐시 크기의 인스턴스를 수십 개 실행해도 오버헤드가 거의 없다는 점이 선택 이유가 됨
  • 많은 “데이터베이스가 느리다” 문제는 실제로 느린 쿼리나 누락된 인덱스에서 시작하므로, 캐시 추가와 함께 쿼리 최적화도 봐야 함
  • memcached의 설계 결정이 궁금하면 memcached blog에서 흥미로운 글을 많이 볼 수 있는데, 그 중 하나는 5월에 올라온 “답변 받는 데 정말 얼마나 걸리는 거죠?(How Long Does That Response Take… For Real?)”

댓글과 토론

Hacker News 의견들
  • Redis는 훌륭한 기술이지만 영속 데이터 구조휘발성 캐시라는 서로 다른 두 역할을 동시에 잘하려다 어려움을 겪는다고 봄
    실제 Redis에서도 둘은 잘 섞이지 않아서, 영속성은 전역으로 켜거나 끄는 식임
    순수 캐시에는 memcached나 동등한 것을 쓰고, 점수판 같은 데이터 구조가 필요할 때만 영속성을 켠 Redis를 쓰겠음
    $WORK에서는 둘 다 도입하지 않았고, 느린 작업용 캐시 계층은 파일시스템과 키-값 저장소처럼 쓰는 DB 테이블 양쪽에 데이터를 둠
    DB는 thundering herd를 조율하는 데 도움이 되고, 같은 서버의 읽기는 파일시스템만 치며, 다른 서버의 읽기는 DB를 한 번 본 뒤 파일시스템에 유지함
    파일시스템 계층을 memcached로 바꿀 수도 있지만 지금까지는 아주 잘 동작하고 있음

    • 2000년대 후반에 Memcachedb(memcache + 영속성용 bdb)를 다뤄본 뒤 거의 같은 결론에 도달했음
      Redis가 기능은 확실히 더 많았고, antirez도 매력적이면서 놀랄 만큼 겸손한 인물이라 Redis가 더 인기 있어진 이유는 이해됨
      그래도 내게 memcached는 항상 지루한 기술을 선택하라의 정점이었음
      플랫폼 엔지니어로서는 둘 다 지원할 수 있지만, 개발자가 Redis의 고급 기능인 영속성, 복제, 클러스터링을 쓰기 시작하면 그 결정의 단점을 제대로 이해했는지 확인하려고 함
    • DB 테이블을 키-값 저장소처럼 쓰고 파일시스템을 곁들이는 것만으로도, 전용 캐시 저장소를 세팅하는 비용을 치르기 전에 할 수 있는 일이 정말 많음
      이런 해법을 제안할 때마다 캐시는 반드시 전용 저장소에 있어야 한다고 느끼는 미숙한 사람들과 엔지니어링 현장에서 수없이 싸워왔음
  • memcache라고 해서 이런 문제를 피할 수 있는 것은 전혀 아님
    2000년대 중반에 memcache를 쓰는 확장 시스템을 다뤘는데, 개발자들이 글에서 Redis 사례로 든 문제와 똑같은 함정에 빠졌음
    memcache로 분산 시스템의 법칙을 우회하려 했고, 캐시 중독 때문에 memcache가 켜져 있다는 전제로 서버군 크기를 잡았다가 장애가 나면 갑자기 DDoS처럼 터졌음
    어떤 호스트가 TPS가 높은 키를 날리면 다른 모든 호스트가 그 키를 다시 채우려고 의존 서비스를 때리는 쓰기 증폭도 있었고, 핫 키가 핫 호스트를 만들었으며 memcached를 서비스 데몬과 함께 올려서 정체불명의 CPU 스파이크로 이어졌음
    오래된 DNS 항목의 고착성 때문에 memcache 호출이 블랙홀로 빠지기도 했음
    모두 memcache를 더 잘 쓰면 피할 수 있었지만, 남용의 유혹이 너무 강했음

  • 글쓴이가 언급한 Redis/Valkey 문제는 프로덕션에서 거의 다 본 것 같음
    Valkey에 메모리 정책이 없어 메모리를 전부 먹고 append-only file 쓰기 오류를 만든 장애가 있었고, 디스크 자체가 꽉 차서 AOF 쓰기가 실패한 경우도 있었음
    Redis가 살아 있고 실행 중이며 모든 사용자 데이터로 채워져 있다고 완전히 기대하면서, 느린 경로로 되돌아가는 장치가 없어 500 오류가 난 적도 있음
    정렬 집합과 다른 데이터 구조를 창의적으로 쓰면서 그 집합이 절대 축출되지 않는다는 데 의존한 경우도 있었음
    현장 관찰에도 불구하고 Redis보다 memcache를 먼저 추천하기는 여전히 어렵다고 봄
    memcache 친화적인 캐시 배치를 갖도록 앱을 설계하는 일이 까다로울 수 있고, 충분히 큰 팀이 memcache를 쓰면 결국 Redis가 필요해지는 길을 찾을 가능성이 매우 높음
    그러면 캐시 기술을 2개 유지해야 함

    • 누군가 Redis를 캐시가 아닌 용도로 쓰기로 하면 사실상 캐시 기술 2개를 가진 셈임
      캐시용으로 설정한 Redis 인스턴스는 다른 목적에 쓸 수 없고, 캐시 인스턴스에는 축출이 있어야 하며 비캐시 인스턴스에는 축출이 없어야 함
      결국 설정이 다른 두 번째 Redis가 필요함
      솔직히 앱을 memcache 친화적인 캐시 배치로 설계하는 일은 Redis 친화적인 캐시 배치로 설계하는 것과 같음
      이런 애플리케이션 캐시의 패턴은 동일해서, 가져오고 없으면 계산해서 설정하는 방식임
    • 이미 없다면 추상화 인터페이스를 만들어서, 키를 요청하면서 캐시 미스 때 원천에서 값을 가져올 비동기 함수나 람다를 넘기는 편임
      var value = cache.lookup( keyname, () => db.query(...), TimeSpan.FromMinutes(5) // or CacheOptions );
      이렇게 하면 캐시 미스 때 바로 대체 경로로 가거나 삽입할 수 있음
    • 캐시 기술 2개를 유지하지 않는다는 건 언제나 이기는 논리임
  • memcache에서 잘 언급되지 않는 또 다른 특징은 모든 연산이 설계상 O(1) 이라는 점임
    저자들이 의식적으로 고른 설계라 제약은 있지만, 단순 연산에서 무작위로 멈추는 일이 없도록 보장함
    반면 Redis는 단일 스레드 코어 설계라 임의 복잡도의 연산을 실행할 수 있고, 개발자 입장에서는 그걸 쓰며 똑똑해진 느낌이 들겠지만 그 연산이 끝날 때까지 나머지가 모두 기다리게 됨

  • 오픈소스 프로젝트나 장기 유지되는 프로그램에서는 이런 일이 자주 생김
    코드베이스가 커지면 원래 계획에 없던 것들을 결국 지원하기 시작함
    기능이 많아지면 사용자도 늘고, 어떤 사람은 예전 기능만 쓰고 어떤 사람은 새 기능을 받아들이며, 결국 특정 값이 사실상의 기본값이 되어 더는 선택 사항처럼 보이지 않게 됨
    Redis를 예로 들면 AOF를 끄면 휘발성 인메모리 캐시로 동작하지만, 대부분은 그렇게 생각하지도 않음
    그래서 기능이 적고 단순한 편이 낫다는 논리가 나오고, 이 맥락에서 Memcached가 그런 구속복식 접근의 예임
    큰 팀에는 완전히 말이 되지만, 오픈소스 프로젝트는 자금이나 기여를 계속 얻으려면 정기적인 업데이트가 필요하니 내재된 긴장이 있음
    때로는 한 틈새 영역에 특화된 포크나 파생 프로젝트로 이어짐
    개인적으로는 정답이 없고 맥락에 달렸다고 봄
    커뮤니케이션 자체도 공짜가 아니기 때문임

    • 커뮤니케이션이 공짜가 아니라는 점이 마이크로서비스에 대해 내가 가진 문제임
      개발자들이 이걸 전혀 모르는 것처럼 보임
    • 가장 명확한 예는 사람들이 Redis가 충돌이나 종료 때 데이터를 잃는 캐시로만 동작한다고 생각하는 것임
      Memcached를 Redis로 바꾸면서 똑같은 것을 기대했기 때문이라고 봄
    • 대규모에서 AOF는 장애를 일으키므로 꺼버리게 됨
      그래도 훌륭한 캐시이긴 함
  • 지난 몇 년 동안 Flask 작업을 꽤 했고, 풀타임은 아니지만 작은 전자상거래 사업의 기술 스택 일부로 써왔음
    MongoEngine, SQLAlchemy, Celery, Google/eBay/Shopify용 Python 스택에서는 온갖 발밑 지뢰와 이상함을 겪었지만 Redis에서는 그런 적이 없음
    아마 Redis를 영속 저장소라고 생각하는 아무에게나 관리자 권한을 주지 않아서일 수도 있지만, 솔직히 Redis는 매우 견고하고 잘 설계된 기술이라고 표현하고 싶음
    API는 극도로 단순하고, 조금 이상한 일을 해야 할 때마다 합리적이고 잘 생각된 방법이 있음

    • 지금 Flask, SQLAlchemy, Celery로 프로젝트를 시작하는 중인데, 왜 Celery를 피해야 하는지와 대신 무엇을 써야 하는지 더 듣고 싶음
    • 내 세계에서 memcached와 Redis 같은 캐시 시스템은 그냥 넣고 가져오는 캐시임
      태깅 같은 무효화 시스템을 쓸 수는 있겠음
      캐시 시스템으로 할 수 있는 이상한 일이 무엇인지, 단순히 데이터를 캐싱하는 것 말고 사람들이 캐시로 무엇을 하는지 진심으로 궁금함
  • memcached를 좋아하지만, Redis를 휘발성 캐시로 설정해놓고 사람들이 영속 데이터 저장소처럼 다룬다면 그건 Redis 탓이 아님
    memcached도 영속적이지 않다는 점에서 비교가 특히 이상함

    • 많은 회사, 아마 대부분에서 Redis는 언제든 사라져도 되는 캐시가 아니라 실제 내구성 있는 프로덕션 데이터베이스로 인식되고 그렇게 운영됨
      별도로 말해주지 않는다면 새 개발자가 그렇게 가정하는 것도 무리는 아님
  • Memcached는 출시 당시 캐싱의 구원자였음
    2003년에 Brad Fitzpatrick이 LiveJournal을 위해 만들었다는 점도 좋음
    사용자 피드의 각 게시물마다 접근 제한이 다를 수 있었고, 덕분에 게시물이나 전체 페이지를 캐시할 수 있었음
    Ruby on Rails와 함께 여러 해 썼고, 페이지가 빨라졌으며 그냥 잘 동작했음
    단점이자 속도 측면의 장점은 캐시가 디스크가 아니라 메모리에 저장된다는 점이었음
    캐시해야 할 데이터가 넓고 대규모 사이트라면 호스팅 비용이 비싸질 수 있음
    그런 경우에는 Solid Cache가 내게 구원자였음
    지금 작업 중인 프로젝트에는 캐시가 100GB 넘게 있고, PostgreSQL 디스크에 저장되며 인덱스로 빠르게 조회하고 Rails가 자동으로 만료를 처리해 해당 행을 지움
    캐시 규모가 더 작고 이미 Redis를 쓰고 있다면 아마 그냥 Redis를 쓸 것 같음
    하지만 속도가 1순위라면 Memcached와 Redis를 벤치마크해 보겠음

  • memcached가 일시적이라는 점과, 사람들이 그것을 영속적인 것처럼 쓸지 여부는 별개의 문제임
    캐시 적중률이 99.9%처럼 보이고 항상 존재한다면, 조만간 누군가는 그 동작에 의존하는 코드를 작성하게 됨
    개발 모드에서는 클라이언트 라이브러리가 10% 정도 null을 반환하도록 도와줄 수도 있지 않을까 싶음

  • memcached는 단순한 키-값 캐시 작업에서 Redis보다 말도 안 되게 빠름
    스레드가 있고, 한 가지 일을 아주 잘하도록 고도로 최적화되어 있음
    반면 Redis는 모든 데이터 구조와 단일 스레드 등을 가진 임의의 공유 Python 힙 같은 느낌에 더 가까움
    Notion에서는 Redis를 여러 용도로 쓰지만, 실제 캐싱은 memcached에 맡김

    • 키-값에서는 그렇게까지 빠르지 않다는 점을 확인할 수 있음
      읽기당 평균 300마이크로초 대 350마이크로초 정도임
      단일 스레드라는 점도 크게 중요하지 않은데, CPU 병목이 아니라 반응형 입출력이기 때문임
    • 스레드는 공짜가 아님
      더 많은 CPU 코어를 쓰게 해주지만, 부하가 그리 높지 않다면 단일 스레드 memcached가 다중 스레드보다 CPU를 덜 씀