본문 바로가기
Retrospective

[DevOps 기술 면접 회고] TLB, K8s 내부 동작과 커널 격리, TLS 핸드셰이크

by 코딩하는 동현 2025. 12. 20.

최근 3학년 2학기 시험기간에 DevOps 엔지니어 경력직/정규직 기술 면접을 보았다.

나름대로 OS(프로세스, 메모리)와 네트워크 기초는 깊게 팠다고 자부했지만, 쿠버네티스 내부 통신 원리, HTTPS의 핸드쉐이크에 대한 부분은 추상적인 답변밖에 내놓지 못했다.

DevOps 엔지니어로서 트러블슈팅을 하려면 '무엇을(What)' 쓰는지보다 '어떻게(How)' 동작하는지를 알아야 한다.

이번 면접을 통해 뼈저리게 느낀 나의 빈틈, K8s Control Plane의 동작 순서TLS Handshake 과정을 정리해 본다.

 


1. TLB Hit rate 튜닝

https://konkukcodekat.tistory.com/282

 

[운영체제] OS의 메모리 할당 관리와 segmentation, 페이징(Paging)

배경지식프로그램은 실행되기 위해 disk에서 memory로 로드되어 프로세스 내에 배치되어야 한다.CPU가 직접 접근할 수 있는 저장소는 main memory와 registers뿐이며, memory unit은 주소와 read 요청, 또는 주

konkukcodekat.tistory.com

TLB 관련해서는 예전에 운영체제 관련 글을 작성할때 다룬적이 있다.

 

TLB에 대해서 요약을 하자면 CPU가 데이터를 읽을때 항상 페이지테이블을 읽어야 되는데, 이 부분을 CPU L1 캐시 같은 곳에 적재를 해서 페이지 테이블 결과값 자체를 캐싱하는것이다.

 

단순히 TLB를 "가상 주소와 물리 주소 변환을 돕는 캐시"라고 정의하고 넘어가면 학부생 수준의 답변에 그친다.

DevOps 엔지니어로서 대용량 트래픽을 처리하는 인프라를 구축하려면, TLB가 왜 성능의 병목이 되며 이를 어떻게 HugePages로 해결하는지 하드웨어 관점에서 이해해야 한다.

하드웨어적 제약: L1 캐시만큼 귀하고 비싼 자원

TLB(Translation Lookaside Buffer)는 CPU 코어 내부, 정확히는 MMU(Memory Management Unit) 내부에 위치한다. 이는 CPU가 가장 빠르게 접근할 수 있는 L1 캐시(Instruction/Data Cache)와 동일한 수준의 속도를 보장해야 하므로, 집적도가 높고 값비싼 SRAM으로 제작된다.

문제는 비용과 물리적 공간의 제약이다. CPU 다이(Die) 면적은 한정적이기에 TLB의 크기를 무작정 늘릴 수 없다. 최신 CPU라 하더라도 TLB가 저장할 수 있는 엔트리(Entry) 개수는 보통 수십 개에서 많아야 수백 개(L1 TLB 기준)에 불과하다. 이 '작고 소중한' 공간을 얼마나 효율적으로 쓰느냐가 성능의 핵심이다.

4KB 페이지의 한계와 TLB Miss

리눅스 기본 메모리 페이지 크기는 4KB다. 만약 64GB의 메모리를 사용하는 고성능 DB(PostgreSQL, MySQL)나 수십 GB의 Heap을 사용하는 Java 애플리케이션을 구동한다고 가정해 보자.

4KB 단위로 쪼개진 수천만 개의 페이지 매핑 정보를 고작 수백 개의 TLB 엔트리에 욱여넣어야 한다. 당연히 프로세스가 메모리의 이쪽저쪽을 접근할 때마다 TLB에는 해당 주소 정보가 없을 확률이 높다. 이를 TLB Miss라고 한다. TLB Miss가 발생하면 CPU는 하던 일을 멈추고 느린 메인 메모리(RAM)의 페이지 테이블을 뒤져야 하는데(Page Walk), 이 과정이 빈번해지면 CPU 사이클이 낭비되고 애플리케이션의 Latency는 치솟는다.

