혼자 지킬 사람이 없어 쿠버네티스를 골랐다
1인 회사라 웹서버 뒤에 톰캣을 두는 흔한 구성으로는 죽었는지와 부하를 사람이 지켜봐야 한다. 하지만 지켜볼 사람이 없어 쿠버네티스를 골랐다. 서버를 통째로 갈아엎을 때 걷어낼 기회가 있었지만 유지했다. 자가 치유는 쓰고 있고 자동 확장은 아직 한 번도 쓰지 않았다.
배경 — 왜 필요했나
서버는 한 대다. 첫 글에서 쓴 대로 하이퍼바이저로 나눠 가상 머신 넷을 만들고, 그 위에 쿠버네티스를 올렸다.
| 항목 | 개수 |
|---|---|
| 물리 서버 | 1 |
| 노드(가상 머신) | 4 |
| 네임스페이스 | 24 |
| 파드 | 100 |
| 배포 단위 | 58 |
| 데이터를 쥔 서비스 | 8 |
| 외부·내부 진입점 | 14 |
| 영구 볼륨 | 21 |
| GitOps 애플리케이션 | 11 |
빌드 서버, 아티팩트 저장소, 이슈 트래커, 인증 서버, 데이터베이스 여러 벌, 메시지 큐, 지표·로그 수집, 메일, 브라우저 테스트 러너, 그리고 제품 서비스의 개발·운영 환경. 다 합쳐 파드 100개가 서버 한 대 위에 있다.
이걸 지키는 사람은 한 명이다. 상근 인프라 담당도 야간 당직도 없다. 대부분의 운영은 자동화가 하고 사람은 승인만 한다.
왜 쿠버네티스를 쓰는지가 어디에도 적혀 있지 않았다. 이유는 사람 머릿속에만 있었다.
OS 지원이 끝나 서버를 통째로 갈아엎을 때 문제가 드러났다. 걷어내기 가장 쉬운 때였지만, 다음 대가는 이미 알고 있었다.
- 파드 100개 중 40개 남짓은 우리 서비스가 아니다. 네트워크·인증서·로드밸런서·저장소 드라이버 등 클러스터가 자기 자신을 굴리는 데 쓰는 몫이다.
- 저장소 설정의 이름 하나 바꾸는 데 중단 30분이 들었다(아래 3번).
- 갈아엎으며 정리한 “여기서 막혔다” 항목만 44건이다.
문서에 남은 근거는 이 한 줄뿐이었다.
단일 베어메탈이라 진짜 HA 의미 없음 → 학습/노드 분리 목적
머릿속의 이유는 이와 달랐고, 이 한 줄로는 위 대가를 설명할 수 없다. 적혀 있지 않은 이유는 없는 이유와 같다 — 잘못됐을 때 되돌릴 기준이 없다.
실제 구성은 다음 글에 있다.
선택 — 왜 걷어내지 않았나
처음 고른 이유는 하나였다. 혼자서는 지켜볼 수 없기 때문이다.
웹서버 뒤에 톰캣을 붙이는 흔한 구성이라면 프로세스가 죽지 않았는지와 부하를 사람이 지켜봐야 한다. 1인 회사에는 그 사람이 없다. 새벽 3시에 톰캣이 죽으면 아침까지 아무도 모른다.
그때 산 기능은 두 개였다.
- 상태를 스스로 확인해서 죽은 것을 다시 띄운다(자가 치유)
- 부하가 늘면 개수를 자동으로 늘린다(수평 확장)
| 산 것 | 지금 | 실제 상태 |
|---|---|---|
| 자가 치유 | 쓰고 있다 | 파드 100개 중 70개에 “살아 있는지” 확인하는 규칙이 붙어 있다. 죽으면 알아서 다시 뜬다 |
| 자동 수평 확장 | 아직 안 쓴다 | 부하에 따라 개수를 조절하는 설정이 클러스터 전체에 0개다 |
확장은 그만한 트래픽이 없어 해 본 적 없다.
| 안 | 내용 | 판단 |
|---|---|---|
| A | 컨테이너를 걷어내고 서버에 직접 설치 | 100개짜리 구성을 사람 손으로 되살려야 하고, 그다음부터는 죽었는지를 사람이 지켜봐야 한다. 사람이 없어서 자동화를 산 건데, 그 사람을 다시 불러오는 안이다 |
| B | 도커 컴포즈 한 벌로 내려오기 | 한 대일 땐 충분하다. 그런데 노드가 넷이 됐다 — 어디에 띄울지, 죽으면 누가 살릴지를 다시 사람이 정해야 한다 |
| C | 클라우드의 관리형 쿠버네티스로 올라가기 | 운영 부담은 줄지만 매달 비용이 생기고, 이미 산 서버 한 대가 논다 |
| D | 단일 노드 쿠버네티스 그대로 유지 | 첫 글에서 지적한 노드 종속 문제가 그대로 남는다 |
| E | 여러 노드로 새로 구성하기 | 부담을 지는 대신 선언과 복원을 얻는다 |
E를 골랐다. 다시 만드는 일은 “복구”가 아니라 “동기화”였다.
무엇이 어디에 어떻게 떠 있어야 하는지가 파일로 적혀 저장소에 있었다. 새 클러스터에 배포 도구를 다시 깔고 저장소를 붙이니 나머지는 돌아왔다. 전체 전환이 1.5일에 끝난 이유다. 백업에서 손으로 되살렸다면 몇 주가 됐을 것이다.
이건 쿠버네티스가 훌륭해서가 아니라 우리 시스템이 이미 파일로 적혀 있었기 때문이다. 대체품은 애플리케이션 11개·볼륨 21개·진입점 14개를 파일로 적고 죽은 것을 스스로 살려야 한다. 그 비용이 위 대가보다 싸지 않았다.
그래서 이번에도 우리가 산 것은 가용성이 아니다.
서버가 한 대인 이상 이중화는 없다. 우리가 산 건 사람이 기억하지 않아도 되는 상태다. 무엇이 어떻게 떠 있어야 하는지가 사람 머리가 아니라 파일에 적혀 있고, 사람은 그 파일만 고친다.
한계는 이렇다.
- 작아지지 않는다. 서비스가 하나뿐이어도 저 40개는 계속 뜬다. 서비스 한두 개라면 거의 확실히 손해다.
- 격리가 약하다. 배포 도구 권한 경계를 하나만 써서 개발과 운영 사이에 사실상 경계가 없다.
- 막히는 곳은 계속 나온다. 44건은 끝난 개수가 아니라 지금까지 밟은 개수다.
- 산 기능을 다 쓰지도 않는다. 자동 확장은 설정 0개다. 필요할 것이라는 가정에 부담을 미리 내고 있다.
적용 — 결정을 실행으로 옮긴 방식
기억하지 않아도 되게 — 클러스터 상태를 저장소로 옮겼다
저장소(파일) ──▶ 배포 도구 ──▶ 클러스터
사람이 고치는 곳 사람이 직접 손대지 않는 곳
파일을 저장소에 넣고 배포 도구가 클러스터와 계속 맞춘다(현재 11개). 사람은 파일만 고치고 클러스터에 직접 명령을 넣는 건 대개 살필 때뿐이다.
클러스터가 파일과 달라지면 도구가 되돌려 손으로 급하게 고친 것도 지워진다. 급한 손질이 남으면 파일이 거짓말이 되고, 다음 재구축은 맨손 복구가 된다.
지켜보지 않아도 되게 — 죽었는지 판단하는 규칙을 붙였다
| 규칙 | 하는 일 | 지금 |
|---|---|---|
| 살아 있는지 확인 | 응답이 없으면 그 컨테이너를 다시 띄운다 | 파드 100개 중 70개 |
| 받을 준비가 됐는지 확인 | 준비 안 된 파드에는 트래픽을 안 보낸다 | 파드 100개 중 71개 |
| 둘 중 하나라도 붙어 있는 파드 | — | 75개 |
새벽 3시에 프로세스가 죽으면 이 규칙이 먼저 반응한다. 왜 죽었는지는 알려주지 않는다 — 별도 감시와 알람이 맡는다.
부하에 따라 개수를 조절하는 설정은 만들지 않았다. 아직 그만한 트래픽이 없어 켤 근거가 없다.
실수한 것들
44건 중 이 글의 주제와 곧바로 이어지는 네 건이다.
1. 죽은 걸 스스로 살리라고 샀는데, 8일 동안 안 살렸다
증상. 로드밸런서 파드 하나가 8일째 Ready가 아니었다. 컨테이너는 Running인데 준비 확인(readiness)에 응답하지 않았다. 재시작도 일어나지 않았다.
원인. 노드를 한꺼번에 재부팅한 뒤 프로세스가 좀비로 남았다. 커널이 쥔 포트를 놓지 않아 새 파드는 포트를 못 잡고 좀비는 응답하지 못했다. 살아 있는지 확인은 계속 실패했지만(6.6일간 4,688회) 재시작은 6회뿐이었다 — 계속 실패하면 재시도 간격을 점점 늘리기 때문이다.
해결. 컨테이너 런타임을 재시작해도 소용없었다(커널이 쥔 자원은 그대로다). 가상 머신을 하드 리셋하고 감시 규칙을 바꿨다. CrashLoopBackOff만 보면 이 유형은 영원히 안 걸린다(죽지 않으니 그 상태가 안 된다) — “Running인데 Ready가 아닌 상태가 한 시간 넘게 지속”을 알람 조건으로 추가했다.
안 죽고 이상해지는 경우는 자동화 대상이 아니다.
2. 자동 재시작이 데이터베이스를 깨뜨렸다
증상. 데이터베이스 파드를 한 대씩 교체했더니 새 파드가 PANIC: could not locate a valid checkpoint record(어디까지 썼는지 표시를 못 찾겠다)로 계속 죽었다.
원인. 종료 신호를 받고 내려가는 중에 공유 파일 서버(NFS) 쪽 쓰기 반영이 늦었다. 그 사이 마지막 저장 지점을 적어 두는 파일이 엉뚱한 곳을 가리킨 채 남았다. 데이터가 깨진 게 아니라 “어디까지 썼는지”가 깨졌다.
해결. 그 자리에서는 저장 지점 기록을 초기화해 살렸다(정상 종료 상태였으므로 손실은 없었다). 근본 해결은 따로 했다 — 데이터베이스를 파일 서버에서 노드 로컬 디스크로 옮겼다.
자동으로 다시 띄우는 기능은 다시 띄워도 되는 것에만 공짜다. 데이터는 종료 절차와 저장 방식을 사람이 설계한다.
3. 이름 하나 바꾸는 데 중단 30분
증상. 저장소 종류(StorageClass)의 이름을 바꾸려 했는데 안 됐다. 이 객체는 한 번 만들면 못 고친다.
원인. 새 이름을 만들고 그 볼륨을 전부 옮겨야 한다. 데이터베이스 세 벌(그중 하나는 18GB)과 메시지 큐가 대상이었다. 각각 백업 → 파드 0개로 내리기 → 데이터 복사 → 새 볼륨으로 다시 만들기.
해결. 그 과정에서 두 번 더 막혔다.
- 배포 도구가 계속 되돌렸다. 자동 동기화를 완전히 꺼야 했다. 되돌리기만 꺼서는 다른 변경이 들어올 때 다시 적용된다.
- 메시지 큐는 아예 거부했다. 운영을 대신하는 도구가
StorageClass변경을 막아서, 지우고 새로 만들었다(개발 환경이라 감당했다).
총 중단 30분. 파일로 관리하는 방식의 대가다. 쉽게 못 바꾸게 막아 주는 대신 바꿀 때는 절차를 밟는다.
4. 파일과 클러스터가 영원히 “다르다”고 떴다
증상. 배포 도구에서 애플리케이션 하나가 계속 OutOfSync(파일과 다름)였다. 되돌려도 없어지지 않았다.
원인. 사람이 적은 파일에는 없는 기본값(빈 conditions 항목)을 쿠버네티스 API 서버가 자동으로 채워 넣기 때문이었다. 도구는 파일대로 되돌리고 쿠버네티스는 다시 채워 넣는다. 영원히 안 끝난다.
해결. 파일 쪽에 그 기본값을 명시했다. 파일과 클러스터가 같아지면서 Synced로 정리됐다.
기능 영향은 0이지만 더 위험하다. 항상 빨간 것이 하나 있으면 진짜 빨간 것을 못 본다. 상시 거짓이 섞이면 운영은 다시 사람에게 돌아온다.
결과
| 항목 | 갈아엎기 전 | 지금 |
|---|---|---|
| 노드 | 1 | 4 |
| 쿠버네티스 | 1.26 | 1.36 |
| 파일로 선언된 애플리케이션 | 7 | 11 |
| 죽으면 자동으로 다시 뜨는 파드 | 기록 없음 | 100개 중 70개 |
| 부하 따라 개수를 조절하는 설정 | 기록 없음 | 0개 |
| 클러스터를 다시 만드는 방법 | 백업에서 손으로(해 본 적 없음) | 저장소를 붙여 동기화(실제 1.5일) |
| 왜 쿠버네티스를 쓰는지 적힌 문서 | 한 줄 | 이 글 |
이번에 센 값이 다음 판단의 기준점이다.
남는 것
- 자동화는 “무엇을 안 봐도 되는가”로 고른다. 산 것의 절반은 아직 안 쓴다.
- 이유는 적혀 있어야 이유다. 머릿속에 있는 이유는 되돌릴 기준이 못 된다.
- 자가 치유는 경계를 알고 써야 한다. 안 죽고 이상해진 것과 데이터를 쥔 것은 사람 몫이다(1·2번).
아직 해결 못 한 것.
- 고가용성은 여전히 없다. 서버가 한 대라 그게 죽으면 넷이 같이 죽는다.
- 자가 치유가 못 잡는 실패 유형이 남아 있다. 1번이 그 예다. 지금은 알람으로 덮었고, 자동 복구가 아니라 사람 호출이다.
- 「선택」에 적은 한계 넷 — 작아지지 않는다, 격리가 약하다, 막히는 곳은 계속 나온다, 자동 확장은 설정 0개 — 도 그대로다.
사람이 한 명이고 지켜야 할 것이 100개인 동안 맞는 선택이다. 둘 중 하나가 바뀌면 다시 계산해야 한다.