DevInsight

개발하며 겪은 판단을 적어요

DevOps
조회 5약 4분 읽기

200 OK는 무중단 배포를 지켜주지 않는다

무중단 배포가 실패하는 지점은 서버가 다운된 순간이 아니라, 헬스체크가 형식적인 200을 돌려주는 순간이다. 준비가 덜 된 신버전으로 트래픽이 몰리고 장애는 배포가 끝난 뒤에야 폭발한다. readiness와 liveness를 DB·Redis·메시지 큐 같은 실제 의존성 상태와 연결하는 방법, 탐지 지연을 줄이는 체크 주기, 그리고 어떤 지표에서 롤백을 눌러야 하는지 기준을 함께 정리한다.

DevInsight에서 발행해요

#무중단배포#헬스체크#롤링배포#블루그린배포#readiness#liveness#롤백#쿠버네티스#배포전략#장애대응

배포가 끝나고 스무 분쯤 지나서야 5xx가 치솟는 사고는 흔하다. 배포 화면은 전부 초록색이다. 파드마다 Ready를 찍었고, 헬스체크도 통과했다. 로그에는 빨간 글씨가 없다. 그런데 장애의 원인은 분명히 이번 배포다. 공통점은 하나다. 헬스체크가 형식적인 200을 돌려줬다는 것.

무중단 배포의 관건은 "서버가 안 죽는 것"이 아니라 "준비가 안 된 서버가 트래픽을 받지 않는 것"이다. 준비 여부를 판정하는 게 readiness다. 이 판정이 형식적으로 끝나면 롤링이든 블루그린이든 결과는 같다. 초록색 신호를 믿고 트래픽을 실은 시점에, 장애는 이미 예약된 셈이다.

이런 실패는 보통 비슷한 모양이다. 컨트롤러 하나에 하드코딩된 JSON을 돌려주는 헬스 엔드포인트, 프로세스가 떠 있기만 하면 {"status":"ok"}를 반환하는 그것. 서비스가 실제로 의존하는 것들과는 아무 상관이 없다. 트래픽이 실리기 전에는 그 200이 진짜인지 알 방법이 없고, 문제는 그 200을 신뢰하는 쪽에서 만들어진다.

"살아 있다"와 "받을 준비가 됐다"는 다른 검사다

헬스체크는 두 종류다. liveness는 프로세스가 살아 있는지 보고, 죽었다고 판정되면 파드를 재시작한다. readiness는 트래픽을 받을 수 있는지 보고, 실패하면 서비스 로테이션에서 뺀다. 재시작과 제외, 목적이 아예 다르다. 그런데 두 체크에 같은 엔드포인트를 쓰는 팀이 의외로 많다. 프로세스가 떠 있는 동안에는 파드가 계속 로테이션에 남는다. DB 커넥션 풀을 못 채웠든, 캐시가 텅 비었든 상관없이.

프레임워크가 HTTP 서버를 바인딩하는 시점과 서비스가 호출을 끝까지 소화하는 시점은 같지 않다. 대부분의 기본 헬스 엔드포인트는 전자만 검사한다. 후자를 검사하는 체크는 직접 만들어야 한다.

readiness에 넣을 것, 뺄 것

readiness가 검사해야 하는 것은 "이 파드가 호출 한 건을 처음부터 끝까지 처리하는가"다. 여기에 DB, Redis, 메시지 큐가 들어가야 하는지는 팀마다 판단이 갈린다. 기준을 하나 제시하면 "그 의존성이 죽었을 때, 이 파드가 호출을 정상 처리하는가"다. 모든 호출이 DB를 건드리는 서비스라면, DB가 죽은 상태에서 200을 돌려주는 readiness는 가치가 없다. 반면 조회 전용 캐시가 죽었다고 파드를 로테이션에서 빼면, 남은 파드로 트래픽이 몰리고 그 부하가 다시 캐시 재구축을 압박한다. 연쇄 장애는 여기서 시작된다.