해결책: HugePages (2MB / 1GB)

# Pod Spec 예시
resources:
  limits:
    hugepages-2Mi: 1Gi
    memory: 2Gi
  requests:
    memory: 2Gi

이 병목을 해결하는 '치트키'가 바로 HugePages다. 페이지의 최소 단위를 기본 4KB에서 2MB(또는 1GB)로 키우는 것이다.

원리는 간단하다. 같은 크기의 메모리를 커버할 때 필요한 페이지 개수를 획기적으로 줄이는 것이다.

  • 4KB 페이지 사용 시: 2MB 메모리를 표현하려면 512개의 TLB 엔트리가 필요하다. (TLB가 꽉 찬다.)
  • 2MB HugePage 사용 시: 2MB 메모리를 표현하는 데 단 1개의 TLB 엔트리만 있으면 된다.

결과적으로 같은 크기의 TLB로 커버할 수 있는 메모리 범위(TLB Reach)가 512배 넓어진다. 이는 TLB Hit Rate를 비약적으로 상승시키며, Page Walk로 인한 오버헤드를 제거하여 DB와 Java 애플리케이션의 처리량을 극대화한다.

결국 DevOps 엔지니어가 커널 파라미터(vm.nr_hugepages)를 튜닝하고 쿠버네티스에서 HugePages 리소스를 할당하는 행위는, 단순한 설정 변경이 아니라 하드웨어(TLB)의 물리적 한계를 소프트웨어적으로 극복하려는 아키텍처적 결정인 것이다.

 


2. Kubernetes: Deployment를 생성할 때 내부 동작과 통신

 

쿠버네티스 컴포넌트들이 어떻게 유기적으로 협력하여 파드(Pod)를 띄우는지에 대한 아키텍처 흐름이다.

쿠버네티스는 선언적 API(Declarative API) 시스템이다. 우리가 명령을 내리면, 각 컴포넌트가 API Server를 통해 상태를 감지(Watch)하고, Desired State(원하는 상태)를 맞추기 위해 끊임없이 루프를 돈다.

상세 동작 시퀀스 (Deployment 생성 직후)

  1. kubectl → API Server:
    • 사용자가 kubectl apply -f deployment.yaml을 실행한다.
    • API Server는 요청의 인증(AuthN)/인가(AuthZ)를 검증하고, 문법 오류를 확인한 뒤 해당 Deployment 객체 정보를 Etcd에 저장한다.
    • 핵심: 아직 파드는 생성되지 않았다. 그저 "기록"만 되었을 뿐이다.
  2. Controller Manager (Deployment Controller):
    • API Server를 Watch하고 있던 Deployment Controller가 "새로운 Deployment가 생겼음"을 감지한다.
    • Deployment 정의에 따라 ReplicaSet 명세(Spec)를 생성하고, 이를 API Server에 요청한다.
  3. Controller Manager (ReplicaSet Controller):
    • 이번엔 ReplicaSet Controller가 "새로운 ReplicaSet이 생겼음"을 감지한다.
    • replicas 개수(예: 3개)만큼의 Pod 객체를 생성하라고 API Server에 요청한다.
    • 핵심: 이때 생성된 Pod 정보는 Etcd에 저장되지만, nodeName 필드가 비어있다. 즉, 아직 어떤 노드에 띄울지 결정되지 않은 상태(Pending)다.
  4. Scheduler:
    • Scheduler는 nodeName이 할당되지 않은(Unscheduled) 파드를 감지한다.
    • 가용한 워커 노드들의 리소스(CPU, Memory) 상태를 확인하고, 제약 조건(Taint/Toleration, Affinity)을 고려해 점수를 매긴다(Filtering & Scoring).
    • 가장 적합한 노드를 선정하여 파드의 nodeName 필드를 업데이트한다(Binding). 이 정보 역시 API Server를 통해 Etcd에 저장된다.
  5. Kubelet (Worker Node):
    • 자신이 속한 노드에 새로운 파드가 할당(Binding)된 것을 API Server를 통해 감지한다.
    • 파드 명세를 바탕으로 컨테이너 런타임(Docker, containerd 등)에 컨테이너 생성을 명령한다. 이때 CRI(Container Runtime Interface)를 사용한다.
    • 컨테이너가 정상적으로 뜨면 파드의 상태를 Running으로 변경하고 API Server에 보고한다.

