설정을 바꿨는데 화면이 안 바뀌어서 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 을 짧게 두는 쪽이 낫고 못 지우는 캐시는 언젠가 문제가 된다.
정리
- 한 요청이 지나는 캐시 계층을 전부 세어 본다
- 캐시를 지웠다는 말이 어느 캐시인지 정해져 있지 않았다
- 헤더와
curl직접 요청으로 앞쪽 계층은 금방 갈린다 - 시작할 때만 읽는 값은 파일을 바꿔도 반영이 안 된다
- 그런 값은 지우는 명령으로 안 없어지고 재기동해야 한다
- 캐시는 뒤에서부터 지우고 앞부터 지우면 뒤에서 다시 채워진다
- 삭제 명령의 성공과 실제로 지워진 것은 다른 확인이다
- 즉시성이 필요 없으면
TTL로 두고 반영 지연을 문서에 적는다 - 적어 두지 않으면 다음 사람이 같은 조사를 처음부터 다시 한다
- 캐시를 넣을 때 무효화 방법을 함께 정하고 없으면
TTL을 짧게 둔다