오늘 새벽 02:00 ~ 03:00. 내 홈랩 클러스터 에서 연달아 사고 :

  1. KubeNodeNotReadylouise 노드 24 시간 다운
  2. KubeContainerWaitingjabis / logistic 의 GHCR 401
  3. KubeAPIErrorBudgetBurn컨트롤 플레인 503
  4. xr.lemuel.co.kr502 Bad Gateway
  5. oms.lemuel.co.krHikari Connection is not available

이 5 가지 가 *우연히 같은 새벽 에 모인 것이 아니다. *서로 *서버 의 다른 layer 의 같은 기본기건드린 것. DNS · TLS · Ingress · Pod · Connection Pool · Storage · 노드어느 한 곳 이 무너지면 *상위 가 다 따라간다.

이 글 은 서버 의 기본기한 요청 의 *여정 의 형태 로 따라간다. 추상적 정의 가 아니라 오늘 새벽 의 *실제 사고들각 layer 의 교재 로*.

내 블로그 의 CPU 캐시, 프로세스 추상화, K8s Watch-Reconcile, Outbox 패턴, 오프셋 과 어셈블리어상위 추상 으로 전부 묶이는 글.


TL;DR — 한 줄 결론

한 개의 요청내 손가락 키 입력 부터 DB 의 디스크 를 거쳐 내 화면 으로 돌아오기까지최소 *12 개의 layer. DNS · TLS · CDN · LB · Ingress · Service · Pod · Process · Thread · Pool · DB · Storage. 각 layer 가 *자기 책임 을 수행하면서 옆 layer 의 *추상화 만 신뢰. *어느 한 곳기본기 가 무너지면 상위 가 다 무너진다. *서버 의 기본기 = *layer 별 *책임 의 시야**.


1. 기본 시야 *— *한 요청 의 *12 단계**

flowchart TD
    U[1. 사용자<br/>브라우저 / curl]
    D[2. DNS<br/>이름 → IP]
    T[3. TLS Handshake<br/>안전한 채널]
    CF[4. CDN/Edge<br/>Cloudflare]
    LB[5. Load Balancer<br/>여러 백엔드 분산]
    IN[6. Ingress<br/>호스트별 라우팅]
    SV[7. Service<br/>가상 IP → Pod]
    PD[8. Pod<br/>컨테이너 격리]
    PR[9. Process<br/>JVM / Node / Python]
    TH[10. Thread/Loop<br/>요청 처리]
    CP[11. Connection Pool<br/>DB 연결 재사용]
    DB[12. DB / Storage<br/>Index, WAL, Page]

    U --> D --> T --> CF --> LB --> IN --> SV --> PD --> PR --> TH --> CP --> DB
    DB -.응답.-> TH -.-> PR -.-> PD -.-> SV -.-> IN -.-> LB -.-> CF -.-> T -.-> U

    classDef client fill:#1e3a5f,stroke:#3b82f6,color:#fff
    classDef net fill:#3f2f1f,stroke:#f59e0b,color:#fff
    classDef k8s fill:#3f1e3f,stroke:#a855f7,color:#fff
    classDef app fill:#1f3f1f,stroke:#22c55e,color:#fff
    classDef data fill:#5f1e1e,stroke:#ef4444,color:#fff
    class U client
    class D,T,CF,LB net
    class IN,SV,PD k8s
    class PR,TH app
    class CP,DB data

12 단계 의 *어디서 든 * 사고 가 발생 할 수 있다. 오늘 새벽 의 *5 사고각각 의 layer :

사고 layer
louise 다운 7~12 (Pod / DB 의 하드웨어 기반)
GHCR 401 8 (Pod 의 이미지 풀)
컨트롤 플레인 503 7~8 (K8s 의 오케스트레이션)
xr 502 6~12 (Ingress → DB 의 전 흐름)
oms 500 11 (Connection Pool)

2. Layer 1 — *사용자 의 요청**

GET /api/users/me HTTP/2
Host: oms.lemuel.co.kr
Authorization: Bearer eyJhbGciOiJIUzI1...

요청 의 본질 — 4 가지:

  • methodGET / POST / … (의도)
  • targetpath + query (대상)
  • headers메타 정보
  • body데이터

