본문으로 건너뛰기

에러 처리와 Backoff

kube에서 에러는 여러 계층에서 발생합니다. 어디서 어떤 에러가 나오고, 각 계층에서 어떻게 처리해야 하는지 정리합니다.

에러 발생 지점 맵

계층에러 타입원인
ClientHyperError, HttpError네트워크, TLS, 타임아웃
ApiError::Api { status }Kubernetes 4xx/5xx 응답
ApiSerializationErrorJSON 역직렬화 실패
watcherInitialListFailed초기 LIST 실패
watcherWatchFailedWATCH 연결 실패
watcherWatchErrorWATCH 중 서버 에러 (410 Gone 등)
Controllerreconciler Error사용자 코드에서 발생

Watcher 에러와 backoff

반드시 backoff을 붙여야 합니다
// ✗ 첫 에러에 스트림 종료 → Controller 멈춤
let stream = watcher(api, wc);

// ✓ 지수 백오프로 자동 재시도
let stream = watcher(api, wc).default_backoff();

default_backoff

ExponentialBackoff를 적용합니다: 800ms → 1.6초 → 3.2초 → ... → 30초(최대). 성공적인 이벤트를 수신하면 backoff가 리셋됩니다. 120초 동안 에러가 없으면 타이머도 리셋됩니다.

커스텀 backoff

use backon::ExponentialBuilder;

let stream = watcher(api, wc).backoff(
ExponentialBuilder::default()
.with_min_delay(Duration::from_millis(500))
.with_max_delay(Duration::from_secs(30)),
);

Reconciler 에러와 error_policy

fn error_policy(obj: Arc<MyResource>, err: &Error, ctx: Arc<Context>) -> Action {
tracing::error!(?err, "reconcile failed");

match err {
// 일시적 에러: 재시도
Error::KubeApi(_) => Action::requeue(Duration::from_secs(5)),
// 영구적 에러: 재시도하지 않음
Error::MissingField(_) => Action::await_change(),
}
}

Controller::run(reconcile, error_policy, ctx):

  • reconciler가 Err를 반환하면 error_policy가 호출됩니다
  • error_policy가 반환한 Action에 따라 scheduler에 예약합니다

현재 한계

  • error_policy동기 함수입니다. async 작업(메트릭 전송, status 업데이트 등)을 할 수 없습니다
  • 성공 시 reset 콜백이 없습니다. per-key backoff를 구현하려면 reconciler를 wrapper로 감싸야 합니다 (Per-key backoff 패턴 참고)

Client 레벨 재시도

kube-client에는 일반 API 호출에 대한 내장 재시도가 없습니다. create(), patch(), get() 등이 실패하면 그대로 에러를 반환합니다.

직접 구현하려면 Tower의 retry 미들웨어를 사용합니다:

use tower::retry::Policy;

struct RetryPolicy;

impl Policy<Request<Body>, Response<Body>, Error> for RetryPolicy {
// 5xx, 타임아웃, 네트워크 에러만 재시도
// 4xx는 재시도하지 않음 (요청 자체가 잘못됨)
}

재시도 가능 여부

에러재시도이유
5xx가능서버 일시 장애
타임아웃가능일시적 네트워크 문제
429 Too Many Requests가능rate limit → 대기 후 재시도
네트워크 에러가능일시적 연결 실패
4xx (400, 403, 404 등)불가요청이 잘못됨
409 Conflict불가SSA 충돌 → 로직 수정 필요

타임아웃 전략

Client 내부 구조에서 다룬 것처럼, 기본 read_timeout이 watch용으로 295초 설정되어 있어 일반 API 호출도 5분 블로킹될 수 있습니다.

대응 1: Client 분리

// watcher용 Client (기본 295초)
let watcher_client = Client::try_default().await?;

// API 호출용 Client (짧은 타임아웃)
let mut config = Config::infer().await?;
config.read_timeout = Some(Duration::from_secs(15));
let api_client = Client::try_from(config)?;

대응 2: 개별 호출 감싸기

let pod = tokio::time::timeout(
Duration::from_secs(10),
api.get("my-pod"),
).await??;

대응 3: Controller에서는 큰 문제가 아닙니다

Controller가 관리하는 watcher는 긴 timeout이 필요합니다. reconciler 내부의 API 호출만 timeout으로 감싸면 됩니다.