← posts/b.log()

blog92@web:~$ cat posts/infra-10-operations-advanced.md

DEVOPS13 min read

풀스택 개발자를 위한 인프라 10강 — 운영 심화

가용성 숫자가 뜻하는 시간, 고가용성 설계와 백업·재해 복구, DB 운영과 캐싱·큐, 보안 운영, 부하 테스트와 용량 관리, SLO와 에러 버짓, 장애 대응과 비난 없는 회고.

마지막 강이다. 지금까지 만든 시스템을 장애와 부하, 그리고 시간 속에서도 계속 굴러가게 하는 운영 주제를 한자리에 모았다.

1. 가용성을 숫자로 이해하기

가용성연간 허용 중단월간 허용 중단
99%약 3.65일약 7.3시간
99.9%약 8.77시간약 43.8분
99.95%약 4.38시간약 21.9분
99.99%약 52.6분약 4.4분
99.999%약 5.3분약 26초

9가 하나 늘 때마다 비용과 복잡도는 몇 배로 뛴다. 99.99%는 사람이 알림을 받고 접속하는 사이에 이미 예산이 소진되므로 자동 복구가 전제다. 목표는 "최대한 높게"가 아니라 서비스와 사용자에게 필요한 만큼이다.

직렬 의존성은 가용성을 곱한다. 앱 99.9% × DB 99.9% × 외부 API 99.9% ≈ 99.7%. 의존성이 늘수록 전체 가용성은 낮아진다.

2. 고가용성 설계

단일 장애점(SPOF) 제거

아키텍처 그림에서 "이것 하나가 죽으면 서비스 전체가 멈추는가?"를 모든 박스에 대해 묻는다.

구성 요소SPOF 제거 방법
앱 서버여러 대 + 로드밸런서, 무상태화
로드밸런서관리형 LB(자체 이중화), 온프레미스는 keepalived(VRRP)로 가상 IP 이중화
DB복제 + 자동 장애 조치 (Multi-AZ, Patroni 등)
캐시복제 또는 캐시 장애 시에도 동작하는 설계
가용 영역/데이터센터멀티 AZ
사람런북, 온콜 교대, 권한 이중화
인증서·도메인만료 알림, 자동 갱신

확장 방식

  • 수직 확장(scale up): 더 큰 서버. 단순하지만 한계와 다운타임이 있다
  • 수평 확장(scale out): 서버를 더 추가. 무상태여야 가능하다

무상태화 체크리스트: 세션은 Redis/JWT로, 업로드 파일은 오브젝트 스토리지로, 메모리 캐시는 공유 캐시로, 예약 작업은 한 인스턴스만 실행되게(분산 락 또는 별도 워커).

장애를 전파하지 않는 설계

  • 타임아웃: 모든 외부 호출에 설정. 기본값(무제한)이 가장 위험하다
  • 재시도 + 지수 백오프 + 지터: 동시에 몰려 재시도하는 폭주를 막는다. 멱등하지 않은 작업은 재시도에 주의 (멱등성 키 사용)
  • 서킷 브레이커: 실패가 누적된 의존성은 잠시 호출하지 않고 즉시 실패
  • 벌크헤드: 의존성별로 커넥션 풀·스레드를 분리해 하나가 전체를 잠식하지 않게
  • 우아한 기능 저하: 추천 API가 죽으면 추천 영역만 숨기고 핵심 기능은 유지
  • 속도 제한과 부하 차단: 과부하 시 일부 요청을 빠르게 거절하는 것이 전체가 느려지는 것보다 낫다

3. 백업과 재해 복구

RPO와 RTO

  • RPO (Recovery Point Objective): 최대 얼마만큼의 데이터 손실을 허용하는가 (예: 15분)
  • RTO (Recovery Time Objective): 최대 얼마 안에 복구해야 하는가 (예: 1시간)
text
   마지막 백업        장애 발생              서비스 복구
──────●───────────────✕──────────────────────●──────▶
      └──── RPO ──────┘└──────── RTO ─────────┘

