원격 장비에 설정을 내려보내는 코드를 짰다. 소켓으로 명령을 보내고 응답을 기다린다.
send(cmd) → wait(ack) → 성공
타임아웃이 나면 재시도한다. 세 번까지 보낸다.
현장 시험에서 값이 이상하게 들어가는 일이 생겼다.
Table of contents
Open Table of contents
응답이 유실된 경우
문제는 이 자리였다.
send(cmd) → 장비가 처리함 → ack 전송 → ✗ 유실
↑
타임아웃
재시도 → 장비가 또 처리함
명령은 도착했고 처리도 됐는데 ack 만 유실됐다. 보내는 쪽은 그것을 실패로 보고 다시 보낸다.
그러면 장비는 같은 명령을 두 번 받는다. 명령이 안 갔을 때와 응답만 사라졌을 때가 보내는 쪽에서 똑같이 타임아웃으로 보인다.
응답이 없다는 사실만으로는 장비가 처리했는지를 알 수 없다. 그러면 다시 보내도 되는지를 다른 근거에서 찾아야 한다.
명령마다 결과가 달랐다
두 번 받으면 어떻게 되는지가 명령마다 달랐다.
SET interval 30 은 두 번 해도 30이다. 값이 그 수로 박히므로 몇 번을 보내든 같다.
ADJUST offset +5 는 두 번 하면 +10 이 된다. 상대적으로 더하는 명령이라 횟수가 결과에 그대로 들어간다.
RESET counter 는 두 번째 초기화 사이에 쌓인 값이 날아간다. ROTATE log 는 두 번 하면 파일이 두 개 더 생긴다.
같은 재시도 로직인데 어떤 명령은 안전하고 어떤 명령은 아니었다. 재시도 코드가 아니라 명령의 성질이 안전을 정하고 있었다.
명령을 두 부류로 나눴다
명령 목록을 놓고 갈랐다.
[같은 결과가 되는 것] 절대값 지정 · 조회 · 상태 확인
[누적되는 것] 상대 변경 · 증감 · 추가 · 회전
앞쪽은 몇 번을 보내도 최종 상태가 같으므로 그대로 재시도한다. 뒤쪽은 다르게 다뤄야 한다.
이 목록을 만들고 나니 어느 명령에 무엇을 붙여야 하는지가 정해졌다. 전부에 같은 장치를 붙일 이유가 없었다.
시퀀스 번호를 붙였다
뒤쪽 명령에는 요청마다 번호를 넣었다.
send(seq=1042, ADJUST offset +5)
장비는 마지막으로 처리한 번호를 기억한다. 같은 번호가 또 오면 처리하지 않고 이전 응답만 다시 보낸다.
장비: last_seq == 1042 ? → 처리 안 함, ack만 재전송
last_seq 하나로 응답이 몇 번 유실되든 실제 처리는 한 번이 된다. 보내는 쪽은 안심하고 다시 보내면 된다.
이것은 장비 쪽 펌웨어를 고칠 수 있어서 가능했다. 장비가 그 번호를 기억할 자리를 갖고 있어야 성립하는 방법이다.
못 고치는 경우
벤더 장비라 펌웨어를 못 건드리는 경우가 더 많다. 그때 쓸 수 있는 것이 셋이었다.
첫째는 상대 명령을 절대 명령으로 바꾸는 것이다. ADJUST +5 대신 현재 값을 읽고 SET 35 를 보내면 읽기가 한 번 더 들어가는 대신 재시도가 안전해진다.
둘째는 재시도 전에 상태를 확인하는 것이다.
타임아웃 → GET current → 기대값이면 성공 처리, 아니면 재전송
타임아웃이 났을 때 바로 재전송하지 않고 GET 으로 먼저 읽는다. 이미 반영돼 있으면 재전송하지 않는다.
셋째는 재시도를 아예 안 하는 것이다. 실패로 기록하고 사람이 판단하게 한다.
셋째가 무성의해 보이는데 잘못 두 번 실행되는 것보다는 낫다. 어떤 명령은 그렇다.
변경 내용 — 로그 형식
이번 일로 로그 형식도 바꿨다. 원래는 이랬다.
[INFO] cmd sent: ADJUST offset +5
[WARN] timeout, retry 1/3
[INFO] ack received
마지막에 ack 가 왔다는 것만 남는다. 성공한 것이 첫 번째 시도인지 두 번째인지를 알 수 없다.
[INFO] seq=1042 cmd=ADJUST attempt=1 sent
[WARN] seq=1042 attempt=1 timeout
[INFO] seq=1042 cmd=ADJUST attempt=2 sent
[INFO] seq=1042 attempt=2 ack (device: duplicate, ignored)
seq 와 attempt 를 함께 적고 장비가 중복으로 보고 무시했다는 것까지 남긴다. 나중에 값이 이상할 때 이 줄을 보면 재시도가 원인인지가 바로 갈린다.
로그가 없으면 같은 조사를 값이 이상할 때마다 처음부터 한다. 한 줄에 무엇을 더 적을지가 그만큼의 시간을 정했다.
정리
- 응답 유실과 처리 실패는 보내는 쪽에서 구분되지 않는다
- 둘 다 타임아웃이라는 같은 모습으로 보인다
- 재시도가 안전한지는 재시도 코드가 아니라 명령의 성질이 정한다
SET은 몇 번을 보내도 같고ADJUST는 횟수가 결과에 들어간다- 명령 목록을 같은 결과가 되는 것과 누적되는 것으로 가른다
- 장비를 고칠 수 있으면
seq와last_seq로 중복을 걸러 낸다 - 못 고치면 절대값으로 바꾸거나
GET으로 확인하거나 재시도를 접는다 - 재시도를 접는 것이 잘못 두 번 실행되는 것보다 나을 때가 있다
- 로그에
attempt와 중복 여부를 남기면 나중에 원인이 갈린다