연동 실패가 쌓이는데 우리 문제인지 상대 문제인지 매번 열어 봐야 알았다.
Table of contents
Open Table of contents
증상 — 이유가 문자열로만 있었다
fail_reason 으로 묶어 봤다.
SELECT fail_reason, COUNT(*) FROM sync_log WHERE result='FAIL' GROUP BY fail_reason;
Connection timed out 412
Invalid parameter: courseId is required 88
SQLSTATE[23000]: Duplicate entry 41
cURL error 6: Could not resolve host 22
메시지가 제각각이라 GROUP BY 로 묶이지 않았다.
어느 것이 누구 몫인지도 한눈에 안 보였다. 상대 쪽 메시지가 바뀌면 그동안 묶어 두던 규칙이 한꺼번에 깨진다는 것도 있었다.
조치 — 책임 소재를 값으로 뒀다
sync_log 에 컬럼을 하나 더했다.
ALTER TABLE sync_log ADD COLUMN fault_side varchar(10);
OURS 우리 코드나 데이터 문제
THEIRS 상대 시스템 문제
NETWORK 중간 구간 문제
UNKNOWN 판단 못 함
fault_side 에 네 값 중 하나가 들어간다.
$side = match (true) {
$e instanceof ValidationException => 'OURS',
$e instanceof HttpServerError => 'THEIRS',
$e instanceof ConnectException => 'NETWORK',
default => 'UNKNOWN',
};
예외 종류로 판단했는데 메시지 문자열이 아니라 클래스로 봐야 안 흔들린다.
결과 — 분류별 건수
같은 기간을 fault_side 로 다시 묶었다.
SELECT fault_side, COUNT(*) FROM sync_log WHERE result='FAIL' AND at >= ? GROUP BY fault_side;
NETWORK 412
OURS 88
THEIRS 41
UNKNOWN 22
NETWORK 가 가장 많아서 우리 코드를 아무리 봐도 안 나올 것이었다.
어디를 봐야 하는지가 이 숫자로 갈렸다. 우리 쪽으로 분류된 것은 우리가 고칠 수 있고 상대 쪽은 알려야 하는 것이라 해야 할 일이 다르다.
대응 — 받는 사람별 알림
fault_side 별로 알림도 나눠 보냈다.
if ($ourFails > 10) {
notify('개발팀', "우리 쪽 원인 연동 실패 {$ourFails}건");
}
if ($theirFails > 50) {
notify('운영팀', "상대 시스템 오류 {$theirFails}건. 상대에 문의 필요");
}
받는 사람이 달랐는데 전에는 전부 한 곳으로 가서 대부분 그냥 넘어갔다.
UNKNOWN 은 처음 22건이었고 열어 보면서 분류를 늘렸다.
if (str_contains($msg, 'Duplicate entry')) { return 'OURS'; }
if (str_contains($msg, 'rate limit')) { return 'THEIRS'; }
몇 주에 걸쳐 UNKNOWN 이 0에 가까워졌고 새 UNKNOWN 이 나오면 알림이 가게 했다.
THEIRS 를 상대에게 알릴 근거도 생겼다.
2019-10-01 ~ 2019-10-15
상대 시스템 5xx 응답: 412건
주요 시각: 매일 02:00~03:00
숫자와 시각을 정리해 보내니 가끔 안 된다보다 이야기가 빨랐고 상대 쪽 배치가 그 시간에 돌면서 응답이 느려지는 것이었다.
예방 — 보내기 전에 거르기
OURS 로 분류된 것부터 줄였다.
OURS 88건
필수 항목 누락 61
중복 전송 19
형식 오류 8
필수 항목 누락이 대부분이라 보내기 전에 확인을 넣었다.
$missing = array_diff($this->requiredFields(), array_keys(array_filter($payload)));
if ($missing) {
throw new ValidationException('필수 항목 없음: ' . implode(',', $missing));
}
requiredFields 로 먼저 걸리니 실패가 아니라 안 보내는 것이 됐다.
상대에게 부담도 안 주고 88건이 12건이 됐다. 재시도 정책도 갈렸는데 우리 문제는 다시 보내도 안 되고 코드를 고쳐야 한다.
$retryable = in_array($side, ['NETWORK', 'THEIRS'], true);
전에는 전부 재시도해서 같은 것을 다섯 번씩 실패하고 있었다.
주의 — 틀릴 수 있는 판단
Exception 종류로 판단하니 대부분 맞았지만 틀리는 경우가 있었다.
상대가 400 을 주는데 실제로는 우리 요청이 아니라 상대 쪽 규격이 바뀐 것이었다.
if ($status === 400) {
// 우리 요청이 잘못됐거나 상대 규격이 바뀌었다
return $this->recentSuccessRate() > 0.9 ? 'THEIRS' : 'OURS';
}
조금 전까지 잘 되던 것이 갑자기 400 이면 상대가 바뀐 쪽에 가깝다.
계속 400 이었으면 우리 문제다. 완벽하지는 않지만 처음 볼 곳을 정하는 데는 충분했고 틀렸을 때 값을 다시 고칠 수 있게 뒀다.
정리
- 실패 이유를 문자열로만 두면 묶이지 않는다
- 누구 몫인지도 한눈에 안 보인다
- 상대 메시지가 바뀌면 묶는 규칙이 깨진다
- 책임 소재를 값으로 남긴다
- 메시지 문자열이 아니라 예외 종류로 판단한다
- 어디를 봐야 하는지가 그 숫자로 갈린다
- 알림을 받는 사람별로 나눈다
- 판단 못 한 것을 줄여 나가고 새 종류가 나오면 알린다
- 상대에게 알릴 때 숫자와 시각을 정리해 보낸다
- 보내기 전에 걸리면 실패가 아니라 안 보내는 것이 된다
- 재시도해도 되는 것이 책임 소재로 갈린다