Etcd란?

쉽게 설명하자면 쿠버네티스의 가볍고 빠른 분산 키-밸류(Key-Value) 저장소다.

쿠버네티스 클러스터의 모든 설정 데이터, 현재 상태(State), 메타데이터가 저장되는 유일한 데이터베이스이자 Single Source of Truth(SSOT)이다.

  • 왜 중요한가?: 쿠버네티스의 다른 컴포넌트(Scheduler, Controller, Kubelet)는 서로 직접 통신하지 않고, 오직 API Server를 통해 Etcd의 데이터를 읽고 쓴다. 즉, Etcd가 죽으면 클러스터는 기억을 잃고 멈춘다.
  • RDBMS가 아닌 이유: 복잡한 관계형 데이터보다는 상태(State) 정보를 빠르게 쓰고 읽어야 하며, 여러 노드에 분산되어 있어도 데이터가 일치해야 하는 일관성(Consistency)이 매우 중요하기 때문이다(Raft 합의 알고리즘 사용).

 

위 다이어그램은 Raft 합의 알고리즘(Consensus Algorithm)을 기반으로 동작하는 Etcd 클러스터의 Leader-Follower 아키텍처를 나타낸다. 분산 시스템에서 데이터의 일관성을 어떻게 보장하는지 보여주는 핵심적인 구조다.

1. Leader-Follower 모델과 데이터 복제

다이어그램 중앙의 Master 노드는 클러스터 내에서 유일하게 쓰기 요청을 처리하는 권한을 가진다. 나머지 Follower 노드들은 리더가 수신한 데이터를 실시간으로 복제(Replication)하여 자신의 스토리지에 동기화한다. 리더에서 뻗어 나가는 화살표는 데이터 복제 스트림이자, 리더의 생존을 알리는 심장 박동(Heartbeat) 신호를 의미한다.

2. Raft 알고리즘의 핵심: Consensus

Etcd는 단순히 데이터를 복사하는 것을 넘어, Raft 알고리즘을 통해 분산 환경에서의 강력한 일관성을 보장한다.

  • 다수결 합의(Quorum): 리더는 클라이언트로부터 데이터 변경 요청을 받으면 즉시 저장하지 않는다. 팔로워들에게 변경 로그를 전파하고, 과반수(50% + 1) 이상의 노드가 "저장 확인" 응답을 보냈을 때 비로소 데이터를 확정(Commit)한다. 이 메커니즘 덕분에 일부 노드에 장애가 발생하더라도 데이터의 정합성은 깨지지 않는다.
  • 리더 선출(Leader Election): 만약 리더 노드에 장애가 발생하여 Heartbeat가 끊기면, 팔로워들은 즉시 투표를 시작한다. 가장 최신의 로그를 보유한 팔로워가 새로운 리더로 선출되며, 이 과정은 수 초 내에 완료되어 클러스터의 가용성을 유지한다.

결국 쿠버네티스가 Etcd를 선택한 이유는, 수천 개의 파드가 생성되고 사라지는 혼란스러운 분산 환경에서도 Raft 알고리즘을 통해 '단 하나의 진실(Single Source of Truth)'을 흔들림 없이 유지할 수 있기 때문이다.


Controller Manager의 Reconciliation Loop

