DevOps 엔지니어 경력직/정규직 1차 기술면접에 합격하여 2차 기술 면접을 보았다.
아래 내가 놓쳤던것을 회고할겸 개념을 정리한다.
1. Kubernetes Control Plane의 확장성과 부하 (Scalability)
상태 체크 루프를 Kubelet(Edge)에서 돌리는 것이 맞는지, Controller Manager(Center)에서 돌리는 것이 맞는지에 대한 질문에, 나는 Kubelet이 로컬 상태를 가장 잘 알기에 신뢰성이 높다는 답변밖에 할 수 없었다. 하지만 이 질문의 본질은 신뢰성이 아닌 분산 시스템의 확장성(Scalability)과 병목(Bottleneck) 문제였다.

Polling과 Event-Driven의 딜레마
전통적인 중앙 집중형(Centralized) 시스템은 컨트롤러가 주기적으로 노드 상태를 확인하는 Polling 방식을 사용한다. 이는 상태 일관성 관리가 용이하지만, 노드 수(N)가 증가하면 중앙 서버에 요청이 몰려 병목 현상이 발생하는 구조적 한계가 있다.
반면, 분산형(Decentralized)은 각 노드가 알아서 상태를 판단하고 보고하지만, 전체 클러스터의 상태 일관성(Consistency)을 보장하기 어렵다는 단점이 있다.
Kubernetes는 이 두 가지 모델의 장점을 결합했다. Kubelet(에이전트)은 독립적으로 노드 상태를 감시하되, 상태 변화가 있을 때만 이벤트를 전송하는 Push(Event-Driven) 방식을 취한다. 중앙의 API Server와 컨트롤러는 이를 수동적으로 감시(Watch)하다가, 의도한 상태(Desired State)와 다를 때만 개입하는 Level-Triggered Reconciliation 구조를 통해 확장성을 확보했다.
- Edge-Triggered (변화 감지):
- "스위치를 켰다(On)!"라는 신호(Event)가 올 때만 불을 켠다.
- 문제점: 만약 "켰다"라는 신호가 네트워크 오류로 유실되면? 전구는 영원히 꺼져 있다. 다음 신호가 올 때까지 절대 복구되지 않는다.
- Level-Triggered (상태 감지) - Kubernetes 방식:
- "지금 스위치가 켜져 있는 상태(On State)다!"라는 사실을 계속 확인한다.
- 장점: "켰다"는 신호를 놓쳐도 상관없다. 나중에 다시 봤을 때 "어? 켜져 있어야 하네? 근데 꺼져 있네?" 하고 다시 켜면 된다. 항상 최종 상태(Final State)를 맞출 수 있다.
왜 쿠버네티스는 Level-Triggered인가?
이게 네가 블로그에 쓴 "100번 변해도 1번만 보낸다(Coalescing)"는 내용과 직결된다.
- 신뢰성 (Reliability): 분산 시스템에서는 네트워크 패킷이 언제든 사라질 수 있다. Edge 방식이라면 패킷 하나 놓치는 순간 클러스터 상태가 영구적으로 꼬인다. Level 방식은 나중에 전체 상태(Full State)를 다시 동기화하면 되므로 장애 복구(Resilience)에 강력하다.
- 부하 감소 (Optimization):
- Edge: 값이 1 -> 2 -> 3 -> 4로 0.1초 만에 변했다면, 이벤트 3번을 다 처리해야 한다.
- Level: 그냥 0.1초 뒤에 쓱 보고 "어? 지금 4네?" 하고 한 번만 처리하면 된다. 중간 과정은 중요하지 않다. 최종 상태(Desired State)만 중요하기 때문이다.
1.2. Node Heartbeat의 진화: 거대 객체에서 경량화된 Lease로
초기에 우려했던 '노드 증가에 따른 패킷 폭주' 문제는 사실 과거 Kubernetes 아키텍처의 유산이었다.
v1.13 이전에는 Kubelet이 10초마다 거대한 NodeStatus 객체를 통째로 업데이트했다.
노드가 수천 개라면 etcd의 쓰기 트래픽(Write I/O)이 감당 불가능한 수준이 된다.
현재는 NodeLease 개념을 도입해 이를 해결했다. Kubelet은 kube-node-lease 네임스페이스에 있는 아주 작은 Lease 객체(Payload가 거의 없는)만 갱신하여 생존(Heartbeat)을 신고하고, 무거운 NodeStatus는 실제 상태 변경이 있을 때만 전송한다. 즉, 단순한 루프가 아니라 객체의 역할 분리를 통해 부하를 제어하는 것이다.
1.3. Informer 패턴과 Watch의 숨겨진 비용 (O(N)의 실체)

