수집 계정이 잠겼다. 인증 실패가 반복돼서였다.
한 프로세스가 그것을 감지하고 스스로 멈췄는데 같은 계정을 쓰는 나머지 셋이 계속 시도했다.
Table of contents
Open Table of contents
각자 시도하고 있었다
프로세스가 넷이고 전부 같은 계정을 쓴다.
collector-01 → 인증 실패 → 재시도 → 실패 → 멈춤
collector-02 → 인증 실패 → 재시도 → 실패 → 재시도 ...
collector-03 → 인증 실패 → 재시도 → 실패 → 재시도 ...
collector-04 → 인증 실패 → 재시도 → 실패 → 재시도 ...
collector-01 만 멈췄다. 나머지 셋이 계속 두드려서 잠금이 풀리지 않았다.
각자 자기 tryAuth 결과만 보고 판단하니 멈추는 시점도 제각각이었다. 한쪽이 멈춘 것이 다른 쪽에는 아무 정보도 아니었다.
원인 — 자격이 공유라는 사실
실패하는 이유가 각 프로세스 안에 있지 않고 넷이 함께 쓰는 API 계정에 있었다.
같은 자격을 쓰는 것들은 한 몸이다. 하나가 막히면 그 시점에 전부 막힌 것이고 그것을 각자 다시 확인할 이유가 없다.
그런데 코드는 collector-01 부터 collector-04 까지 따로 판단하게 돼 있었다. 자원은 공유인데 판단은 개별이라는 어긋남이 문제의 뿌리였다.
상태를 공유했다
인증 상태를 한 자리에 뒀다.
// 실패하면 표시
$this->cache->save('auth:locked', time(), 1800);
실패를 감지한 쪽이 cache 의 auth:locked 에 time() 을 적는다. 시작할 때는 그것부터 본다.
$locked = $this->cache->get('auth:locked');
if ($locked && (time() - $locked) < 1800) {
log_message('warning', '인증 잠김 상태. 대기');
return;
}
한 프로세스가 감지한 것을 나머지 셋도 그 자리에서 안다. 1800 초가 지나면 표시가 만료돼 자동으로 풀린다.
한 대만 시도하게 했다
그런데 30분 뒤에 넷이 동시에 tryAuth 를 부르면 또 잠긴다. 아직 안 풀렸으면 실패가 네 번 쌓인다.
$got = $this->cache->save('auth:probe', getmypid(), 60, true); // 없을 때만 저장
if (!$got) {
return; // 다른 프로세스가 시도 중
}
if ($this->tryAuth()) {
$this->cache->delete('auth:locked');
log_message('info', '인증 복구');
} else {
$this->cache->save('auth:locked', time(), 1800); // 다시 30분
}
$this->cache->delete('auth:probe');
auth:probe 를 없을 때만 저장하게 해서 그것을 얻은 하나가 대표로 시도한다. 못 얻은 쪽은 그냥 돌아간다.
성공하면 auth:locked 를 지워 넷이 함께 재개하고 실패하면 다시 30분을 건다. 시도 횟수가 넷에서 하나로 줄어드니 잠금이 연장될 일도 없다.
선택지 — 계정을 나누기
프로세스마다 계정을 따로 주는 길도 있었다. 하나가 잠겨도 나머지 셋은 계속 돈다.
현재 1개 (공유)
가능 최대 5개
상대에게 물어보니 계약상 다섯까지 가능하다고 해서 API 계정을 넷으로 나눴다.
나누면 한 번의 사고가 미치는 폭이 준다. 대신 관리할 계정이 넷이 되고 각각의 만료와 권한을 따로 봐야 한다.
왜 잠겼는지 봤다
여기까지는 잠겼을 때의 처리이고 근본은 왜 실패했느냐였다. 로그를 보니 비밀번호가 만료돼 있었다.
90일마다 바꿔야 하는 계정인데 아무도 그 날짜를 몰랐으므로 config 에 만료일을 넣고 확인하게 했다.
$expire = $this->config->item('auth_expire_date');
$days = (strtotime($expire) - time()) / 86400;
if ($days < 14) {
notify("인증 정보 만료 {$days}일 전: {$expire}");
}
auth_expire_date 와 지금을 빼서 남은 날을 세고 14일 아래면 알린다. 2주면 바꿀 시간이 있다.
대응 — 잠김과 복구 알림
잠금 상태에 들어가면 알림을 보내게 했다.
if (!$this->cache->get('auth:notified')) {
notify("인증 잠김. 수집 중단됨");
$this->cache->save('auth:notified', 1, 3600);
}
auth:notified 로 한 시간에 한 번만 보낸다. 넷이 각자 보내면 같은 내용이 네 번 온다.
복구됐을 때도 알렸다.
notify("인증 복구. 수집 재개");
$this->cache->delete('auth:notified');
멈춘 것만 알리고 재개를 안 알리면 받는 쪽은 계속 멈춰 있는 줄 안다. notify 는 상태가 바뀌는 양쪽에서 다 나가야 지금이 어느 쪽인지 안다.
제약 — 밀린 자료의 상한
잠긴 동안 들어온 자료를 버릴지 쌓을지도 정해야 했다. 쌓는 쪽으로 했다.
if ($this->isLocked()) {
$this->spool($data); // 파일로 저장
return;
}
isLocked 이면 spool 로 파일에 적어 두고 복구되면 보낸다. 한꺼번에 보내면 요청 한도에 다시 걸리므로 나눠서 보냈다.
쌓이는 양에 상한도 뒀다. 하루치가 넘으면 오래된 것부터 버리는데 무한정 쌓으면 잠금이 길어질 때 디스크가 찬다.
정리
- 같은 자격을 쓰는 것들은 하나가 막히면 전부 막힌 것이다
- 자원은 공유인데 판단이 개별이면 각자 다른 시점에 멈춘다
auth:locked를 한 자리에 두어 감지한 것을 넷이 함께 본다- 복구 시도는
auth:probe를 얻은 하나만 하게 한다 - 동시에 시도하면 풀리자마자 다시 잠긴다
- 계정을 나누면 사고가 미치는 폭은 주는 대신 관리할 것이 는다
- 만료일을 세어 2주 전에 알리면 잠기기 전에 처리한다
- 알림은 한 시간에 한 번만 보내고 복구도 함께 알린다
- 멈춘 동안의 자료를 쌓을지 정하고 쌓으면 상한을 둔다