Skip to content
isdnetworks
Go back

지운 캐시가 그 캐시가 아니었다

설정을 바꿨는데 화면이 안 바뀌어서 memcached 를 비웠는데 여전히 그대로였다.

한 번 더 비워도 그대로였는데 같은 조치를 두 번 해서 같은 결과가 나오면 그 조치가 안 맞는 것이다.

Table of contents

Open Table of contents

캐시가 몇 겹인지 세어 봤다

세 번째로 같은 것을 하는 대신 요청 경로를 따라가며 캐시를 전부 적어 봤다. memcached 하나만 보고 있었던 것이다.

브라우저 캐시
  → CDN 캐시
    → 웹서버 캐시 (정적 파일)
      → 애플리케이션 캐시 (파일/메모리)
        → ORM 쿼리 캐시
          → DB 쿼리 캐시

여섯 겹이었고 내가 지운 것은 네 번째였다. 한 요청이 화면까지 오는 사이에 값을 담아 두는 자리가 하나뿐이라는 보장이 없었다.

여섯 중 어느 것이 이 화면에 관여하는지는 세어 본 뒤에야 물을 수 있었다. 세기 전에는 memcached 하나만 머릿속에 있었다.

비운 것이 안 먹혔다는 사실만 두 번 확인한 셈인데 같은 명령을 또 치는 것보다 경로를 그리는 쪽이 빨랐다.

캐시를 지웠다는 말이 어느 캐시를 지웠다는 뜻인지가 정해져 있지 않았다. 여섯을 적어 놓고 나서야 내가 무엇을 안 지웠는지가 보였다.

판단 기준 — 어느 계층인지 가르는 법

각 계층은 다르게 확인한다.

브라우저    개발자 도구에서 캐시 끄고 새로고침 — 되면 브라우저다
CDN         응답 헤더의 X-Cache 를 본다. 원본 서버로 직접 요청해 대조한다
웹서버      설정을 보고 캐시 디렉터리를 확인한다
애플리케이션  캐시 저장소를 직접 조회한다. 키가 있는지 값이 뭔지
ORM·DB      대개 요청 단위라 오래 안 남는다

Cache-Control 헤더와 직접 요청으로 앞의 셋은 금방 갈린다. curl 로 원본 서버에 바로 쏴서 옛 값이 오면 앞쪽 셋은 아니다.

이번에도 그렇게 해서 앞의 셋을 지웠다. 남은 것은 애플리케이션 캐시와 프로세스 메모리 둘이었다.

각 계층을 확인하는 방법이 다르다는 것도 정리됐다. 헤더를 보는 것과 memcached 조회는 다른 작업이다.

방법을 모르면 그 계층이 의심 목록에서 그냥 빠지고 빠진 자리에 답이 있으면 못 찾는다.

원인 — 프로세스 메모리

memcached 는 제대로 비워져 있었다. 그런데 php-fpm 워커가 시작할 때 설정을 메모리에 올려 두고 있었다.

class Config {
    private static $cache = null;
    public static function get($key) {
        if (self::$cache === null) {
            self::$cache = json_decode(file_get_contents(CONFIG_FILE), true);
        }
        return self::$cache[$key] ?? null;
    }
}

self::$cache 가 한 번 채워지면 프로세스가 사는 동안 CONFIG_FILE 을 다시 안 읽는다. 파일을 바꿔도 이미 떠 있는 프로세스는 옛 값을 들고 있다.

지우는 명령으로는 절대 안 없어지는 종류였다. 워커를 재기동하니 그제야 반영됐다.

memcached 처럼 이름이 붙은 것만 캐시가 아니다. 한 번 읽고 안 버리는 static 변수도 같은 일을 한다.

self::$cache 에는 지우는 명령이 없고 프로세스를 끝내는 것이 유일한 방법이다. 이름이 없어 목록에도 안 올라간다.

전체 흐름 — 확인 순서

설정을 바꿨는데 반영이 안 된다는 말을 들으면 이 순서로 본다.

1. 값이 실제로 바뀌었나 — 파일이나 DB에서 직접 읽는다
2. 프로세스가 다시 읽나 — 시작할 때만 읽으면 재기동이 필요하다
3. 캐시 계층을 뒤에서부터 지운다
4. 지웠는지 확인한다 — 키를 조회해 없는지 본다

셋째의 방향이 중요했는데 앞에서부터 지우면 memcached 에 남은 옛 값이 올라와 다시 채워진다.

넷째는 여러 번 겪어서 넣었다. 삭제 명령이 다른 memcached 를 가리키거나 키 접두사가 달라 안 지워지기도 했다.

명령이 성공했다는 것과 지워졌다는 것은 다른 확인이라 memcached 에서 그 키를 다시 꺼내 봐야 갈린다.

재기동이 필요한지 알려주는 게 낫다

근본적으로는 설정을 바꾸면 프로세스가 알아채는 것이 맞다. 방법이 셋 있었다.

파일 수정 시각 확인   요청마다 stat 한 번. 바뀌었으면 다시 읽는다
TTL              60초마다 다시 읽는다. 즉시는 아니지만 재기동이 필요 없다
신호 수신         특정 시그널을 받으면 다시 읽는다. 재기동보다 가볍다

즉시성이 중요하지 않아 TTL 쪽으로 갔고 설정 변경이 최대 1분 뒤 반영된다는 것을 문서에 적었다.

적어 두지 않으면 다음 사람이 또 안 바뀐다고 한다. 이번에 내가 한 것과 똑같은 조사를 처음부터 다시 하게 된다.

반영이 늦는 것 자체는 문제가 아니고 얼마나 늦는지를 모르는 것이 문제였다.

한 줄을 적어 두면 바꾼 사람이 1분을 기다리는데 안 적어 두면 그 1분 동안 조사를 시작한다.

제약 — 캐시를 늘리기 전에

이 일 이후로 캐시를 추가할 때 하나를 더 묻는다. 이것을 어떻게 무효화하느냐다.

넣는 것은 쉽고 지우는 것이 어려운데 무효화 방법 없이 넣으면 나중에 반드시 겪는다.

키를 알면 지울 수 있나
관련된 것을 한꺼번에 지울 수 있나 (접두사 삭제)
최악의 경우 전체를 비울 수 있나

셋 다 안 되면 TTL 을 짧게 두는 쪽이 낫고 못 지우는 캐시는 언젠가 문제가 된다.

정리


Share this post on:

Previous Post
세지 않아도 되는 것을 세고 있었다
Next Post
평균이 감추는 것