Skip to content
isdnetworks
Go back

캐시가 개발과 운영에서 반대로 굴었다

개발에서 잘 되던 화면이 운영에서 옛 내용을 보여 줬다. 반대로 개발에서만 나는 오류도 있었다.

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_DRIVERarrayredis 냐가 가장 큰 차이였다.

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));

$memberNoroleHash 처럼 결과를 달라지게 하는 입력은 전부 키에 넣는다.

조회 조건과 정렬 기준과 권한 등급과 언어가 전부 그 대상이고 하나라도 빠지면 서로 다른 결과가 같은 자리를 덮어쓴다.

구조가 바뀌면 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 가 같은지 보면 파일만 바뀌고 프로세스가 옛 코드를 물고 있는 경우가 갈린다.

정리


Share this post on:

Previous Post
매핑 누락인가 원시 오수집인가
Next Post
계산값이던 화면의 날짜