구현은 어렵지 않다. /ready 엔드포인트에 타임아웃을 가진 의존성 검사 하나를 넣으면 된다. SELECT 1 같은 형식적 쿼리보다 서비스가 실제로 거치는 최소한의 경로를 고르는 편이 낫다. 2초 안에 응답이 없으면 503을 돌려주는 식이다. 쿠버네티스는 httpGet 프로브가 400 이상을 받으면 readiness 실패로 판정한다. 이 한 줄이 트래픽 전환 게이트 역할을 한다.

readinessProbe: httpGet: path: /ready port: 8080 periodSeconds: 5 timeoutSeconds: 2 failureThreshold: 3

체크 주기가 사고를 만드는 순간

탐지 지연은 체크 주기와 실패 임계값의 곱으로 정해진다. periodSeconds가 10초, failureThreshold가 3이면 파드가 이상해져도 트래픽을 빼는 데 최악 30초가 걸린다. 그 30초 동안 라우터는 계속 그 파드로 호출을 보낸다. 주기를 2초로 줄이면 반대의 문제가 생긴다. 파드가 50개면 프로브만으로 의존성에 초당 수십 건의 부하를 얹는다. 정상일 때는 감당되지만, 의존성이 흔들리는 순간 모든 파드의 프로브가 동시에 실패하면서 사태를 키운다.

그래서 주기는 탐지 지연과 프로브 부하 사이에서 타협한다. readiness는 5초, liveness는 10초, 임계값은 3이 무난한 출발점이다. 기동이 오래 걸리는 서비스는 startupProbe를 따로 둔다. JVM이 뜨는 데 40초 걸리는 애플리케이션에 liveness를 바로 걸면 warm-up 도중 파드가 재시작 루프에 빠진다. startupProbe가 성공하기 전까지 liveness·readiness를 잠재우는 게 정석이다.

배포가 끝난 뒤에 폭발하는 이유

왜 장애가 배포 직후가 아니라 한참 뒤에 나올까. 배포 중에는 파드가 아직 실트래픽을 거의 받지 않았기 때문이다. 헬스체크는 "지금 트래픽을 받아도 되는가"를 검사하지, "받은 뒤에 제대로 처리하는가"를 검사하지 못한다. 신버전이 필요로 하는 컬럼이 아직 마이그레이션되지 않았거나, 새로 붙은 외부 API가 느려지는 상황은 프로세스 수준의 체크로 드러나지 않는다. 트래픽이 실린 뒤에야 커넥션 풀이 차오르고, 캐시 미스가 쌓이고, 타임아웃이 겹친다.

여기서 배포 전략의 차이가 갈린다. 롤링 배포는 readiness를 통과한 배치부터 순차로 트래픽을 실는다. 그 readiness가 형식적이면 첫 배치부터 깨진 코드가 실리고 나머지 배치가 뒤따른다. maxSurge와 maxUnavailable을 아무리 조여도, 검사가 형식적이면 그 숫자는 무의미하다. 블루그린 배포는 전환 자체가 순간이라 빠르지만, 그린 환경은 전환 전까지 유휴 상태다. 실트래픽을 한 건도 겪지 않은 채 판정이 내려진다. 전환 전에 그린 환경으로 합성 트래픽을 흘려 보거나, 소량의 실트래픽을 먼저 태우는 카나리아를 끼워 두는 편이 안전하다.

롤백은 사고 중에 결정하지 않는다

장애 대응에서 가장 비싼 단계는 감지가 아니라 판정이다. 감지 수단은 이미 서비스에서 쏟아진다. 문제는 "어느 정도면 되돌릴까"가 사고 현장에서 정해진다는 것이다. 그 순간 결정은 미뤄지고, 미루는 동안 신버전이 피해를 키운다. 배포 전에 숫자로 적어 두면 된다.

  • 5xx 비율이 평소 대비 3배를 넘으면서 3분 연속이면 롤백
  • p99 응답이 평소의 2배를 넘는 상태가 5분 지속되면 롤백
  • 신버전 배포 이후에만 등장하는 오류 로그가 급증하면 롤백

