수집 장비가 통신이 끊겼다 붙으면 그동안 못 보낸 자료를 몰아서 보냈다. 그때마다 서버가 밀렸다.
Table of contents
Open Table of contents
몰아서 보내고 있었다
보내는 쪽 코드는 이랬다.
while ((row = queue_pop()) != NULL) {
if (send_row(row) < 0) { queue_push_front(row); break; }
free(row);
}
queue_pop 이 빌 때까지 계속 보낸다. 두 시간 끊겼으면 7,200건이 한 번에 나간다.
장비가 스무 대다. 동시에 붙으면 14만 건이 몰린다.
서버가 밀리면 다른 장비의 전송도 느려지고 그쪽도 실패한다. 그러면 그쪽도 나중에 몰아서 보내게 되어 문제가 번진다.
원인 — 끊긴 이유가 여러 가지
끊기는 이유가 하나가 아니었다.
장비 쪽 통신 모듈 문제 붙으면 정상. 밀린 것을 보내면 된다
서버 점검 서버가 아직 준비 중일 수 있다
서버 과부하 몰아 보내면 더 나빠진다
망 자체 문제 여러 대가 동시에 붙는다
앞의 둘과 뒤의 둘이 달랐다. 장비 쪽 문제였으면 서버는 멀쩡하니 빨리 보내도 된다.
서버 쪽 문제였으면 같은 속도로 보내는 순간 다시 실패한다. 그런데 코드는 네 경우를 전부 같은 속도로 보내고 있었다.
전에 왜 실패했는지가 지금 어떻게 보낼지를 정해야 했다.
실패 이유를 기록했다
무엇 때문에 실패했는지를 남기게 했다.
struct send_stat {
int last_errno;
int http_status;
int consecutive_fails;
time_t first_fail_at;
};
last_errno 와 http_status 를 함께 두어 어느 계층에서 실패했는지를 가른다.
연결 안 됨 (ECONNREFUSED) 서버가 안 떠 있다
시간 초과 (ETIMEDOUT) 서버가 느리거나 망이 느리다
503 서버가 과부하라고 말하고 있다
429 너무 많이 보내고 있다
ECONNREFUSED 와 503 은 둘 다 실패인데 뜻이 다르다. 앞은 서버가 없는 것이고 뒤는 있는데 힘들다고 말하는 것이다.
이유별로 다르게 했다
기록이 쌓이니 다음 간격이 그 이유에서 나왔다.
static int send_interval_ms(const struct send_stat *st) {
if (st->http_status == 429 || st->http_status == 503) {
return 5000; /* 서버가 힘들다고 말했다. 천천히 */
}
if (st->last_errno == ETIMEDOUT) {
return 2000; /* 느리다. 조금 천천히 */
}
return 50; /* 장비 쪽 문제였다. 정상 속도 */
}
429 나 503 이었으면 5초 간격으로 보내고 장비 쪽 문제였으면 50밀리초로 보낸다.
상대가 언제 오라고 말해 주면 그것이 가장 정확하다.
if (retry_after > 0) return retry_after * 1000;
Retry-After 헤더가 오면 우리 판단보다 그쪽을 따랐다. 서버가 자기 상태를 우리보다 잘 안다.
동시에 붙는 것을 흩었다
망 문제로 스무 대가 동시에 복구되면 각자 속도를 조절해도 한꺼번에 몰린다.
unsigned seed = device_id_hash();
int jitter = (seed % 30) * 1000; /* 0~30초 */
sleep_ms(jitter);
장비마다 다른 시간을 기다린 뒤 시작한다. device_id_hash 로 정하니 매번 같은 순서가 되는데 그것이 오히려 좋았다.
무작위로 하면 몰리는 시점이 매번 달라져서 문제가 났을 때 재현이 안 된다. 같은 순서면 어느 대가 먼저 붙었는지가 늘 같아서 확인할 수 있다.
제약 — 얼마나 쌓아 둘지
무한정 쌓으면 저장 공간이 찬다.
#define MAX_QUEUE_ROWS 86400 /* 1초에 1건이면 하루치 */
if (queue_size() >= MAX_QUEUE_ROWS) {
row_t *old = queue_pop(); /* 오래된 것부터 버린다 */
dropped_count++;
free(old);
}
MAX_QUEUE_ROWS 를 하루치로 두고 넘으면 오래된 것부터 버린다. 최근 값이 더 쓸모 있는 자료라 앞쪽을 버리는 쪽으로 정했다.
버릴 때 dropped_count 를 센다. 몇 개를 잃었는지를 서버에서 알 수 있어야 한다.
검증 — 숫자가 맞아떨어지는가
세어 둔 값을 요약으로 보내게 했다.
/* 매 시간 요약을 보낸다 */
send_summary(hour, collected, sent, dropped, queued);
수집한 것과 보낸 것과 버린 것과 남은 것을 매시간 보낸다. 서버에서 이 넷이 맞는지를 본다.
collected = sent + dropped + queued
안 맞으면 어딘가에서 사라진 것이다. 실제로 한 번 안 맞아서 큐 코드의 결함을 찾았다.
각 값만 봐서는 이상을 알 수 없다. 관계식으로 묶어야 그 안의 어긋남이 드러난다.
정리
- 밀린 것을 몰아 보내면 상황을 더 나쁘게 만들 수 있다
- 서버가 밀리면 다른 장비도 실패해 문제가 번진다
- 끊긴 이유가 여럿인데 코드는 전부 같은 속도로 보내고 있었다
last_errno와http_status를 함께 남겨 어느 계층의 실패인지 가른다429나503이면 천천히 보내고 장비 쪽 문제면 평소 속도로 보낸다Retry-After가 오면 우리 판단보다 그것을 따른다- 동시 복구는
device_id_hash로 흩고 무작위 대신 고정 순서를 쓴다 - 고정 순서여야 문제가 났을 때 재현된다
- 무한정 쌓지 않고 무엇을 버릴지 정한 뒤 버린 개수를 센다
- 수집과 전송과 폐기와 대기를 관계식으로 묶어 어긋남을 본다