단순히 "컨트롤러가 있다"고 넘어갔지만, 사실 kube-controller-manager는 하나의 거대한 프로세스 안에 수십 개의 컨트롤러(Node, Job, Endpoint, ServiceAccount Controller 등)가 패키징 된 형태다.

쿠버네티스의 루프는 단순 반복이 아니라, Level Triggered 방식으로 설계되어 있어 중간에 네트워크 오류로 이벤트를 놓쳐도 결국에는 시스템이 올바른 상태로 수렴(Converge)하게 된다.

  • Informer: API Server로부터 리소스의 변경 이벤트를 구독. (Add / Update / Delete)
    Watch와 List를 통해 리소스 동기화 및 캐싱.
  • WorkQueue: 이벤트가 발생한 리소스의 이름(namespacedName)을 queue에 enqueue한다.
    동일 리소스에 대한 중복 요청 방지
  • Reconcile Loop: WorkQueue에서 리소스를 가져와서 사용자 정의된 Reconcile 함수를 실행

 

이들의 핵심 동작 원리는 Reconciliation Loop(재조정 루프)다.

  1. Observe (관찰): API Server를 통해 현재 클러스터의 상태(Current State)를 끊임없이 지켜본다(Watch).
  2. Diff (비교): 사용자가 정의한 상태(Desired State, 예: 파드 3개)와 현재 상태(예: 파드 1개)의 차이를 발견한다.
  3. Act (조치): 그 차이를 없애기 위해 명령을 내린다(예: 파드 2개 더 생성 요청).

즉, 쿠버네티스가 '자가 치유(Self-healing)' 능력을 가지는 이유는 바로 이 컨트롤러 매니저가 잠들지 않고 끊임없이 Current와 Desired를 맞추기 위해(Make it so) 루프를 돌고 있기 때문이다.

Deployment를 생성할 때도 Deployment Controller가 ReplicaSet을 만들고, ReplicaSet Controller가 다시 Pod를 만드는 계층적 루프가 작동한 것이다.


3. Kubelet: Pod Lifecycle Manager

면접에서 나는 Kubelet을 "명령을 받아 수행하는 에이전트" 정도로 설명했다. 하지만 Kubelet은 각 노드의 Pod Lifecycle Manager다.

Kubelet의 핵심 역할

  • API Server와 Node의 연결고리: 노드의 상태(Heartbeat)를 주기적으로 마스터에 보고한다.
  • CRI (Container Runtime Interface): 과거에는 도커를 직접 제어했지만, 이제는 CRI라는 표준 인터페이스를 통해 containerd, CRI-O 등 다양한 런타임을 제어한다. 즉, Kubelet은 "컨테이너 좀 띄워줘(gRPC)"라고 CRI에 요청하고, 실제 작업은 런타임이 수행한다.
  • Probes 관리: 단순히 프로세스를 띄우는 것에서 끝나지 않는다.
    • Liveness Probe: 컨테이너가 살아있는지 체크 (죽으면 재시작).
    • Readiness Probe: 트래픽을 받을 준비가 되었는지 체크 (준비 안 되면 서비스 엔드포인트에서 제외).
    • Startup Probe: 애플리케이션 시작이 완료되었는지 체크.

즉, Kubelet은 마스터가 시키는 대로만 하는 게 아니라, "내 노드에 할당된 파드는 내가 책임지고 살려낸다"는 자율성을 가진 에이전트다.


4. K8s 커널 취약 방어 (샌드박스 컨테이너)

우리는 흔히 "컨테이너는 격리되어 있다"고 말한다. 하지만 표준 컨테이너 런타임인 runc의 격리는 생각보다 얇다.

runc는 호스트의 리눅스 커널을 공유한다. Namespaces와 Cgroups로 "벽이 있는 척" 눈속임을 할 뿐이다. 만약 컨테이너 내부에서 커널 취약점(CVE)을 건드리거나, 커널 패닉을 유발한다면? 해당 노드(Node) 전체가 마비되거나 탈취당할 수 있다. (Container Escape)