컨트롤러가 API Server를 바라보는 방식은 Polling이 아닌 Watch(Streaming)다. HTTP 연결을 맺고 변경 사항(Event)이 생길 때만 데이터를 받기에 효율적으로 보인다. 하지만 감시 대상(Watcher)이 많아지면 이벤트 하나에 수천 번의 직렬화(Serialization)와 네트워크 전송(Fan-out)이 발생한다.
이것이 바로 '2,000개 네임스페이스 생성 시 발생하는 오버헤드'의 실체다. 런타임에 API Server의 메모리 내 캐시(Deserialization Cache) 부하와 Watch Event Fan-out 비용이 시스템을 느리게 만드는 주원인이다.
1.4. 'Thundering Herd'를 방지하는 3중 안전장치
"모든 노드가 동시에 이벤트를 Push하면 사실상 DDoS 공격이 아닌가?"라는 의문이 들 수 있다. 하지만 Kubernetes는 트래픽 폭주(Thundering Herd)를 막기 위해 정교한 3가지 방어 기제를 내장하고 있다.
- 지터(Jitter) & Backoff ("시간차 공격"): 모든 Kubelet과 컨트롤러는 요청을 보낼 때 Wait.Jitter 함수 등을 통해 랜덤한 시간차를 둔다. 0.1초, 0.5초 등 미세하게 어긋난 타이밍에 요청을 보내게 하여, 요청이 칼같이 동시에 도착하는 스파이크(Spike) 현상을 물리적으로 분산시킨다.
- Rate Limiting ("입장 제한"): API Server와 클라이언트(Client-go) 내부에는 토큰 버킷(Token Bucket) 알고리즘이 적용되어 있다. "초당 50개(QPS) 처리, 최대 100개(Burst) 허용" 같은 규칙을 통해 한계치를 넘는 트래픽은 강제로 대기(Throttle)시키거나 429 Too Many Requests로 차단하여 서버의 다운을 방지한다.
- 이벤트 병합(Coalescing): 상태가 1초에 100번 변한다고 해서 100번 보고하지 않는다. 일정 주기(Sync Period) 동안 발생한 변화를 하나의 최신 상태(Latest State)로 병합하여 한 번만 전송한다. 이것이 바로 Kubernetes가 Edge-Triggered가 아닌 Level-Triggered 방식을 채택한 이유이며, 시스템의 안정성을 보장하는 핵심이다.
2. 네임스페이스 없는 격리: 논리적/물리적 보안의 대안
네임스페이스는 etcd의 키 접두사일 뿐이지 않냐는 반문에, 나는 런타임 오버헤드를 고려하지 못한 채 저장 구조에만 매몰되어 있었다. 네임스페이스 없이도 테넌트를 격리하는 기술은 네트워크 스택의 깊은 곳(Kernel Level)과 암호화 계층(Application Level)에서 이루어진다.
2.1. Network Policy의 실체 (L3/L4)
쿠버네티스의 네트워크 정책은 선언(YAML)일 뿐, 실제 차단은 CNI 플러그인이 수행한다.
- iptables 모드 (Calico 등): 정책을 리눅스 커널의 iptables 규칙이나 ipset으로 변환한다. 패킷이 들어올 때 커널의 Netfilter 훅에서 소스 IP를 확인하고 즉시 폐기(Drop)하므로, 애플리케이션은 상대방의 존재조차 알 수 없다.
- eBPF 모드 (Cilium): iptables를 거치지 않고, 네트워크 인터페이스 카드(NIC) 드라이버 단계(XDP/TC)에서 샌드박스 코드를 실행하여 패킷을 바로 버린다. 컨텍스트 스위칭 비용이 없어 훨씬 빠르다.
2.2. Service Mesh와 mTLS (L7 Deep Dive)
네트워크 레벨을 넘어 애플리케이션 레벨의 격리가 필요할 때는 mTLS(상호 인증)를 사용한다. 일반적인 TLS는 클라이언트가 서버만 확인하지만, mTLS는 서버도 클라이언트를 검증한다.
[상세 핸드셰이크 과정]
- Client Hello: 암호화 방식(Cipher Suite)과 난수(Random)를 보낸다.
- Server Hello & Certificate: 서버가 암호화 방식을 선택하고, 자신의 공개키(Public Key)가 포함된 인증서를 보낸다. (중요) 이때 서버도 클라이언트에게 "너도 인증서 줘(Certificate Request)"라고 요청한다.
- Client Certificate: 클라이언트가 자신의 인증서를 서버에 보낸다.
- Verification (격리의 핵심):
- 양측은 서로의 인증서가 신뢰된 CA(Certificate Authority)로부터 서명되었는지 검증한다.
- 여기서 인증서 내의 식별자(SPIFFE ID 등)가 허용 목록(Allow List)에 없으면 연결은 즉시 거부된다. 이것이 네임스페이스 없는 논리적 격리다.
- Key Exchange: 클라이언트가 서버의 공개키를 사용하여 Pre-master Secret을 암호화해 보낸다.
- Session Key Derivation: 교환된 Secret을 바탕으로 양측은 동일한 대칭키(Symmetric Key)를 생성한다.
[왜 키를 두 번 쓰는가?]
- 공개키(비대칭키): 인증과 키 교환에 사용된다. 보안성은 높지만 연산 비용이 매우 비싸다.
- 대칭키(세션키): 실제 데이터 전송(Bulk Data Transfer)에 사용된다. 연산 속도가 빠르다(AES 등). mTLS는 이 하이브리드 방식을 통해 보안과 성능을 모두 잡는다.
2.3. 어떻게 구현하는가? (Cilium vs Istio)
방법 A: Cilium Service Mesh
Cilium은 사이드카(envoy 컨테이너)를 주렁주렁 달지 않고, eBPF 레벨에서 mTLS를 처리하거나 Node 단위의 Proxy를 사용.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: secure-backend-access
spec:
endpointSelector:
matchLabels:
app: backend
ingress:
- fromEndpoints:
- matchLabels:
app: frontend
authentication:
mode: required # <-- 여기가 핵심! (무조건 mTLS 해라)
- 그냥 정책(Policy)만 던지면, Cilium 에이전트가 알아서 인증서를 발급받고 회전(Rotation)시키고 핸드셰이크까지 한다.
방법 B: Istio (전통적인 방식)
Istio는 각 파드마다 istio-proxy라는 컨테이너를 옆에 붙여서 해결한다.
설정 (YAML 예시):
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: default
spec:
mtls:
mode: STRICT # <-- mTLS 인증서 없으면 아예 연결 끊어버림
3. Push vs Poll과 Backpressure

- Push 모델: 생산자가 보내는 대로 소비자가 받아야 한다. 실시간성은 좋지만, 생산 속도가 소비 속도를 압도하면 소비자의 TCP 버퍼가 가득 차고 결국 OOM(Out Of Memory)으로 프로세스가 죽는다. 이를 막으려면 복잡한 흐름 제어(Flow Control) 프로토콜이 필요하다.
- Poll 모델: 소비자가 처리할 수 있는 만큼만 가져온다(Pull). 시스템 부하가 높아져도 처리 속도가 느려질 뿐, 서비스가 죽지는 않는다. Kafka나 Task Queue 시스템이 Poll 방식을 채택하는 이유는 데이터 유실보다 시스템 전체의 생존성(Availability)이 더 중요하기 때문이다.
'Retrospective' 카테고리의 다른 글
| [DevOps 기술 면접 회고] TLB, K8s 내부 동작과 커널 격리, TLS 핸드셰이크 (0) | 2025.12.20 |
|---|
댓글