HTTP/1.1텍스트, HTTP/2바이너리 프레임, HTTP/3QUIC (UDP) 위. 위 layer 로 갈수록 추상 은 같은데 전송 효율 이 다름.

서버 개발자 의 *기본기 * — “이 요청 이 어떤 HTTP 버전 인지”, “어떤 헤더 가 *오는지/나가는지”, “Content-Type 이 무엇인지”* 가 디버깅 의 절반.


3. Layer 2 — *DNS — 이름 의 해석**

oms.lemuel.co.kr내 브라우저 가 *어떻게 IP 로 바꾸는가 :

flowchart LR
    B[Browser] --> R[OS resolver<br/>cache]
    R --> S[Stub resolver<br/>local DNS]
    S --> RC[Recursive<br/>1.1.1.1 / 8.8.8.8]
    RC --> R1[Root .]
    R1 --> R2[.kr TLD]
    R2 --> R3[.co.kr]
    R3 --> R4[lemuel.co.kr<br/>Cloudflare]
    R4 --> A[A record / CNAME]
    A -.응답.-> RC -.-> S -.-> R -.-> B

    classDef client fill:#1e3a5f,stroke:#3b82f6,color:#fff
    classDef resolver fill:#3f2f1f,stroke:#f59e0b,color:#fff
    classDef auth fill:#3f1e3f,stroke:#a855f7,color:#fff
    class B client
    class R,S,RC resolver
    class R1,R2,R3,R4,A auth

내 lemuel.co.kr 의 *DNS pattern * — 오늘 새벽 발견:

  • 모든 도메인 이 *CNAME*.cfargotunnel.com (Cloudflare Tunnel)
  • 클러스터 의 *공인 IP 를 노출 하지 않음
  • staging-academyA record 자체 가 없어서dig 응답 NULL → 사용자 연결 불가

서버 개발자 의 *기본기 * :

  • dig +short hostname이름 이 *IP 가 있는지
  • dig +trace hostname권위 응답 까지 의 경로
  • DNS TTL변경 후 *전파 시간
  • 클러스터 안 의 *kube-dns / CoreDNSPod 안에서 의 *서비스 이름 해석

4. Layer 3 — *TLS — *안전한 채널**

DNS 로 IP 받은 후 443 포트 로 TLS handshake:

1. Client Hello — 지원 cipher suites + SNI (hostname)
2. Server Hello — 선택 cipher + 인증서 체인
3. 키 교환 (ECDHE) — 양쪽 이 *공통 비밀* 산출
4. 인증서 검증 — 신뢰 체인 + 만료 / 호스트명 일치
5. Finished — *암호화 된 채널* 시작

TLS 1.31-RTT. 0-RTT resume 도 가능.

서버 개발자 의 기본기 :

  • 내 *cert 가 *어디서 발급 되는지* (Let’s Encrypt / cert-manager)
  • renewal 자동화만료 전 자동 갱신
  • SNI한 IP 에 *여러 호스트각자 cert
  • mTLS클라이언트 도 인증서 제시

내 클러스터cert-manager 가 *모든 도메인 의 cert 자동 발급/갱신. 내가 신경 안 써도 *Let’s Encrypt 의 *3 개월 갱신 이 자동*.


5. Layer 4 — *CDN / Edge — *Cloudflare**

Cloudflare 가 *내 요청제일 먼저 받는 곳.

오늘 새벽 xr 의 502 의 진실 :

  • Cloudflare → cloudflared tunnel → 클러스터 backend
  • backend (Pod) 가 DB 연결 실패 → Spring Boot 가 500
  • Cloudflare 가 *origin 의 5xx 를 그대로 전달
  • 사용자에게 *Cloudflare 502 Bad Gateway

CDN 의 *진짜 역할 * — 3 가지:

  1. 정적 자산 *캐싱내 origin 부담 감소
  2. DDoS 차단공격 IP 의 *경계
  3. Tunnel내 origin 의 *공인 IP 을 *숨김

서버 개발자 의 기본기 :

  • Cache-Control / ETag / VaryCDN 캐싱 의 *제어
  • Origin ShieldCDN 내부 의 *중간 캐시
  • Tunnel vs Public IP보안 vs 단순성

6. Layer 5~7 — *Kubernetes 의 *오케스트레이션**

K8s Watch-Reconcile 글바로 그 layer.