백업 원칙

  • 3-2-1 규칙: 사본 3개, 서로 다른 매체 2종, 1개는 다른 장소(오프사이트)
  • 불변(immutable) 백업: 랜섬웨어·실수로 삭제되지 않게 (S3 Object Lock, 별도 계정)
  • 복구 테스트: 복구해 본 적 없는 백업은 백업이 아니다. 정기적으로 실제 복구 훈련을 하고 소요 시간을 잰다
  • 무엇을 백업하는가: DB만이 아니다. 오브젝트 스토리지, 설정 파일, 인증서, 비밀, IaC state, 쿠버네티스 etcd, 컨테이너 이미지(폐쇄망이라면 반입본)
  • 백업 모니터링: 백업 작업 성공 여부와 백업 파일 크기 추이에 알림

DB 백업 방식

방식예특징
논리 백업pg_dump, mysqldump이식성 좋음, 대용량에서 느림
물리 백업pg_basebackup, 스냅샷빠름, 동일 버전 필요
연속 아카이빙 (WAL/binlog)pgBackRest, WAL-G특정 시점 복구(PITR), RPO 최소화
bash
# 예: PostgreSQL 논리 백업 + 보관 기간 정리
pg_dump -Fc -h db -U app app > /backup/app_$(date +%F_%H%M).dump
find /backup -name 'app_*.dump' -mtime +14 -delete
# 복구
pg_restore -h db -U app -d app_restore --clean /backup/app_2026-09-17_0300.dump

DR 전략

전략방식RTO/RPO비용
백업·복원백업만 원격 보관, 장애 시 새로 구축시간~일낮음
파일럿 라이트DB 복제만 유지, 앱은 장애 시 기동수십 분~시간중간
웜 스탠바이축소된 전체 환경 상시 운영분 단위높음
액티브-액티브여러 지역에서 동시 서비스거의 0매우 높음

8강의 IaC가 있으면 "백업·복원" 전략의 RTO가 크게 줄어든다. 인프라를 코드로 다시 만들 수 있다는 것 자체가 DR 전략이다.

4. 데이터베이스 운영

복제

  • 동기 복제: 복제본이 받았다고 확인해야 커밋 → 데이터 손실 없음, 쓰기 지연 증가
  • 비동기 복제: 먼저 커밋 후 전송 → 빠름, 장애 시 최근 데이터 손실 가능
  • 복제 지연(replication lag): 읽기 복제본에서 방금 쓴 데이터가 안 보일 수 있다. "내가 쓴 데이터를 바로 읽는" 요청은 primary로 보낸다

커넥션 풀

DB 연결은 비싸고(2강), DB가 허용하는 최대 연결 수는 제한적이다.

text
인스턴스 수 × 인스턴스당 풀 크기 ≤ DB max_connections (여유분 제외)
예: Pod 20개 × 풀 20 = 400 → DB 한도 200이면 장애
  • 앱 풀 크기는 생각보다 작게 잡는다. 풀이 크다고 빨라지지 않고, DB 내부 경합만 늘어난다
  • 인스턴스가 많거나 서버리스 환경이면 PgBouncer(트랜잭션 풀링), RDS Proxy 같은 외부 풀러를 둔다
  • 트랜잭션 풀링 모드에서는 세션 단위 기능(일부 prepared statement, 세션 변수)에 제약이 있다
  • 풀 대기 시간, 활성 연결 수를 메트릭으로 수집한다

쿼리 성능

sql
-- 실행 계획 확인
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE user_id = 42 ORDER BY created_at DESC LIMIT 20;
 
-- PostgreSQL: 가장 많은 시간을 쓴 쿼리 (pg_stat_statements 확장)
SELECT query, calls, total_exec_time, mean_exec_time
FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;
  • 슬로우 쿼리 로그를 켠다 (log_min_duration_statement)
  • 인덱스는 조회 조건과 정렬에 맞춰 설계하되, 쓰기 비용과 저장 공간이 늘어난다
  • N+1 쿼리, 페이지네이션의 큰 OFFSET, 트랜잭션을 오래 여는 코드가 흔한 원인
  • 대용량 테이블 변경은 잠금을 고려해 온라인 방식으로 (5강 expand-contract)

