Skip to content
isdnetworks
Go back

전에 실패한 이력이 처리 방식을 정했다

수집 장비가 통신이 끊겼다 붙으면 그동안 못 보낸 자료를 몰아서 보냈다. 그때마다 서버가 밀렸다.

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_errnohttp_status 를 함께 두어 어느 계층에서 실패했는지를 가른다.

연결 안 됨 (ECONNREFUSED)   서버가 안 떠 있다
시간 초과 (ETIMEDOUT)        서버가 느리거나 망이 느리다
503                          서버가 과부하라고 말하고 있다
429                          너무 많이 보내고 있다

ECONNREFUSED503 은 둘 다 실패인데 뜻이 다르다. 앞은 서버가 없는 것이고 뒤는 있는데 힘들다고 말하는 것이다.

이유별로 다르게 했다

기록이 쌓이니 다음 간격이 그 이유에서 나왔다.

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;                            /* 장비 쪽 문제였다. 정상 속도 */
}

429503 이었으면 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

안 맞으면 어딘가에서 사라진 것이다. 실제로 한 번 안 맞아서 큐 코드의 결함을 찾았다.

각 값만 봐서는 이상을 알 수 없다. 관계식으로 묶어야 그 안의 어긋남이 드러난다.

정리


Share this post on:

Previous Post
타임아웃이 네 곳에 있다
Next Post
다 끝났다는 신호가 먼저 왔다