수수료 정책 표가 들어 있던 테이블이 비어 있었는데 백업도 그 시점 것이 없었다.
Table of contents
Open Table of contents
증상 — 표가 비어 있었다
먼저 정말로 비어 있는지부터 확인했다.
SELECT COUNT(*) FROM fee_policy;
0
언제 없어졌는지 보려고 이력 테이블도 함께 봤다.
SELECT MAX(reg_date) FROM fee_policy_history;
fee_policy_history 도 비어 있어서 같이 지워진 것이었다.
fee_policy 백업은 일 단위였는데 지워진 것이 그 전날이라 백업에도 없었다. 그래서 되살릴 곳이 어디인지부터 목록으로 만들어 봐야 했다.
검증 — 코드에 남은 기본값
그다음으로 정책 표를 읽는 코드를 열어 봤다.
$policy = $this->feePolicy->find($market, $type);
$fee = $amount * $policy->rate;
fee_policy 가 없으면 계산이 안 되는데 find 안에 기본값이 있었다.
public function find(string $market, string $type): FeePolicy {
$row = $this->db->row("SELECT * FROM fee_policy WHERE ...");
if (!$row) {
return new FeePolicy(self::DEFAULT_RATES[$type] ?? 0.001);
}
}
private const DEFAULT_RATES = [
'MAKER' => 0.0005,
'TAKER' => 0.001,
];
DEFAULT_RATES 가 코드에 있어서 표가 비어도 그 값으로 돌고 있었다.
자료가 표에서 없어졌다고 해서 그 값 자체가 사라진 것은 아니었다. 그것을 쓰는 코드나 그것으로 만들어진 결과에 흔적이 남아 있다.
조치 — 계산 결과에서 되살렸다
DEFAULT_RATES 만으로는 부족했는데 시장마다 다른 값이 있었다.
SELECT market, fee_type, amount, fee, fee / amount AS rate
FROM trade_log WHERE reg_date >= '2018-10-01' LIMIT 20;
market fee_type rate
A MAKER 0.00050
A TAKER 0.00100
B MAKER 0.00030
B TAKER 0.00080
fee / amount 로 실제 계산에 쓰인 비율이 나왔다.
trade_log 에 남은 이 비율을 모으면 잃어버린 표가 된다.
SELECT market, fee_type, ROUND(fee / amount, 6) AS rate, COUNT(*)
FROM trade_log WHERE reg_date >= '2018-09-01'
GROUP BY market, fee_type, ROUND(fee / amount, 6);
market 별로 하나씩만 나오면 그 값이 맞고 여럿 나오면 기간에 따라 달라진 것이다.
한 시장에서 값이 둘 나왔고 언제 바뀌었는지도 그 경계로 함께 알았다. 결과에서 거꾸로 올라가는 방법이 DEFAULT_RATES 보다 정확한 값을 줬다.
판단 기준 — 적용된 값과 적용할 값
trade_log 에서 복원한 값을 그대로 넣지는 않기로 했다.
복원한 정책표를 담당 쪽에 보내 확인 요청
두 건이 달랐는데 최근에 바꾸기로 정해 놓고 아직 반영 전이던 것이었다.
계산 결과에서 되살린 것은 실제로 적용된 값이지 적용해야 할 값이 아니다. 물어보지 않았으면 그 차이를 모르고 잘못된 값을 그대로 고정할 뻔했다.
원인 — 운영에서 돈 시험용 스크립트
fee_policy 가 애초에 왜 없어졌는지도 함께 찾아봤다.
$ grep -rn "fee_policy" --include=*.php --include=*.sql . | grep -i "delete\|truncate"
scripts/reset-test-data.php:41: $db->query("TRUNCATE fee_policy");
reset-test-data.php 가 운영에서 실수로 돌아간 것이었다.
그 스크립트 맨 앞에 환경을 확인하는 줄을 넣었다.
// 스크립트 맨 위에 넣었다
if (ENVIRONMENT === 'production') {
exit('이 스크립트는 운영에서 실행할 수 없습니다.');
}
$host = $this->db->hostname;
if (in_array($host, PRODUCTION_HOSTS, true)) { exit('운영 DB 입니다.'); }
ENVIRONMENT 와 PRODUCTION_HOSTS 로 두 겹으로 막았다.
환경 변수 하나에만 기대면 그것이 잘못 설정됐을 때 막을 것이 없다. 접속 정보가 운영을 가리킬 수 있으니 hostname 쪽에서도 함께 봤다.
재발 방지 — 되살릴 수 있게 두기
같은 일이 또 났을 때 빨리 되살릴 수 있게 준비했다.
// 정책표를 매일 파일로 내린다
$rows = $this->db->all('fee_policy');
file_put_contents("/backup/policy/fee_policy_" . date('Ymd') . ".json",
json_encode($rows, JSON_PRETTY_PRINT));
DB 백업과 별개로 작은 파일로 남겨 두면 되살릴 때 전체 백업을 풀지 않아도 된다.
되살리는 스크립트도 같이 만들어 시험 환경에서 실제로 돌려 봤다. 백업이 있다는 것과 그것으로 복구가 된다는 것은 다른 사실이다.
정리
- 자료가 없어져도 그것을 쓰는 코드에 기본값이 있을 수 있다
- 되살릴 곳을 먼저 목록으로 만든다
- 계산 결과에서 거꾸로 되살릴 수 있다
- 여러 값이 나오면 기간에 따라 달라진 것이다
- 언제 바뀌었는지도 그 경계로 안다
- 되살린 값을 확인받고 넣는다
- 적용된 값과 적용해야 할 값이 다를 수 있다
- 시험용 스크립트가 운영에서 돌 수 있다
- 환경 확인을 두 겹으로 둔다
- 작은 기준 자료는 따로 파일로 남긴다
- 되살리는 것을 실제로 해 본다