5. 캐싱

계층예
브라우저Cache-Control
CDNCloudFront, 정적 파일·공개 API 응답
리버스 프록시Nginx proxy_cache
애플리케이션Redis
DB버퍼 캐시, 머티리얼라이즈드 뷰

Cache-aside 패턴:

ts
async function getProduct(id: string) {
  const key = `product:${id}`;
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);
 
  const product = await db.product.findUnique({ where: { id } });
  const ttl = 300 + Math.floor(Math.random() * 60);   // TTL에 지터 → 동시 만료 방지
  await redis.set(key, JSON.stringify(product), "EX", ttl);
  return product;
}
// 변경 시: DB 갱신 후 redis.del(key)

주의할 문제:

  • 무효화: 변경 시 캐시 삭제. 완벽한 일관성이 필요한 데이터는 캐시하지 않는다
  • 캐시 스탬피드: 인기 키가 만료되는 순간 요청이 한꺼번에 DB로 몰림 → 락, 조기 갱신, TTL 지터
  • 캐시 장애: 캐시가 죽어도 서비스가 동작하는지(타임아웃, DB 보호) 확인
  • 캐시 적중률을 메트릭으로 본다

6. 비동기 처리와 큐

오래 걸리는 작업(메일 발송, 리포트 생성, 이미지 처리, 외부 시스템 연동)을 요청 흐름에서 분리한다. 3강의 504 문제를 근본적으로 해결하는 방법이다.

text
요청 → API: 작업 등록 후 즉시 202 + jobId 응답
            └─▶ 큐 (Redis/BullMQ, RabbitMQ, SQS, Kafka)
                   └─▶ 워커: 처리 → 결과 저장
클라이언트: jobId로 상태 조회 (폴링, SSE, WebSocket)

원칙:

  • 큐는 보통 최소 한 번(at-least-once) 전달 → 같은 메시지가 두 번 올 수 있으므로 처리를 멱등하게
  • 실패 메시지는 재시도 후 Dead Letter Queue로 격리해 따로 확인
  • 큐 길이와 처리 지연을 메트릭으로 수집 (KEDA로 워커 자동 확장 가능, 9강)
  • 스케줄 배치(동기화 작업 등)는 중복 실행 방지, 실행 이력, 실패 알림을 갖춘다

7. 보안 운영

심층 방어 (Defense in Depth)

한 겹이 뚫려도 다음 겹이 막도록 여러 계층에 방어를 둔다.

계층조치
네트워크망 분리, 최소 포트 개방, private 서브넷, NetworkPolicy, WAF
신원·권한SSO, MFA, 최소 권한, 역할 기반 임시 자격 증명, 정기 권한 검토
호스트OS 패치, SSH 키 인증만 허용(비밀번호·root 로그인 금지), 불필요 서비스 제거
컨테이너non-root, 최소 이미지, 취약점 스캔, 서명 검증
애플리케이션입력 검증, 의존성 업데이트, 보안 헤더, 속도 제한
데이터전송 암호화(TLS), 저장 암호화, 개인정보 마스킹
감시감사 로그(CloudTrail, K8s audit), 이상 탐지, 로그 무결성

시크릿 관리

단계방식
피해야 할 것코드·Git·이미지에 하드코딩, 채팅으로 공유
기본환경 변수 + 서버 파일 권한 600
권장비밀 저장소(HashiCorp Vault/OpenBao, AWS Secrets Manager), SOPS
고급동적 비밀(요청 시 발급되는 짧은 수명의 DB 계정), 자동 교체

유출이 의심되면 삭제가 아니라 교체(rotate)가 우선이다. Git 기록에서 지워도 이미 복제되었을 수 있다. gitleaks 같은 도구로 커밋 전 검사를 건다.