flowchart TD
    CF[Cloudflare<br/>Edge] --> CT[cloudflared<br/>tunnel client]
    CT --> ING[Ingress Controller<br/>nginx / traefik]
    ING --> SVC[Service<br/>ClusterIP 가상 IP]
    SVC --> EP[Endpoints<br/>Pod IP 목록]
    EP --> P1[Pod 1<br/>app]
    EP --> P2[Pod 2<br/>app]

    classDef edge fill:#3f2f1f,stroke:#f59e0b,color:#fff
    classDef k8s fill:#3f1e3f,stroke:#a855f7,color:#fff
    classDef app fill:#1f3f1f,stroke:#22c55e,color:#fff
    class CF,CT edge
    class ING,SVC,EP k8s
    class P1,P2 app

6.1 Ingress

HTTP 의 *호스트 헤더 를 보고 어느 Service 로 보낼지 결정. L7 라우터.

spec:
  rules:
  - host: oms.lemuel.co.kr
    http:
      paths:
      - backend:
          service:
            name: order-oms-prod-app
            port:
              number: 8080

6.2 Service

가상 IP (ClusterIP). kube-proxy 가 *iptables / IPVS실제 Pod 로 *분산. L4 LB.

6.3 Pod

컨테이너 의 *최소 단위. 공유 네트워크 + 볼륨집합. 이미지 로 부터 실행 가능.

Pod 가 동작 하려면 4 가지:

  1. 이미지 풀 성공오늘 새벽 GHCR 401 의 이슈
  2. 컨테이너 시작 성공코드 의 부팅
  3. Readiness probe 통과외부 트래픽 받을 준비
  4. Liveness probe 통과살아있음 의 증명

4 개 중 1 개 라도 실패 면 *외부 요청 못 받음. 오늘 새벽 의 *대부분 의 사고이 단계.


7. Layer 8~9 — *프로세스 와 *스레드**

Pod 안 의 컨테이너 가 결국 *프로세스. 내 프로세스 글 의 그 추상화.

7.1 JVM 의 경우 — Spring Boot 의 *Threading 모델**

JVM 프로세스 (PID=1)
  ├── EDT — *없음* (서버 라서)
  ├── Tomcat Connector 스레드 (NIO)
  │     └── *epoll 의 *이벤트 루프* — *대량 connection 의 *낮은 비용 처리*
  ├── Tomcat Worker 스레드 풀 (200 개 기본)
  │     └── *각 요청 이 *한 스레드 점유* — *블로킹 처리*
  ├── Spring 의 *@Async 스레드 풀*
  ├── JIT C1/C2 컴파일러 스레드
  └── GC 스레드 (G1 / ZGC)

Java 21+ 의 Virtual ThreadTomcat 도 *Loom 기반 변경 가능. 그러면 *스레드 비용 0 에 가까움*.

7.2 Node.js 의 경우

Event Loop 1 개 + libuv worker pool. 단일 스레드 이지만 *비동기 I/O수만 요청 동시 처리.

7.3 Python 의 경우

GIL 때문에 진짜 병렬 처리 가 제한 — Uvicorn / gunicorn 의 *프로세스 multi-worker * 로 우회.

서버 개발자 의 기본기 :

  • 요청 1 개 가 *스레드 1 개 인지 이벤트 루프 인지
  • 블로킹 IO 가 *스레드 풀 을 *고갈 시킬 위험
  • Thread dump (jstack) 의 *읽는 법
  • N+1 query 가 *왜 *스레드 풀 점유 문제 도 되는지

8. Layer 10~11 — *Connection Pool**

오늘 새벽 oms.lemuel.co.kr진짜 사고 :

Caused by: HikariPool-1 - Connection is not available, 
           request timed out after 30010ms 
           (total=9, active=0, idle=9, waiting=0)
Caused by: org.postgresql.util.PSQLException: 
           This connection has been closed.

분석 :

  • total = 9풀 에 9 개 connection 보유
  • active = 0지금 사용 중 0
  • idle = 9유휴 9 개
  • 그러나 *모두 *closed 상태DB 가 *연결 끊었거나, idle timeout 으로 서버 가 끊음
  • application 이 *그걸 모르고 *idle 풀 에서 꺼냄 → 사용 시점 에 *예외

해결 방법 — *Hikari 의 *3 줄 설정 * :

