Skip to content
isdnetworks
Go back

한 대만 켜는 것과 한 대에만 나가는 것은 달랐다

새 기능을 한 대에서만 켜서 시험하기로 했다. 켜기는 한 대에서만 켰는데 다른 대에서 문제가 났다.

Table of contents

Open Table of contents

상황 — 한 대에서만 켰다

환경 변수로 갈랐다.

web01   NEW_MATCHING=on
web02   NEW_MATCHING=off
web03   NEW_MATCHING=off

NEW_MATCHING 을 켠 web01 에서만 새 코드가 돈다고 생각했다.

배포는 세 대에 다 나갔으므로 코드는 전부에 있고 켜진 것만 하나였는데 그 차이를 그때는 구분하지 않았다.

켠다는 말이 그 코드가 그 대에만 있다는 뜻으로 읽혔다. 실제로는 어디에 있느냐와 어디서 도느냐가 따로 정해진다.

증상 — 꺼진 대에서 문제가 났다

web02 에서 오류가 났다.

Fatal error: Class 'MatchingV2' not found

NEW_MATCHING 이 꺼져 있는데 MatchingV2 를 찾고 있었다.

$engines = [
    'v1' => new MatchingV1(),
    'v2' => new MatchingV2(),      // 만들기는 다 한다
];
$engine = $engines[Feature::on('new_matching') ? 'v2' : 'v1'];

Feature::on 으로 고르는데 그 전에 new 가 둘 다 돈다.

MatchingV2.php 가 배포에서 빠져 있었고 꺼진 대에서도 그 줄을 지났다. 켜고 끄는 것은 동작이지 코드의 존재가 아니었다.

web01 에서는 파일이 있으니 아무 일도 안 났다. 켠 쪽이 멀쩡하고 끈 쪽이 죽는 모양이라 처음에는 원인이 안 잡혔다.

원인 — 배포 범위와 켜는 범위

두 범위를 나눠 보면 이렇다.

배포 범위   코드가 어디에 있는가        → 전부
켜는 범위   그 코드가 어디서 도는가      → 한 대

rsync 로 코드가 전부에 나가므로 꺼진 곳에서도 읽힌다.

문법 오류나 없는 파일은 꺼져 있어도 터지고 초기화 부분이 있으면 그것도 꺼진 대에서 함께 돈다.

끈 것이 완전히 꺼졌는지 확인하지 않으면 시험 자체가 성립하지 않고 꺼진 쪽이 영향을 받으면 비교 대상이 아니게 된다.

조치 — 필요할 때만 만들게

고치는 방향은 NEW_MATCHING 이 꺼져 있으면 아무것도 안 건드리게 하는 것이었다.

$engine = Feature::on('new_matching') ? new MatchingV2() : new MatchingV1();

삼항으로 고른 뒤에 new 를 부르면 꺼진 쪽은 MatchingV2 를 아예 안 건드린다.

배열에 둘을 담아 두고 고르는 방식이 읽기에는 깔끔했는데 그 깔끔함이 꺼진 쪽까지 만들게 하는 원인이었다.

플래그를 쓰는 코드에서는 고르는 시점보다 만드는 시점이 앞서지 않아야 한다. 무엇을 쓸지 정한 뒤에 그것만 준비하는 순서가 필요했다.

검증 — 배포가 다 나갔는지

새 파일이 한 대에 안 나간 것이 진짜 원인이었다.

$ for h in web01 web02 web03; do
    echo -n "$h "; ssh $h 'ls /app/src/Matching/MatchingV2.php 2>&1'
  done
web01 /app/src/Matching/MatchingV2.php
web02 ls: cannot access ...: No such file or directory
web03 /app/src/Matching/MatchingV2.php

web02 에만 MatchingV2.php 가 없었는데 배포는 성공으로 끝나 있었다.

for h in $HOSTS; do
    rsync -a --delete release/ $h:/app/ || echo "FAIL: $h"
done

rsync 가 실패해도 다음으로 넘어가고 스크립트는 0으로 끝난다.

fail=0
for h in $HOSTS; do
    rsync -a --delete release/ "$h:/app/" || { echo "FAIL: $h"; fail=1; }
done
[ $fail -eq 0 ] || exit 1

fail 을 두어 하나라도 실패하면 배포가 실패로 끝나게 했다.

한 대만 실패하는 경우가 드물지 않다. 드물지 않은데 성공으로 보고되면 그 상태가 며칠씩 남는다.

세 대가 같은 것을 갖고 있는지도 확인하게 했다.

$ for h in $HOSTS; do
    echo -n "$h "; ssh $h 'find /app/src -type f -name "*.php" | sort | xargs md5sum | md5sum'
  done
web01 3f9a2c11...
web02 3f9a2c11...
web03 3f9a2c11...

md5sum 을 모아 다시 md5sum 하니 하나라도 다르면 값이 달라진다.

일부러 한 대에 파일 하나를 고쳐 놓고 이 검사가 잡는지 봤고 잡았는데 부분 실패를 성공으로 보고하면 그 뒤의 판단이 전부 틀린 전제 위에 선다.

비교 — 서버로 가르기와 사용자로 가르기

한 대에서만 켜는 방식 자체도 다시 봤다.

같은 사용자가 요청마다 다른 대로 간다
→ 어떤 요청은 새 동작, 어떤 요청은 옛 동작

앞단이 요청을 어느 대로 보낼지는 정해져 있지 않다.

거래 관련이라 같은 userId 가 요청마다 다른 결과를 받는 것은 위험했다. 기능이 있다가 없다가 하는 것으로 보이고 그 상태에서 난 오류는 재현도 안 된다.

서버가 아니라 사용자로 가르게 바꿨다.

$on = Feature::onFor('new_matching', $userId);
public static function onFor(string $key, int $userId): bool {
    $pct = self::percent($key);          // 5 이면 5%
    return (crc32($key . ':' . $userId) % 100) < $pct;
}

crc32userId 를 가르니 어느 서버로 가든 같은 결과를 받는다.

percent 로 비율을 늘릴 수 있어 5퍼센트로 시작해 넓히는 방식이 가능해졌고 무엇으로 가르느냐가 시험의 성격을 정하고 있었다.

정리


Share this post on:

Previous Post
저장소마다 커밋 작성자가 달랐다
Next Post
수동 정리는 세 가지를 맞춘다