공급망 보안

  • 의존성 자동 업데이트(Dependabot/Renovate)와 취약점 알림
  • npm ci와 lockfile로 재현 가능한 설치
  • 컨테이너 이미지 스캔(Trivy), SBOM(구성 요소 목록) 생성
  • 이미지 서명(cosign)과 배포 시 검증
  • CI의 서드파티 Action 버전 고정

폐쇄망 보안의 특수성

망이 분리되어 있어도 반입 경로(USB, 반입 PC, 망연계 솔루션)가 공격 경로가 된다. 반입 파일의 해시 검증과 악성코드 검사, 반입 이력 관리가 필요하다. 외부 업데이트가 늦어지기 쉬우므로 정기 패치 반입 주기를 정해 둔다. DB 암호화 에이전트, 보안 솔루션 같은 에이전트가 서비스 기동 순서에 영향을 주는 경우가 있으므로, 서버 재기동 시 에이전트 기동 확인을 런북에 포함한다.

8. 성능과 용량 관리

측정이 먼저

추측으로 튜닝하지 않는다. 7강의 메트릭과 트레이스로 병목을 찾고, 부하 테스트로 확인한다.

js
// k6 부하 테스트 예 (load.js)
import http from "k6/http";
import { check, sleep } from "k6";
 
export const options = {
  stages: [
    { duration: "1m", target: 50 },
    { duration: "3m", target: 50 },
    { duration: "1m", target: 200 },
    { duration: "1m", target: 0 },
  ],
  thresholds: {
    http_req_failed: ["rate<0.01"],
    http_req_duration: ["p(95)<500"],
  },
};
 
export default function () {
  const res = http.get("https://stage.example.com/api/products");
  check(res, { "status 200": (r) => r.status === 200 });
  sleep(1);
}
bash
k6 run load.js

부하 테스트 종류: 기준 부하(평소), 스트레스(한계 찾기), 스파이크(급증), 장시간(메모리 누수). 운영 환경에 직접 하지 않는다.

자주 만나는 한계값

bash
ulimit -n                                  # 프로세스당 열 수 있는 파일(=소켓) 수
cat /proc/<PID>/limits
sysctl net.core.somaxconn                  # 연결 대기열 크기
sysctl net.ipv4.ip_local_port_range        # 출발 포트 범위 (2강 TIME_WAIT 고갈)

systemd 서비스는 LimitNOFILE=65535, Nginx는 worker_rlimit_nofile로 올린다. "Too many open files" 에러가 대표 증상이다.

Node.js 관점

  • CPU를 오래 쓰는 동기 작업은 이벤트 루프를 막아 모든 요청을 지연시킨다 → 워커 스레드, 별도 워커 프로세스, 큐
  • 이벤트 루프 지연(nodejs_eventloop_lag_seconds)을 메트릭으로 본다
  • 멀티 코어 활용은 인스턴스 수평 확장(컨테이너 여러 개)이 가장 단순하다
  • 메모리 누수는 힙 스냅샷 비교로 찾는다

용량 계획

현재 사용량, 증가 추세, 피크 배수를 바탕으로 언제 한계에 도달하는지 예측한다(7강 predict_linear). 디스크, DB 연결 수, 라이선스, IP 대역, 클라우드 서비스 한도(quota)도 용량이다.

9. SRE — 신뢰성을 관리하는 방법

SLI, SLO, SLA

용어의미예
SLI (Indicator)측정 지표성공 응답 비율, 500ms 이내 응답 비율
SLO (Objective)내부 목표30일 기준 가용성 99.9%
SLA (Agreement)고객과의 계약, 위반 시 보상99.5% 미만 시 크레딧

SLO는 SLA보다 엄격하게 잡아 계약 위반 전에 대응할 여유를 둔다.

좋은 SLI는 사용자 경험을 반영한다. "서버 CPU"가 아니라 "사용자 요청 중 성공하고 빠르게 응답한 비율"이다.

promql
# 30일 가용성 SLI
sum(rate(http_request_duration_seconds_count{status!~"5.."}[30d]))
  / sum(rate(http_request_duration_seconds_count[30d]))