spring.datasource.hikari.connection-test-query=SELECT 1
spring.datasource.hikari.keepalive-time=60000           # 1분 마다 ping
spring.datasource.hikari.max-lifetime=1800000          # 30분 마다 강제 재생성

기본기 :

  • Connection Pool 이 *왜 필요한가DB 연결 의 *비용 (TCP + TLS + auth)
  • 풀 크기 의 *적정값DB 의 max_connections / (앱 수 × 1.5) 정도
  • idle / leak / lifetime 의 *3 가지 timeout
  • connection 의 *서버측 종료 (Postgres 의 tcp_keepalives_idle) 와 클라이언트 의 합의

oms 의 rollout restart풀 초기화 → *모든 connection 새로 생성즉시 회복. 물리적 으로 *해결됨.


9. Layer 12 — *DB / Storage

flowchart LR
    Q[Query] --> P[Parser]
    P --> O[Optimizer<br/>plan 선택]
    O --> E[Executor]
    E --> B[Buffer Pool<br/>RAM]
    B -->|cache miss| D[Disk<br/>page read]
    E --> W[WAL<br/>Write-Ahead Log]
    W --> D

    classDef plan fill:#3f1e3f,stroke:#a855f7,color:#fff
    classDef exec fill:#1f3f1f,stroke:#22c55e,color:#fff
    classDef storage fill:#5f1e1e,stroke:#ef4444,color:#fff
    class P,O plan
    class E exec
    class B,D,W storage

DB 의 모든 쓰기WAL 먼저 → page 는 나중 (write-ahead). crash 시 *WAL 로 *재구성.

내 DB 배치 글모든 내용이 layer 의 *기본기.

서버 개발자 의 기본기 :

  • EXPLAIN ANALYZE읽는 법
  • Index 가 *언제 안 타는지
  • MVCC 의 *vacuum 필요성
  • Replication 의 *lag 측정

10. Layer 의 *역방향 — 응답**

12 단계 를 *역순으로 가는 응답 :

DB → Connection Pool → Thread → Process → Pod → Service → Ingress → cloudflared → Cloudflare → 브라우저

각 layer 가 *자신의 응답상위 에 넘김. 어느 한 곳 이 *지연 되면 *전체 latency 가 누적.

p99 응답 시간 = *각 layer 의 *최악 응답 의 합** 이 아니라 *조합 의 *worst case.


11. 관측 (Observability) — *3 기둥**

기둥 무엇 도구
Logs 어떤 일 이 일어났는지 Fluent-bit + Loki / ELK
Metrics 얼마나 자주 / 얼마나 빨리 Prometheus + Grafana
Traces 요청 의 *layer 간 *흐름 Jaeger / Tempo + OpenTelemetry

오늘 새벽 의 사고이 3 가지 의 *각각 이 진단 한 결과 :

  • LogsSpring Boot 의 *HikariCP 예외진짜 원인 알려줌
  • MetricsPrometheus 알람 (KubeContainerWaiting / KubeNodeNotReady)최초 신호
  • Traces아직 운영 안 함. 다음 단계 의 나의 *기본기 강화 영역

12. 실패 의 *기본기 — *어떻게 죽고 *어떻게 부활 하는가***

오늘 새벽 의 5 가지 사고각 layer 의 *실패 패턴 을 가르쳐줌:

12.1 노드 (Layer 8 의 기반) 의 죽음 — louise

  • 모든 포트 *refused — TCP stack 은 살아있지만 모든 service 죽음
  • 24 시간 동안 *kubelet heartbeat 없음 — k8s 가 NotReady 처리
  • 해결 — *물리 적 재부팅
  • 복구 후 *30 초 안에 *Pod 의 Postgres 까지 살아남

12.2 Pod (Layer 8) 의 *이미지 풀 실패 — jabis / logistic*

  • Kubelet 의 *15 시간 4,053 회 재시도Watch-Reconcile 의 자기 치유
  • 해결 — *Secret 갱신 + rollout restart
  • backoff timer 의 *반본 적 재시도 의 *복원 력

12.3 컨트롤 플레인 (Layer 7) 의 *503

  • apiserver not readyetcd 응답 지연 또는 *내부 컴포넌트 부팅 중
  • 데이터 플레인 은 *영향 없음 (이미 실행 중인 Pod)
  • 해결 — *기다림 (자체 복구)