gVisor (by Google) : "가짜 커널"

  • 컨셉: 커널과 컨테이너 사이에 'Sentry'라는 중재자를 둔다.
  • 원리: 컨테이너가 시스템 콜(System Call)을 보내면, Sentry가 가로채서(Intercept) 검사하고 유저 스페이스에서 처리한다. 즉, 호스트 커널에 직접 말을 걸지 못하게 한다.
  • 장점: VM 방식보다 가볍다.
  • 단점: 시스템 콜 오버헤드가 있어 I/O가 많은 작업에선 느려질 수 있다.

Kata Containers (by OpenStack Foundation) : "초경량 VM"

  • 컨셉: 컨테이너 하나를 띄울 때마다 아주 작은 VM(MicroVM)을 생성한다.
  • 원리: QEMU/KVM 기반의 하드웨어 가상화를 사용한다. 컨테이너마다 자신만의 독립된 커널을 가진다.
  • 장점: 격리 끝판왕. 호스트 커널과 완벽히 분리된다.
  • 단점: 일반 컨테이너보다 리소스를 더 먹고, 시동 속도가 미세하게 느리다.

높은 보안 수준과 확실한 격리(Hard Multi-tenancy)가 필요하다면, 하드웨어 레벨에서 격리하는 Kata Containers가 더 확실한 선택지다.


Architecture: 모든 곳에 Kata를 써야 할까?

보안이 중요하다고 해서 모든 마이크로서비스를 Kata로 띄우는 것은 오버엔지니어링이다. 비용 낭비이자 성능 저하를 초래한다.

우리는 'MSA(Microservices Architecture)'와 쿠버네티스의 'RuntimeClass'를 조합하여 [위험의 격리 (Isolation of Risk)] 전략을 취해야 한다.

Bad Pattern (Monolith + Kata)

  • 구조: 메인 API 서버(Java Spring)가 비즈니스 로직과 이미지 변환을 동시에 수행하며, 통째로 Kata 위에서 동작.
  • 문제점: 단순 조회 요청(Safe Workload)까지 VM 오버헤드를 겪어야 함.

Best Practice (Selective Isolation)

시스템을 '안전한 영역'과 '위험한 영역'으로 분리한다.

1. Main API Server (Java/Spring)-> runc 사용

  • 역할: 로그인, 채팅 로직 등 신뢰할 수 있는 내부 코드 실행.
  • 이유: 빠른 응답 속도(Latency)가 생명이므로 가벼운 표준 런타임 사용.

2. Image Worker (Python/Go) -> Kata Containers 사용

  • 역할: 사용자 업로드 파일 처리, ImageMagick 등 외부 라이브러리 사용.
  • 이유: 보안 취약점이 발견될 확률이 높은 High Risk 작업군.
  • 효과: 해커가 악성 이미지를 통해 공격하더라도, Worker VM 하나만 죽고 끝난다. 메인 서비스와 호스트 노드는 안전하다. (Blast Radius 최소화)

Implementation: 어떻게 적용하는가?

쿠버네티스에서는 RuntimeClass 리소스를 통해 파드(Pod)별로 런타임을 다르게 지정할 수 있다.

 

Step 1. RuntimeClass 정의

apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: kata-qemu  # 개발자가 부를 이름
handler: kata      # containerd 설정에 등록된 핸들러 이름

 

Step 2. Pod 배포 시 적용 (위험한 Worker에만)

apiVersion: v1
kind: Pod
metadata:
  name: image-worker
spec:
  runtimeClassName: kata-qemu  # <--- 핵심! 이 한 줄로 격리 레벨이 달라진다.
  containers:
  - name: worker
    image: my-image-processor:v1

 

데브옵스 엔지니어의 역할은 무조건 최신 기술, 무조건 강력한 보안 도구를 도입하는 것이 아니다.

"속도가 필요한 곳엔 속도를, 보안이 필요한 곳엔 격리를."

비즈니스의 요구사항에 맞춰 적재적소에 기술을 배치하는 설계 능력이야말로 진정한 엔지니어링이다.