에러 버짓

SLO 99.9%라면 0.1%는 실패해도 되는 예산이다(30일 기준 약 43분).

  • 예산이 남으면 → 기능 출시, 실험, 과감한 배포
  • 예산을 다 쓰면 → 신규 출시를 늦추고 안정화 작업 우선

신뢰성과 개발 속도의 긴장을 감정이 아닌 숫자로 조정하는 장치다. 알림도 "에러율 5%"보다 에러 버짓 소진 속도(burn rate) 기반으로 걸면 오탐이 줄어든다.

토일(Toil) 줄이기

수작업·반복·자동화 가능·서비스 성장에 비례해 늘어나는 운영 업무를 토일이라 한다. 반복되는 수작업(수동 재기동, 수동 동기화 실행, 인증서 수동 교체)을 찾아 스크립트 → 자동화 → 자가 치유 순서로 줄여 나간다.

10. 장애 대응

대응 단계

text
탐지 → 선언·역할 분담 → 영향 파악 → 완화 → 복구 → 모니터링 → 회고
  1. 탐지: 알림, 사용자 제보
  2. 선언: 장애임을 알리고 역할을 나눈다 — 지휘자(결정), 작업자(조치), 커뮤니케이터(공지·기록)
  3. 영향 파악: 누가, 어떤 기능이, 언제부터
  4. 완화 우선: 원인 규명보다 서비스 회복이 먼저. 최근 배포 롤백, 트래픽 차단, 기능 끄기(feature flag), 인스턴스 증설
  5. 복구와 관찰: 정상화 후에도 일정 시간 지표를 지켜본다
  6. 회고: 재발 방지

장애 시 첫 질문: "최근에 무엇이 바뀌었나?" (배포, 설정 변경, 인증서 만료, 트래픽 패턴, 의존 서비스, OS 패치)

장애 중에는 타임라인을 기록한다. 몇 시 몇 분에 무엇을 보고 무엇을 했는지가 회고의 재료다.

런북

알림마다, 주요 작업마다 절차서를 둔다. 새벽에 처음 보는 사람도 따라 할 수 있어야 한다.

markdown
# 런북: API 에러율 급증
 
## 증상
- HighErrorRate 알림, 5xx 비율 5% 초과
 
## 확인
1. Grafana "API 개요" 대시보드: 특정 라우트에 집중되는가?
2. 최근 배포 여부: `kubectl -n blog rollout history deployment/api`
3. 에러 로그: Loki `{service="api"} | json | level="error"`
4. 의존성: DB 연결 수, Redis 상태, 외부 API 상태
 
## 조치
- 최근 배포 후 발생 → `kubectl -n blog rollout undo deployment/api`
- DB 연결 고갈 → PgBouncer 상태 확인, 장시간 쿼리 종료
- 외부 API 장애 → 해당 기능 feature flag 끄기
 
## 에스컬레이션
- 15분 내 미해결 시 백엔드 리드에게 연락

인수인계 없이 받은 시스템이라면, 파악하는 과정 자체를 런북과 구성도로 남기는 것이 가장 가치 있는 산출물이다: 서버 목록, 서비스 기동 순서, 설정 파일 위치, 볼륨 위치, 의존 관계, 재기동 절차, 확인 명령.

비난 없는 회고 (Blameless Postmortem)

사람을 탓하면 다음 장애 때 정보가 숨겨진다. "누가"가 아니라 "어떤 시스템과 절차가 이 실수를 가능하게 했는가"를 묻는다.

markdown
# 회고: 2026-09-17 API 장애
 
## 요약
- 영향: 14:02~14:37 (35분), 주문 API 요청의 약 30% 실패
- 원인: 마이그레이션이 대용량 테이블에 잠금을 걸어 쿼리 대기
 
## 타임라인
- 14:00 배포 시작 (마이그레이션 포함)
- 14:02 에러율 알림
- 14:10 DB 잠금 대기 확인
- 14:25 마이그레이션 중단 결정
- 14:37 정상화
 