수치의 의미는 정상 오차 범위를 안다는 전제에서 성립한다. 평소에도 5xx가 0.3%씩 나오는 서비스라면 0.5%는 이상 징후가 아니다. 기준을 잡기 전에 한 달치 지표의 평균과 분산을 먼저 뽑아야 한다.

롤백이 항상 정답은 아니다. 데이터 마이그레이션이 이미 적용됐다면, 이전 버전으로 되돌려도 스키마가 맞지 않아 더 크게 깨진다. 신버전이 쓴 데이터를 이전 버전이 읽지 못하는 경우다. 이때는 롤백 대신 전진 수정이 맞고, 마이그레이션을 배포 단위에서 분리해 두면 이 판단 자체를 피해 갈 수 있다. 롤백은 "코드만 되돌려도 되는가"를 확인한 뒤에 누르는 버튼이다.

다음 배포부터 /ready 한 줄에 의존성 검사 하나와 2초 타임아웃을 넣고, 롤백 트리거 수치를 배포 런북에 적어 두자. 그리고 테스트 환경에서 의존성 장애를 주입해 그 수치가 실제로 발동하는지 확인하는 것까지. 형식적인 200은 무중단 배포를 지켜주지 않는다. 사고를 정시 배달할 뿐이다.

댓글

댓글을 읽어오는 중입니다.

같이 읽으면 좋은 글

방금 읽은 주제와 이어지는 글을 골랐습니다.

DevOps 전체 보기
DevOps

캐시 적중률 98%인데 배포판은 왜 자꾸 구버전일까

의존성 캐시와 빌드 캐시, 매트릭스 전략은 CI 실행 시간을 반으로 줄이지만 무효화 기준을 잘못 잡으면 오래된 패키지가 그대로 빌드를 통과시키는 예상 밖의 사고로 돌아온다. 이 글은 캐시가 실제로 빌드를 깨뜨리는 구체적인 상황들을 짚고, 잠금 파일·해시 입력·매트릭스 키 설계에서 무효화 시점을 정확히 읽는 원칙을 정리해 캐시를 안전하게 쓰는 기준을 제시한다.

#GitHub Actions#CI/CD#캐시 전략#빌드 최적화
DevOps

이미지를 얇게 만들수록 빌드가 비대해지는 역설

Next.js/Node.js 컨테이너를 multi-stage 빌드와 standalone 출력으로 최적화하다 보면 용량은 80% 줄었는데 빌드 시간은 되레 늘고, 캐시 무효화로 매 배포마다 의존성을 새로 내려받는 경우가 생긴다. 이 글은 무엇을 줄여야 하고 무엇은 절대 건드리면 안 되는지를 가르는 기준을 결정 메모 형식으로 정리하고, 최적화가 역효과를 내는 지점을 짚는다.

#Docker#Next.js#Node.js#multi-stage 빌드
DevOps

INFO, WARN, ERROR만으로는 부족하다

console.log에서 JSON 로그로 가는 건 시작일 뿐이다. 로그 레벨을 모호하게 정의하면 알람이 무의미해지고, 스키마 없이 쌓은 로그는 검색조차 불가능하다. 마스킹을 미루면 개인정보가 로그 플랫폼에 그대로 노출된다. 급증하는 로그 비용도 간과할 수 없다. 이 글은 로그 레벨 기준, 공통 필드, 마스킹, 비용 거버넌스 등 구조화 로깅 도입 전에 반드시 정해야 할 결정들을 기록한다.

#구조화로깅#로그레벨#observability#DevOps

이전 글

문서를 잘게 쪼갤수록 답이 멀어지는 이유

다음 글

any 지옥에서 strict로 승급한 어느 프로젝트의 사투기

DevInsight Digest

새 글이 쌓이면, 피드에서 바로 이어 읽으세요.

과장된 알림 대신 발행한 글 전체를 RSS로 제공합니다.

RSS 구독