5. HTTPS & SSL/TLS Handshake

HTTPS에 대해서 "HTTP에 보안(SSL)을 입힌 것"이라고 답했지만, 그 내부 과정인 Handshake를 설명하지 못했다.

DevOps 엔지니어에게 인증서 에러와 연결 지연 트러블슈팅은 일상이다. 이 과정을 모르면 해결할 수 없다.

HTTPS는 대칭키 암호화(속도)비대칭키 암호화(보안)를 혼합해서 사용한다. 이를 합의하는 과정이 바로 TLS Handshake다.

TLS Handshake

Goal: 클라이언트와 서버가 데이터를 주고받을 때 사용할 '세션 키(대칭키)'를 안전하게 공유하는 것.

  1. Client Hello (나 이거 지원해)
    • 클라이언트(브라우저)가 서버에게 인사를 건넨다.
    • 전송 정보:
      • 사용 가능한 암호화 방식(Cipher Suite) 목록.
      • 나중에 키를 만들 때 쓸 난수(Client Random).
      • 지원하는 TLS 버전.
  2. Server Hello (그래, 이걸로 하자)
    • 서버가 답변한다.
    • 전송 정보:
      • 클라이언트가 제시한 것 중 선택한 Cipher Suite.
      • 서버가 생성한 난수(Server Random).
      • 서버의 인증서(Certificate) (여기엔 서버의 공개키가 들어있다!).
  3. Certificate Verification (너 진짜 맞니?)
    • 클라이언트는 서버가 준 인증서가 믿을만한지 확인한다.
    • 브라우저에 내장된 CA(Root Certificate Authority)의 공개키를 이용해 인증서의 서명을 복호화해 본다. 복호화가 성공하면 "이 인증서는 CA가 보증한 게 맞군"이라며 신뢰한다.
  4. Key Exchange (비밀 공유하자)
    • 이 단계가 보안의 핵심이다. 'Pre-master Secret(임시 비밀키)'라는 중요한 데이터를 서버에 보내야 한다. 그냥 보내면 해킹당한다.
    • 클라이언트는 아까 받은 서버의 공개키(비대칭키)로 Pre-master Secret을 암호화해서 보낸다.
    • 서버는 자신이 가진 개인키(Private Key)로만 이것을 복호화할 수 있다. (해커가 패킷을 가로채도 개인키가 없어서 못 푼다.)
  5. Session Key Generation (최종 무기 생성)
    • 이제 클라이언트와 서버 모두 다음 3가지 재료를 가지고 있다.
      1. Client Random
      2. Server Random
      3. Pre-master Secret
    • 양쪽은 약속된 알고리즘으로 이 재료들을 섞어 최종 세션 키(Session Key, 대칭키)를 생성한다.
  6. Finished (통신 시작)
    • 이제부터 주고받는 모든 HTTP 데이터는 이 세션 키로 암호화되어 전송된다. 대칭키이므로 속도가 매우 빠르다.

회고

면접을 복기하며 느낀 점은, 내가 편리한 도구(K8s, HTTPS)의 '사용법'에만 익숙해져 있었다는 것이다.

  • kubectl run을 쳤을 때 마법처럼 파드가 뜨는 게 아니라, 5단계의 정교한 통신(API Server ↔ Controller ↔ Scheduler ↔ Kubelet)이 있었다.
  • HTTPS 주소창의 자물쇠 뒤에는 공개키로 대칭키를 안전하게 교환하려는 치열한 암호화 프로토콜이 숨어 있었다.

DevOps 엔지니어는 이 마법이 깨졌을 때 고쳐야 하는 사람이다. 그렇기에 Internals(내부 동작)Fundamentals(기초 원리)는 선택이 아닌 필수다. 이번 면접은 나에게 그 '깊이'의 중요성을 일깨워준 소중한 계기였다. 이제 다시는 이 질문들에 막히지 않을 것이다.

반응형

댓글