12.4 Connection Pool (Layer 11) 의 *고갈 — oms*

  • idle 9 개 가 *모두 *closed — DB 의 idle timeout 또는 Postgres restart
  • 해결 — *rollout restart풀 의 *clean start
  • 예방 — *connection-test-query + keepalive-time

12.5 Storage (Layer 12 의 PVC) 의 *노드 의존 — xr postgres

  • local-path PVlouise 노드 의 디스크물리 적 묶임
  • louise 부활 없이는 *데이터 접근 불가
  • 해결 — *노드 부활 (가장 빠름) 또는 *백업 으로 다른 노드 복원

13. 서버 개발자 의 *기본기 체크리스트15 가지**

오늘 새벽 점검 의 결론 — 이 15 가지서버 의 *진짜 기본기:

  • DNS 의 *dig / nslookup 사용 가능
  • TLS 의 *handshake + cert chain 이해
  • HTTP 의 *상태 코드 의 *진짜 의미 (2xx/3xx/4xx/5xx)
  • CDN 의 *cache hit / miss 의 진단
  • Kubernetes 의 *Ingress / Service / Pod 관계
  • 컨테이너 의 *이미지 / 레이어 / 풀 *흐름
  • 프로세스 와 *스레드 의 차이
  • 동기 / 비동기 IO 의 *체감 적 차이
  • Connection Pool 의 *3 가지 timeout
  • DB Index 의 *언제 안 타는지
  • MVCC 의 *vacuum 의 *필요성
  • Log 의 *level 분리집중적 분석
  • Metrics 의 *4 가지 황금 신호 (latency / traffic / errors / saturation)
  • Trace 의 *layer 간 흐름 의 시각화
  • 재시도 / 멱등 / 백오프3 가지 *분산 시스템 기본기

절반 이상체크 안 된다면내가 *기본기 의 한 layer 에 *깊지 못함. 오늘 새벽 의 *5 사고 같은 연쇄 가 *재발.


14. 맺음 *— *기본기 의 *시야**

“서버 의 기본기”각 *layer 의 *모든 세부 를 외우는 것* 이 아니다.

층 의 *시야 * 다. 내 요청 이 *어디 부터 *어디 까지 어떤 순서로 흐르는가전체 그림. 그 그림 위 에서 각 layer 의 *대표 *원리 만 *알면 *어디서 든 *디버깅 가능.

오늘 새벽 의 내 점검5 사고5 분 안에 정확 한 원인 으로 좁혀진 이유 가 layer 의 시야 때문. “이건 *Connection Pool 의 *idle 끊김”, “이건 *Pod 의 *이미지 풀”, “이건 *노드 의 *물리 다운”* — 각각 의 *layer 추정 + *증거 수집.

기본기 의 시야 가 없으면“왜 안 되지” 만 *반복. 기본기 의 시야 가 있으면“어디 부터 *층 별 분리 검사” 가 반사.

내일 *내 시스템 이 *느려지거나 *죽으면위 12 단계 의 *어디 인가문제. 시야 가 *해답 의 길.


부록 — 오늘 *내가 *실전 으로 쓰는 *진단 5 줄**

# 1. DNS — 이름 살아있나
dig +short oms.lemuel.co.kr

# 2. TLS + HTTP — *전 layer 통합 응답*
curl -sI --max-time 5 https://oms.lemuel.co.kr/actuator/health

# 3. K8s — *Pod 까지 이름 닿는가*
kubectl -n order-oms-prod get pods -o wide

# 4. App — *Pod 이 *실제 *에러 토하는가*
kubectl -n order-oms-prod logs deploy/order-oms-prod-app --tail=50 | grep -iE 'error|exception'

# 5. DB — *연결 가능 하고 query 빠른가*
kubectl -n order-oms-prod exec -it order-oms-prod-postgres-0 -- \
  psql -U postgres -c 'SELECT 1' -c 'SELECT pg_database_size(current_database())'

5 분 안에 *어느 layer 의 문제 인지 * 판단 가능. 오늘 새벽 의 *모든 사고이 5 줄 의 조합 으로 5 ~ 30 분 안에 진단.

5 줄 이 익숙해 지면서버 의 *기본기 가 *체화 됨. 어떤 시스템 이든 *진단 가능.


관련 글