## 잘된 점 / 아쉬운 점
- 알림은 2분 만에 도착
- 마이그레이션 영향 사전 검토 절차가 없었음
 
## 재발 방지 (담당·기한)
- [ ] 대용량 테이블 변경은 온라인 방식 필수, PR 체크리스트에 추가
- [ ] 스테이징에 운영 규모 데이터로 마이그레이션 사전 실행
- [ ] 마이그레이션 lock_timeout 설정

11. 변경 관리

장애의 상당수는 변경에서 시작된다.

  • 모든 변경은 코드(앱, IaC, 설정)로, 리뷰를 거쳐, 기록이 남게
  • 작게, 자주, 되돌릴 수 있게 (feature flag, 카나리, 롤백 절차)
  • 위험한 변경은 트래픽이 적은 시간에, 담당자가 자리에 있을 때 (금요일 저녁 배포 지양)
  • 배포 이벤트를 대시보드에 표시(Grafana annotation)해 지표 변화와 연결
  • 정기 점검: 인증서 만료, 디스크 추세, 백업 복구 테스트, 권한 검토, 패치 적용

12. 실습 과제 — 최종 프로젝트

지금까지의 모든 내용을 하나의 개인 프로젝트에 적용한다.

  1. SLO 정의: 가용성·지연 SLO를 정하고 Grafana에 에러 버짓 패널을 만든다.
  2. 백업: DB 자동 백업 + 오프사이트 복사 + 보관 정책을 구성하고, 실제로 다른 환경에 복구해 RTO를 측정한다.
  3. 장애 주입: 앱 Pod 삭제, DB 중지, 디스크 가득 채우기, 외부 API 지연(타임아웃)을 일으키고 서비스와 알림의 반응을 관찰한다.
  4. 복원력: 외부 호출에 타임아웃·재시도·서킷 브레이커를 적용하고, 오래 걸리는 작업을 큐로 분리한다.
  5. 커넥션 풀: 인스턴스 수를 늘려 가며 DB 연결 수를 관찰하고, PgBouncer를 도입해 비교한다.
  6. 부하 테스트: k6로 SLO를 만족하는 최대 처리량을 찾고, 병목을 트레이스로 확인해 한 가지를 개선한다.
  7. 보안: Trivy 스캔을 CI에 넣고, 시크릿을 SOPS 또는 비밀 저장소로 옮기고, NetworkPolicy를 적용한다.
  8. 문서화: 구성도, 런북 3개(에러율 급증, 디스크 가득, DB 장애), 장애 주입 결과에 대한 회고 1건을 작성한다.
  9. 재현성: 모든 환경을 삭제한 뒤 IaC와 파이프라인만으로 처음부터 재구축하고 소요 시간을 기록한다.

13. 핵심 정리

  • 가용성 목표는 필요한 만큼, 의존성은 가용성을 곱한다
  • SPOF 제거, 무상태 수평 확장, 타임아웃·재시도·서킷 브레이커
  • RPO/RTO를 먼저 정하고, 복구 테스트를 하지 않은 백업은 믿지 않는다
  • 커넥션 풀은 작게, 인스턴스 수를 곱해 DB 한도를 확인
  • 캐시는 무효화·스탬피드·장애 시 동작까지 설계
  • 오래 걸리는 일은 큐로, 처리는 멱등하게
  • 심층 방어, 비밀은 저장소에, 유출 시 교체
  • 튜닝은 측정 후에, 부하 테스트는 운영 밖에서
  • SLO와 에러 버짓으로 속도와 안정성을 숫자로 조정
  • 장애 시 완화가 먼저, "최근에 무엇이 바뀌었나?", 회고는 비난 없이
  • 런북과 구성도는 다음 사람(미래의 나)을 위한 가장 값진 인프라
COMMENTS (…)

댓글을 불러오는 중이에요.

NEW COMMENT0 / 1000
⌘↵ 전송

blog92@web:~$ cd ..