개발에서 잘 되던 화면이 운영에서 옛 내용을 보여 줬다. 반대로 개발에서만 나는 오류도 있었다.
Table of contents
Open Table of contents
증상 — 한쪽에서만 나던 문제
두 증상이 서로 반대 방향이었다.
한쪽은 운영에서만 옛 내용이 남고 다른 쪽은 개발에서만 오류가 나서 방향이 반대라 처음에는 서로 다른 문제로 봤다.
같은 코드가 환경에 따라 다르게 동작한다면 원인은 코드 밖에 있으므로 두 증상이 하나의 원인에서 나왔는지부터 확인해야 했다.
원인 — 설정이 반대로 되어 있었다
런타임 설정부터 봤다.
; 개발
opcache.enable = 0
opcache.validate_timestamps = 1
; 운영
opcache.enable = 1
opcache.validate_timestamps = 0
운영은 validate_timestamps 가 0이라 배포 뒤 재시작을 안 하면 옛 코드가 계속 돈다.
개발은 opcache.enable 이 0이라 고치면 바로 반영되고 여기까지는 의도한 설정이다.
문제는 opcache 말고도 환경 사이에 반대인 것이 더 있었다는 점이다.
반대인 것을 전부 세어 봤다
한자리에 모아 비교했다.
$ diff <(ssh dev 'php -i | grep -E "opcache|realpath|apc"') \
<(ssh web01 'php -i | grep -E "opcache|realpath|apc"')
opcache.enable 0 | 1
opcache.validate_timestamps 1 | 0
opcache.revalidate_freq 0 | 60
realpath_cache_size 16K | 4096K
realpath_cache_ttl 0 | 120
애플리케이션 쪽도 봤다.
// config/cache.php
'default' => env('CACHE_DRIVER', 'array'),
개발 CACHE_DRIVER=array (요청 안에서만 산다)
운영 CACHE_DRIVER=redis (요청을 넘어 산다)
CACHE_DRIVER 가 array 냐 redis 냐가 가장 큰 차이였다.
opcache 쪽 다섯 줄은 반영 시점의 차이지만 CACHE_DRIVER 는 성격 자체의 차이다. 두 증상을 다 설명하는 것도 이쪽이었다.
제약 — 요청을 넘어 사는 캐시
이런 코드가 있었다.
public function menu(int $memberNo): array
{
return Cache::remember('menu', 3600, function () use ($memberNo) {
return $this->buildMenu($memberNo);
});
}
Cache::remember 에 넘긴 키에 $memberNo 가 없다.
개발에서는 array 라 요청이 끝나면 사라지므로 이 실수가 드러날 기회가 없다. 운영에서는 redis 에 남아 먼저 들어온 사람의 메뉴가 다음 사람에게 그대로 나간다.
권한이 다른 계정끼리 섞이면 안 보여야 할 메뉴가 보이는데 개발에서 아무리 시험해도 이 종류는 안 나타난다.
조치 — 키에 무엇이 들어가야 하는가
키를 만드는 규칙을 세웠다.
$key = sprintf('menu:v2:%d:%s', $memberNo, $this->roleHash($memberNo));
$memberNo 와 roleHash 처럼 결과를 달라지게 하는 입력은 전부 키에 넣는다.
조회 조건과 정렬 기준과 권한 등급과 언어가 전부 그 대상이고 하나라도 빠지면 서로 다른 결과가 같은 자리를 덮어쓴다.
구조가 바뀌면 v2 같은 표시를 올려 옛 것과 섞이지 않게 했다. v2 로 안 올리면 담는 형태가 바뀐 뒤에 옛 값을 읽어 모양이 안 맞아 깨진다.
키를 만드는 자리를 한 곳으로 모아 빠뜨리기 어렵게 한 것이 마지막이었다.
설정 — 환경 일치와 배포 시 정리
근본은 개발에서도 같은 종류를 쓰게 하는 것이었다.
개발 CACHE_DRIVER=redis REDIS_DB=3
운영 CACHE_DRIVER=redis REDIS_DB=0
REDIS_DB 만 나누니 같은 종류를 쓰면서 개발에서도 요청을 넘어 산다.
대신 개발에서 redis 에 값이 남아 헷갈리는 일이 늘어서 비우는 명령을 짧게 뒀다.
$ php artisan cache:clear
배포에서 무엇을 비울지도 갈랐다.
php artisan config:cache
php artisan route:cache
php artisan view:clear
# 데이터 캐시는 건드리지 않는다
config:cache 처럼 코드가 만든 것은 배포마다 비우고 자료에서 온 것은 그 키만 지운다.
전부 비우면 배포 직후가 항상 가장 느린데 비우는 것이 안전해 보여도 그 부하가 매 배포마다 돌아온다.
마지막으로 코드가 실제로 바뀌었는지 확인할 자리를 뒀다.
// health 응답에 배포 표시를 넣었다
return ['rev' => trim(file_get_contents(BASE_PATH.'/REVISION'))];
$ curl -s http://web01/health | grep rev
{"rev":"2018.05.13-2"}
REVISION 과 도는 rev 가 같은지 보면 파일만 바뀌고 프로세스가 옛 코드를 물고 있는 경우가 갈린다.
정리
- 캐시 설정이 환경마다 반대면 한쪽에서만 나는 버그가 생긴다
- 방향이 반대인 두 증상이 하나의 원인일 수 있다
- 반대인 것을 한자리에 모아 놓고 센다
- 런타임 설정과 애플리케이션 설정을 둘 다 본다
- 가장 큰 차이는 캐시가 요청을 넘어 사는지다
- 안 넘어가면 키를 빠뜨린 실수가 안 드러난다
- 결과를 달라지게 하는 입력은 전부 키에 넣는다
- 담는 형태가 바뀌면 표시를 올려 옛 값과 섞이지 않게 한다
- 개발에서도 같은 종류를 쓰되 자리를 나눈다
- 배포에서 비울 것과 안 비울 것을 가른다
- 배포 표시를 응답으로 확인한다